oxideav-pdf
Pure-Rust PDF writer + reader for the oxideav framework. The writer
emits PDF 1.4+ vector documents from
VectorFrame /
Scene inputs (paths stay paths, fills
stay fills); the reader walks bytes back into a Scene, with optional
decryption for password- and certificate-protected files. Zero C
dependencies.
Part of the oxideav
framework — a pure-Rust media stack. Implemented from ISO 32000-1:2008
and ISO 32000-2:2020 (no C codec libraries linked or wrapped, no *-sys
crates).
Vector writing
The writer emits the full vector IR:
- Paths:
MoveTo(m),LineTo(l),CubicCurveTo(c),QuadCurveTo(lifted to cubic),ArcTo(flattened to cubic per SVG 1.1 Appendix F.6.5),Close(h). - Fills:
Paint::Solid(DeviceRGBsc),Paint::LinearGradient(axial shading,Pattern Type 2+Function Type 2),Paint::RadialGradient(radial shading,Function Type 3). - Strokes: width (
w), cap (J), join (j), miter limit (M), dash pattern (d). - Transforms: each
Group::transformemits onecmoperator. - Groups:
q … Qsave/restore brackets; group opacity becomes anExtGStateresource referenced via/GSx gs. - Clip paths (
W n/W* n) and fill rules (NonZero/EvenOdd). - Embedded raster: an
ImageRefwhoseVideoFrameis RGBA8 lands as a FlateDecodeImageXObject painted withDo.
let pdf = write_pdf?; // single VectorFrame
let pdf = write_pdf_from_scene?; // multi-page Scene
File structure: xref tables, streams, ObjStm, linearization
Both reader and writer support every PDF file-structure form:
- Classic
xreftable (PDF 1.0–1.4) — the writer's default, also accepted on input. - Cross-reference streams (PDF 1.5, §7.5.8) —
/Type /XRefwith/W-packed big-endian fields, Flate-compressed with/Predictor 12. Opt in via [write_pdf_from_scene_xref_stream]. Hybrid-reference files (§7.5.8.4 — classic subsection plus an/XRefStmsupplement) are merged on the read path with the spec resolution order and a newer-wins policy;/Prevand/XRefStmchains are bounded and cycle-guarded. Unknown entry types resolve to null per §7.5.8.3. - Object streams (
/Type /ObjStm, §7.5.7) — the reader resolvesCompressedentries; the writer packs every compressible indirect object into one container via [write_pdf_from_scene_object_stream]. - Linearization (Fast Web View) (§7.5.6 + Annex F) —
[
write_pdf_from_scene_linearized] emits a complete linearization parameter dictionary in the first 1024 bytes plus a hint stream with per-page offset entries. The output is also a valid plain PDF. - Incremental updates (§7.5.6) —
[
write_pdf_incremental_update] appends a new revision (changed slots/Prev); the reader follows the/Prevchain, newest-wins.
Indirect stream /Length references (§7.3.10) are resolved against the
xref table — the shape every one-pass writer produces.
The reader resolves the inheritable page attributes MediaBox,
Resources, and Rotate (§7.7.3.4) by walking the leaf page's
/Parent chain, so a document that defines them once on an intermediate
/Pages node renders at the right size, with its fonts / XObjects /
shadings in scope, and with the correct clockwise rotation on
Page::orientation (normalised to 0 / 90 / 180 / 270). The walk
is depth-bounded and cycle-guarded.
Stream filters
decode_stream recovers a stream's raw payload by applying its
/Filter (single Name or Array chain) in array order:
/FlateDecode— zlib DEFLATE; the writer's default./LZWDecode— variable-width (9–12-bit) MSB-first LZW with the/EarlyChangeparameter honoured./ASCII85Decode,/ASCIIHexDecode,/RunLengthDecode— in single + chain position, including the inline-image abbreviations./DecodeParms /Predictorpost-filter — PNG predictors (10..=15) and TIFF Predictor 2, with sub-byte/BitsPerComponenthandling.
Terminal image-codec filters (/DCTDecode, /JPXDecode,
/JBIG2Decode, /CCITTFaxDecode) are not decoded here — they route to
the dedicated image walkers that hand the opaque payload to a codec
crate.
The DEFLATE/zlib layer runs on compcol,
the workspace-wide pure-Rust compression collection.
Encryption
Password (standard security handler)
Reader and writer cover the full revision range ISO 32000 defines:
R=2 (RC4-40), R=3 (RC4-128), R=4 (AES-128 CBC or RC4-128 via CFM),
R=5 (AES-256, Adobe extension level 3), R=6 (AES-256, ISO 32000-2:2020,
Algorithm 2.B iterated hash chain + /Perms validation). Both user and
owner passwords authenticate; the empty user password is tried first.
// Read — empty user password tried automatically.
let scene = read_pdf_to_scene
.or_else?;
// Write.
use EncryptionConfig;
let cfg = aes_256_r6;
let pdf = write_pdf_from_scene_encrypted?;
A stream may opt out of per-object encryption via /Crypt /Identity
(§7.6.5) on both read and write — the classic case is searchable XMP
metadata in an encrypted file.
Public-key (certificate)
Reader and writer handle public-key-encrypted PDFs under the
adbe.pkcs7.s3 / s4 / s5 SubFilters (ISO 32000-1 §7.6.4 +
ISO 32000-2 §7.6.5):
- KTRI (key transport) —
RSAES-PKCS1-v1_5, matched byIssuerAndSerialNumberorSubjectKeyIdentifier. - KARI (key agreement) — ECDH on P-256 / P-384 / P-521, X25519, and X448, with X9.63 and HKDF KDFs and RFC 3394 AES Key Wrap.
Content algorithms RC4 / AES-128 / AES-256 CBC encode and decode;
legacy RC2-CBC and DES-EDE3-CBC decode (read-only). Long-term-cert
originators resolve through a TrustStore, with temporal-validity
lookup for multi-generation archives.
use ;
let credential = from_der?;
let scene = read_pdf_to_scene_with_certificate?;
Writer entry points: [write_pdf_from_scene_pubsec_encrypted],
[write_pdf_from_scene_pubsec_kari] (key-agreement recipients), and
[write_pdf_from_scene_pubsec_multi_cf] (per-crypt-filter permission
sets, each its own envelope).
Digital signatures
The sig module emits signed PDFs with valid /ByteRange + CMS
SignedData /Contents blobs (ISO 32000-1 §12.7.4.5 + §12.8.1 +
RFC 5652). The placeholder-fill-in pattern is implemented end-to-end; a
[Signer] trait decouples the crypto (reference
[RsaPkcs1v15Sha256Signer] / [EcdsaP256Sha256Signer] provided, or
bring your own HSM).
use ;
let signer = new;
let identity = from_signer_cert_der?;
let signed_pdf = sign_pdf_from_scene?;
[add_document_timestamp] appends an RFC 3161 Document Time-Stamp
revision (§12.8.5) via a [TsaSigner] integration seam.
Verification: pubsec::verify::verify_signature resolves the signer
cert from a pool, hashes the canonical signedAttrs re-encoding, and
verifies against signature. Hash: SHA-1 / SHA-256 / SHA-384 /
SHA-512. Signature: RSA-PKCS#1 v1.5, RSA-PSS, and ECDSA on P-256 /
P-384 / P-521; the messageDigest attribute is cross-checked against
the eContent hash, and detached (PAdES) signatures are supported.
DocumentReader::signatures() surfaces each /Sig field with its
/ByteRange, /Contents, /SubFilter, metadata, and parsed
SignedData; PdfSignature::signed_message rebuilds the hashed bytes
for an end-to-end verify.
Reader extraction surfaces
[DocumentReader::open] gives access to a family of extraction walkers:
- Text extraction —
text_extraction()emits oneTextRunperTj/TJ/'/"show with text-matrix origin, font + size, and Unicode mapping via/ToUnicodeCMap (mixed-width codespaces honoured) or simple-font encoding (WinAnsi/MacRoman//Differencesover the Adobe Glyph List, includinguniXXXX/uXXXXXXXXescapes).TJword-break gaps are recovered, and each run carries its text render mode (Tr— including the invisible OCR layer) and text rise (Ts). Consecutive shows on one line advance the text origin per §9.4.4 (tx = ((w0 − Tj/1000)·Tfs + Tc + Tw)·Th) using per-glyph/Widths(simple) or/W//DW(Type0 Identity) metrics, so runs without an explicitTd/Tmstill get distinct positions. Type 3 font widths are scaled into text space by the font's/FontMatrix(§9.6.5), not the 1/1000 Type1 convention. - Logical reading order —
read_in_logical_order()walks the/StructTreeRoottree (Tagged PDF, §14.6–14.8) and emits runs in author order, falling back to raster order when no struct tree exists. - Image XObjects —
image_xobjects()surfaces every/DCTDecodeImage XObject as a self-contained JPEG stream with dimensions, colour space, and bits-per-component. - Inline images —
inline_images()surfaces everyBI … ID … EItriplet (§8.9.7) with its filter tag. The content-stream walker also consumes inline images in place (so a binary payload no longer corrupts the surrounding shapes or aborts the parse) and reports each onParsedContent::inline_imagesas aContentInlineImagewith the CTM (unit-square → user-space placement) and active clip. - Annotations —
annotations()decodes the §12.5.6 subtype taxonomy (Text, FreeText, the markup variants, Line, Polygon, PolyLine, Ink, Caret, Popup, FileAttachment, Watermark, Redact, Sound, Movie, Screen, PrinterMark, TrapNet, 3D, …) with common Table 164 fields, plus each annotation's/APappearance summary (N/R/Dpresence + the union of appearance-state names) and its/ASselector. - Optional content / OCG layers —
optional_content()resolves group visibility from/OCProperties(§8.11), including OCMD membership and/VEvisibility expressions. - Actions —
actions()enumerates every action carrier (catalog / page / annotation / form-field/AA+/A, JavaScript name tree), following/Nextchains, with per-type payload decode. - XMP metadata —
xmp_packet()parses the document/Metadatapacket into a structuredXmpPacket(Dublin Core, XMP Basic, PDF schema, PDF/A identification). - Embedded attachments — [
read_pdf_attachments] walks the/Names → /EmbeddedFilesname tree, surfacing PDF 2.0 Associated Files (/AFRelationship).
Content-stream colour & state
The content parser honours DeviceGray / DeviceRGB / DeviceCMYK (g /
rg / k and the cs/CS + sc/scn forms, §8.6), resource colour
spaces (ICCBased via /Alternate or /N; Indexed; the CIE-based
families CalGray (§8.6.5.2), CalRGB (§8.6.5.3) and Lab (§8.6.5.4),
each decoded to CIE 1931 XYZ via its /WhitePoint / /Gamma / /Matrix
/ /Range and reduced to device RGB through the standard sRGB display
colorimetry; Separation and
DeviceN (§8.6.6.5) with Type 0 sampled / Type 2 / Type 3 / Type 4
PostScript-calculator tint transforms, §7.10 — Type 0 sampled functions
interpolate over any number of input dimensions, /Order 1 multilinear
or /Order 3 cubic-spline (a per-axis Catmull-Rom blend through the four
nearest knots, with the §7.10.2 /Size < 4 linear fallback), so a
multi-colorant DeviceN tint transform maps through its device
alternate), the gs ExtGState operator (line state
- alpha, cumulative),
Tj/TJtext shows resolved against/Resources /Font, and the marked-content operators (BMC/BDC/EMC/MP/DP, §14.6) with named-property resolution.
Form XObjects (§8.10) painted via name Do are spliced into the
Scene: the page's /Resources /XObject subdictionary is resolved, each
/Subtype /Form entry's content stream decoded and recursively parsed
against its own /Resources (including nested Form XObjects), and the
result becomes a nested Group carrying the form's /Matrix as its
transform and the /BBox rectangle as its clip — the §8.10.1
q / concat-Matrix / clip-BBox / paint / Q algorithm. Form recursion is
depth-bounded and cycle-guarded, so a self-referential appearance stream
terminates. Image XObjects stay a vector-side no-op (surfaced separately
by image_xobjects()).
Type 3 font glyphs (§9.6.5) are painted into the Scene as vector
geometry. A Type 3 font is the one simple-font family whose glyphs are
themselves content streams (/CharProcs) of PDF marking operators — no
external font program, so no glyph rasteriser is needed. On a
Tj / TJ / ' / " show under a Type 3 font, the reader resolves
each character code through /Encoding /Differences to a glyph name
(§9.6.6.1), looks the name up in /CharProcs to get its description
stream (parsed against the font's own /Resources into a Group), and
splices that group at the glyph's text-rendering matrix —
Tm ∘ [Tfs·Th 0 0 Tfs 0 Trise] ∘ /FontMatrix (§9.4.4) — advancing the
glyph origin between the bytes of a single show by each glyph's
/Widths displacement. The d0 / d1 glyph-metric operators
(Table 113) are consumed (the width comes from /Widths, the bbox is
advisory); a d1 shape-only glyph is recoloured to the current fill
colour at paint time (its own colour operators disregarded, Table 113
NOTE 2), while a d0 self-coloured glyph keeps its own colours.
Render mode 3 (invisible OCR layer) paints nothing, and a glyph absent
from /Encoding or /CharProcs is skipped. Glyph descriptions that
themselves show Type 3 text are depth-bounded.
The sh shading-paint operator (§8.7.4.5) surfaces a ContentShading
event per paint with the resolved shading dictionary, the effective CTM,
and the active clip. A clipped axial / radial sh is additionally
painted into the Scene: the active clip path is filled with the
equivalent Paint::LinearGradient / Paint::RadialGradient, so a
gradient drawn by … W n /Sh sh is visible rather than event-only. (An
unclipped sh would fill the whole page, so it stays event-only;
function-based and mesh shadings have no Paint analogue.) Type 1–3
shadings (function-based / axial /
radial, §8.7.4.5.2–4) are evaluated to geometry + sampled colour stops
on ContentShading::gradient: an axial shading carries its axis
endpoints + Extend flags + 64 RGB stops across the parametric domain; a
radial shading carries its two circles + stops; a function-based shading
carries its domain rectangle + Matrix + a 16×16 RGB sample grid of its
2-in/n-out colour function. Type 4–7 (mesh) shadings (§8.7.4.5.5–8)
are evaluated to device-space geometry on ContentShading::mesh:
free-form
(Type 4) and lattice-form (Type 5) Gouraud triangle meshes become a list
of triangles with per-vertex RGB; Coons (Type 6) and tensor-product
(Type 7) patch meshes become a list of bicubic patches with four corner
colours (Coons patches expanded to the 16-control-point tensor form via
the §8.7.4.5.8 internal-control-point equations). The bit-packed stream
body is unpacked at the dictionary's BitsPerCoordinate /
BitsPerComponent / BitsPerFlag widths, decoded through the Decode
array (§8.9.5.2), and each vertex / corner colour reduced through the
shading's ColorSpace and optional parametric /Function. Edge-flag
triangle/patch continuation (Tables 85/86) is honoured. mesh and
gradient are mutually exclusive — a Type 1–3 shading populates
gradient (and leaves mesh None), a Type 4–7 shading the reverse. A
shading's /ColorSpace may be an inline array or a named
/Resources /ColorSpace key (resolved like cs/CS).
Annotation appearance streams (§12.5.5) paint into the Scene on
top of the page content. Each /Annots annotation's applicable
appearance — the normal (/N) stream, or the /AS-selected entry when
/N is a state subdictionary (the checkbox On/Off shape) — is a
Form XObject parsed against its own /Resources and placed by the
§12.5.5 algorithm: the /BBox corners transform through /Matrix, the
enclosing upright rectangle maps onto the annotation /Rect by a
scale+translate A, and content maps through AA = Matrix × A.
Hidden / NoView-flagged annotations (§12.5.3) and Popups (§12.5.6.14)
paint nothing, and the /OC entry (§12.5.2) is honoured — an
annotation on an OFF optional-content layer (direct OCG or OCMD with
/P policy / /VE expression, §8.11) is skipped as if absent.
Annotations without a usable appearance stay event-only on the
annotations() surface.
Shading-pattern fills (/PatternType 2, §8.7.3.3) paint directly
into the Scene: a scn/SCN whose /Pattern operand names a shading
pattern becomes a Paint::LinearGradient (axial) or
Paint::RadialGradient (radial), with the shading axis / circles mapped
to device space through the pattern /Matrix composed with the CTM.
Tiling-pattern fills (/PatternType 1, §8.7.3) replicate the pattern
cell across the filled region. Each tiling pattern's cell content stream
is decoded and parsed against its own /Resources (fonts, ExtGState,
shadings, colour spaces, nested Form XObjects, even nested tiling
patterns) into a cell Group; a scn/SCN naming the pattern then
tiles that cell at integer multiples of /XStep / /YStep (§8.7.3.1)
over the painted region's bounding box, clipping each tile to the cell
/BBox and the whole tiling to the fill path. The cell lattice is
anchored to the page's default coordinate space through the pattern
/Matrix independent of any cm in force (§8.7.2 NOTE 1); the tile
count is hard-capped (4096) and a degenerate / singular pattern matrix
falls back to black. A coloured cell (/PaintType 1) paints with its
own colours; an uncoloured cell (/PaintType 2, §8.7.3.3) is a
stencil poured with the underlying colour the scn supplies before the
pattern name (c… /Pname scn, read by component count — gray / RGB /
CMYK), so the same cell shape tiles different regions in different
colours.
Interactive-form & annotation writers
- [
write_pdf_with_form] emits an/AcroFormwith Text, Checkbox, Radio, Choice, and Signature widgets (§12.7.4). Checkbox and radio-kid widgets carry two-state/AP << /N << /<on> … /Off … >> >>appearance subdictionaries (self-contained vector streams — border box + check mark, ellipse border + dot) matching their/AS, so rendering no longer depends on the PDF 2.0-deprecated/NeedAppearances. - [
write_pdf_with_annotations] emits the §12.5.6 subtype taxonomy symmetric to the reader (Text, Link, FreeText, the markup variants, Square, Circle, Ink, Line, Polygon, PolyLine, Caret, Popup, FileAttachment, Sound, Watermark, PrinterMark). Geometry-determined kinds additionally get a normal appearance stream (§12.5.5 —/AP /Nform XObject with/BBox=/Rect): Square / Circle (inscribed per §12.5.6.8, border inset per §12.5.4,/ICfill +/Cstroke), Line / Ink / Polygon / PolyLine (stroked geometry, Polygon pours/IC), and the text-markup family (Highlight quad fills; Underline / StrikeOut / Squiggly strokes at documented quad-relative positions). - [
write_pdf_with_attachments] embeds files as/EmbeddedFilestreams with/Filespecdictionaries in the/Names → /EmbeddedFilestree, optionally with/FileAttachmentannotation markers and PDF 2.0/AFRelationship. - [
write_pdf_from_scene_with_outlines] / [write_pdf_from_scene_with_xmp] add document outline and XMP packet.
Fuzzing & benchmarks
A cargo-fuzz harness lives under fuzz/ with three decode-side targets
(parse, xref, decrypt) asserting the public reader entry points
always return a Result rather than panicking, aborting, or OOMing.
The corpus is seeded with in-tree fixtures; a hard parse-depth ceiling
and cycle guards protect the resolver and parser. CI runs the suite
daily.
Three Criterion bench binaries under benches/ measure the reader hot
paths (reader_open, xref, content_stream) against writer-emitted
PDFs. examples/profile_read.rs is a reproducible profiling harness for
the bytes → Scene path.
Deferred
- Writer-side
BT … Tj … ETtext emission forNode::Text(the reader-side extraction surface is complete). The same gap defers FreeText annotation appearance streams (they need laid-out text). - Table 176 line-ending glyphs (
/LEarrows / diamonds / …) in the generated Line / PolyLine appearances. - Writer-side JPEG passthrough on
ImageRef(needs core IR support for raw codec bytes; the reader-side surface is complete). - Ed25519 / Ed448 signature dispatch in
pubsec::verify. - Transparency groups beyond per-
Group/ca+/CAopacity. - DeviceN
/AttributesNChannel custom-blending hints (/Colorants,/Process,/MixingHints); the space still renders through itsalternateSpace+tintTransform, which §8.6.6.5 permits.
Usage
[]
= "0.1"
= "0.0"
use ;
let mut p = new;
p.move_to
.line_to
.line_to
.line_to
.close;
let frame = VectorFrame ;
let pdf = write_pdf.expect;
write.unwrap;
# Ok::
License
MIT — see LICENSE.