Skip to main content

oxideav_pdf/
lib.rs

1//! Pure-Rust PDF writer for the oxideav framework — round 1.
2//!
3//! Round 1 supports a single-page PDF 1.4 document built from one
4//! [`oxideav_core::vector::VectorFrame`]. The imaging-model surface
5//! emitted is the SVG / PDF intersection that `oxideav-core` already
6//! models:
7//!
8//! * **Paths** — move / line / cubic / quadratic (lifted to cubic) /
9//!   elliptic-arc (flattened to cubic) / close.
10//! * **Solid + linear/radial gradient fills** (Pattern Type 2 +
11//!   Function Type 2 / Type 3 stitching).
12//! * **Strokes** — width, cap, join, miter limit, dash array+offset.
13//! * **2D affine transforms** (`cm`).
14//! * **Groups** with optional opacity (ExtGState `/ca` + `/CA`) and
15//!   clip path (`W n`).
16//! * **Embedded raster images** as FlateDecode `Image` XObjects with
17//!   per-pixel alpha via `SMask`. RGBA8 only — JPEG passthrough,
18//!   palette images, and 16-bit depth are round 2.
19//!
20//! Text rendering, multi-page output, transparency groups beyond a
21//! per-`Group` `/ca`+`/CA` opacity, and PDF *reading* are all out of
22//! scope for round 1 — see `README.md` for the full deferred list.
23//!
24//! # Public surface
25//!
26//! ```rust
27//! use oxideav_core::{Group, VectorFrame, time::TimeBase};
28//!
29//! let frame = VectorFrame {
30//!     width: 100.0,
31//!     height: 100.0,
32//!     view_box: None,
33//!     root: Group::default(),
34//!     pts: None,
35//!     time_base: TimeBase::new(1, 1),
36//! };
37//! let bytes = oxideav_pdf::write_pdf(&frame).unwrap();
38//! assert!(bytes.starts_with(b"%PDF-1.4"));
39//! ```
40//!
41//! The crate also registers itself with [`oxideav_core::CodecRegistry`]
42//! so it can be selected via the standard codec-id lookup
43//! (`"pdf"`).
44
45pub mod acroform;
46pub mod annotations;
47pub mod arc;
48pub mod attachments;
49pub mod decrypt;
50pub mod encrypt;
51pub mod error;
52pub mod info;
53pub mod linearize;
54pub mod objects;
55pub mod operators;
56pub mod outline;
57pub mod page;
58pub mod pubsec;
59pub mod reader;
60pub mod resources;
61pub mod sig;
62pub mod writer;
63mod zlib;
64
65pub use acroform::{
66    write_pdf_with_form, FieldJustification, FormField, FormFieldCheckbox, FormFieldChoice,
67    FormFieldRadioGroup, FormFieldSignature, FormFieldText, RadioOption,
68};
69pub use annotations::{
70    write_pdf_with_annotations, Annotation, AnnotationKind as WriterAnnotationKind, CaretSymbol,
71    FixedPrintSpec, FreeTextQuadding, SoundEncoding,
72};
73pub use attachments::{
74    write_pdf_with_annotations_and_attachments, write_pdf_with_attachments, AfRelationship,
75    Attachment,
76};
77pub use error::PdfError;
78pub use outline::{LinkAnnotationSpec, LinkTarget, OutlineDestination, OutlineSpec};
79pub use pubsec::{
80    open_with_certificate, open_with_certificate_and_trust_store,
81    open_with_certificate_and_trust_store_with_permissions, open_with_certificate_with_permissions,
82    CertRef, KariRecipient, PubSecCfGroup, PubSecCredential, PubSecEncoderConfig,
83    PubSecEncryptionState, PubSecKariConfig, PubSecMatch, PubSecMultiCfConfig, PubSecRecipient,
84    PubSecSubFilter, TrustStore,
85};
86pub use reader::doc_timestamps as read_pdf_doc_timestamps;
87pub use reader::{
88    actions as read_pdf_actions, annotations as read_pdf_annotations,
89    attachments as read_pdf_attachments, extract_text as extract_pdf_text,
90    extract_text_marked as extract_pdf_text_marked, image_xobjects as read_pdf_image_xobjects,
91    inline_images as read_pdf_inline_images, links as read_pdf_links, outline as read_pdf_outline,
92    parse_linearization_dict, pdfa_signals as read_pdf_pdfa_signals,
93    read_in_logical_order as read_pdf_in_logical_order, read_pdf_to_scene,
94    read_pdf_to_scene_with_certificate, read_pdf_to_scene_with_certificate_and_trust_store,
95    read_pdf_to_scene_with_password, signatures as read_pdf_signatures,
96    signed_bytes as pdf_signed_bytes, verify_hierarchy as verify_pdf_hierarchy, ActionKind,
97    ActionTrigger, AnnotationAppearance, AnnotationKind, ColorSpace, FixedPrint, HierarchyIssue,
98    HierarchyReport, InlineImageFilter, IssueSeverity, LayoutMode, LinearizationParams,
99    MarkedTextRun, MovieActivation, OutlineNode, PdfACatalogSignals, PdfAConformance, PdfAction,
100    PdfAnnotation, PdfAttachment, PdfDocTimestamp, PdfImageXObject, PdfInlineImage, PdfLink,
101    PdfLinkTarget, PdfMarkedTextExtraction, PdfOutline, PdfSignature, PdfTextExtraction,
102    ReadingOrderText, TextMarkupVariant, TextRenderMode, TextRun, ThreeDActivation,
103    ThreeDViewSelector, XmpPacket,
104};
105pub use sig::{
106    add_document_timestamp, build_tst_info, pkcs7_wrap_signed_data, sign_pdf_from_scene,
107    wrap_tst_in_signed_data, EcdsaP256Sha256Signer, MessageImprint, MockTsaSigner,
108    RsaPkcs1v15Sha256Signer, SigWriter, Signer, SignerIdentity, SigningAlgorithm, TsaSigner,
109    OID_CT_TST_INFO,
110};
111pub use writer::{
112    write_pdf, write_pdf_from_scene, write_pdf_from_scene_encrypted,
113    write_pdf_from_scene_linearized, write_pdf_from_scene_object_stream,
114    write_pdf_from_scene_object_stream_encrypted, write_pdf_from_scene_pubsec_encrypted,
115    write_pdf_from_scene_pubsec_kari, write_pdf_from_scene_pubsec_multi_cf,
116    write_pdf_from_scene_with_outlines, write_pdf_from_scene_with_outlines_and_links,
117    write_pdf_from_scene_with_xmp, write_pdf_from_scene_xref_stream, write_pdf_incremental_update,
118};
119
120use oxideav_core::{
121    CodecCapabilities, CodecId, CodecInfo, CodecParameters, CodecRegistry, ContainerRegistry,
122    Encoder, Error, Frame, MediaType, Muxer, Packet, Result, RuntimeContext, StreamInfo, TimeBase,
123    WriteSeek,
124};
125
126/// String form of the [`oxideav_core::CodecId`] this crate registers
127/// under. Use it from container code that wants to claim PDF as an
128/// output target.
129pub const CODEC_ID_STR: &str = "pdf";
130
131// ───────────────────────── Encoder ─────────────────────────
132
133struct PdfEncoder {
134    output_params: CodecParameters,
135    pending: Option<Vec<u8>>,
136    eof: bool,
137}
138
139fn make_encoder(params: &CodecParameters) -> Result<Box<dyn Encoder>> {
140    let mut output_params = params.clone();
141    output_params.media_type = MediaType::Video;
142    output_params.codec_id = CodecId::new(CODEC_ID_STR);
143    Ok(Box::new(PdfEncoder {
144        output_params,
145        pending: None,
146        eof: false,
147    }))
148}
149
150impl Encoder for PdfEncoder {
151    fn codec_id(&self) -> &CodecId {
152        &self.output_params.codec_id
153    }
154
155    fn output_params(&self) -> &CodecParameters {
156        &self.output_params
157    }
158
159    fn send_frame(&mut self, frame: &Frame) -> Result<()> {
160        match frame {
161            Frame::Vector(v) => {
162                let bytes = write_pdf(v).map_err(Error::from)?;
163                self.pending = Some(bytes);
164                Ok(())
165            }
166            _ => Err(Error::invalid(
167                "PDF encoder: only vector frames are accepted (Frame::Vector)",
168            )),
169        }
170    }
171
172    fn receive_packet(&mut self) -> Result<Packet> {
173        if let Some(bytes) = self.pending.take() {
174            let mut pkt = Packet::new(0, TimeBase::new(1, 1), bytes);
175            pkt.flags.keyframe = true;
176            return Ok(pkt);
177        }
178        if self.eof {
179            return Err(Error::Eof);
180        }
181        Err(Error::NeedMore)
182    }
183
184    fn flush(&mut self) -> Result<()> {
185        self.eof = true;
186        Ok(())
187    }
188}
189
190// ───────────────────────── Muxer ─────────────────────────
191
192struct PdfMuxer {
193    output: Box<dyn WriteSeek>,
194    written_packet: bool,
195}
196
197fn open_muxer(output: Box<dyn WriteSeek>, _streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
198    Ok(Box::new(PdfMuxer {
199        output,
200        written_packet: false,
201    }))
202}
203
204impl Muxer for PdfMuxer {
205    fn format_name(&self) -> &str {
206        "pdf"
207    }
208
209    fn write_header(&mut self) -> Result<()> {
210        // The PDF encoder writes a complete file in one packet, so the
211        // muxer header is a no-op — the entire byte sequence (header,
212        // body, xref, trailer) lands in `write_packet`.
213        Ok(())
214    }
215
216    fn write_packet(&mut self, packet: &Packet) -> Result<()> {
217        if self.written_packet {
218            return Err(Error::invalid(
219                "PDF muxer: round-1 supports a single page; got more than one packet",
220            ));
221        }
222        use std::io::Write;
223        self.output.write_all(&packet.data)?;
224        self.written_packet = true;
225        Ok(())
226    }
227
228    fn write_trailer(&mut self) -> Result<()> {
229        // Already self-contained — nothing more to flush.
230        Ok(())
231    }
232}
233
234// ───────────────────────── Registration ─────────────────────────
235
236/// Register the PDF encoder with `codecs`.
237pub fn register_codecs(reg: &mut CodecRegistry) {
238    let caps = CodecCapabilities::video("pdf_sw")
239        .with_intra_only(true)
240        .with_lossless(true);
241    reg.register(
242        CodecInfo::new(CodecId::new(CODEC_ID_STR))
243            .capabilities(caps)
244            .encoder(make_encoder),
245    );
246}
247
248/// Register the PDF muxer with `containers`.
249pub fn register_containers(reg: &mut ContainerRegistry) {
250    reg.register_muxer("pdf", open_muxer);
251    reg.register_extension("pdf", "pdf");
252}
253
254/// Unified registration entry point — installs the PDF encoder into
255/// the codec sub-registry and the PDF muxer into the container
256/// sub-registry of the supplied [`RuntimeContext`].
257///
258/// Also wired into [`oxideav_meta::register_all`] via the
259/// [`oxideav_core::register!`] macro below.
260pub fn register(ctx: &mut RuntimeContext) {
261    register_codecs(&mut ctx.codecs);
262    register_containers(&mut ctx.containers);
263}
264
265oxideav_core::register!("pdf", register);
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn registration_adds_encoder_and_muxer() {
273        let mut ctx = RuntimeContext::new();
274        register(&mut ctx);
275        assert!(ctx.codecs.has_encoder(&CodecId::new(CODEC_ID_STR)));
276        assert!(ctx.containers.muxer_names().any(|n| n == "pdf"));
277    }
278
279    #[test]
280    fn register_via_runtime_context_installs_both_sides() {
281        let mut ctx = RuntimeContext::new();
282        register(&mut ctx);
283        let id = CodecId::new(CODEC_ID_STR);
284        assert!(
285            ctx.codecs.has_encoder(&id),
286            "PDF encoder factory not installed via RuntimeContext"
287        );
288        assert_eq!(
289            ctx.containers.container_for_extension("pdf"),
290            Some("pdf"),
291            "PDF container extension not installed via RuntimeContext"
292        );
293    }
294}