Skip to main content

skia_safe/docs/
pdf_document.rs

1pub mod pdf {
2    use std::{ffi::CString, fmt, io, marker::PhantomData, mem, ptr};
3
4    use skia_bindings::{
5        self as sb, SkPDF_AttributeList, SkPDF_DateTime, SkPDF_Metadata, SkPDF_StructureElementNode,
6    };
7
8    use crate::{
9        Canvas, Document, MILESTONE,
10        interop::{AsStr, RustWStream, SetStr},
11        prelude::*,
12        scalar,
13    };
14
15    #[repr(transparent)]
16    pub struct AttributeList<'a>(Handle<SkPDF_AttributeList>, PhantomData<&'a ()>);
17    unsafe_send_sync!(AttributeList<'_>);
18
19    impl NativeDrop for SkPDF_AttributeList {
20        fn drop(&mut self) {
21            unsafe { sb::C_SkPDF_AttributeList_destruct(self) }
22        }
23    }
24
25    impl Default for AttributeList<'_> {
26        fn default() -> Self {
27            Self(
28                Handle::from_native_c(unsafe { SkPDF_AttributeList::new() }),
29                PhantomData,
30            )
31        }
32    }
33
34    impl fmt::Debug for AttributeList<'_> {
35        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36            f.debug_struct("AttributeList").finish()
37        }
38    }
39
40    /// Attributes for nodes in the PDF tree.
41    ///
42    /// Each attribute must have an owner (e.g. "Layout", "List", "Table", etc)
43    /// and an attribute name (e.g. "BBox", "RowSpan", etc.) from PDF32000_2008 14.8.5,
44    /// and then a value of the proper type according to the spec.
45    ///
46    /// Parameters of type [`CString`] will not be copied and the pointers must remain valid until
47    /// [`crate::Document::close`] or [`crate::Document::abort`] is called.
48    /// Names are expected to be constants and are taken as [`CString`].
49    /// Parameters not taken as [`CString`] will be copied.
50    impl<'a> AttributeList<'a> {
51        pub fn append_int(
52            &mut self,
53            owner: &'a CString,
54            name: &'a CString,
55            value: i32,
56        ) -> &mut Self {
57            unsafe {
58                self.0
59                    .native_mut()
60                    .appendInt(owner.as_ptr(), name.as_ptr(), value)
61            }
62            self
63        }
64
65        pub fn append_float(
66            &mut self,
67            owner: &'a CString,
68            name: &'a CString,
69            value: f32,
70        ) -> &mut Self {
71            unsafe {
72                self.0
73                    .native_mut()
74                    .appendFloat(owner.as_ptr(), name.as_ptr(), value)
75            }
76            self
77        }
78
79        pub fn append_float_array(
80            &mut self,
81            owner: &'a CString,
82            name: &'a CString,
83            value: &[f32],
84        ) -> &mut Self {
85            unsafe {
86                sb::C_SkPDF_AttributeList_appendFloatArray(
87                    self.0.native_mut(),
88                    owner.as_ptr(),
89                    name.as_ptr(),
90                    value.as_ptr(),
91                    value.len(),
92                )
93            }
94            self
95        }
96
97        pub fn append_name(
98            &mut self,
99            owner: &'a CString,
100            name: &'a CString,
101            value: &'a CString,
102        ) -> &mut Self {
103            unsafe {
104                sb::C_SkPDF_AttributeList_appendName(
105                    self.0.native_mut(),
106                    owner.as_ptr(),
107                    name.as_ptr(),
108                    value.as_ptr(),
109                )
110            }
111            self
112        }
113
114        pub fn append_text_string(
115            &mut self,
116            owner: &'a CString,
117            name: &'a CString,
118            value: &'a CString,
119        ) -> &mut Self {
120            unsafe {
121                sb::C_SkPDF_AttributeList_appendTextString(
122                    self.0.native_mut(),
123                    owner.as_ptr(),
124                    name.as_ptr(),
125                    value.as_ptr(),
126                )
127            }
128            self
129        }
130
131        pub fn append_node_id_array(
132            &mut self,
133            owner: &'a CString,
134            name: &'a CString,
135            node_ids: &[i32],
136        ) -> &mut Self {
137            unsafe {
138                sb::C_SkPDF_AttributeList_appendNodeIdArray(
139                    self.0.native_mut(),
140                    owner.as_ptr(),
141                    name.as_ptr(),
142                    node_ids.as_ptr(),
143                    node_ids.len(),
144                )
145            }
146            self
147        }
148    }
149
150    #[repr(transparent)]
151    pub struct StructureElementNode<'a>(
152        ptr::NonNull<SkPDF_StructureElementNode>,
153        PhantomData<&'a ()>,
154    );
155
156    impl NativeAccess for StructureElementNode<'_> {
157        type Native = SkPDF_StructureElementNode;
158
159        fn native(&self) -> &SkPDF_StructureElementNode {
160            unsafe { self.0.as_ref() }
161        }
162        fn native_mut(&mut self) -> &mut SkPDF_StructureElementNode {
163            unsafe { self.0.as_mut() }
164        }
165    }
166
167    impl Drop for StructureElementNode<'_> {
168        fn drop(&mut self) {
169            unsafe { sb::C_SkPDF_StructureElementNode_delete(self.native_mut()) }
170        }
171    }
172
173    impl Default for StructureElementNode<'_> {
174        fn default() -> Self {
175            Self(
176                ptr::NonNull::new(unsafe { sb::C_SkPDF_StructureElementNode_new() }).unwrap(),
177                PhantomData,
178            )
179        }
180    }
181
182    impl fmt::Debug for StructureElementNode<'_> {
183        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184            f.debug_struct("StructureElementNode")
185                .field("type_string", &self.type_string())
186                .field("child_vector", &self.child_vector())
187                .field("node_id", &self.node_id())
188                .field("attributes", &self.attributes())
189                .field("alt", &self.alt())
190                .field("lang", &self.lang())
191                .finish()
192        }
193    }
194
195    /// A node in a PDF structure tree, giving a semantic representation
196    /// of the content.  Each node ID is associated with content
197    /// by passing the [`crate::Canvas`] and node ID to [`set_node_id()`] when drawing.
198    /// NodeIDs should be unique within each tree.
199    impl<'a> StructureElementNode<'a> {
200        pub fn new(type_string: impl AsRef<str>) -> Self {
201            let mut node = Self::default();
202            node.set_type_string(type_string);
203            node
204        }
205
206        pub fn set_type_string(&mut self, type_string: impl AsRef<str>) -> &mut Self {
207            self.native_mut().fTypeString.set_str(type_string);
208            self
209        }
210
211        pub fn type_string(&self) -> &str {
212            self.native().fTypeString.as_str()
213        }
214
215        pub fn set_child_vector(
216            &mut self,
217            mut child_vector: Vec<StructureElementNode>,
218        ) -> &mut Self {
219            // strategy is to move them out by setting them to nullptr (drop() will handle a nullptr on the rust side)
220            unsafe {
221                sb::C_SkPDF_StructureElementNode_setChildVector(
222                    self.native_mut(),
223                    child_vector.as_mut_ptr() as _,
224                    child_vector.len(),
225                )
226            }
227            self
228        }
229
230        pub fn append_child(&mut self, node: StructureElementNode) -> &mut Self {
231            unsafe {
232                sb::C_SkPDF_StructElementNode_appendChild(self.native_mut(), node.0.as_ptr());
233            }
234            mem::forget(node);
235            self
236        }
237
238        pub fn child_vector(&self) -> &[StructureElementNode] {
239            let mut ptr = ptr::null();
240            unsafe {
241                let len = sb::C_SkPDF_StructureElementNode_getChildVector(self.native(), &mut ptr);
242                safer::from_raw_parts(ptr as _, len)
243            }
244        }
245
246        pub fn set_node_id(&mut self, node_id: i32) -> &mut Self {
247            self.native_mut().fNodeId = node_id;
248            self
249        }
250
251        pub fn node_id(&self) -> i32 {
252            self.native().fNodeId
253        }
254
255        pub fn attributes(&self) -> &AttributeList<'a> {
256            unsafe { transmute_ref(Handle::from_native_ref(&self.native().fAttributes)) }
257        }
258
259        pub fn attributes_mut(&mut self) -> &mut AttributeList<'a> {
260            unsafe {
261                transmute_ref_mut(Handle::from_native_ref_mut(
262                    &mut self.native_mut().fAttributes,
263                ))
264            }
265        }
266
267        pub fn set_alt(&mut self, alt: impl AsRef<str>) -> &mut Self {
268            self.native_mut().fAlt.set_str(alt);
269            self
270        }
271
272        pub fn alt(&self) -> &str {
273            self.native().fAlt.as_str()
274        }
275
276        pub fn set_lang(&mut self, lang: impl AsRef<str>) -> &mut Self {
277            self.native_mut().fLang.set_str(lang);
278            self
279        }
280
281        pub fn lang(&self) -> &str {
282            self.native().fLang.as_str()
283        }
284    }
285
286    #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
287    #[repr(C)]
288    pub struct DateTime {
289        /// The number of minutes that this is ahead of or behind UTC.
290        pub time_zone_minutes: i16,
291        /// e.g. 2005
292        pub year: u16,
293        /// 1..12
294        pub month: u8,
295        /// 0..6, 0==Sunday
296        pub day_of_week: u8,
297        /// 1..31
298        pub day: u8,
299        /// 0..23
300        pub hour: u8,
301        /// 0..59
302        pub minute: u8,
303        /// 0..59
304        pub second: u8,
305    }
306
307    native_transmutable!(SkPDF_DateTime, DateTime);
308
309    /// Optional metadata to be passed into the PDF factory function.
310    #[derive(Debug)]
311    pub struct Metadata<'a> {
312        /// The document's title.
313        pub title: String,
314        /// The name of the person who created the document.
315        pub author: String,
316        /// The subject of the document.
317        pub subject: String,
318        /// Keywords associated with the document. Commas may be used to delineate keywords within
319        /// the string.
320        pub keywords: String,
321        /// If the document was converted to PDF from another format, the name of the conforming
322        /// product that created the original document from which it was converted.
323        pub creator: String,
324        /// The product that is converting this document to PDF.
325        pub producer: String,
326        /// The date and time the document was created. The zero default value represents an
327        /// unknown/unset time.
328        pub creation: Option<DateTime>,
329        /// The date and time the document was most recently modified. The zero default value
330        /// represents an unknown/unset time.
331        pub modified: Option<DateTime>,
332        /// The natural language of the text in the PDF. If `lang` is empty, the root
333        /// [`StructureElementNode`]'s `lang` will be used (if not empty). Text not in this
334        /// language should be marked with [`StructureElementNode`]'s `lang`.
335        pub lang: String,
336        /// The DPI (pixels-per-inch) at which features without native PDF support will be
337        /// rasterized (e.g. draw image with perspective, draw text with perspective, ...) A larger
338        /// DPI would create a PDF that reflects the original intent with better fidelity, but it
339        /// can make for larger PDF files too, which would use more memory while rendering, and it
340        /// would be slower to be processed or sent online or to printer.
341        pub raster_dpi: Option<scalar>,
342        /// If `true`, include XMP metadata, a document UUID, and sRGB output intent information.
343        /// This adds length to the document and makes it non-reproducible, but are necessary
344        /// features for PDF/A-2b conformance.
345        pub pdf_a: bool,
346        /// Encoding quality controls the trade-off between size and quality. By default this is
347        /// set to 101 percent, which corresponds to lossless encoding. If this value is set to a
348        /// value <= 100, and the image is opaque, it will be encoded (using JPEG) with that
349        /// quality setting.
350        pub encoding_quality: Option<i32>,
351
352        /// An optional tree of structured document tags that provide a semantic representation of
353        /// the content. The caller should retain ownership.
354        pub structure_element_tree_root: Option<StructureElementNode<'a>>,
355
356        pub outline: Outline,
357
358        /// PDF streams may be compressed to save space. Use this to specify the desired
359        /// compression vs time tradeoff.
360        pub compression_level: CompressionLevel,
361    }
362
363    impl Default for Metadata<'_> {
364        fn default() -> Self {
365            Self {
366                title: Default::default(),
367                author: Default::default(),
368                subject: Default::default(),
369                keywords: Default::default(),
370                creator: Default::default(),
371                producer: format!("Skia/PDF m{MILESTONE}"),
372                creation: Default::default(),
373                modified: Default::default(),
374                lang: Default::default(),
375                raster_dpi: Default::default(),
376                pdf_a: Default::default(),
377                encoding_quality: Default::default(),
378                structure_element_tree_root: None,
379                outline: Outline::None,
380                compression_level: Default::default(),
381            }
382        }
383    }
384
385    pub type Outline = skia_bindings::SkPDF_Metadata_Outline;
386    variant_name!(Outline::StructureElements);
387
388    pub type CompressionLevel = skia_bindings::SkPDF_Metadata_CompressionLevel;
389    variant_name!(CompressionLevel::HighButSlow);
390
391    /// Create a PDF-backed document, writing the results into a writer.
392    ///
393    /// PDF pages are sized in point units. 1 pt == 1/72 inch == 127/360 mm.
394    ///
395    /// - `writer` - A PDF document will be written to this writer. The document may write
396    ///   to the writer at anytime during its lifetime, until either [`crate::Document::close`] is
397    ///   called or the document is deleted.
398    /// - `metadata` - a PDF metadata object. Some fields may be left empty.
399    ///
400    /// Returns `None` if there is an error, otherwise a newly created PDF-backed [`Document`].
401    pub fn new_document<'a>(
402        writer: &'a mut impl io::Write,
403        // We need to make the metadata alive as long as the document, because of `structure_element_tree_root`.
404        metadata: Option<&'a Metadata<'a>>,
405    ) -> Document<'a> {
406        let mut md = InternalMetadata::default();
407        if let Some(metadata) = metadata {
408            let internal = md.native_mut();
409            internal.fTitle.set_str(&metadata.title);
410            internal.fAuthor.set_str(&metadata.author);
411            internal.fSubject.set_str(&metadata.subject);
412            internal.fKeywords.set_str(&metadata.keywords);
413            internal.fCreator.set_str(&metadata.creator);
414            internal.fProducer.set_str(&metadata.producer);
415            if let Some(creation) = metadata.creation {
416                internal.fCreation = creation.into_native();
417            }
418            if let Some(modified) = metadata.modified {
419                internal.fModified = modified.into_native();
420            }
421            internal.fLang.set_str(&metadata.lang);
422            if let Some(raster_dpi) = metadata.raster_dpi {
423                internal.fRasterDPI = raster_dpi;
424            }
425            internal.fPDFA = metadata.pdf_a;
426            if let Some(encoding_quality) = metadata.encoding_quality {
427                internal.fEncodingQuality = encoding_quality
428            }
429            if let Some(structure_element_tree) = &metadata.structure_element_tree_root {
430                internal.fStructureElementTreeRoot = structure_element_tree.0.as_ptr();
431            }
432            internal.fOutline = metadata.outline;
433            internal.fCompressionLevel = metadata.compression_level
434        }
435
436        // We enable harfbuzz font sub-setting in PDF documents if textlayout is enabled.
437        #[cfg(all(feature = "textlayout", feature = "embed-icudtl"))]
438        crate::icu::init();
439
440        let mut stream = RustWStream::new(writer);
441        let document = RCHandle::from_ptr(unsafe {
442            sb::C_SkPDF_MakeDocument(stream.stream_mut(), md.native())
443        })
444        .unwrap();
445
446        Document::new(stream, document)
447    }
448
449    //
450    // Helper for constructing the internal metadata struct and setting associated strings.
451    //
452
453    type InternalMetadata = Handle<SkPDF_Metadata>;
454
455    impl NativeDrop for SkPDF_Metadata {
456        fn drop(&mut self) {
457            unsafe { sb::C_SkPDF_Metadata_destruct(self) }
458        }
459    }
460
461    impl Default for Handle<SkPDF_Metadata> {
462        fn default() -> Self {
463            Self::construct(|pdf_md| unsafe { sb::C_SkPDF_Metadata_Construct(pdf_md) })
464        }
465    }
466
467    pub mod node_id {
468        pub const NOTHING: i32 = 0;
469        pub const OTHER_ARTIFACT: i32 = -1;
470        pub const PAGINATION_ARTIFACT: i32 = -2;
471        pub const PAGINATION_HEADER_ARTIFACT: i32 = -3;
472        pub const PAGINATION_FOOTER_ARTIFACT: i32 = -4;
473        pub const PAGINATION_WATERMARK_ARTIFACT: i32 = -5;
474        pub const LAYOUT_ARTIFACT: i32 = -6;
475        pub const PAGE_ARTIFACT: i32 = -7;
476        pub const BACKGROUND_ARTIFACT: i32 = -8;
477    }
478
479    /// Associate a node ID with subsequent drawing commands in a [`Canvas`].
480    ///
481    /// The same node ID can appear in a [`StructureElementNode`] in order to associate
482    /// a document's structure element tree with its content.
483    ///
484    /// A node ID of zero indicates no node ID. Negative node IDs are reserved.
485    ///
486    /// - `canvas` - The canvas used to draw to the PDF.
487    /// - `node_id` - The node ID for subsequent drawing commands.
488    pub fn set_node_id(canvas: &Canvas, node_id: i32) {
489        unsafe {
490            sb::C_SkPDF_SetNodeId(canvas.native_mut(), node_id);
491        }
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use std::ffi::CString;
498
499    use crate::{
500        Color, Paint, Rect,
501        pdf::{self, Metadata, StructureElementNode},
502    };
503
504    #[test]
505    fn create_attribute_list() {
506        let mut _al = pdf::AttributeList::default();
507        let owner = CString::new("Owner").unwrap();
508        let name = CString::new("Name").unwrap();
509        _al.append_float_array(&owner, &name, &[1.0, 2.0, 3.0]);
510    }
511
512    #[test]
513    fn structure_element_node_child_vector() {
514        let mut root = StructureElementNode::new("root");
515        root.append_child(StructureElementNode::new("nested"));
516        root.append_child(StructureElementNode::new("nested2"));
517        let v = root.child_vector();
518        assert_eq!(v[0].type_string(), "nested");
519        assert_eq!(v[1].type_string(), "nested2");
520    }
521
522    #[test]
523    fn generate_pdf_with_structure_and_attributes() {
524        // String storage - must outlive the PDF document
525        let layout_owner = CString::new("Layout").unwrap();
526        let bbox_name = CString::new("BBox").unwrap();
527        let table_owner = CString::new("Table").unwrap();
528        let colspan_name = CString::new("ColSpan").unwrap();
529        let rowspan_name = CString::new("RowSpan").unwrap();
530
531        // Create structure tree with attributes
532        let mut root = StructureElementNode::new("Document");
533        root.set_node_id(1);
534
535        // Add a paragraph element with bounding box attribute
536        let mut paragraph = StructureElementNode::new("P");
537        paragraph.set_node_id(2);
538        paragraph.attributes_mut().append_float_array(
539            &layout_owner,
540            &bbox_name,
541            &[10.0, 10.0, 200.0, 50.0],
542        );
543
544        // Add a table element with column/row span attributes
545        let mut table = StructureElementNode::new("Table");
546        table.set_node_id(3);
547        table
548            .attributes_mut()
549            .append_int(&table_owner, &colspan_name, 2)
550            .append_int(&table_owner, &rowspan_name, 3);
551
552        root.append_child(paragraph);
553        root.append_child(table);
554
555        // Create metadata with structure tree
556        let metadata = Metadata {
557            title: "Test Document with Structure".to_string(),
558            author: "Rust Skia Test".to_string(),
559            structure_element_tree_root: Some(root),
560            ..Default::default()
561        };
562
563        // Generate PDF
564        let mut output = Vec::new();
565        let document = pdf::new_document(&mut output, Some(&metadata));
566
567        // Draw content on first page
568        let mut page = document.begin_page((200, 200), None);
569        let canvas = page.canvas();
570
571        // Mark the paragraph content area
572        pdf::set_node_id(canvas, 2);
573        let mut paint = Paint::default();
574        paint.set_color(Color::from_rgb(100, 150, 200));
575        canvas.draw_rect(Rect::from_xywh(10.0, 10.0, 190.0, 40.0), &paint);
576
577        // Mark the table content area
578        pdf::set_node_id(canvas, 3);
579        paint.set_color(Color::from_rgb(200, 150, 100));
580        canvas.draw_rect(Rect::from_xywh(10.0, 60.0, 190.0, 130.0), &paint);
581
582        let document = page.end_page();
583        document.close();
584
585        // Verify PDF was generated
586        assert!(!output.is_empty(), "PDF output should not be empty");
587
588        // Basic PDF format validation
589        assert!(
590            output.starts_with(b"%PDF-"),
591            "Output should start with PDF header"
592        );
593        assert!(
594            output.windows(5).any(|w| w == b"%%EOF"),
595            "Output should contain EOF marker"
596        );
597
598        // Check for structure-related content in the PDF
599        let pdf_str = String::from_utf8_lossy(&output);
600        assert!(
601            pdf_str.contains("/StructTreeRoot") || pdf_str.contains("Document"),
602            "PDF should contain structure tree references"
603        );
604    }
605}