# Platform Bugs This is the current ledger of browser and operating-system behavior that affects Pretext. `Exact` means the linked report contains our repro or the same failure. `Related` means the report helps explain the browser behavior but does not prove our exact case. `Unfiled` means we have a local repro or compatibility requirement but no exact public issue. Tracker statuses were last checked on September 12, 2026. ## Open, exact platform bugs | Platform | Bug | Tracker status | Effect on Pretext | Current handling | |---|---|---|---|---| | Chrome on macOS | Canvas `measureText()` reports Apple Color Emoji wider than DOM text at small font sizes. The discrepancy is size-dependent and disappears at larger sizes. | [Chromium #489494015](https://issues.chromium.org/issues/489494015), open | Raw canvas widths can force premature emoji line breaks. | `prepare()` capability-detects the canvas/DOM difference once per font and subtracts it per emoji grapheme: one holding an emoji-presentation character, or an emoji character followed by U+FE0F. The correction is cached and stays out of `layout()`. On September 15, headed at DPR 2 in 16px and 24px Helvetica Neue and Arial, Chrome 153 and Firefox 155 drew `1`, `#`, `*`, `©` and `✔` followed by U+FE0F from the emoji font with the full gap, even without U+20E3 and where the glyph drew gray. U+FE0F after a letter, a space, U+3000 or a soft hyphen added no gap. | | Firefox on macOS | The same Apple Color Emoji canvas/DOM width split, with a different size curve from Chrome. Mozilla's investigation points at the bitmap font being measured at different effective device-pixel ratios. | [Mozilla #2020894](https://bugzilla.mozilla.org/show_bug.cgi?id=2020894), `UNCONFIRMED` | Raw canvas widths can force premature emoji line breaks. | The same capability-detected correction handles Firefox without a Firefox version check. | | Chrome on macOS | Canvas and DOM resolve `system-ui` to different SF Pro optical variants at some sizes. The affected size bands have moved between browser releases. | [Chromium #489579956](https://issues.chromium.org/issues/489579956), open, assigned | Canvas widths can be wrong by enough to invalidate line-count predictions. | `system-ui` remains unsupported for accuracy. Use a named font. | | Firefox on macOS | Canvas and DOM resolve `system-ui` / `-apple-system` to different physical fonts. The magnitude and direction have moved between browser releases. | [Mozilla #2020917](https://bugzilla.mozilla.org/show_bug.cgi?id=2020917), `UNCONFIRMED` | Canvas widths are too far from DOM widths for reliable line-count prediction. | `system-ui` remains unsupported for accuracy. Use a named font. | | Chrome on macOS | On one Canvas 2D context, `measureText()` of the same string depends on what that context measured earlier. Blink's per-canvas shape caches key whole texts, and the pieces split at bidi runs and spaces, by characters and direction only. One-byte text is shaped as Latin, while a punctuation-only piece cut from two-byte text resolves to the Common script. In fonts with Latin-specific punctuation forms (Amiri, Noto Naskh Arabic), whichever form was cached first decides later widths. At 16px Amiri, `)` measured 4.080px on a new context and 7.328px after `(\u0628\u2060\u0628)`. That string measured 30.960px on a new context and 27.712px after `(` or `)`. Headless DOM layout gave 4.094px and 30.969px. Installed Chrome 153.0.8010.36 and headless Chrome for Testing 147 gave the same widths; headless WebKit 26.4 was order-independent. | [Chromium #560614560](https://issues.chromium.org/issues/560614560), `Unconfirmed` | Widths of brackets and other affected punctuation beside Arabic or Hebrew can depend on what the page prepared earlier, so the same paragraph can wrap differently depending on preparation order. Reassigning the font, `clearRect()`, `reset()` and resizing do not clear the caches. Frame switches drop only entries last used after the first frame and not in the current one; the 32,768-entry limit, critical memory pressure and web-font loads also drop entries. | No workaround. Preparation keeps one measurement context and replaces it only when `` changes. A new context per preparation was rejected because it moved unrelated Amiri results in both directions, and a new context still disagrees with DOM layout for `\u0101 )` (18.720px against 15.484px). Read Chrome wrapping-suite changes in LTR Amiri rows with brackets beside Arabic as order-sensitive. | ### Retina retest: June 22, 2026 We reran the exact attached repro inputs in headed browsers on a Retina display at `devicePixelRatio = 2`: - With the capability-detected emoji correction enabled, the headed Chrome and Firefox accuracy sweeps were both `7680/7680`. Temporarily disabling only that correction reduced Chrome to `7660/7680` and Firefox to `7652/7680`, so the workaround is still necessary. Safari does not reproduce either filed canvas/DOM bug: its emoji canvas and DOM widths agree, and its `system-ui` canvas and DOM widths agreed across the project's `10-28px` scan. The public recommendation still says to avoid `system-ui` on macOS because the same application may run in Chrome or Firefox. The Markdown chat overflow in [Pretext #202](https://github.com/chenglou/pretext/issues/202) reproduces this font-resolution problem in headed Firefox 152 at DPR 2. One ordinary fragment at `14px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` measures 206.68px in OffscreenCanvas, 238.12px in DOM text, and 257.97px in an HTML canvas. ## Safari/WebKit issues and compatibility behavior | Behavior | Classification and issue links | Evidence in this project | Current handling | |---|---|---|---| | A `Range` crossing wrapped text can report an extra zero-width rectangle on the preceding line, inflating `getClientRects()` / `getBoundingClientRect()` geometry. | `Related`: [WebKit #296765](https://bugs.webkit.org/show_bug.cgi?id=296765), `NEW`, with Apple Radar `157274174`. It is the same wrapped-`Range` geometry family, but not an exact report for every URL-query or `pre-wrap` extraction failure we have seen. | Safari 26.4 still produces extractor-sensitive results around preserved whitespace, hard breaks, and some URL/query boundaries even when DOM height and span extraction are exact. | This affects diagnostics, not the runtime line breaker. Cross-check suspicious Safari `Range` results with the span extractor before changing Pretext. | | Safari needs a `1/64px` line-fit allowance instead of the `0.005px` Chromium/Gecko allowance. | `Unfiled`; [WebKit #145393](https://bugs.webkit.org/show_bug.cgi?id=145393) is useful implementation background because it documents WebKit's `LayoutUnit` as `1/64px`, and [WebKit #144990](https://bugs.webkit.org/show_bug.cgi?id=144990) is a resolved historical float-precision wrapping bug. Neither is an exact report for our current case. | On Safari 26.4, replacing `1/64px` with `0.005px` reintroduced one `18px Verdana` Arabic/Latin mismatch in the 7,680-case sweep. | Keep the Safari line-fit epsilon in the engine profile. | | Safari's `word-break: keep-all` punctuation boundary differs from Chromium and Gecko for no-space mixed CJK text. | `Exact, fixed upstream`: [WebKit #312099](https://bugs.webkit.org/show_bug.cgi?id=312099) landed as [311090@main](https://commits.webkit.org/311090@main). The broader break-all/keep-all punctuation report [WebKit #298022](https://bugs.webkit.org/show_bug.cgi?id=298022) remains open. | CSS Text says `word-break` does not affect punctuation opportunities, and [WPT `word-break-keep-all-006`](https://wpt.fyi/results/css/css-text/word-break/word-break-keep-all-006.html) tests exactly that. Safari 26.4 still fails; the recorded Safari 26.5 stable and preview runs also fail while Chrome and Firefox pass. | Keep the `webkit-spaces` keep-all pair model until the fixed WebKit build ships in Safari, then retest and remove it. | | Safari fits overlong shaped runs more accurately from measured segment prefixes than from isolated grapheme widths. | `Unfiled`; no exact WebKit issue found. | A 2,564-case quarter-pixel kerning/ligature sweep had 307 raw-height misses with prefix fitting and 1,018 with isolated grapheme sums. | Keep `preferPrefixWidthsForBreakableRuns` for Safari, capped at 96 graphemes to avoid a superlinear `prepare()` path. | | Safari measures text in an OffscreenCanvas, in the OffscreenCanvas returned by `transferControlToOffscreen()` and in a detached `` with no language, while DOM text and a connected `` follow the page language. | `Related`: [WebKit #285993](https://bugs.webkit.org/show_bug.cgi?id=285993), `NEW`. It tracks the canvas `lang` attribute, and the missing behavior is that attribute's default `"inherit"`. Our comment attaches a repro. | In installed Safari 26.5.2 with ``, three test strings measured 7.6px to 45.3px away from DOM text in an OffscreenCanvas; with `ja` or `zh-Hans`, some differed by up to 32.1px. The difference appears where a generic family, `system-ui` or glyph fallback depends on the language. | No workaround: preparation measures with an OffscreenCanvas. Prefer named fonts that cover the text when the page language changes font selection. | | `Intl.Segmenter` `containing(n)` returns the wrong segment when `n` is a segment boundary and code unit `n` starts a surrogate pair: the result also covers the preceding segment. For `' \u{1F600}'`, `containing(1)` reports index 0 and length 3, while iterating the same segments gives `' '` at 0 and `\u{1F600}` at 1. Word and sentence granularity fail the same way, whatever text comes before. `containing()` on the trail surrogate, at index 0, or before BMP clusters such as `a\u{301}`, CRLF and BMP ZWJ sequences is correct. | `Filed`: [WebKit #324036](https://bugs.webkit.org/show_bug.cgi?id=324036). `IntlSegments::containing()` (`Source/JavaScriptCore/runtime/IntlSegments.cpp` lines 74-82 at WebKit `7189f73167ef`) finds the start with `ubrk_preceding(index + 1)`; ICU moves an offset on a trail surrogate back to the lead, so that call returns the previous boundary. The code is unchanged since the original 2020 implementation, and test262's `containing()` tests never call it at that index. | Reproduced in installed Safari 26.5.2, where 17 of 29 rows of a `containing()` versus iteration matrix are wrong, the same rows as in headless Playwright WebKit 26.4, and in Bun 1.4.0 (JavaScriptCore). Installed Chrome 153, installed Firefox 155 and headless Chromium 147 return `\u{1F600}` at index 1. After a space, every supplementary code point that does not extend the space is reported as joining it, so a check built on `containing()` drops the no-break after a ZWJ (LB8a) when an emoji follows a space, which Chrome keeps. | Analysis does not call `containing()`. To decide whether a code point extends the preceding space, it reads the first iterated segment of the two-code-point window. | | During a wheel scroll on macOS, after a page removes content above the viewport and calls `scrollTo()` to keep the view in place, the next wheel event can apply to the position from before that call, so `scrollTop` reads a stale position and the content skips. | `Related`: [WebKit #262287](https://bugs.webkit.org/show_bug.cgi?id=262287), `RESOLVED FIXED` in [308215@main](https://commits.webkit.org/308215@main), whose change describes the same race between a page's scroll request and user-scroll updates. Safari 26.5.2's branch predates the fix, and Safari Technology Preview 27.0 reads no stale positions on the same page. What's left there, wheel input dropped while the page's `scrollTo()` is pending, is `Exact`: [WebKit #324310](https://bugs.webkit.org/show_bug.cgi?id=324310), `NEW`. | In installed Safari 26.5.2, a 56-line plain scroller that removes 2,000px of rows above and scrolls by the same amount read 10 to 18 stale positions per run under posted wheel bursts and trackpad-like gestures, and none when it never corrects. Chrome 153 and Firefox 155 read none. In Safari Technology Preview 27.0 the list instead ends 19 to 500px short of the wheel input per burst, about one event per correction, and nothing is lost without corrections. The Markdown chat's chunked history window (#312) skips up to 28 messages in Safari for the same reason. | #312 stays a draft. Main's chat scrolls programmatically only for anchoring on resize and for jumps, not while wheel scrolling. | The three Safari engine-profile decisions above—line-fit allowance, `keep-all` punctuation policy, and prefix fitting—were rechecked on shipping Safari 26.4. Safari Technology Preview was not installed, so they should not be described as verified against WebKit tip-of-tree. Pretext applies them wherever the user agent names WebKit (`getLayoutEngine()` in `src/measurement.ts`); iOS browsers and in-app web views were checked only through their user agents in headless Playwright WebKit, not on a device. ## Firefox/Gecko compatibility behavior | Behavior | Classification and issue links | Evidence in this project | Current handling | |---|---|---|---| | Firefox's canvas measures text at a rounded font size, while DOM text uses the requested size on a 1/64px grid. The size keeps 7 significant bits: 1/8px steps from 8px to 16px, 1/4px steps from 16px to 32px. An OffscreenCanvas rounds the CSS pixel size at any device pixel ratio; a connected `` rounds the size divided by `devicePixelRatio` and scales it back, which is what pdf.js's `#quantizeFontSize` copies. | `Unfiled`; intentional in Gecko. `QuantizeFontSize()` in `dom/canvas/CanvasRenderingContext2D.cpp` rounds sizes so the font cache doesn't fill with near-identical sizes. A Bugzilla search found no report. | In installed Firefox 155.0.1, `ctx.font = '13.33px Arial'` on an OffscreenCanvas reads back `13.375px Arial`, and every width matched canvas text at 13.375px, at DPR 1, 1.25, 1.5 and 2. Canvas and DOM differed by up to 2.57px on an 83-character string, about 0.3%, and the same 3 paragraphs changed line count at every DPR, all at 13.33px or 14.1px. Whole-pixel sizes matched DOM text within 0.02px at every DPR tested. Odd multiples of 1/8px, such as 13.375px, still measured up to 0.93px narrower than DOM text at the same size; the cause is unknown. | No workaround: rescaling widths from the rounded size would leave that residual. README recommends whole-pixel sizes. | ## Investigated, but not platform bugs - [Pretext #195](https://github.com/chenglou/pretext/issues/195): September 3 diagnostics reproduce the Shantell Sans bold wrap mismatch in Chrome 152 and Firefox 152, but whole-run Canvas and DOM measurements agree. See [FONT_DIAGNOSTICS.md](FONT_DIAGNOSTICS.md). - An element's own `lang` doesn't reach an offscreen canvas. The September 3 probe set `lang=ja` on its test element in a `lang=en` page, so generic `serif` DOM text differed from the OffscreenCanvas in Chrome and Firefox, and an HTML canvas with matching `lang` restored the DOM width. With `lang` on ``, the September 14 probes found Chrome 153's and Firefox 155's OffscreenCanvas following the page language, within 0.05px of DOM text for generic families; Safari's doesn't (see the WebKit #285993 row above). pdf.js says Firefox's OffscreenCanvas uses the operating system's language ([Mozilla #1869001](https://bugzilla.mozilla.org/show_bug.cgi?id=1869001), on Windows); an en-US Firefox 155 on Chinese-language macOS followed the page instead. See [FONT_DIAGNOSTICS.md](FONT_DIAGNOSTICS.md). - After `` changes, Chrome's OffscreenCanvas keeps measuring an unchanged font string with the fonts chosen under the previous language; a new context follows the new language. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lang) documents an off-screen canvas's inherited language as a snapshot, so we did not file it. Preparation replaces the measurement context and cached widths when the page language changes. See [FONT_DIAGNOSTICS.md](FONT_DIAGNOSTICS.md). - Chromium's Korean closing-quote behavior is still modeled by `carryCJKAfterClosingQuote`, but the minimal repro behaved the same in Chrome 149 and Firefox 148. It follows the browser's `overflow-wrap: break-word` handling of the normal unbreakable unit before emergency grapheme breaking, so we did not file it as a Chromium bug. - Safari emoji widths are wider than `font-size` at some small sizes because of Apple Color Emoji's non-linear scaling, but canvas and DOM agree. Comparing either one to `font-size` is the bug in the measurement model, not a Safari bug.