test(assets): the RLE8 rule is short of the independent decoder, in 703 of 22 620 streams
RED, 3 of 23 on assert about behaviour, 0 on import.
PM's checkpoint on 44ad845 found that four end-of-line escapes and an
end-of-bitmap carry an 8x4 frame with 32 of 32 pixels never decoded, while
Pillow -- the independent decoder this file names -- refuses the same bytes.
The sentence the last round wrote into `assets.py`, that a delta and an
end-of-line both leave pixels every decoder agrees on, is false for the
end-of-line half.
Measured here before anything is fixed, and the class is wider than the one
construction: over every opcode sequence of length 1 to 4 on a 4x3 frame
(22 620 streams), 703 are carried by this package and refused by Pillow, and
1 492 more are carried by both and drawn DIFFERENTLY. PM's recommendation on
its own -- refuse a stream that painted nothing -- leaves 512 and 1 171 of
those, so it would narrow the class a third time rather than close it.
Four new arms in CURSOR_CASES, one per clause the table could not see:
the ROW clause (a stream stopping one row early with that row complete, which
is PM's P8 mutant `height - 1` -> `height - 2`, a mutant that survived 51
tests), and three end-of-line constructions that reach the end of the frame
without closing a row. The sweep is the guard the table cannot be: a
hand-picked table holds the shapes its author imagined, and this defect has
now been closed one shape at a time twice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
23588e5ada
commit
7cf758dab7
1 changed files with 200 additions and 10 deletions
|
|
@ -28,6 +28,7 @@ from __future__ import annotations
|
|||
|
||||
import hashlib
|
||||
import io
|
||||
import itertools
|
||||
import struct
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
|
@ -612,12 +613,16 @@ def test_a_truncated_rle8_stream_is_refused_with_a_published_code() -> None:
|
|||
assert whole.media_type == "image/png"
|
||||
|
||||
|
||||
#: Eight RLE8 streams for one 8x4 frame, each naming what its terminator
|
||||
#: Twelve RLE8 streams for one 8x4 frame, each naming what its terminator
|
||||
#: leaves behind, and whether an INDEPENDENT decoder will read the file.
|
||||
#: `reaches` is the property measured below: at end-of-bitmap the cursor
|
||||
#: stands at or past the end of the last row, so every row was addressed.
|
||||
#: The values are Pillow's, measured 2026-09-19 on the eight files this
|
||||
#: module builds -- not a rule read off our own code.
|
||||
#: stands at or past the end of the last row, so every row was addressed,
|
||||
#: AND every end-of-line escape closed a row it had started.
|
||||
#: The values are Pillow's, measured 2026-09-19 on the twelve files this
|
||||
#: module builds -- not a rule read off our own code. The table is CURATED and
|
||||
#: the arms are chosen so the two readers give the same verdict; the general
|
||||
#: relation between them is one-way and is measured by the sweep below, not
|
||||
#: here.
|
||||
def _encoded(count: int, index: int) -> bytes:
|
||||
return bytes((count, index))
|
||||
|
||||
|
|
@ -657,6 +662,36 @@ CURSOR_CASES: tuple[tuple[str, bytes, bool], ...] = (
|
|||
True,
|
||||
),
|
||||
("the shipped fixture, which uses a delta", RLE8_STREAM, True),
|
||||
# One arm per CLAUSE of the cursor rule. Until 2026-09-19 the table had
|
||||
# none for the ROW clause: every short stream above stops on the last row,
|
||||
# so `y < height - 1` could be off by a row (PM's P8, `height - 2`) and
|
||||
# survive 51 tests. This arm stops one row early with that row complete,
|
||||
# which is the only shape that separates the two readings.
|
||||
(
|
||||
"the row before the last painted to its end, then end-of-bitmap",
|
||||
(_encoded(8, 1) + END_OF_LINE) * 2 + _encoded(8, 1) + END_OF_BITMAP,
|
||||
False,
|
||||
),
|
||||
# The cursor can REACH the end of the frame without a pixel being painted,
|
||||
# and the four arms below are that class. An end-of-line escape at column 0
|
||||
# does not close a row it started -- it claims a whole row nothing wrote --
|
||||
# and an independent decoder does not follow it there.
|
||||
("four end-of-line escapes and not one pixel painted", END_OF_LINE * 4 + END_OF_BITMAP, False),
|
||||
(
|
||||
"three rows painted and closed, then a bare end-of-line",
|
||||
(_encoded(8, 1) + END_OF_LINE) * 3 + END_OF_LINE + END_OF_BITMAP,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"three end-of-line escapes, then the last row painted",
|
||||
END_OF_LINE * 3 + _encoded(8, 1) + END_OF_BITMAP,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"one pixel painted, then four end-of-line escapes",
|
||||
_encoded(1, 1) + END_OF_LINE * 4 + END_OF_BITMAP,
|
||||
False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -671,12 +706,20 @@ def test_a_stream_that_stops_before_the_frame_is_refused() -> None:
|
|||
stream can say so anywhere.
|
||||
|
||||
THE LINE IS THE CURSOR, NOT THE PIXELS, and it is read off an independent
|
||||
decoder rather than chosen: a delta escape and an end-of-line escape both
|
||||
leave pixels at index 0 and every decoder agrees on them, because the
|
||||
stream stated the skip. Pixels the stream never reached have no agreed
|
||||
value at all. Measured on the eight streams above, Pillow reads the five
|
||||
whose cursor reaches the end of the frame and refuses the three whose does
|
||||
not -- including one that is short by a single pixel.
|
||||
decoder rather than chosen. A DELTA escape states its skip as a distance,
|
||||
and the pixels it passes over keep index 0 in every decoder; an
|
||||
END-OF-LINE escape states only "the rest of this row", so an end-of-line
|
||||
at column 0 claims a row nothing wrote, and the independent decoder does
|
||||
not follow it there. That is not a refinement of the previous round's
|
||||
sentence, it is a correction of it: the sentence said every decoder agreed
|
||||
on an end-of-line skip, and Pillow refuses four of the twelve streams
|
||||
above on exactly that construction.
|
||||
|
||||
Measured on the twelve streams above, Pillow reads the five whose cursor
|
||||
reaches the end of the frame through painted rows and stated deltas, and
|
||||
refuses the seven whose does not -- including one short by a single pixel,
|
||||
one stopping a whole row early, and four that move the cursor with an
|
||||
end-of-line escape that closed no row.
|
||||
|
||||
A PIXEL coverage count would be a different rule and a wrong one: it
|
||||
refuses the delta the format defines, and 0 of the 25 RLE8 BMPs
|
||||
|
|
@ -715,6 +758,153 @@ def test_the_independent_decoder_draws_the_same_line() -> None:
|
|||
assert read is reaches, f"{label}: the independent decoder disagrees with the table"
|
||||
|
||||
|
||||
def test_a_delta_may_not_carry_the_cursor_out_of_its_row() -> None:
|
||||
"""A delta states a distance, and a distance past the row end states nothing.
|
||||
|
||||
The format puts the delta's horizontal offset INSIDE the line; there is no
|
||||
defined wrap. This reader's cursor keeps `x` past the row end and paints
|
||||
nothing more on that row, while a flat decoder rolls the same offset into
|
||||
the next row and paints there -- two different pictures from one stream.
|
||||
|
||||
THIS IS THE ONE DIRECTION THIS PACKAGE ALLOWS: it refuses a file the
|
||||
independent decoder reads. Pillow carries this stream (its flat buffer
|
||||
reaches 32 bytes); we do not, because we cannot say which of the two
|
||||
pictures is the file's. The reverse direction -- carrying a file the
|
||||
independent decoder refuses -- is the defect, and the sweep below measures
|
||||
it at 0.
|
||||
|
||||
Cost measured before it was written: over every BMP reachable on this
|
||||
machine (11 441 files scanned across the four raw standard deliveries and
|
||||
the K2 reference corpus, 25 BMPs, all RLE8, all from one publisher), 0 of
|
||||
25 use a delta escape at all, and 0 of 25 use an end-of-line at column 0.
|
||||
Both clauses cost nothing measured, and the clause that WOULD have cost
|
||||
something -- refusing a run that overruns its row, which closes the last
|
||||
disagreement class -- is not here: 15 of those 25 files overrun a row, and
|
||||
refusing them would drop 15 real figures and move a pinned bundle's bytes.
|
||||
"""
|
||||
stream = _encoded(4, 1) + bytes((0, 2, 8, 3)) + END_OF_BITMAP
|
||||
with pytest.raises(ExtractionError) as caught:
|
||||
read_image(bmp_rle8(8, 4, PALETTE, stream), name="delta.bmp")
|
||||
assert caught.value.code == CODE_SAMPLES_INVALID, caught.value.code
|
||||
|
||||
# The known-positive on the same shape: a delta that stays inside its row
|
||||
# is the shipped fixture's own construction and is still carried.
|
||||
inside = _encoded(4, 1) + bytes((0, 2, 4, 3)) + END_OF_BITMAP
|
||||
assert read_image(bmp_rle8(8, 4, PALETTE, inside), name="delta.bmp").media_type == "image/png"
|
||||
|
||||
|
||||
def _overruns_a_row(stream: bytes, width: int) -> bool:
|
||||
"""Does this stream paint a run that runs off the end of its row?
|
||||
|
||||
Written here rather than imported: it is the shape the sweep below is
|
||||
allowed to disagree on, so it has to be stated independently of the reader
|
||||
it judges.
|
||||
"""
|
||||
position, x = 0, 0
|
||||
while position + 1 < len(stream):
|
||||
count, value = stream[position], stream[position + 1]
|
||||
position += 2
|
||||
if count:
|
||||
if x + count > width:
|
||||
return True
|
||||
x += count
|
||||
elif value == 0:
|
||||
x = 0
|
||||
elif value == 1:
|
||||
break
|
||||
elif value == 2:
|
||||
x += stream[position]
|
||||
position += 2
|
||||
else:
|
||||
if x + value > width:
|
||||
return True
|
||||
x += value
|
||||
position += value + (value & 1)
|
||||
return False
|
||||
|
||||
|
||||
def test_no_stream_this_reader_carries_is_one_the_independent_decoder_refuses() -> None:
|
||||
"""The class, swept, rather than the three constructions someone thought of.
|
||||
|
||||
Two rounds closed this defect one construction at a time -- a truncated
|
||||
stream, then an immediate end-of-bitmap -- and each time the class stayed
|
||||
open one step down, because a hand-picked table can only hold the shapes
|
||||
its author imagined. This enumerates every opcode sequence of length 1 to 4
|
||||
over twelve opcodes on a 4x3 frame (22 620 streams: encoded runs that fit
|
||||
and runs that do not, absolute blocks, end-of-line, five deltas) and asks
|
||||
one question of each: if THIS reader carries it, does the independent
|
||||
decoder carry it too, and draw the same picture?
|
||||
|
||||
Measured 2026-09-19, before the rule was written: 703 of 22 620 streams
|
||||
were carried here and refused by Pillow, and 1 492 more were drawn
|
||||
differently. With the end-of-line and delta clauses: 0 and 32. Every one
|
||||
of the 32 is a run or absolute block that OVERRUNS its row, which this
|
||||
reader clips at the row end (as the format says) and Pillow spills into the
|
||||
next row; refusing those costs 15 of the 25 real files, so the residual is
|
||||
stated here and not closed. The direction that matters is closed: nothing
|
||||
this package carries is a file the other decoder will not read.
|
||||
"""
|
||||
Image = pytest.importorskip("PIL.Image")
|
||||
width, height = 4, 3
|
||||
ops = (
|
||||
_encoded(1, 1),
|
||||
_encoded(2, 2),
|
||||
_encoded(4, 1),
|
||||
_encoded(5, 3),
|
||||
END_OF_LINE,
|
||||
bytes((0, 2, 0, 1)),
|
||||
bytes((0, 2, 1, 0)),
|
||||
bytes((0, 2, 2, 1)),
|
||||
bytes((0, 2, 0, 3)),
|
||||
bytes((0, 2, 4, 0)),
|
||||
bytes((0, 3, 1, 2, 3, 0)),
|
||||
bytes((0, 4, 3, 2, 1, 2)),
|
||||
)
|
||||
carried_and_refused: list[bytes] = []
|
||||
differing: list[bytes] = []
|
||||
both_carry = both_refuse = 0
|
||||
streams = 0
|
||||
for length in (1, 2, 3, 4):
|
||||
for combo in itertools.product(ops, repeat=length):
|
||||
stream = b"".join(combo) + END_OF_BITMAP
|
||||
data = bmp_rle8(width, height, PALETTE, stream)
|
||||
streams += 1
|
||||
try:
|
||||
ours = read_image(data, name="sveip.bmp").data
|
||||
mine: bytes | None = png_rgb(ours)[1]
|
||||
except ExtractionError:
|
||||
mine = None
|
||||
try:
|
||||
picture = Image.open(io.BytesIO(data))
|
||||
picture.load()
|
||||
theirs: bytes | None = picture.convert("RGB").tobytes()
|
||||
except Exception:
|
||||
theirs = None
|
||||
if mine is not None and theirs is not None:
|
||||
if mine == theirs:
|
||||
both_carry += 1
|
||||
else:
|
||||
differing.append(stream)
|
||||
elif mine is not None:
|
||||
carried_and_refused.append(stream)
|
||||
elif theirs is None:
|
||||
both_refuse += 1
|
||||
|
||||
assert streams == 22620, streams
|
||||
# Non-vacuity first: a sweep where nothing is carried proves nothing.
|
||||
assert both_carry > 1000, both_carry
|
||||
assert both_refuse > 1000, both_refuse
|
||||
assert carried_and_refused == [], (
|
||||
f"{len(carried_and_refused)} of {streams} streams are carried here and refused by the "
|
||||
f"independent decoder, e.g. {carried_and_refused[0]!r}"
|
||||
)
|
||||
unexplained = [s for s in differing if not _overruns_a_row(s, width)]
|
||||
assert unexplained == [], (
|
||||
f"{len(unexplained)} of {streams} streams are drawn differently for a reason this file "
|
||||
f"does not state, e.g. {unexplained[0]!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_the_uncompressed_path_refuses_the_same_shape() -> None:
|
||||
"""The twin this rule was aligned to, so the two BMP paths cannot drift."""
|
||||
short = bmp_24(RGB_ROWS)[:-30]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue