Skip to content

labels: what a viewer shows in its toolbar, and the labels every verb was losing - #44

Merged
tannevaled merged 1 commit into
mainfrom
labels-a-viewer-shows
Oct 11, 2026
Merged

tannevaled merged 1 commit into
mainfrom
labels-a-viewer-shows

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Doc.SetPageLabels and Doc.PageLabels, with pdfops labels over them: the
six styles the format has, prefixes, start values, page ranges, and numbering
that carries across the pieces of a disjoint range. This closes BentoPDF's
add-page-labels.

pdfops labels -list book.pdf
pdfops labels -rule 'pages=1-12;style=r' -rule 'pages=13-;style=D' book.pdf out.pdf

⛔ And it fixes a loss nobody had noticed

A /PageLabels tree is keyed by page index, so it cannot be carried across
a rebuild the way the other document keys are: after a select or a reverse
every label would be on the wrong page. So this package dropped it — and a
document whose front matter was numbered i, ii, iii came back from a
watermark numbered 1, 2, 3.

The labels are now asked for per page and rebuilt, which is right in both
directions:

watermark, stamp, metadata the labels stay where they were
reverse, select, merge the labels move with their pages

A mutation that removes the new write is caught by the watermark test, so
the fix is held in place by a test that fails in the old state.

Four things the format says that a reader has to be told

  • ⛔ a page-label range is contiguous. The file holds the first page of each
    run and the viewer counts forward, so a rule over 1-9,30-40 is compiled
    into two runs. pages=odd over a hundred pages is fifty runs of one;
  • ⛔ a page-label tree has no gaps. An entry labels from its own key until
    the next one, so "1, 2, then nothing" came back as 1, 2, 3, 4 until the
    stop was said out loud with an entry carrying neither style nor prefix. The
    round-trip through the file is what caught it, not a reading of the standard;
  • ⛔ /St shall be at least 1 (ISO 32000-1 table 159). A zero-based scheme
    is not expressible as a numeric style, so a start below one is refused
    rather than written out of specification. BentoPDF's own tool offers "set to
    0 for zero-based numbering"
    , and what that writes is a value the standard
    forbids;
  • ⛔⛔ the letters are not a spreadsheet's columns. Table 159: "A to Z for
    the first 26 pages, AA to ZZ for the next 26" — so 27 is AA and 28 is
    BB, where a spreadsheet's 28 is AB. Borrowing the familiar rule relabels
    every appendix past the twenty-sixth, and a mutation that does exactly that
    is caught.

Roman numerals stop at 3999 — 4000 needs a bar over the M that neither a PDF
string nor a page number has — and fall back to decimal past it, because a
label a viewer shows wrongly is worse than one in the wrong style. A numeral
is read only in its canonical spelling: "IIII" is four to a person and is
not what roman(4) writes, so reading it would build a run that renumbers the
page after it. A leading zero is not decimal for the same reason.

⛔ Four guards removed rather than tested

Each was dead code a coverage gap found, and each is now an invariant named
where it is established
:

  • a bounds check on page numbers ParseRange has already validated — it
    refuses a spec naming a page the document has not got rather than clipping it;
  • a round-trip check on letters the parse loop has already made canonical;
  • two error branches saying a second time what their nil answer already said
    (a dangling reference is the null object, per the format).

A guard no test can reach is not a guard.

Checks

100 % statement coverage with zero uncovered blocks, race-clean, go vet
and gofmt clean. Five mutations caught, including the state this package was
in before the change
.

🤖 Generated with Claude Code

… was losing

Doc.SetPageLabels and Doc.PageLabels, with `pdfops labels` over them: the six
styles the format has, prefixes, start values, page ranges, and numbering that
carries across the pieces of a disjoint range. This closes BentoPDF's
add-page-labels.

⛔ AND IT FIXES A LOSS NOBODY HAD NOTICED. A /PageLabels tree is keyed by page
INDEX, so it cannot be carried across a rebuild the way the other document keys
are: after a select or a reverse every label would be on the wrong page. So
this package dropped it — and a document whose front matter was numbered
i, ii, iii came back from a WATERMARK numbered 1, 2, 3. The labels are now
asked for per page and rebuilt, which is right in both directions, and a
mutation removing the new write is caught by the watermark test.

Four things the format says that a reader has to be told:

  - a page-label RANGE is contiguous. The file holds the first page of each run
    and the viewer counts forward, so a rule over "1-9,30-40" is compiled into
    two runs; "odd" over a hundred pages is fifty runs of one.
  - a page-label TREE has no gaps. An entry labels from its own key until the
    next, so "1, 2, then nothing" came back as 1, 2, 3, 4 until the stop was
    said out loud with an entry carrying neither style nor prefix. The
    round-trip through the file is what caught it.
  - /St SHALL BE AT LEAST 1 (ISO 32000-1 table 159). A zero-based scheme is not
    expressible as a numeric style, so a start below one is refused rather than
    written out of specification. BentoPDF's tool offers "set to 0 for
    zero-based numbering", and what that writes is a value the standard forbids.
  - ⛔ THE LETTERS ARE NOT A SPREADSHEET'S COLUMNS. Table 159: "A to Z for the
    first 26 pages, AA to ZZ for the next 26" — so 27 is AA and 28 is BB, where
    a spreadsheet's 28 is AB. Borrowing the familiar rule relabels every
    appendix past the twenty-sixth, and a mutation that does exactly that is
    caught.

Roman numerals stop at 3999 and fall back to decimal past it, because 4000
needs a bar over the M that neither a PDF string nor a page number has. A
numeral is read only in its canonical spelling: "IIII" is four to a person and
is not what roman(4) writes, so reading it would build a run that renumbers the
page after it. A leading zero is not decimal for the same reason.

The runs are compressed on the way out: three hundred decimal pages are one
entry. A label no style can explain becomes a run of one with the whole label
as a literal prefix.

⛔ Four guards were REMOVED rather than tested, each one dead code a coverage
gap found: a bounds check on page numbers ParseRange has already validated, a
round-trip check on letters the parse loop has already made canonical, and two
error branches saying a second time what their nil answer already said. A guard
no test can reach is not a guard, and the invariant is now named where it is
established.

100% statement coverage with zero uncovered blocks, race-clean, go vet and
gofmt clean. Five mutations caught, including the state this package was in
before the change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit e7ec92a into main Oct 11, 2026
1 check passed
@tannevaled
tannevaled deleted the labels-a-viewer-shows branch October 11, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant