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.