Skip to content

layers: the optional content, and the hidden one every verb was revealing - #45

Merged
tannevaled merged 1 commit into
mainfrom
layers-that-survive-a-rewrite
Oct 11, 2026
Merged

tannevaled merged 1 commit into
mainfrom
layers-that-survive-a-rewrite

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Doc.Layers, SetLayerVisible, AddLayer and DropLayer, with
pdfops layers over them. This closes BentoPDF's pdf-layers.

⛔⛔ And it closes a disclosure

Measured before the change, with a probe written for the purpose: a one-page
drawing with a layer switched off, run through pdfops watermark, came back
with no /OCProperties at all — the content still marked as belonging to
a layer, the layer object still reachable through the page's own resources, and
nothing left saying which layer was hidden.

So the hidden layer showed. A drawing distributed with an "internal
pricing" layer turned off revealed it to any verb at all.

before after
/OCProperties after a watermark gone kept
the hidden layer visible still off

The witness test asserts the visibility, not the presence of the layers: a
carry that kept both and lost which was off would pass a test that counted
them, and would have exactly the same consequence.

What the file says about its own layers is obeyed

  • ⛔ /BaseState OFF inverts the question — everything is hidden unless
    /ON names it. A reader that only looked at /OFF would report every layer
    of such a document as visible, and a drawing that ships with all its layers
    off but one is exactly how an alternatives document is built;
  • ⛔ /Locked is the document asking that a layer not be switched, and
    -hide on one is refused: a drawing's dimension layer is what everything
    else is positioned against;
  • /D /Order is the nesting, written back nested, and a string inside it is
    the group's label rather than a layer.

⛔ Dropping a hidden layer is refused. What was drawn on it does not go with
it: the content stays in the page and stops being optional, so the hidden
drawing becomes permanently visible — the opposite of what somebody deleting
a layer called "internal pricing" expects.

⛔ A name is not an identifier. A drawing assembled from two others often
has two "Markup" layers, so an ambiguous name is refused with the count.

A merge keeps every source's layers, which keepCatalogue would not: it
refuses to merge two catalogues because two forms whose fields are named the
same cannot honestly become one — but two drawings' layers can.

⛔ Three defects the tests found in my own code, each invisible to the one before

the ceiling bounded the recursion, not the list it was checked on entry to the walk rather than inside its loop, so one flat /Order of six thousand layers passed it once and added all six thousand. The test that lowered the ceiling to two and got six back found it
/Order was written flat every hierarchy was lost on the way out. The test that read a nested order could not see it; only the round trip could
a child landed under the wrong parent with the nesting written, a child added under "Plan" after an unrelated layer became that layer's child, because the order is positional. The insertion goes after the parent's own subtree now, and the reported list and the written file share that one rule so the report cannot be a lie

Four guards were removed as dead code a coverage gap found: an explicit test
for a group's string label, which add already refuses by type, and three
not-found returns the validation upstream makes unreachable.

Checks

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

🤖 Generated with Claude Code

…ling

Doc.Layers, SetLayerVisible, AddLayer and DropLayer, with `pdfops layers` over
them. This closes BentoPDF's pdf-layers.

⛔⛔ AND IT CLOSES A DISCLOSURE. Measured before the change, with a probe
written for the purpose: a one-page drawing with a layer switched OFF, run
through `pdfops watermark`, came back with no /OCProperties at all — the
content still marked as belonging to a layer, the layer object still reachable
through the page's own resources, and NOTHING left saying which layer was
hidden. So the hidden layer showed. A drawing distributed with an "internal
pricing" layer turned off revealed it to any verb at all. The witness test
asserts the VISIBILITY and not the presence of the layers, because a carry that
kept both and lost which was off would have the same consequence.

The layers are constructed on the way out rather than carried, which is what
lets them survive a reorder AND a merge. keepCatalogue refuses to merge two
catalogues because two forms whose fields are named the same cannot honestly
become one — but two drawings' layers can: a layer is its object, and two
layers named "Markup" are two layers.

What the file says about its own layers is obeyed: /BaseState OFF inverts the
question (hidden unless /ON names it, which is how an alternatives document is
built); /Locked is the document asking that a layer not be switched, and -hide
on one is refused; /D /Order is the nesting, and a string inside it is the
group's LABEL rather than a layer.

⛔ Dropping a HIDDEN layer is refused: what was drawn on it stays in the page
and stops being optional, so the hidden drawing becomes permanently VISIBLE.
And an ambiguous name is refused with the count, because a name is not an
identifier.

⛔ THREE DEFECTS THE TESTS FOUND IN MY OWN CODE, each invisible to the one
before it:

  - the layer ceiling was checked on ENTRY to the walk rather than inside its
    loop, so it bounded the recursion and not the list: one flat /Order of six
    thousand layers passed it once and added all six thousand. The test that
    lowered the ceiling to two and got six back is what found it.
  - /Order was written FLAT, so every hierarchy was lost on the way out. The
    test that read a nested order could not see it; only the round trip could.
  - with the nesting written, a child added under a parent landed after an
    unrelated layer and became ITS child, because the order is positional. The
    insertion goes after the parent's own subtree now, and the reported list
    and the written file share that one rule so the report cannot be a lie.

Four guards were removed as dead code a coverage gap found: an explicit test
for a group's string label, which `add` already refuses by type, and three
not-found returns the validation upstream makes unreachable.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit df4cc5f into main Oct 11, 2026
1 check passed
@tannevaled
tannevaled deleted the layers-that-survive-a-rewrite branch October 11, 2026 16:38
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