Skip to content

feat: Add a PDF asset_io hander - #7

Merged
gpeacock merged 10 commits into
mainfrom
feature/pdf
Oct 22, 2026
Merged

gpeacock merged 10 commits into
mainfrom
feature/pdf

Conversation

@gpeacock

Copy link
Copy Markdown
Member

Follows the zero copy, near zero dependencies and secure fuzzed model of the other drivers.
This can read and write PDF files with some limitations, such as no support for encrypted PDFs.

gpeacock and others added 10 commits October 16, 2026 11:35
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Decode compact xref streams and resolve compressed objects on demand
- Lazily retrieve Flate-compressed XMP and C2PA payloads
- Enforce input, output, and cumulative decompression limits
- Support PNG predictors and narrowly scoped legacy xref termination
- Add malformed-input, decompression-limit, and large-file tests
- Document supported features and report absent metadata in inspect

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ners

This commit combines two related changes:

1. Refactor SegmentMetadata into opaque, cloneable typed attachments
   - Keep container-specific metadata types in their own handlers.
   - Preserve JPEG extended XMP and thumbnail behavior.
   - Provide safe typed access without new dependencies or payload copies.

2. Add bounded incremental PDF XMP and C2PA updates
   - Stream the original bytes unchanged and append metadata revisions.
   - Preserve unrelated catalog values and attachments.
   - Support exact-length, uncompressed payload finalization.
   - Allow unsigned AcroForms while rejecting populated signature fields,
     certification restrictions, XFA, and unsupported attachment trees.
   - Preflight edits and safely replace destination paths on success.

Add regression tests and PDF write fuzz assertions, and document supported
behavior, resource limits, and logical-removal semantics in PDF.md.
main replaced the 0.1 API (Asset, per-format *_io modules, MediaType) that
this branch was built on. Every conflict is resolved to main's side. The old
PDF reader, writer, tests and benchmark are removed from the tree because they
cannot compile against the new API; they remain in history (c296806) and are
being ported to a ContainerHandler on the new architecture.

Kept: PDF.md (design notes, to be updated), the pdf_metadata.pdf fixture, and
Error::UnsupportedFeature.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ecode hooks

Structure::attachment / StructureBuilder::attach / PlanBuilder::attach keep one
opaque value with a structure, for what a container needs later that is not
geometry (how a stream is encoded, what a write must know). A write's output
structure can carry one too.

decode_entity and encode_entity now receive the structure and the entity, so a
handler can look up how that entity is stored. SVG is updated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ports the bounded PDF reader from the 0.1 API onto ContainerHandler and Io.
A synchronous lexer works on a window of bytes and asks for a larger one when
it runs short, so the syntax code has no I/O and no async recursion; the async
layer follows the cross-reference chain (classic tables, cross-reference
streams, /Prev), resolves objects (including compressed object streams) and
finds the current catalog's /Metadata and document-level /AF C2PA store.

Streams are segments holding their payload as it is in the file. A Flate
compressed store is an entity whose bytes are the encoded ones; Structure::xmp
and Structure::store decode it through the handler's decode_entity, using the
encoding recorded in the structure's attachment.

All limits and refusals from the old reader are kept. The 0.1 read tests are
ported (43 tests over synthetic documents, including the exact-limit cases and
bounded-I/O checks) and pass unchanged in what they assert. Writing is not
ported yet.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ports the incremental writer. Parsing records what a revision needs (catalog,
EmbeddedFiles name tree, trailer, remaining parse budgets, and whether the
document can be edited at all) in the structure's attachment; planning is then a
pure function of that and the update. A write is the whole source followed by an
appended revision, so the plan is a Copy of the source and a handful of Puts.
Segments that parsing the output would merge (a superseded metadata stream is
just bytes) are merged in the plan, so the output reparses to exactly the
planned structure, and the planned structure carries the new editing context so
it can be written again without parsing.

In-place finalization of an exact-length store uses Structure::patch_exact; a new
check_patch hook lets the handler refuse (compressed streams, signed or
certified documents, wrong length) before anything is written, and patch_exact
now asks it before it complains about the length.

Behaviour that differs from the 0.1 writer: a zero-length metadata stream is not
exposed (a segment cannot be empty), the temp-file replacement helper and chunk
processing options are gone (callers write to any Write; a Sink replaces the
processor), and the form/permission/name-tree refusals are unchanged.

Tests: the 0.1 write suite is ported (16 tests: combinations, multiple
revisions, compressed catalogs, forms and signatures, exact-length patching,
exact budget limits, refusals that leave the output untouched), the shared
document builders moved into tests/pdf_support, and the conformance kit runs
over editable, blocked, compressed, cross-reference-stream, form and
multi-revision documents.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Io::read_vec panicked for a zero-length read after an earlier read_vec had taken
the window. The SVG scan offered a <metadata> inside an xpacket as the place to
write a new packet; a packet written there changes where the outer one ends, so
the output did not parse to the planned structure. Nothing inside an xpacket is
a place to write now.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Handlers now use only the core's public API (crate-root items plus the
plan, jumbf and testing modules) and the external crates they own.

- Move jumbf from containers/ into the core as asset_io::jumbf.
- Make bmff a module directory with its private heif_items helper.
- Register built-in handlers through containers::register_builtin, the
  single place the core reaches into the handlers.
- Export MAX_SEGMENT_SIZE and make Structure::entity_list public; handlers
  read Updates through its accessors instead of pub(crate) fields.
- Add tests/container_isolation.rs to enforce these boundaries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@gpeacock
gpeacock merged commit 42cb804 into main Oct 22, 2026
1 check passed
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