Repository navigation
reader: read and check a file's linearization (ISO 32000-1 Annex F) - #37
Merged
Merged
Conversation
Linearized PDF -- Adobe's "Fast Web View" -- is ordinary PDF rearranged so a
reader with a byte-range transport can show the first page before the rest has
arrived. Annex F describes it in eleven parts; this reads the parameter
dictionary at the front (F.3.3, Table F.1) and checks it against the file.
BIBLIOGRAPHY, opened before any code: ISO 32000-1:2008 Annex F, downloaded to
~/Library/Application Support/go-pdfkit/spec/ so the next session has it.
F.3.1 (object groups and numbering), F.3.2-F.3.6 (the parts), Table F.1 (the
dictionary), Table F.2 (the hint tables). Every rule this enforces is quoted
beside the code that enforces it.
⛔ THIS READS AND CHECKS; IT DOES NOT LINEARIZE. The checker comes first
because a writer cannot be trusted without one: "qpdf says it is linearized" is
a judgement from outside, and what a writer needs while being built is the same
judgement from inside, on the bytes it just produced.
⛔ AND IT IS NOT A yes/no. Table F.1 says of the file length that "a mismatch
indicates that the file is not linearized and shall be treated as ordinary
PDF". A file can carry a well-formed dictionary and not be linearized, because
something was appended -- which is the ordinary fate of these files, since
F.3.6 says any incremental update invalidates the hint tables. So there are
three errors and they mean different things: ErrNotLinearized (never was),
ErrLinearizationMalformed (breaks a rule of Table F.1), ErrLinearizationStale
(was, and the file changed underneath it).
WHAT IS CHECKED: the dictionary is the FIRST object of the body (F.3.3 -- not
"a dictionary somewhere near the front", because searching would find one left
by an earlier write and call the file linearized when what a reader looks at
first is something else); it lies entirely within the first 1024 bytes, the
rule that makes the format worth having; every value is DIRECT, so no
cross-reference table is needed to read it; the required entries are present
with the right types; and /L equals the real file length.
NOT checked: the hint tables themselves (F.4). That is a much larger job and a
file can pass this and still send a reader to the wrong offset. The
documentation says so rather than leaving it to be assumed.
VERIFICATION -- the positives are made by QPDF, not by me. A checker tested
only on files its own author wrote agrees with its author's reading of Annex F,
which is the one thing it must not do. qpdf is the linearizer BentoPDF itself
uses.
- 1, 2 and 7-page documents linearized by qpdf: accepted, with /N matched
against the document's own page count and /O matched against the object
number our reader gives the first page.
- the un-linearized originals: refused. Without that control every assertion
above would pass for a checker that said yes to everything.
- a linearized file with bytes appended: ErrLinearizationStale.
- sixteen malformed dictionaries, each CORRECT except for the one rule named,
so a failure says what it says.
- a second population: five real library scans from the pdfscans corpus,
linearized by qpdf and judged by `qpdf --check` beside us. 5 agree, 0
disagree, including a 68-page 13 MB document. The 7 skipped are encrypted.
⛔ A FINDING, from a branch the tests had left uncovered: Annex F's own
Example 2 writes `/Linearized 1.0` while qpdf writes `/Linearized 1`. Both are
"a number" per Table F.1 and both must be accepted. My tests used only the
integer, and chasing the uncovered real branch is what surfaced the
disagreement between the specification's example and the implementation
everyone uses.
⛔ And one unreachable branch REMOVED rather than covered: a second presence
check on /Linearized, after firstBodyDict already refuses the file without it.
The distinction it would have duplicated is a real one -- absence means "not
linearized", a wrong type means "malformed" -- so the value is passed in
instead of looked up twice.
100.0% of statements, 0 uncovered blocks of 1697.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The coverage gate failed at Linearized 96.0% while reading 100.0% here, and the cause is the thing the test file's own header warns about: every positive was built by RUNNING qpdf, the runner has no qpdf, so they all skipped. A skipped test that looks like a pass. testdata/linearized.pdf is a two-page document this package wrote and qpdf linearized, 1 367 bytes. Still an independent implementation's output -- now a deterministic one that needs nothing installed. The staleness test reads it too, since that path had no other cover. ⛔ .gitignore ignores *.pdf repo-wide and every test here builds its documents in CODE, which is the better habit and stays the default. The exception is narrow and says why: a linearized file is precisely what this package cannot produce yet. When it can, the fixture should be generated and the exception removed. Verified by running only the qpdf-free tests: Linearized back to 100.0%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
reader: read and check a file's linearization (ISO 32000-1 Annex F)
Linearized PDF -- Adobe's "Fast Web View" -- is ordinary PDF rearranged so a
reader with a byte-range transport can show the first page before the rest has
arrived. Annex F describes it in eleven parts; this reads the parameter
dictionary at the front (F.3.3, Table F.1) and checks it against the file.
BIBLIOGRAPHY, opened before any code: ISO 32000-1:2008 Annex F, downloaded to
~/Library/Application Support/go-pdfkit/spec/ so the next session has it.
F.3.1 (object groups and numbering), F.3.2-F.3.6 (the parts), Table F.1 (the
dictionary), Table F.2 (the hint tables). Every rule this enforces is quoted
beside the code that enforces it.
⛔ THIS READS AND CHECKS; IT DOES NOT LINEARIZE. The checker comes first
because a writer cannot be trusted without one: "qpdf says it is linearized" is
a judgement from outside, and what a writer needs while being built is the same
judgement from inside, on the bytes it just produced.
⛔ AND IT IS NOT A yes/no. Table F.1 says of the file length that "a mismatch
indicates that the file is not linearized and shall be treated as ordinary
PDF". A file can carry a well-formed dictionary and not be linearized, because
something was appended -- which is the ordinary fate of these files, since
F.3.6 says any incremental update invalidates the hint tables. So there are
three errors and they mean different things: ErrNotLinearized (never was),
ErrLinearizationMalformed (breaks a rule of Table F.1), ErrLinearizationStale
(was, and the file changed underneath it).
WHAT IS CHECKED: the dictionary is the FIRST object of the body (F.3.3 -- not
"a dictionary somewhere near the front", because searching would find one left
by an earlier write and call the file linearized when what a reader looks at
first is something else); it lies entirely within the first 1024 bytes, the
rule that makes the format worth having; every value is DIRECT, so no
cross-reference table is needed to read it; the required entries are present
with the right types; and /L equals the real file length.
NOT checked: the hint tables themselves (F.4). That is a much larger job and a
file can pass this and still send a reader to the wrong offset. The
documentation says so rather than leaving it to be assumed.
VERIFICATION -- the positives are made by QPDF, not by me. A checker tested
only on files its own author wrote agrees with its author's reading of Annex F,
which is the one thing it must not do. qpdf is the linearizer BentoPDF itself
uses.
against the document's own page count and /O matched against the object
number our reader gives the first page.
above would pass for a checker that said yes to everything.
so a failure says what it says.
linearized by qpdf and judged by
qpdf --checkbeside us. 5 agree, 0disagree, including a 68-page 13 MB document. The 7 skipped are encrypted.
⛔ A FINDING, from a branch the tests had left uncovered: Annex F's own
Example 2 writes
/Linearized 1.0while qpdf writes/Linearized 1. Both are"a number" per Table F.1 and both must be accepted. My tests used only the
integer, and chasing the uncovered real branch is what surfaced the
disagreement between the specification's example and the implementation
everyone uses.
⛔ And one unreachable branch REMOVED rather than covered: a second presence
check on /Linearized, after firstBodyDict already refuses the file without it.
The distinction it would have duplicated is a real one -- absence means "not
linearized", a wrong type means "malformed" -- so the value is passed in
instead of looked up twice.
100.0% of statements, 0 uncovered blocks of 1697.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com