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