Font Metrics API


#1

This is a proposal for adding a text measurement and font metrics API.

In many ways it is similar to the Canvas TextMetrics API however it is designed to match the text rendering capabilities of the DOM and allows either in-document or out-of-document measurements.

An accurate and fast text measurement API is something that current generation web applications have been asking for and the alternatives aren’t great. Without CSS Custom Layout this is likely to become an even bigger problem.

Current alternatives:

  • In-document measurements. Accurate but slow, requires mutating the DOM and forcing layout.
  • Canvas TextMetrics API. Limited API and results aren’t guaranteed to match DOM measurements.
  • Using opentype.js. Very low level, requiring a lot of extra work by author. Bypasses entire text stack in browser and as such measurements are not guaranteed to match.

The proposed API provides a high-level API without any of the drawbacks mentioned above.

Proposed API

partial interface Document {
    FontMetrics measureElement(Element element);
    FontMetrics measureText(DOMString text, StylePropertyMapReadOnly styleMap);
};

Two methods are provided for measuring text, one for in-document measurements and another for out-of-document measurements. Both return a FontMetrics object.

measureElement() takes an Element and returns a FontMetrics object. If the Element is not in the document or isn’t rendered an empty FontMetrics object is returned.

measureText() takes a DOMString and an optional StylePropertyMapReadOnly, returning a FontMetrics object. Unless a font is specified as a part of the styleMap the user agents default will be used.

Note: The only styles that apply to the measureText() method are those that are passed in as a part of the styleMap. Document styles do not apply.

FontMetrics object

interface FontMetrics {
  readonly attribute double width;
  readonly attribute sequence<double> advances;

  readonly attribute double boundingBoxLeft;
  readonly attribute double boundingBoxRight;

  readonly attribute double height;
  readonly attribute double emHeightAscent;
  readonly attribute double emHeightDescent;
  readonly attribute double boundingBoxAscent;
  readonly attribute double boundingBoxDescent;
  readonly attribute double fontBoundingBoxAscent;
  readonly attribute double fontBoundingBoxDescent;

  readonly attribute Baseline dominantBaseline;
  readonly attribute sequence<Baseline> baselines;
  readonly attribute sequence<Font> fonts;
};

Full Proposed API with further details.

Minimal Example

Example of out-of-document text measurement.

  let metrics = document.measureText('Hello WICG');
  let width = metrics.width;

Middle-truncation Example

Using the measurement API to implement a very simple middle-truncation method. Unlike text-overflow this truncates from the middle of a string which is often useful for things like URLs and phone numbers where the leading and trailing ends tends to be more important than the middle.

This example demonstrates the use of the advances field in the metrics object.

function truncateMiddle(textNode, maxWidth) {
  var string = textNode.textContent;
  let metrics = measureText(string);
  if (metrics.width < maxWidth)
    return;

  let ellipsis = '\u2026';
  let ellipsisWidth = measureText(ellipsis).width;
  let availableWidth = maxWidth - ellipsisWidth;

  // Allow at maximum half the available width before the ellipsis.
  let availableLeadingWidth = availableWidth / 2;
  let leadingOffset = 0;
  for (let i = 0; i < metrics.advances.length; i++) {
    if (metrics.advances[i] > availableLeadingWidth)
      break;
    leadingOffset = i;
  }
  let leadingWidth = metrics.advances[leadingOffset];

  // Allow all remaining width after the ellipsis.
  let availableTrailingWidth = availableWidth - leadingWidth;
  let trailingOffset = string.length - 1;
  for (let i = metrics.advances.length - 1; i > leadingOffset; i--) {
    let width = metrics.width - metrics.advances[i];
    if (width > availableTrailingWidth)
      break;
    trailingOffset = i;
  }
  let trailingWidth = metrics.width - metrics.advances[trailingOffset];

  // Replace text content with truncated string.
  let truncatedString = string.substr(0, leadingOffset) +
      ellipsis +
      string.substr(trailingOffset, string.length - trailingOffset);
  textNode.textContent = truncatedString;
}

Note: This work started as a part of the Houdini Task Force before it was decided to move it to WICG for incubation.


#2

I’m really excited for this API specifically for how it can benefit math typesetting library likes KaTeX and MathJax.

Some high-level feedback:

  • the writing-mode CSS property support vertical layouts so the Font Metrics API should handle that situation
  • there should be some way to get font wide values such as underline thickness, underline position, italic angle from the HHEA table and other values like strikethrough position, strikethrough thickness, superscript/subscript positioning, etc. from the OS/2 (and windows) table.

Some lower-level feedback

  • I assume that dominantBaseline.value is always 0, it might be good to include that in the docs
  • How is the dominantBaseline determined?
  • boundingBoxAscent <= fontBoundingBoxAscent <= emHeightAscent
  • boundingBoxDescent >= fontBoundingBoxDescent >= emHeightDescent
  • I found “Positive numbers indicating that the {{FontMetrics/dominantBaseline}} is below the bottom of that em square (so this value will usually be negative).” to be hard to parse in the description of emHeightDescent. Maybe “Positive numbers indicating that em square is above the dominantBaseline (so this value will usually be negative).”
  • emHeightAscent and emHeightDescent sound a little weird b/c I’ve never heard of em “height”, it’s usually em “box” or em “size” (at least in my experience), also using emBoxAscent, emBoxDescent will allow for emBoxLeft and emBoxRight for vertical writing-mode support