Chromium vs WebKit Glyph Rasterization in Map Labels

Feed two engines the same style, the same camera, the same tiles and the same font file, and their labels still differ — not randomly, but by a consistent offset along every glyph edge in the frame. Teams meet this as a mysterious 2% diff that a looser threshold seems to almost fix, and the almost is the problem: the tolerance needed to absorb it is large enough to hide a dropped label. This page explains where the difference comes from, which layer of it a flag can remove, and what the remainder means for how the two engines are compared.

This is a task within Cross-Browser Baseline Matrix, under CI/CD & Visual Testing Operations. The environment pinning it assumes is built in Containerized Rendering Environments for Map Tests.

Prerequisites

Where the difference comes from

Text becomes pixels in three stages, and the engines agree on only the first.

Three stages of text rendering, and where the two engines diverge A label travels through three stages on its way to pixels. Shaping turns a string plus a font into positioned glyph outlines, and the two engines agree here because both implement the same shaping rules. Hinting adjusts those outlines onto the pixel grid, and the engines diverge because each ships a different hinting implementation and a different default hinting level. Rasterisation fills the hinted outline with coverage values, and the engines diverge again because their anti-aliasing filters differ. A caption states the consequence: the divergence is systematic rather than random, so it is a consistent offset across every label rather than scattered noise, which is why a looser tolerance cannot absorb it. The divergence is systematic, so a threshold cannot absorb it 1 · shaping string + font → positioned glyph outlines engines agree 2 · hinting outlines snapped to the pixel grid different implementation and default 3 · rasterisation outline → coverage values different anti-aliasing filter a consistent offset on every label, not scattered noise

Shaping turns a string and a font into positioned glyph outlines, applying kerning, ligatures and script rules. Both engines implement the same specifications here, and given the same font they produce the same outlines in the same places. This is why a label never lands on a different feature or in a different order between engines.

Hinting adjusts those outlines so their stems and baselines land on pixel boundaries. Each engine ships its own hinting implementation, with its own default level, and the adjustments differ by fractions of a pixel per glyph. Because the adjustment is a function of the outline and the grid, it is consistent — the same glyph at the same size gets the same treatment every time on that engine, and a different one on the other.

Rasterisation converts the hinted outline into coverage values. The engines use different anti-aliasing filters, so even identical outlines produce slightly different edge pixels.

The systematic nature of all this is what makes it resistant to thresholds. Random noise averages out and stays small; a consistent per-glyph offset applied to every label in a dense frame accumulates into a large, structured delta that is indistinguishable in magnitude from a real cartographic fault.

What configuration can and cannot remove

Disabling hinting removes the largest single contributor, and Chromium exposes flags for it:

--font-render-hinting=none
--disable-lcd-text
--disable-font-subpixel-positioning
What disabling hinting removes and what it leaves behind A stacked bar shows the measured label-region divergence between two engines before and after hinting is disabled. Before, the bar has three parts: hinting differences dominate, anti-aliasing filter differences follow, and subpixel positioning contributes the remainder. After, the hinting part is gone and the bar is roughly half as long, leaving the anti-aliasing filter and subpixel positioning, neither of which a flag can remove. A note explains that disabling hinting also changes the pixels on the reference engine, so it is a change to both baselines rather than a way of making one match the other. Disabling hinting halves the gap and changes both sides hinting on hinting off hinting AA filter subpixel AA filter subpixel what remains is irreducible — it is what the per-engine baseline exists to hold and disabling hinting moves the reference engine too, so both baselines change

Two things about this are commonly misunderstood. First, disabling hinting does not make WebKit match Chromium — it changes what Chromium draws, so both engines’ baselines move and the gap narrows rather than closing. Second, WebKit does not expose an equivalent switch, so the two engines end up at different points on the same axis rather than at the same point.

What remains after the flags is the anti-aliasing filter and subpixel positioning, neither of which is configurable. That residue is the reason the matrix has an engine axis at all.

What the per-engine baseline is left holding

Which label faults survive a per-engine baseline and which do not Two columns separate what a per-engine baseline still catches from what it necessarily cannot. It still catches a label that disappeared, a label that moved to a different feature, a font that fell back to a substitute, a halo that changed width, and a collision rule that thinned the layer. It cannot catch a one-pixel difference in glyph edge coverage, a subpixel horizontal offset, or a difference in how the two engines round a fractional layer position, because those are exactly what the separate baselines were created to absorb. A caption states the resulting rule: assert content faults per engine and never try to compare engines pixel for pixel. Per-engine baselines keep the faults and drop the noise still caught a label that disappeared a label on the wrong feature a font that fell back a halo that changed width collision thinning the layer necessarily invisible one-pixel glyph edge coverage a subpixel horizontal offset fractional layer position rounding exactly what the separate baselines absorb assert content faults per engine; never compare engines pixel for pixel

The practical rule that follows is worth stating plainly: compare each engine against its own golden, and never compare the engines against each other except as a diagnostic. A pairwise engine diff is a useful measurement when investigating — it tells you how far apart the two are and whether a change moved them closer or further — and it is not a gate, because there is no threshold at which “these two engines agree” is a meaningful assertion.

That also settles a question that comes up early: whether to run fewer engines to avoid the multiplication. The answer is that the engine axis should contain exactly the engines the product ships to and someone can name a bug in. Running an engine because it is available adds a full set of baselines, a full set of re-blesses and a full share of review load, in exchange for coverage of a rendering path nobody uses.

Verification

Confirm the procedure worked before wiring it into a blocking gate:

Troubleshooting

Symptom Likely cause Fix
The engines differ across the whole frame, not just labels Something other than text is diverging — usually the GL backend, since only Chromium accepts the software-rasteriser flags Check the fill and line region classes; a divergence there is an environment problem, not a text one
The gap between engines varies between runs A font is being resolved differently on one of them, so shaping is producing different outlines rather than hinting adjusting the same ones Verify the exact font files are present in both images; a fallback changes label metrics, not just their edges
Disabling hinting made the diff worse The baselines were blessed with hinting on, so the flag change moved the reference engine away from its own golden Re-bless both engines after the flag change; it is an environment change and needs the same treatment as any other

Frequently asked questions

Can one engine's baseline ever be reused for another?

Only for scenes containing no vector text and no vector geometry — a pure raster tile view, typically. That case is worth measuring for, because it collapses three cells into one, and the procedure is the hash-agreement audit described in Maintaining Per-Engine Baselines for Leaflet and OpenLayers. Adding a single label to a collapsed scene invalidates the collapse.

Does the same reasoning apply to Firefox?

Yes, with a third set of values. Firefox has its own text stack and its own compositor rounding, so it sits at a third point rather than near either of the others. Nothing about the analysis changes; the practical difference is that its divergence from Chromium is often larger than WebKit’s, which surprises teams who expect the two non-Chromium engines to cluster.

Should hinting be disabled at all, then?

Usually yes, for a reason unrelated to cross-engine comparison: it removes a source of within-engine variation across host font configurations, which makes each engine’s own baseline more portable between machines. Treat it as an environment-determinism measure that happens to narrow the cross-engine gap, rather than as a cross-engine fix.

How large a divergence is normal between Chromium and WebKit?

On a label-dense frame at high zoom, one to three percent of pixels changed is typical with hinting disabled and the same fonts in place. Much more than that usually means a font difference rather than a rasterisation one; much less usually means the frame does not contain enough text for the measurement to say anything.