LibCompress: Treat LZW decoding errors as end of stream

The LZW data for both GIF and TIFF images is sometimes intentionally
missing an end-of-information (EOI) code, which technically is a
decoding error, but in practive is handled gracefully by Firefox, Safari
and Chrome for GIFs and Safari for TIFFs. Let's mirror their behavior.

The included WPT test exposes the fact that trailing garbage bytes can
also result in decoding errors. We handle this in the LZW logic rather
than in the image decoding since our LZW implementation is currently
only used by GIF and TIFF decoding. The error is logged behind the
LZW_DEBUG flag.
This commit is contained in:
Jelle Raaijmakers
2026-04-29 14:27:57 +02:00
committed by Jelle Raaijmakers
parent d8ad66d95d
commit 1aeb080250
Notes: github-actions[bot] 2026-04-29 18:29:23 +00:00
7 changed files with 93 additions and 13 deletions

View File

@@ -109,7 +109,15 @@ public:
u16 const end_of_data_code = lzw_decompressor.add_control_code();
while (true) {
auto const code = TRY(lzw_decompressor.next_code());
// Some encoders omit the End-of-Information code or emit trailing padding that decodes as invalid LZW
// codes. Treat any LZW error as an implicit end of the compressed stream and use whatever was successfully
// decoded so far - this matches the behavior of Firefox, Chrome and Safari.
auto code_or_error = lzw_decompressor.next_code();
if (code_or_error.is_error()) {
dbgln_if(LZW_DEBUG, "LZW stream ended unexpectedly: {}", code_or_error.release_error());
break;
}
auto const code = code_or_error.release_value();
if (code == clear_code) {
lzw_decompressor.reset();

View File

@@ -1,21 +1,21 @@
Viewport <#document> at [0,0] [0+0+0 800 0+0+0] [0+0+0 600 0+0+0] [BFC] children: not-inline
BlockContainer <html> at [0,0] [0+0+0 800 0+0+0] [0+0+0 0 0+0+0] [BFC] children: not-inline
TableWrapper <(anonymous)> at [8,8] positioned [8+0+0 0 0+0+8] [8+0+0 0 0+0+8] [BFC] children: not-inline
Box <body> at [8,8] table-box [0+0+0 0 0+0+0] [0+0+0 0 0+0+0] [TFC] children: not-inline
Box <(anonymous)> at [8,8] table-row [0+0+0 0 0+0+0] [0+0+0 0 0+0+0] children: not-inline
BlockContainer <(anonymous)> at [8,8] table-cell [0+0+0 0 0+0+0] [0+0+0 0 0+0+0] [BFC] children: not-inline
ImageBox <img> at [8,8] [0+0+0 0 0+0+0] [0+0+0 0 0+0+0] children: not-inline
BlockContainer <(anonymous)> at [8,8] [0+0+0 0 0+0+0] [0+0+0 0 0+0+0] children: inline
TableWrapper <(anonymous)> at [8,8] positioned [8+0+0 1 0+0+8] [8+0+0 1 0+0+8] [BFC] children: not-inline
Box <body> at [8,8] table-box [0+0+0 1 0+0+0] [0+0+0 1 0+0+0] [TFC] children: not-inline
Box <(anonymous)> at [8,8] table-row [0+0+0 1 0+0+0] [0+0+0 1 0+0+0] children: not-inline
BlockContainer <(anonymous)> at [8,8] table-cell [0+0+0 1 0+0+0] [0+0+0 1 0+0+0] [BFC] children: not-inline
ImageBox <img> at [8,8] [0+0+0 1 0+0+0] [0+0+0 1 0+0+0] children: not-inline
BlockContainer <(anonymous)> at [8,9] [0+0+0 1 0+0+0] [0+0+0 0 0+0+0] children: inline
TextNode <#text> (not painted)
ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x0]
PaintableWithLines (TableWrapper(anonymous)) [8,8 0x0]
PaintableBox (Box<BODY>) [8,8 0x0]
PaintableBox (Box(anonymous)) [8,8 0x0]
PaintableWithLines (BlockContainer(anonymous)) [8,8 0x0]
ImagePaintable (ImageBox<IMG>) [8,8 0x0]
PaintableWithLines (BlockContainer(anonymous)) [8,8 0x0]
PaintableWithLines (TableWrapper(anonymous)) [8,8 1x1]
PaintableBox (Box<BODY>) [8,8 1x1]
PaintableBox (Box(anonymous)) [8,8 1x1]
PaintableWithLines (BlockContainer(anonymous)) [8,8 1x1]
ImagePaintable (ImageBox<IMG>) [8,8 1x1]
PaintableWithLines (BlockContainer(anonymous)) [8,9 1x0]
SC for Viewport<#document> [0,0 800x600] [children: 1] (z-index: auto)
SC for BlockContainer<HTML> [0,0 800x0] [children: 0] (z-index: auto)

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>gif no graphics control extension</title>
<link rel="help" href="https://www.w3.org/Graphics/GIF/spec-gif89a.txt">
<meta charset="utf-8"/>
</head>
<body>
<div style="display: inline-block; width: 400px; height: 400px; background: lime;"></div>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html class="reftest-wait">
<head>
<title>gif no graphics control extension</title>
<link rel="help" href="https://www.w3.org/Graphics/GIF/spec-gif89a.txt">
<meta charset="utf-8"/>
<link rel="match" href="../../../expected/wpt-import/gif/reset-no-gce-ref.html"/>
<script src="../common/reftest-wait.js"></script>
<script>
function doTest()
{
takeScreenshotDelayed(1000);
}
document.documentElement.addEventListener("TestRendered",doTest);
</script>
</head>
<body>
<!--
reset-no-gce.gif is a non-looping 2x2 gif with 4 colors. It has 3 frames
and 200 ms for each frame. There is a global color table with 4 colors:
0=black, 1=red, 2=green, 3=blue. The first frame is all red, the gce says
the transparent index is 0 (black), so there is no transparency. The second
frame is all green, the gce says the transparent index is green (2), so the
whole frame is transparent. The third frame is all green, there is no gce
preceeding it, so there is no transparent index.
-->
<img src="reset-no-gce.gif" style="width: 400px; height: 400px; image-rendering: pixelated;">
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

View File

@@ -0,0 +1,2 @@
GIF: loaded, width=1, height=1
TIFF: loaded, width=1, height=1

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
asyncTest(async done => {
const sources = [
{ format: "GIF", src: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" },
{ format: "TIFF", src: "data:image/tiff;base64,SUkqAAwAAACAACAgCQAAAQMAAQAAAAEAAAABAQMAAQAAAAEAAAACAQMAAQAAAAgAAAADAQMAAQAAAAUAAAAGAQMAAQAAAAEAAAARAQQAAQAAAAgAAAAWAQMAAQAAAAEAAAAXAQQAAQAAAAMAAAAcAQMAAQAAAAEAAAAAAAAA" },
];
for (const { format, src } of sources) {
await new Promise(resolve => {
const image = document.createElement("img");
image.onload = () => {
println(`${format}: loaded, width=${image.width}, height=${image.height}`);
resolve();
};
image.onerror = () => {
println(`${format}: error :^(`);
resolve();
};
image.src = src;
});
}
done();
});
</script>