Chromium’s Text Autosizer
[email protected] (Steve Kobes)
p[email protected] (Philip Rogers)
last updated: 30 Jul 2014
The Text Autosizer is a component of the Blink rendering engine that is responsible for boosting font sizes on small screens. (Text autosizing is also known as "font boosting" or "font inflation".)
Text autosizing normally runs only on Chrome for Android. However, it can be applied in desktop Chrome by enabling device emulation in Dev Tools.
Unlike the previous implementation of text autosizing which required two layout passes, the new text autosizer is "single-pass", running concurrently with layout.
The text autosizer incorporates "fingerprinting", which uses heuristics to detect similarities in page elements. This improves consistency of the output on comment forums such as Hacker News, which have historically been handled poorly by text autosizing.
The text autosizer was enabled in trunk on 29 Apr 2014 in r172846, and launched with M36 to Beta channel on 30 May and stable channel on 21 Jul. The code lives primarily in renderer/core/layout/text_autosizer.cc. Tracking bug: https://crbug.com/302005.
The primary reason to boost font sizes is that a page is wider than the device.
Chrome for Android shows the page zoomed out, so that the entire width of the page fits on the screen. But because it is zoomed out, the text is too small to read without boosting.
Note that we have measured the screen width in density-independent pixels (DIPs) to ensure consistency across devices. The ratio of physical pixels to DIPs is given by the "device pixel ratio" (window.devicePixelRatio), and is often 2 or 3 on high-density screens.
Instead of boosting the font size, we could display the page at 100% (1 CSS pixel = 1 DIP). Then the text would be legible, but the user would need to scroll horizontally to read it, which is a very unpleasant experience.
Alternatively, we could reduce the layout viewport to match the screen width of the device. But this often destroys the appearance of a page that was designed for a desktop browser. Here's http://en.wikipedia.org/ at a layout width of 360 CSS pixels:
Note the unpleasant overlap of elements in the header. Furthermore there is still horizontal scrolling because the page content overflows the initial containing block.
To avoid such results, Blink applies a default minimum width of 980 pixels to the layout viewport through the user-agent stylesheet in Source/core/css/viewportAndroid.css.
A webpage that supports narrower viewports can override this by specifying its own viewport parameters. For example, mobile-optimized or "responsive" websites often include a meta viewport tag such as
<meta name="viewport" content="width=device-width">
This results in a layout width equal to the device width, and usually disables text autosizing on the page (but see below for the exception).
Accessibility Text Scaling
The second reason for font boosting is to implement the effects of the "Text scaling" slider on the Accessibility preferences page.
The position of this slider acts as a multiplier on the effects of the text autosizer.
If the slider is > 100%, some text autosizing may be performed even on a page with width=device-width in its meta viewport tag.
If the slider is < 100%, the text autosizer applies less boosting than it normally would.
A "cluster" is a subset of the layout tree in which a particular text autosizing multiplier is applied. This multiplier is called the "cluster multiplier". A cluster is identified by a block known as its "cluster root". The text autosizer divides the page into clusters by classifying certain blocks as cluster roots when they enter layout.
Every LayoutObject "belongs to" exactly one cluster, namely the cluster associated with the LayoutObject's nearest cluster-root ancestor in the layout tree. The text autosizer "inflates" (boosts the font of) every text node using the multiplier of the cluster it belongs to.
Clusters allow inflation to be proportional to the width of the column in which the text is laid out. Wider columns are more inflated, and narrower columns are less inflated. This serves two purposes:
Suppressing Clusters
Clusters are also created to suppress autosizing in a region of the page. This is called a "suppressing cluster". Generally we prevent parts of the page from autosizing for several reasons, such as:
Computed Font Size
The computed font size of autosized text is given by:
if specifiedSize ≤ 16: computedSize = specifiedSize × clusterMultiplier if 16 ≤ specifiedSize ≤ (32 × clusterMultiplier − 16): computedSize = (specifiedSize ∕ 2) + (16 × clusterMultiplier − 8) if specifiedSize ≥ (32 × clusterMultiplier − 16): computedSize = specifiedSize |
Here, specifiedSize is the font size that would be used in the absence of autosizing based on the CSS and markup on the page, and clusterMultiplier is the multiplier of the cluster the text belongs to. The formula for determining clusterMultiplier is given in the next section. All units are in CSS pixels.
This formula can be summarized as: the cluster multiplier is fully applied up to a specified size of 16px, with a linear backoff beyond this threshold. The computed size is never less than the specified size.
The computed size is available to script via getComputedStyle(element)['font-size'], or in the computed styles section of the Elements tab in Dev Tools.
Cluster Multiplier
The cluster multiplier is generally computed as follows:
clusterMultiplier = max(1, textScalingSlider × systemFontScale × clusterWidth ∕ screenWidth) |
where:
(The fact that a meta viewport with width=device-width disables autosizing can be derived from this formula. Under default accessibility settings, textScalingSlider = systemFontScale = 1. If layout width = screenWidth, and clusterWidth ≤ layout width, then we have clusterWidth ∕ screenWidth ≤ 1, and clusterMultiplier = 1.)
The value of (textScalingSlider × systemFontScale) is passed into Blink through the accessibilityFontScaleFactor setting.
A suppressing cluster has a multiplier of 1.
Device Scale Adjustment
The device scale adjustment is an additional "fudge factor" that is applied to websites that have no explicit meta viewport or @viewport (i.e., most websites designed for desktop browsers).
When the device scale adjustment is applied, the formula for the cluster multiplier becomes:
clusterMultiplier = max(1, deviceScaleAdjustment × textScalingSlider × systemFontScale × clusterWidth ∕ screenWidth) |
The device scale adjustment ranges from 1.05 to 1.3 depending on the screen size. It is computed in chrome_content_browser_client.cc, and passed into Blink through the deviceScaleAdjustment setting.
The device scale adjustment was introduced to compensate for poor legibility on tablets caused by differences in viewing distance. A legible font size on a phone would appear smaller on a tablet despite being equal in DIPs, due to the tablet being held further from the user's eyes. For more background, see http://crbug.com/229151.
Since M32, the device scale adjustment is disabled when the page specifies explicit viewport parameters using meta viewport or @viewport. This ensures that the page author can reliably disable autosizing with width=device-width (unless the user has boosted the accessibility text scaling slider past 100%).
Alternative proposals for applying the device scale adjustment are explored in Autosizing Fudge Factor Behavior Proposals.
Each Document owns a lazily constructed instance of TextAutosizer, which is responsible for inflating the text and keeping track of the information it needs.
Inflation during layout
To avoid having multiple layout passes, inflation must occur during layout, not before or after. This is because:
Text autosizing hooks into the layout treewalk through TextAutosizer::LayoutScope, and its subclass TextAutosizer::TableLayoutScope. This allows inflation of each text node to occur after its containing block has computed its width, but before that block's children enter layout.
The simplified model is:
The text autosizer inflates a LayoutText node by writing the cluster multiplier into the text's RenderStyle using RenderStyle::setTextAutosizingMultiplier. This updates the FontDescription with the appropriate computed font size, and calls Font::update to update the underlying font object.
The cluster stack
The text autosizer keeps a stack of Cluster objects representing the clusters that have entered layout. The TextAutosizer::LayoutScope ctor looks at each block that enters layout to decide whether it is a cluster root. If it is, it creates a new Cluster object and pushes it onto the stack, where it stays until the LayoutScope destructor pops it off.
This ensures that the text autosizer can always access the "current cluster" by peeking at the top of the cluster stack.
Note that clusters only exist during layout. The Cluster objects are owned by the cluster stack, and the cluster stack is empty when layout finishes.
Updating page info
The text autosizer is notified via updatePageInfo() when any input to the cluster multiplier computation changes. These are:
The text autosizer caches the last-seen values of these inputs in the TextAutosizer::PageInfo object. If updatePageInfo() decides that the cluster multipliers are invalid as a result of one of these changes, it will mark all the text nodes as needing relayout so that the multipliers are updated.
Fingerprinting and superclusters
During style recalculation, TextAutosizer::record() computes "fingerprints" (hashes) for blocks on the page. Fingerprints are used for recognizing similarities between clusters on websites such as forums, where each post or comment may be a separate cluster. The mapping between blocks and their fingerprints is maintained by TextAutosizer::FingerprintMapper.
During layout, if there are two or more clusters whose roots share a common fingerprint, a "supercluster" is created. Superclusters enforce a uniform multiplier and uniform suppression status (e.g., if at least one cluster has enough text to be autosized, the entire supercluster is permitted to autosize).
Fingerprint source data
To compute a fingerprint for a LayoutObject, the text autosizer initializes a FingerprintSourceData struct with the corresponding element's tag name and several of its computed style properties, then uses WTF::StringHasher to compute an unsigned integer hash value from the bytes in this struct. This value is the fingerprint.
In addition to computed style values, the FingerprintSourceData struct stores the fingerprint of the parent element, recursively computed and cached by the FingerprintMapper. This means that the fingerprint incorporates styles from all the elements in the node's ancestor chain.
Fingerprint example 1. body 2. body>div,color(purple),font(italic) 3. body>div,color(green)>div,color(red) 4. body>div,color(green)>div,color(red) 5. body>div,color(purple),font(small) Blocks 3 and 4 share a fingerprint. If these blocks end up creating clusters, they will also share a supercluster. |
Below are some screenshots and timing statistics of forum websites Reddit and Hacker News rendered in Chrome for Android, showing improved consistency and performance with the new TextAutosizer.
TextAutosizer (old) | TextAutosizer (new) |
Robohornet Pro Benchmark: 5.44s | Robohornet Pro Benchmark: 4.94s |
Wikipedia ww2 desktop layout: 4013ms | Wikipedia ww2 desktop layout: 2393ms |
Reddit.com layout: 375ms | Reddit.com layout: 261ms |
Background:
Other browsers' implementations of text autosizing:
Fingerprinting: