[2026-08-30 22:31:42 CEST]
The baseline's finding 3 said the inversions are concentrated and that two of
the three concentrations are "not yet explained": all 173 (samples)
inversions in fr-cerfa and nowhere else, and 28 of us-opm's 29 masks. I
chased both to a cause and measured which side is wrong.
Our pictures are right. pdfimages is the odd one, in two separate ways —
and most of the 173 are not inversions at all, they are this repository's
matcher pairing pictures that have nothing to do with each other. Nothing in
go-pdfkit/render or the codecs needs changing; what needs changing is here.
Everything below is a count over the corpus or a reading of a document I built
for the purpose. Nothing is inferred from what would explain it.
Conditions
Same as the baseline: render v0.19.0, reader v0.6.0, gfx v0.16.0,
pdfimages/pdftoppm from poppler 26.04.0, page 1 only, corpus
/Users/Shared/pdfforms. The 173 and the 28 both reproduce exactly.
1. 144 of the 173 are the size matcher, not a decoder
The 173 live in 8 documents of the 237 in fr-cerfa that draw anything on
page 1. 168 of the 173 are 2x2 pixels, 4 are 4x1 and 1 is 1x1. Not one is
larger than four pixels.
These documents set coloured text by stretching a tiny solid-colour image under
a large /SMask that carries the glyph shapes. Page 1 of cerfa_10074.pdf
draws 211 distinct 2x2 images; pdfimages -list reports the same 211
objects. All 211 have the same two stream bytes, 00 00 — every sample is
palette index 0 — so every one of them is a uniform four-pixel swatch, all
ink or all paper, and nothing else.
images.match pairs a picture of ours with the first unclaimed picture of
theirs of the same size. With 211 same-size uniform swatches on one page and
the two sides walking them in different orders, a black swatch of ours is
routinely paired with a white swatch of theirs. difference then reads 1.0 and
Judge writes Inverted: true. On cerfa_10074.pdf that produces 84
"inversions".
Matched by object identity instead — resolving each of our resource names to
its object number and pairing it with the pdfimages -list row of the same
object — the same 3449 pictures of the same 8 documents come out:
|
this repository (size and order) |
matched by object identity |
| agreeing |
2906 |
3414 |
| exact complements |
173 |
29 |
| otherwise differing |
370 |
6 |
cerfa_10074.pdf alone goes from 84 reported inversions to 1.
So 144 of the 173 are an artefact of the pairing, and the matcher is also
wrong about 508 pictures in total on these 8 documents. The doc comment on
match already says the guess "is right as often as it is wrong and is
reported either way" — it is not reported either way, and this is what it
costs.
2. The 29 that survive: pdfimages writes an /Indexed palette as black only when it is exactly black
29 of 29 of the surviving complements are the same shape:
- 29 of 29 are 2x2,
/BitsPerComponent 1, /ColorSpace [/Indexed /DeviceRGB 1 <...>]
- 29 of 29 carry no
/Decode array
- 29 of 29 have every sample bit zero, so every pixel is palette entry 0
- palette entry 0 is one of five saturated dark colours:
#174A9A (4),
#174A9B (10), #1B2B4E (7), #213A7E (4), #333333 (4)
A 661-byte document with one such image and nothing else, swept over palette
entry 0 from #000000 to #FFFFFF, says what pdfimages does:
| palette entry 0 |
pdfimages writes |
| gray 0 |
black |
| gray 1, 2, 4, 8, 16, 32, 64, 96, 127, 128, 129, 160, 200, 254, 255 |
white |
It is not a luminance threshold. pdfimages writes the pixel black only when
the palette entry is exactly (0,0,0); #111111 and #333333 come out as
paper. Poppler's own renderer does not agree with it: pdftoppm -cropbox -aa no paints the page (33, 58, 126) for the #213A7E case, and go-pdfkit reads
that picture as ink. Page 1 of cerfa_12946.pdf has 74 116 pixels of that
navy on it — the second most common colour on the page after white.
3. us-opm: pdfimages writes every stencil with the opposite polarity
The 28 are real: matched by object identity, 28 of 28 are exact
complements, none agree and none merely differ. All 28 are the same shape:
/ImageMask true, /BitsPerComponent 1, no /Decode array (27
/CCITTFaxDecode, 1 /FlateDecode).
A 644-byte document settles which side is wrong. Its 8x8 mask has every row
0xF0 — the four left bits 1, the four right bits 0 — and PDF 32000-1 8.9.6.2
says a sample of 0 marks the area that shall be painted, so the right half
must be black:
|
left half |
right half |
pdftoppm (what a person sees) |
white |
black |
pdfimages |
black |
white |
| go-pdfkit |
white |
black |
The filter is not the cause — this document has no filter at all — and neither
is /Decode, which it does not carry.
What decides it is what kind of mask it is, and that holds across the corpus
with no exceptions:
| population |
/ImageMask stencils |
/Mask stencils |
/SMask soft masks |
fr-cerfa |
13 of 13 inverted |
2 of 2 inverted |
0 of 733 inverted |
us-opm |
28 of 28 inverted |
— |
0 of 1 inverted |
4. Two published claims here are false
baseline/README.md and the doc comment on images.Result.Inverted both say:
pdfimages writes a JBIG2 mask's bitmap with the opposite polarity when that
stream is a soft mask; that is a convention, not a disagreement.
Both halves are wrong. It is not about JBIG2 — 27 of the 28 us-opm cases
are CCITT, one is Flate, and the minimal document has no filter. And it is
not soft masks — soft masks are the one kind poppler does not invert, 0
of 734 of them across these two populations. The behaviour is: pdfimages
writes a stencil with the opposite polarity to the samples, whatever filter
it arrived in.
That claim is exactly the kind that was published and disproved once before on
this subject, over /Decode arrays. It should not be left standing.
What to do
- Correct the two false claims. Small, and I have a PR for it.
- Do not pair pictures by size when the size is ambiguous. When more than
one of theirs has the size of one of ours, the pairing is a guess and the
result is not evidence, so it belongs in a column of its own beside
remapped and unmatched rather than in exact or inverted. This
repository already refuses to count what could not be compared; this is the
same rule applied to a case it currently lets through. It moves every number
in the baseline, so it is not a change to slip in beside a documentation
fix.
- Consider whether
inverted should be a column at all, now that no
instance of it in this corpus is a disagreement about a decoded picture. In
both mechanisms above the picture we produce is the one on the page.
Reproduction
Both minimal documents, and the three readings of each, come out of one script
that needs only poppler and Pillow. It is at
/Users/Shared/pdfneg/repro/polarity.py on the machine this was measured on,
and it writes:
stencil.pdf, 644 bytes, sha256 2549fd4821fa9f9cc88ba39e955b15ada3d3d7b7db4b98b8f1c1c88a0c6bb73f
indexed.pdf, 661 bytes, sha256 811903933236b85d30e2f2e843f1e970fcb3791b373a2ddcfead61ff4b84dc60
Smallest corpus documents that carry each defect:
|
path |
bytes |
sha256 |
| indexed swatch |
/Users/Shared/pdfforms/fr-cerfa/cerfa_12946.pdf |
212 649 |
1fbbac90ba93135c3460b8daf4f8b5418b4c7efe4d86f6f06fa3c5592379cc4b |
| stencil polarity |
/Users/Shared/pdfforms/us-opm/sf3103.pdf |
95 325 |
589e6e2ec85cd162c3df71e7ebe18bf9062d509daf5a1e02f544091502e23d59 |
What is not established
- I did not check the other two inversion concentrations the baseline names —
JBIG2Decode mask (199, mostly ia-medical) and (samples) mask in
ia-uscourts (34 of 51). Finding 3 predicts both are the same stencil
polarity, and that prediction is untested here.
- The 6 pictures that still differ after identity matching are not explained.
They are 390x133 and similar, not the tiny swatches, and they are a separate
question.
- Whether poppler considers either behaviour a defect is not something I
measured. Both are its extractor disagreeing with its own renderer, which is
a reason to stop treating pdfimages as the last word on a picture's
polarity, not a claim about poppler's intent.
[2026-08-30 22:31:42 CEST]
The baseline's finding 3 said the inversions are concentrated and that two of
the three concentrations are "not yet explained": all 173
(samples)inversions in
fr-cerfaand nowhere else, and 28 ofus-opm's 29 masks. Ichased both to a cause and measured which side is wrong.
Our pictures are right.
pdfimagesis the odd one, in two separate ways —and most of the 173 are not inversions at all, they are this repository's
matcher pairing pictures that have nothing to do with each other. Nothing in
go-pdfkit/renderor the codecs needs changing; what needs changing is here.Everything below is a count over the corpus or a reading of a document I built
for the purpose. Nothing is inferred from what would explain it.
Conditions
Same as the baseline:
renderv0.19.0,readerv0.6.0,gfxv0.16.0,pdfimages/pdftoppmfrom poppler 26.04.0, page 1 only, corpus/Users/Shared/pdfforms. The 173 and the 28 both reproduce exactly.1. 144 of the 173 are the size matcher, not a decoder
The 173 live in 8 documents of the 237 in
fr-cerfathat draw anything onpage 1. 168 of the 173 are 2x2 pixels, 4 are 4x1 and 1 is 1x1. Not one is
larger than four pixels.
These documents set coloured text by stretching a tiny solid-colour image under
a large
/SMaskthat carries the glyph shapes. Page 1 ofcerfa_10074.pdfdraws 211 distinct 2x2 images;
pdfimages -listreports the same 211objects. All 211 have the same two stream bytes,
00 00— every sample ispalette index 0 — so every one of them is a uniform four-pixel swatch, all
ink or all paper, and nothing else.
images.matchpairs a picture of ours with the first unclaimed picture oftheirs of the same size. With 211 same-size uniform swatches on one page and
the two sides walking them in different orders, a black swatch of ours is
routinely paired with a white swatch of theirs.
differencethen reads 1.0 andJudgewritesInverted: true. Oncerfa_10074.pdfthat produces 84"inversions".
Matched by object identity instead — resolving each of our resource names to
its object number and pairing it with the
pdfimages -listrow of the sameobject — the same 3449 pictures of the same 8 documents come out:
cerfa_10074.pdfalone goes from 84 reported inversions to 1.So 144 of the 173 are an artefact of the pairing, and the matcher is also
wrong about 508 pictures in total on these 8 documents. The doc comment on
matchalready says the guess "is right as often as it is wrong and isreported either way" — it is not reported either way, and this is what it
costs.
2. The 29 that survive:
pdfimageswrites an /Indexed palette as black only when it is exactly black29 of 29 of the surviving complements are the same shape:
/BitsPerComponent 1,/ColorSpace [/Indexed /DeviceRGB 1 <...>]/Decodearray#174A9A(4),#174A9B(10),#1B2B4E(7),#213A7E(4),#333333(4)A 661-byte document with one such image and nothing else, swept over palette
entry 0 from
#000000to#FFFFFF, says whatpdfimagesdoes:pdfimageswritesIt is not a luminance threshold.
pdfimageswrites the pixel black only whenthe palette entry is exactly (0,0,0);
#111111and#333333come out aspaper. Poppler's own renderer does not agree with it:
pdftoppm -cropbox -aa nopaints the page(33, 58, 126)for the#213A7Ecase, and go-pdfkit readsthat picture as ink. Page 1 of
cerfa_12946.pdfhas 74 116 pixels of thatnavy on it — the second most common colour on the page after white.
3.
us-opm:pdfimageswrites every stencil with the opposite polarityThe 28 are real: matched by object identity, 28 of 28 are exact
complements, none agree and none merely differ. All 28 are the same shape:
/ImageMask true,/BitsPerComponent 1, no/Decodearray (27/CCITTFaxDecode, 1/FlateDecode).A 644-byte document settles which side is wrong. Its 8x8 mask has every row
0xF0— the four left bits 1, the four right bits 0 — and PDF 32000-1 8.9.6.2says a sample of 0 marks the area that shall be painted, so the right half
must be black:
pdftoppm(what a person sees)pdfimagesThe filter is not the cause — this document has no filter at all — and neither
is
/Decode, which it does not carry.What decides it is what kind of mask it is, and that holds across the corpus
with no exceptions:
/ImageMaskstencils/Maskstencils/SMasksoft masksfr-cerfaus-opm4. Two published claims here are false
baseline/README.mdand the doc comment onimages.Result.Invertedboth say:Both halves are wrong. It is not about JBIG2 — 27 of the 28
us-opmcasesare CCITT, one is Flate, and the minimal document has no filter. And it is
not soft masks — soft masks are the one kind poppler does not invert, 0
of 734 of them across these two populations. The behaviour is:
pdfimageswrites a stencil with the opposite polarity to the samples, whatever filter
it arrived in.
That claim is exactly the kind that was published and disproved once before on
this subject, over
/Decodearrays. It should not be left standing.What to do
one of theirs has the size of one of ours, the pairing is a guess and the
result is not evidence, so it belongs in a column of its own beside
remappedandunmatchedrather than inexactorinverted. Thisrepository already refuses to count what could not be compared; this is the
same rule applied to a case it currently lets through. It moves every number
in the baseline, so it is not a change to slip in beside a documentation
fix.
invertedshould be a column at all, now that noinstance of it in this corpus is a disagreement about a decoded picture. In
both mechanisms above the picture we produce is the one on the page.
Reproduction
Both minimal documents, and the three readings of each, come out of one script
that needs only poppler and Pillow. It is at
/Users/Shared/pdfneg/repro/polarity.pyon the machine this was measured on,and it writes:
stencil.pdf, 644 bytes, sha2562549fd4821fa9f9cc88ba39e955b15ada3d3d7b7db4b98b8f1c1c88a0c6bb73findexed.pdf, 661 bytes, sha256811903933236b85d30e2f2e843f1e970fcb3791b373a2ddcfead61ff4b84dc60Smallest corpus documents that carry each defect:
/Users/Shared/pdfforms/fr-cerfa/cerfa_12946.pdf1fbbac90ba93135c3460b8daf4f8b5418b4c7efe4d86f6f06fa3c5592379cc4b/Users/Shared/pdfforms/us-opm/sf3103.pdf589e6e2ec85cd162c3df71e7ebe18bf9062d509daf5a1e02f544091502e23d59What is not established
JBIG2Decode mask(199, mostlyia-medical) and(samples) maskinia-uscourts(34 of 51). Finding 3 predicts both are the same stencilpolarity, and that prediction is untested here.
They are 390x133 and similar, not the tiny swatches, and they are a separate
question.
measured. Both are its extractor disagreeing with its own renderer, which is
a reason to stop treating
pdfimagesas the last word on a picture'spolarity, not a claim about poppler's intent.