Skip to content

Build1 publisher3 min readPublished

Raising canvas JPEG quality to 0.99 leaves red text fringed in Chromium

Chromium 149 canvas JPEGs grew 2.25x from quality 0.80 to 0.99 while red-text edge error fell only from 8.11 to 6.35, one developer measured. Firefox drops chroma subsampling at a lower setting, so the reviewer's browser decides whether the fringe shows.

The Engineer · Build desk

What happened

  • JPEG's 4:2:0 mode stores brightness for every pixel but only one colour sample per 2x2 block, so red-on-white edges smear while black-on-white text stays clean.
  • At quality 1.00 Chromium switched to 4:4:4 and edge error fell to 0.19, but the poster file reached 428 KB, 3.5 times the size of the source PNG.
  • Chromium 149 and WebKit 26.5 stay at 4:2:0 through 0.99 and flip at 0.995, because Chromium rounds quality to an integer and 0.995 becomes 100.
  • A real 1242x2688 holiday-notice template showed the same pattern: its JPEG grew 3.2 times from 0.80 to 0.99, and only 1.00 produced 4:4:4, at a size larger than the PNG.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • cost Answering a fringe complaint by raising quality to 0.99 costs about 150 KB more per poster in Chromium, and the red edges still score above the visibility line of 5 Delta E.
  • exposure Visual QA done in Firefox passes exports that Chromium and WebKit users receive fringed, because at quality 0.9 the browsers write different sampling modes.
  • decision For red-on-white posters the clean JPEG outweighs its source PNG, so the practical options are PNG, a server-side encode with explicit 4:4:4, or shipping the fringe.

The export in question is `canvas.toBlob('image/jpeg', 0.8)`, behind a save-poster button in an admin console, according to a developer's post on dev.to [1]. Raising the number to 0.95 left the pink fringe on the 22 px red line [3]. The post scores cleanliness as mean CIE76 Delta E over a 2 px band around glyph edges, and anything above 5 is commonly treated as clearly visible [6][9]. Chromium at 0.99 scored 6.35 on the synthetic poster [7]. Going there from 0.80 cut edge error by about a fifth [1] and added 150,477 bytes per file [2]. On the holiday template the same move cut it by about a third, to 4.48, just under the visibility line, in a file of 2,004,878 bytes [3][11].

Two conditions decide whether these figures transfer to another workload. The text has to differ from its background mostly in colour, because 4:2:0 keeps brightness at full resolution [5]. It also has to be small. The 22 px red line fringed [3]. On the template's roughly 40 px text the halo takes close inspection, while the small calendar characters on the same image show the dulling plainly [21].

Firefox 151 behaves differently. It is already 4:4:4 at 0.90, and a finer sweep put its threshold at 0.895, a value that also rounds to 90 [13]. At 0.9 the poster came out at 246,096 bytes from Firefox and 157,626 from Chromium [14]. Firefox's file is about 1.56 times larger [4]. It matched Pillow at quality 90 with 4:4:4 in size and decoded to identical pixels [14]. "The size gap is the subsampling mode, not a better or worse encoder," the author wrote [15].

"A teammate on Firefox would approve the export at 0.9 while Chromium users get the fringe," the author wrote [17]. The answer in the post is to inspect the output. A short helper walks the JPEG marker segments to the first baseline (0xC0) or progressive (0xC2) start-of-frame marker and reads the byte at offset 11 [20]. The high nibble is the horizontal luma factor and the low nibble the vertical one, so `1x1` means 4:4:4 and `2x2` means 4:2:0 [20]. On the three 0.9 exports it returned 2x2 for Chromium, 1x1 for Firefox and 2x2 for WebKit [18]. It handles only those two SOF types, and those covered everything toBlob produced in the author's tests [18]. I like this part most. Every threshold in the post comes from sampling factors read out of each file's SOF header [12], and the helper puts that same check into production code.

A third-party tool hit the same limit. Run in the same Chromium build, ImgIng's quality-100 setting wrote a JPG byte-identical to toBlob at 0.99, still 4:2:0, and its UI warned the file was 114% larger than the source [19]. Left on defaults, it did not pick JPEG; it detected line art and wrote a 124-colour output [19].

In my context, a campaign-poster button with red text on white, I would stop tuning the quality number. Given the 1.00 result [8], the PNG is the smaller clean file for this content. Where JPEG is a hard requirement, I'd encode server-side with 4:4:4 set explicitly, since Pillow at quality 90 with 4:4:4 reproduced Firefox's pixels exactly [14]. The measurements come from open-source builds on a 16 GB Apple M4, and the author has not verified the Firefox or WebKit behaviour on release Firefox or Safari [4][16].

What to watch

  • Whether release Firefox and Safari keep the thresholds the author measured on open-source Firefox 151 and WebKit 26.5 builds.
  • A change to how Chromium rounds toBlob quality to an integer would move its 4:4:4 switch point away from 0.995.
  • Edge-error figures for Firefox's 4:4:4 export at quality 0.9, absent from the post, would show whether a mid-range full-chroma JPEG is clean enough for red text.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories