mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-05-08 16:12:23 +02:00
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:
committed by
Jelle Raaijmakers
parent
d8ad66d95d
commit
1aeb080250
Notes:
github-actions[bot]
2026-04-29 18:29:23 +00:00
Author: https://github.com/gmta Commit: https://github.com/LadybirdBrowser/ladybird/commit/1aeb0802503 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/9158
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
32
Tests/LibWeb/Ref/input/wpt-import/gif/reset-no-gce-1.html
Normal file
32
Tests/LibWeb/Ref/input/wpt-import/gif/reset-no-gce-1.html
Normal 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>
|
||||
BIN
Tests/LibWeb/Ref/input/wpt-import/gif/reset-no-gce.gif
Normal file
BIN
Tests/LibWeb/Ref/input/wpt-import/gif/reset-no-gce.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 B |
@@ -0,0 +1,2 @@
|
||||
GIF: loaded, width=1, height=1
|
||||
TIFF: loaded, width=1, height=1
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user