Skip to content

The concentrated inversions are pdfimages, not our decoding — and 144 of the 173 are this repository's size matcher #13

Description

@tannevaled

[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

  1. Correct the two false claims. Small, and I have a PR for it.
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions