Repository navigation
Evaluate optional content visibility expressions - #609
Merged
Merged
Conversation
A read was rejected as cyclic whenever it reached an object already being decoded, whatever decoder asked for it. An /OCMD that names itself in its own /VE visibility expression is read once as a membership and once as a group, which cannot recurse, yet the second read failed and the whole expression was dropped. The active set now holds (object, cycle key) pairs. A decoder decodes an object the same way every time, so only the same decoder re-entering an object can recurse forever. FromPdfObject gains cycle_key(), defaulting to the decoder's TypeId; this needs FromPdfObject: 'static, which every implementation already met. Option<T> and PageTree decode their object only through another decoder and forward that decoder's key, so reaching an object through them is still the same visit; a kid naming the page tree root remains a cycle. The ordered active path is gone. Its reference-depth limit duplicated decode_depth, which already counts every reference hop, and following a reference chain is iterative and stopped by the cycle check. Nothing read the path from errors or the active_path() accessor. ReadLimits keeps only the decode depth, CyclicReference keeps only the repeated object, and ReferenceDepthExceeded is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An /OCMD with a /VE visibility expression was always treated as visible, so content it hides was drawn. /VE now decodes into a VisibilityExpression (Group, Not, And, Or) in its own module, replacing the has_visibility_expression flag on OptionalContentMembership. When present it decides visibility in place of /OCGs and /P; an /And or /Or without operands is false. A malformed expression falls back to /OCGs and /P. depends_on also walks the expression so SetOCGState refreshes pages that depend on it. The canvas attached its optional content filter only when some group was off, but /Not expressions and the /AnyOff and /AllOff policies hide content while every group is on, so the filter is now attached for every state. The TypeScript annotation contract is regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conformance readsDocuments that Safe-PDF reads differently from the base commit, compared with PDFium. A read pdf.js corpus1009 documents compared with Improvements
840 documents of the PDFium corpus read as on Details are in the job summaries of the workflow run. Rerun a case locally with |
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.
Summary
An
/OCMDwith a/VEvisibility expression was always treated as visible. In the repro, an 8pt black frame drawn under/VE [/And 20 0 R [/Not 30 0 R] [/Not 40 0 R]](groups 30 and 40 on, so hidden) was rendered around the page images, while PDFium hides it.Optional content
/VEdecodes into a newVisibilityExpression(Group,Not,And,Or) in its own module, replacinghas_visibility_expression. When present it decides visibility in place of/OCGsand/P; a malformed expression falls back to them.depends_onwalks the expression soSetOCGStaterefreshes dependent pages./Notand the/AnyOff//AllOffpolicies hide content while every group is on, so the filter is now always attached.Reader: cycles are detected per decoder
The
/OCMDnames itself (20 0 R) inside its own/VE. The reader rejected any re-entry of an object being decoded, so this read failed and the expression was dropped. A decoder decodes an object the same way every time, so only the same decoder re-entering an object can recurse forever:(object, cycle key)pairs.FromPdfObject::cycle_key()defaults to the decoder'sTypeId(FromPdfObjectis now'static; every implementation already was).Option<T>andPageTreedecode their object only through another decoder and forward its key, so a self-referencing/SMaskand a kid naming the page tree root are still cycles.active_pathis removed: its reference-depth limit duplicateddecode_depth, which counts every reference hop, and nothing read the path.ReadLimitskeeps only the decode depth;CyclicReferencekeeps onlyrepeated;ReferenceDepthExceededis removed.Test plan
cargo test --workspace --exclude pdf-conformance --exclude pdf-graphics-skia: 1067 passed (the excluded crates need a Skia binary download that was unavailable locally)cargo clippy --workspace --all-targets -- -D warnings,cargo fmt --check,cargo xtask annotation-types --checkdocument_openA/B againstmain, 5 interleaved rounds, shipped release profile, for the per-decoder cycle key: all eight documents within −1.3%…+0.4% (noise)active_path: medians across rounds within −2.3%…+1.5% with mixed signs (noise; onewebgl.pdfround outlier at 126.7 ms, the others 107–109 ms)🤖 Generated with Claude Code