1use std::path::Path;
6
7use pdfboss_core::{Dict, Name, ObjRef, Object};
8
9use crate::canvas::{Canvas, CanvasParts};
10use crate::content::serialize_ops;
11use crate::element::{self, Content};
12use crate::error::{Error, Result};
13use crate::font::Standard14;
14use crate::sink::AsyncByteSink;
15use crate::writer::{WriteOptions, Writer};
16
17#[derive(Debug, Clone, Copy, PartialEq, Default)]
19pub enum PageSize {
20 A3,
22 #[default]
24 A4,
25 A5,
27 Letter,
29 Legal,
31 Custom {
33 width: f32,
35 height: f32,
37 },
38}
39
40impl PageSize {
41 pub fn dimensions(self) -> (f32, f32) {
43 match self {
44 PageSize::A3 => (841.89, 1190.55),
45 PageSize::A4 => (595.28, 841.89),
46 PageSize::A5 => (419.53, 595.28),
47 PageSize::Letter => (612.0, 792.0),
48 PageSize::Legal => (612.0, 1008.0),
49 PageSize::Custom { width, height } => (width, height),
50 }
51 }
52
53 pub fn landscape(self) -> PageSize {
55 let (width, height) = self.dimensions();
56 PageSize::Custom {
57 width: height,
58 height: width,
59 }
60 }
61
62 pub fn by_name(name: &str) -> Option<PageSize> {
66 match name.to_ascii_lowercase().as_str() {
67 "a3" => Some(PageSize::A3),
68 "a4" => Some(PageSize::A4),
69 "a5" => Some(PageSize::A5),
70 "letter" => Some(PageSize::Letter),
71 "legal" => Some(PageSize::Legal),
72 _ => None,
73 }
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct Date {
82 pub year: u16,
84 pub month: u8,
86 pub day: u8,
88 pub hour: u8,
90 pub minute: u8,
92 pub second: u8,
94 pub utc_offset_minutes: i16,
96}
97
98impl Date {
99 pub fn to_pdf_string(self) -> String {
102 let Date {
103 year,
104 month,
105 day,
106 hour,
107 minute,
108 second,
109 utc_offset_minutes,
110 } = self;
111 let mut out = format!("D:{year:04}{month:02}{day:02}{hour:02}{minute:02}{second:02}");
112 if utc_offset_minutes == 0 {
113 out.push('Z');
114 return out;
115 }
116 let sign = if utc_offset_minutes < 0 { '-' } else { '+' };
117 let magnitude = utc_offset_minutes.unsigned_abs();
118 out.push_str(&format!(
119 "{sign}{:02}'{:02}",
120 magnitude / 60,
121 magnitude % 60
122 ));
123 out
124 }
125
126 pub(crate) fn to_iso8601(self) -> String {
130 let Date {
131 year,
132 month,
133 day,
134 hour,
135 minute,
136 second,
137 utc_offset_minutes,
138 } = self;
139 let mut out = format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}");
140 if utc_offset_minutes == 0 {
141 out.push('Z');
142 return out;
143 }
144 let sign = if utc_offset_minutes < 0 { '-' } else { '+' };
145 let magnitude = utc_offset_minutes.unsigned_abs();
146 out.push_str(&format!(
147 "{sign}{:02}:{:02}",
148 magnitude / 60,
149 magnitude % 60
150 ));
151 out
152 }
153}
154
155#[derive(Debug, Clone, Default, PartialEq)]
158pub struct Metadata {
159 pub title: Option<String>,
161 pub author: Option<String>,
163 pub subject: Option<String>,
165 pub keywords: Option<String>,
167 pub creator: Option<String>,
169 pub producer: Option<String>,
171 pub creation_date: Option<Date>,
173 pub modification_date: Option<Date>,
175}
176
177#[derive(Debug, Default)]
179pub struct Page {
180 pub size: PageSize,
182 pub rotation: i32,
184 pub canvas: Canvas,
186 pub content: Vec<Content>,
189 pub links: Vec<LinkAnnotation>,
191}
192
193#[derive(Debug, Clone, PartialEq)]
197pub struct LinkAnnotation {
198 pub rect: [f32; 4],
200 pub target: LinkTarget,
202}
203
204#[derive(Debug, Clone, PartialEq)]
206pub enum LinkTarget {
207 Uri(String),
209 Page(usize),
212}
213
214impl Page {
215 pub fn new(size: PageSize) -> Page {
217 Page {
218 size,
219 ..Page::default()
220 }
221 }
222}
223
224#[derive(Debug, Clone, Default, PartialEq)]
227pub struct Outline {
228 pub bookmarks: Vec<Bookmark>,
230}
231
232#[derive(Debug, Clone, Default, PartialEq)]
234pub struct Bookmark {
235 pub title: String,
237 pub page: usize,
240 pub children: Vec<Bookmark>,
242}
243
244impl Bookmark {
245 pub fn new(title: impl Into<String>, page: usize) -> Bookmark {
247 Bookmark {
248 title: title.into(),
249 page,
250 children: Vec::new(),
251 }
252 }
253}
254
255#[derive(Debug, Clone, PartialEq)]
260pub struct Attachment {
261 pub name: String,
264 pub data: Vec<u8>,
267 pub mime: Option<String>,
270 pub modified: Option<Date>,
272 pub description: Option<String>,
274}
275
276#[derive(Debug, Clone, Copy, PartialEq)]
279pub enum LabelStyle {
280 Decimal,
282 RomanUpper,
284 RomanLower,
286 LettersUpper,
288 LettersLower,
290}
291
292#[derive(Debug, Clone, PartialEq)]
297pub struct PageLabel {
298 pub first_page: usize,
300 pub style: Option<LabelStyle>,
303 pub prefix: Option<String>,
305 pub start_at: u32,
308}
309
310#[derive(Debug, Clone, Copy, PartialEq)]
313pub enum PageLayout {
314 SinglePage,
316 OneColumn,
318 TwoColumnLeft,
320 TwoColumnRight,
322 TwoPageLeft,
324 TwoPageRight,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq)]
331pub enum PageMode {
332 UseNone,
334 UseOutlines,
336 UseThumbs,
338 FullScreen,
340}
341
342#[derive(Debug, Clone, Default, PartialEq)]
345pub struct Viewer {
346 pub layout: Option<PageLayout>,
348 pub mode: Option<PageMode>,
350 pub open_to: Option<usize>,
353}
354
355#[derive(Debug, Default)]
358pub struct Pdf {
359 pub metadata: Option<Metadata>,
361 pub pages: Vec<Page>,
363 pub outline: Option<Outline>,
365 pub attachments: Vec<Attachment>,
369 pub page_labels: Vec<PageLabel>,
374 pub viewer: Option<Viewer>,
376 pub options: WriteOptions,
378}
379
380impl Pdf {
381 pub fn to_bytes(self) -> Result<Vec<u8>> {
390 let (w, root) = self.assemble()?;
391 w.finish(root)
392 }
393
394 pub fn write_into(self, out: impl std::io::Write) -> Result<()> {
399 let (w, root) = self.assemble()?;
400 w.finish_into(root, out)
401 }
402
403 pub async fn write_into_with<S: AsyncByteSink>(self, sink: S) -> Result<S> {
407 let (w, root) = self.assemble()?;
408 w.finish_into_with(root, sink).await
409 }
410
411 fn assemble(self) -> Result<(Writer, ObjRef)> {
414 let Pdf {
415 metadata,
416 pages,
417 outline,
418 attachments,
419 page_labels,
420 viewer,
421 options,
422 } = self;
423 if pages.is_empty() {
424 return Err(Error::Other(
425 "a document needs at least one page".to_string(),
426 ));
427 }
428 let mut w = Writer::new(options);
429 let pages_root = w.reserve();
430 let page_count = pages.len();
431 let page_refs: Vec<ObjRef> = pages.iter().map(|_| w.reserve()).collect();
432 let mut font_cache: Vec<(Standard14, ObjRef)> = Vec::new();
433 for (index, page) in pages.into_iter().enumerate() {
434 let Page {
435 size,
436 rotation,
437 mut canvas,
438 content,
439 mut links,
440 } = page;
441 if rotation % 90 != 0 {
442 return Err(Error::Other(format!(
443 "page rotation {rotation} is not a multiple of 90"
444 )));
445 }
446 element::lower(content, &mut canvas, &mut links)?;
447 let (width, height) = size.dimensions();
448 let parts = canvas.into_parts();
449 let content_ref = w.put_stream(Dict::new(), serialize_ops(&parts.ops));
450 let mut fonts = Dict::new();
451 for (index, face) in parts.fonts.iter().enumerate() {
452 let font_ref = cached_font(&mut w, &mut font_cache, face);
453 fonts.insert(Name(format!("F{}", index + 1)), Object::Ref(font_ref));
454 }
455 let mut xobjects = Dict::new();
456 for (index, image) in parts.images.iter().enumerate() {
457 let image_ref = image.build_xobject(&mut w);
458 xobjects.insert(Name(format!("Im{}", index + 1)), Object::Ref(image_ref));
459 }
460 for (index, (group_parts, bbox)) in parts.groups.into_iter().enumerate() {
461 let group_ref = build_form(&mut w, group_parts, bbox, &mut font_cache)?;
462 xobjects.insert(Name(format!("Gp{}", index + 1)), Object::Ref(group_ref));
463 }
464 let mut ext_gstates = Dict::new();
465 for (index, state) in parts.gstates.iter().enumerate() {
466 let gstate_ref = w.put(Object::Dict(state.ext_gstate_dict()));
467 ext_gstates.insert(Name(format!("Gs{}", index + 1)), Object::Ref(gstate_ref));
468 }
469 let mut resources = Dict::new();
470 if !fonts.is_empty() {
471 resources.insert(name("Font"), Object::Dict(fonts));
472 }
473 if !xobjects.is_empty() {
474 resources.insert(name("XObject"), Object::Dict(xobjects));
475 }
476 if !ext_gstates.is_empty() {
477 resources.insert(name("ExtGState"), Object::Dict(ext_gstates));
478 }
479 let mut dict = Dict::new();
480 dict.insert(name("Type"), Object::Name(name("Page")));
481 dict.insert(name("Parent"), Object::Ref(pages_root));
482 dict.insert(
483 name("MediaBox"),
484 Object::Array(vec![
485 Object::Int(0),
486 Object::Int(0),
487 Object::Real(f64::from(width)),
488 Object::Real(f64::from(height)),
489 ]),
490 );
491 dict.insert(name("Contents"), Object::Ref(content_ref));
492 dict.insert(name("Resources"), Object::Dict(resources));
493 if !links.is_empty() {
494 let mut annots = Vec::with_capacity(links.len());
495 for link in links {
496 let action = match link.target {
497 LinkTarget::Uri(uri) => {
498 let mut action = Dict::new();
499 action.insert(name("S"), Object::Name(name("URI")));
500 action.insert(name("URI"), text_string(&uri));
501 action
502 }
503 LinkTarget::Page(target_index) => {
504 let target = page_refs.get(target_index).copied().ok_or_else(|| {
505 Error::Other(format!(
506 "link target page {target_index} is out of range: the document has {page_count} pages"
507 ))
508 })?;
509 let mut action = Dict::new();
510 action.insert(name("S"), Object::Name(name("GoTo")));
511 action.insert(
512 name("D"),
513 Object::Array(vec![
514 Object::Ref(target),
515 Object::Name(name("XYZ")),
516 Object::Null,
517 Object::Null,
518 Object::Null,
519 ]),
520 );
521 action
522 }
523 };
524 let mut annot = Dict::new();
525 annot.insert(name("Type"), Object::Name(name("Annot")));
526 annot.insert(name("Subtype"), Object::Name(name("Link")));
527 annot.insert(
528 name("Rect"),
529 Object::Array(
530 link.rect
531 .iter()
532 .map(|v| Object::Real(f64::from(*v)))
533 .collect(),
534 ),
535 );
536 annot.insert(
537 name("Border"),
538 Object::Array(vec![Object::Int(0), Object::Int(0), Object::Int(0)]),
539 );
540 annot.insert(name("A"), Object::Dict(action));
541 annots.push(Object::Ref(w.put(Object::Dict(annot))));
542 }
543 dict.insert(name("Annots"), Object::Array(annots));
544 }
545 if rotation != 0 {
546 dict.insert(name("Rotate"), Object::Int(i64::from(rotation)));
547 }
548 w.fill(page_refs[index], Object::Dict(dict))?;
549 }
550 let kids: Vec<Object> = page_refs.iter().copied().map(Object::Ref).collect();
551 let mut tree = Dict::new();
552 tree.insert(name("Type"), Object::Name(name("Pages")));
553 tree.insert(name("Count"), Object::Int(kids.len() as i64));
554 tree.insert(name("Kids"), Object::Array(kids));
555 w.fill(pages_root, Object::Dict(tree))?;
556 let xmp_ref = match metadata {
557 Some(meta) => {
558 let packet = crate::xmp::packet(&meta);
559 if let Some(info) = info_dict(meta) {
560 let info_ref = w.put(Object::Dict(info));
561 w.set_info(info_ref);
562 }
563 let mut xmp_dict = Dict::new();
564 xmp_dict.insert(name("Type"), Object::Name(name("Metadata")));
565 xmp_dict.insert(name("Subtype"), Object::Name(name("XML")));
566 Some(w.put_stream_raw(xmp_dict, packet))
567 }
568 None => None,
569 };
570 let outline_ref = match outline {
571 Some(outline) if !outline.bookmarks.is_empty() => {
572 let root_ref = w.reserve();
573 let refs = reserve_bookmarks(&mut w, &outline.bookmarks);
574 let (first, last, count) = fill_bookmarks(
575 &mut w,
576 outline.bookmarks,
577 &refs,
578 root_ref,
579 &page_refs,
580 page_count,
581 )?;
582 let mut dict = Dict::new();
583 dict.insert(name("Type"), Object::Name(name("Outlines")));
584 dict.insert(name("First"), Object::Ref(first));
585 dict.insert(name("Last"), Object::Ref(last));
586 dict.insert(name("Count"), Object::Int(count));
587 w.fill(root_ref, Object::Dict(dict))?;
588 Some(root_ref)
589 }
590 _ => None,
591 };
592 let names = embedded_files_dict(&mut w, attachments)?;
593 let page_labels_entry = page_labels_dict(page_labels)?;
594 let mut catalog = Dict::new();
595 catalog.insert(name("Type"), Object::Name(name("Catalog")));
596 catalog.insert(name("Pages"), Object::Ref(pages_root));
597 if let Some(outline_ref) = outline_ref {
598 catalog.insert(name("Outlines"), Object::Ref(outline_ref));
599 }
600 if let Some(xmp_ref) = xmp_ref {
601 catalog.insert(name("Metadata"), Object::Ref(xmp_ref));
602 }
603 if let Some(names) = names {
604 catalog.insert(name("Names"), Object::Dict(names));
605 }
606 if let Some(page_labels_entry) = page_labels_entry {
607 catalog.insert(name("PageLabels"), Object::Dict(page_labels_entry));
608 }
609 if let Some(viewer) = viewer {
610 let Viewer {
611 layout,
612 mode,
613 open_to,
614 } = viewer;
615 if let Some(layout) = layout {
616 catalog.insert(
617 name("PageLayout"),
618 Object::Name(name(page_layout_name(layout))),
619 );
620 }
621 if let Some(mode) = mode {
622 catalog.insert(name("PageMode"), Object::Name(name(page_mode_name(mode))));
623 }
624 if let Some(open_to) = open_to {
625 let target = page_refs.get(open_to).copied().ok_or_else(|| {
626 Error::Other(format!(
627 "open_to target page {open_to} is out of range: the document has {page_count} pages"
628 ))
629 })?;
630 catalog.insert(
631 name("OpenAction"),
632 Object::Array(vec![
633 Object::Ref(target),
634 Object::Name(name("XYZ")),
635 Object::Null,
636 Object::Null,
637 Object::Null,
638 ]),
639 );
640 }
641 }
642 let root = w.put(Object::Dict(catalog));
643 Ok((w, root))
644 }
645
646 pub fn save(self, path: impl AsRef<Path>) -> Result<()> {
648 let path = path.as_ref();
649 let bytes = self.to_bytes()?;
650 std::fs::write(path, bytes)?;
651 Ok(())
652 }
653}
654
655fn name(text: &str) -> Name {
657 Name(text.to_string())
658}
659
660fn info_dict(meta: Metadata) -> Option<Dict> {
662 let mut dict = Dict::new();
663 let texts = [
664 ("Title", meta.title),
665 ("Author", meta.author),
666 ("Subject", meta.subject),
667 ("Keywords", meta.keywords),
668 ("Creator", meta.creator),
669 ("Producer", meta.producer),
670 ];
671 for (key, value) in texts {
672 if let Some(value) = value {
673 dict.insert(name(key), text_string(&value));
674 }
675 }
676 let dates = [
677 ("CreationDate", meta.creation_date),
678 ("ModDate", meta.modification_date),
679 ];
680 for (key, value) in dates {
681 if let Some(date) = value {
682 dict.insert(name(key), Object::String(date.to_pdf_string().into_bytes()));
683 }
684 }
685 if dict.is_empty() {
686 return None;
687 }
688 Some(dict)
689}
690
691const DEFAULT_ATTACHMENT_MIME: &str = "application/octet-stream";
693
694fn embedded_files_dict(w: &mut Writer, mut attachments: Vec<Attachment>) -> Result<Option<Dict>> {
700 if attachments.is_empty() {
701 return Ok(None);
702 }
703 attachments.sort_by(|a, b| a.name.cmp(&b.name));
704 for pair in attachments.windows(2) {
705 if pair[0].name == pair[1].name {
706 return Err(Error::Other(format!(
707 "duplicate attachment name: {:?}",
708 pair[0].name
709 )));
710 }
711 }
712 let mut entries = Vec::with_capacity(attachments.len() * 2);
713 for attachment in attachments {
714 let Attachment {
715 name: file_name,
716 data,
717 mime,
718 modified,
719 description,
720 } = attachment;
721 let mime = mime.unwrap_or_else(|| DEFAULT_ATTACHMENT_MIME.to_string());
722
723 let mut params = Dict::new();
724 params.insert(name("Size"), Object::Int(data.len() as i64));
725 if let Some(modified) = modified {
726 params.insert(
727 name("ModDate"),
728 Object::String(modified.to_pdf_string().into_bytes()),
729 );
730 }
731 let mut stream_dict = Dict::new();
732 stream_dict.insert(name("Type"), Object::Name(name("EmbeddedFile")));
733 stream_dict.insert(name("Subtype"), Object::Name(Name(mime)));
734 stream_dict.insert(name("Params"), Object::Dict(params));
735 let stream_ref = w.put_stream(stream_dict, data);
736
737 let mut ef = Dict::new();
738 ef.insert(name("F"), Object::Ref(stream_ref));
739
740 let mut filespec = Dict::new();
741 filespec.insert(name("Type"), Object::Name(name("Filespec")));
742 filespec.insert(name("F"), text_string(&file_name));
743 filespec.insert(name("UF"), text_string(&file_name));
744 if let Some(description) = description {
745 filespec.insert(name("Desc"), text_string(&description));
746 }
747 filespec.insert(name("EF"), Object::Dict(ef));
748 let filespec_ref = w.put(Object::Dict(filespec));
749
750 entries.push(text_string(&file_name));
751 entries.push(Object::Ref(filespec_ref));
752 }
753 let mut name_tree = Dict::new();
754 name_tree.insert(name("Names"), Object::Array(entries));
755 let mut embedded_files = Dict::new();
756 embedded_files.insert(name("EmbeddedFiles"), Object::Dict(name_tree));
757 Ok(Some(embedded_files))
758}
759
760fn page_labels_dict(mut labels: Vec<PageLabel>) -> Result<Option<Dict>> {
767 if labels.is_empty() {
768 return Ok(None);
769 }
770 for label in &labels {
771 if label.start_at == 0 {
772 return Err(Error::Other(format!(
773 "page label at page {} has start_at 0: numbering starts at 1",
774 label.first_page
775 )));
776 }
777 }
778 labels.sort_by_key(|label| label.first_page);
779 if labels[0].first_page != 0 {
780 return Err(Error::Other("page labels must start at page 0".to_string()));
781 }
782 for pair in labels.windows(2) {
783 if pair[0].first_page == pair[1].first_page {
784 return Err(Error::Other(format!(
785 "duplicate page label at page {}",
786 pair[0].first_page
787 )));
788 }
789 }
790 let mut nums = Vec::with_capacity(labels.len() * 2);
791 for label in labels {
792 let PageLabel {
793 first_page,
794 style,
795 prefix,
796 start_at,
797 } = label;
798 let mut range = Dict::new();
799 if let Some(style) = style {
800 range.insert(name("S"), Object::Name(name(label_style_name(style))));
801 }
802 if let Some(prefix) = prefix {
803 range.insert(name("P"), text_string(&prefix));
804 }
805 if start_at != 1 {
806 range.insert(name("St"), Object::Int(i64::from(start_at)));
807 }
808 nums.push(Object::Int(first_page as i64));
809 nums.push(Object::Dict(range));
810 }
811 let mut dict = Dict::new();
812 dict.insert(name("Nums"), Object::Array(nums));
813 Ok(Some(dict))
814}
815
816fn label_style_name(style: LabelStyle) -> &'static str {
818 match style {
819 LabelStyle::Decimal => "D",
820 LabelStyle::RomanUpper => "R",
821 LabelStyle::RomanLower => "r",
822 LabelStyle::LettersUpper => "A",
823 LabelStyle::LettersLower => "a",
824 }
825}
826
827fn page_layout_name(layout: PageLayout) -> &'static str {
829 match layout {
830 PageLayout::SinglePage => "SinglePage",
831 PageLayout::OneColumn => "OneColumn",
832 PageLayout::TwoColumnLeft => "TwoColumnLeft",
833 PageLayout::TwoColumnRight => "TwoColumnRight",
834 PageLayout::TwoPageLeft => "TwoPageLeft",
835 PageLayout::TwoPageRight => "TwoPageRight",
836 }
837}
838
839fn page_mode_name(mode: PageMode) -> &'static str {
841 match mode {
842 PageMode::UseNone => "UseNone",
843 PageMode::UseOutlines => "UseOutlines",
844 PageMode::UseThumbs => "UseThumbs",
845 PageMode::FullScreen => "FullScreen",
846 }
847}
848
849struct BookmarkRef {
853 r: ObjRef,
854 children: Vec<BookmarkRef>,
855}
856
857fn reserve_bookmarks(w: &mut Writer, bookmarks: &[Bookmark]) -> Vec<BookmarkRef> {
862 bookmarks
863 .iter()
864 .map(|bookmark| BookmarkRef {
865 r: w.reserve(),
866 children: reserve_bookmarks(w, &bookmark.children),
867 })
868 .collect()
869}
870
871fn fill_bookmarks(
878 w: &mut Writer,
879 bookmarks: Vec<Bookmark>,
880 refs: &[BookmarkRef],
881 parent: ObjRef,
882 page_refs: &[ObjRef],
883 page_count: usize,
884) -> Result<(ObjRef, ObjRef, i64)> {
885 let last_index = bookmarks.len() - 1;
886 let mut total = 0i64;
887 for (index, bookmark) in bookmarks.into_iter().enumerate() {
888 let Bookmark {
889 title,
890 page,
891 children,
892 } = bookmark;
893 let dest = page_refs.get(page).copied().ok_or_else(|| {
894 Error::Other(format!(
895 "bookmark target page {page} is out of range: the document has {page_count} pages"
896 ))
897 })?;
898 let mut dict = Dict::new();
899 dict.insert(name("Title"), text_string(&title));
900 dict.insert(name("Parent"), Object::Ref(parent));
901 if index > 0 {
902 dict.insert(name("Prev"), Object::Ref(refs[index - 1].r));
903 }
904 if index < last_index {
905 dict.insert(name("Next"), Object::Ref(refs[index + 1].r));
906 }
907 dict.insert(
908 name("Dest"),
909 Object::Array(vec![
910 Object::Ref(dest),
911 Object::Name(name("XYZ")),
912 Object::Null,
913 Object::Null,
914 Object::Null,
915 ]),
916 );
917 let mut subtree_count = 0i64;
918 if !children.is_empty() {
919 let (first, last, count) = fill_bookmarks(
920 w,
921 children,
922 &refs[index].children,
923 refs[index].r,
924 page_refs,
925 page_count,
926 )?;
927 dict.insert(name("First"), Object::Ref(first));
928 dict.insert(name("Last"), Object::Ref(last));
929 dict.insert(name("Count"), Object::Int(count));
930 subtree_count = count;
931 }
932 w.fill(refs[index].r, Object::Dict(dict))?;
933 total += 1 + subtree_count;
934 }
935 Ok((refs[0].r, refs[last_index].r, total))
936}
937
938fn cached_font(
944 w: &mut Writer,
945 font_cache: &mut Vec<(Standard14, ObjRef)>,
946 face: &Standard14,
947) -> ObjRef {
948 if let Some((_, r)) = font_cache.iter().find(|(seen, _)| seen == face) {
949 return *r;
950 }
951 let r = w.put(Object::Dict(face.font_dict()));
952 font_cache.push((*face, r));
953 r
954}
955
956fn build_form(
962 w: &mut Writer,
963 parts: CanvasParts,
964 bbox: [f32; 4],
965 font_cache: &mut Vec<(Standard14, ObjRef)>,
966) -> Result<ObjRef> {
967 let content = serialize_ops(&parts.ops);
968 let mut fonts = Dict::new();
969 for (index, face) in parts.fonts.iter().enumerate() {
970 let font_ref = cached_font(w, font_cache, face);
971 fonts.insert(Name(format!("F{}", index + 1)), Object::Ref(font_ref));
972 }
973 let mut xobjects = Dict::new();
974 for (index, image) in parts.images.iter().enumerate() {
975 let image_ref = image.build_xobject(w);
976 xobjects.insert(Name(format!("Im{}", index + 1)), Object::Ref(image_ref));
977 }
978 for (index, (group_parts, group_bbox)) in parts.groups.into_iter().enumerate() {
979 let group_ref = build_form(w, group_parts, group_bbox, font_cache)?;
980 xobjects.insert(Name(format!("Gp{}", index + 1)), Object::Ref(group_ref));
981 }
982 let mut ext_gstates = Dict::new();
983 for (index, state) in parts.gstates.iter().enumerate() {
984 let gstate_ref = w.put(Object::Dict(state.ext_gstate_dict()));
985 ext_gstates.insert(Name(format!("Gs{}", index + 1)), Object::Ref(gstate_ref));
986 }
987 let mut resources = Dict::new();
988 if !fonts.is_empty() {
989 resources.insert(name("Font"), Object::Dict(fonts));
990 }
991 if !xobjects.is_empty() {
992 resources.insert(name("XObject"), Object::Dict(xobjects));
993 }
994 if !ext_gstates.is_empty() {
995 resources.insert(name("ExtGState"), Object::Dict(ext_gstates));
996 }
997 let mut dict = Dict::new();
998 dict.insert(name("Type"), Object::Name(name("XObject")));
999 dict.insert(name("Subtype"), Object::Name(name("Form")));
1000 dict.insert(
1001 name("BBox"),
1002 Object::Array(bbox.iter().map(|v| Object::Real(f64::from(*v))).collect()),
1003 );
1004 dict.insert(name("Resources"), Object::Dict(resources));
1005 Ok(w.put_stream(dict, content))
1006}
1007
1008fn text_string(value: &str) -> Object {
1012 if value.is_ascii() {
1013 return Object::String(value.as_bytes().to_vec());
1014 }
1015 let mut bytes = vec![0xFE, 0xFF];
1016 for unit in value.encode_utf16() {
1017 bytes.extend_from_slice(&unit.to_be_bytes());
1018 }
1019 Object::String(bytes)
1020}
1021
1022#[cfg(test)]
1023mod tests {
1024 use super::*;
1025
1026 #[test]
1027 fn dimensions_match_the_contract() {
1028 assert_eq!(PageSize::A3.dimensions(), (841.89, 1190.55));
1029 assert_eq!(PageSize::A4.dimensions(), (595.28, 841.89));
1030 assert_eq!(PageSize::A5.dimensions(), (419.53, 595.28));
1031 assert_eq!(PageSize::Letter.dimensions(), (612.0, 792.0));
1032 assert_eq!(PageSize::Legal.dimensions(), (612.0, 1008.0));
1033 assert_eq!(
1034 PageSize::Custom {
1035 width: 10.0,
1036 height: 20.0
1037 }
1038 .dimensions(),
1039 (10.0, 20.0)
1040 );
1041 }
1042
1043 #[test]
1044 fn by_name_parses_the_five_named_sizes_case_insensitively() {
1045 for (name, expected) in [
1046 ("a3", PageSize::A3),
1047 ("A4", PageSize::A4),
1048 ("a5", PageSize::A5),
1049 ("Letter", PageSize::Letter),
1050 ("LEGAL", PageSize::Legal),
1051 ] {
1052 assert_eq!(PageSize::by_name(name), Some(expected), "{name}");
1053 }
1054 }
1055
1056 #[test]
1057 fn by_name_rejects_anything_else() {
1058 assert_eq!(PageSize::by_name("tabloid"), None);
1059 assert_eq!(PageSize::by_name(""), None);
1060 }
1061
1062 #[test]
1063 fn landscape_swaps_into_custom() {
1064 assert_eq!(
1065 PageSize::A4.landscape(),
1066 PageSize::Custom {
1067 width: 841.89,
1068 height: 595.28
1069 }
1070 );
1071 assert_eq!(
1072 PageSize::Custom {
1073 width: 1.0,
1074 height: 2.0
1075 }
1076 .landscape(),
1077 PageSize::Custom {
1078 width: 2.0,
1079 height: 1.0
1080 }
1081 );
1082 assert_eq!(PageSize::Letter.landscape().dimensions(), (792.0, 612.0));
1083 }
1084
1085 #[test]
1086 fn date_utc_formats_with_z() {
1087 let date = Date {
1088 year: 2026,
1089 month: 8,
1090 day: 27,
1091 hour: 12,
1092 minute: 30,
1093 second: 15,
1094 utc_offset_minutes: 0,
1095 };
1096 assert_eq!(date.to_pdf_string(), "D:20260827123015Z");
1097 }
1098
1099 #[test]
1100 fn date_positive_offset_pads_single_digits() {
1101 let date = Date {
1102 year: 987,
1103 month: 1,
1104 day: 2,
1105 hour: 3,
1106 minute: 4,
1107 second: 5,
1108 utc_offset_minutes: 120,
1109 };
1110 assert_eq!(date.to_pdf_string(), "D:09870102030405+02'00");
1111 }
1112
1113 #[test]
1114 fn date_negative_offset_keeps_minutes() {
1115 let date = Date {
1116 year: 1999,
1117 month: 12,
1118 day: 31,
1119 hour: 23,
1120 minute: 59,
1121 second: 58,
1122 utc_offset_minutes: -330,
1123 };
1124 assert_eq!(date.to_pdf_string(), "D:19991231235958-05'30");
1125 }
1126
1127 #[test]
1128 fn iso8601_utc_formats_with_z() {
1129 let date = Date {
1130 year: 2026,
1131 month: 8,
1132 day: 27,
1133 hour: 12,
1134 minute: 30,
1135 second: 15,
1136 utc_offset_minutes: 0,
1137 };
1138 assert_eq!(date.to_iso8601(), "2026-08-27T12:30:15Z");
1139 }
1140
1141 #[test]
1142 fn iso8601_positive_offset_pads_single_digits() {
1143 let date = Date {
1144 year: 987,
1145 month: 1,
1146 day: 2,
1147 hour: 3,
1148 minute: 4,
1149 second: 5,
1150 utc_offset_minutes: 120,
1151 };
1152 assert_eq!(date.to_iso8601(), "0987-01-02T03:04:05+02:00");
1153 }
1154
1155 #[test]
1156 fn iso8601_negative_offset_keeps_minutes() {
1157 let date = Date {
1158 year: 1999,
1159 month: 12,
1160 day: 31,
1161 hour: 23,
1162 minute: 59,
1163 second: 58,
1164 utc_offset_minutes: -330,
1165 };
1166 assert_eq!(date.to_iso8601(), "1999-12-31T23:59:58-05:30");
1167 }
1168
1169 fn two_page_doc() -> Pdf {
1172 let mut first = Page::new(PageSize::A4);
1173 first
1174 .canvas
1175 .text("Streamed parity", 72.0, 720.0, Standard14::Helvetica, 14.0)
1176 .expect("ASCII encodes");
1177 let image = crate::image::ImageData::gray8(2, 2, vec![0, 85, 170, 255])
1178 .expect("2x2 grayscale builds");
1179 let handle = first.canvas.add_image(image);
1180 first.canvas.draw_image(handle, 72.0, 400.0, 144.0, 144.0);
1181 let mut second = Page::new(PageSize::Letter);
1182 second
1183 .canvas
1184 .text("Page two", 72.0, 700.0, Standard14::TimesRoman, 12.0)
1185 .expect("ASCII encodes");
1186 Pdf {
1187 pages: vec![first, second],
1188 ..Pdf::default()
1189 }
1190 }
1191
1192 #[test]
1196 fn write_into_and_write_into_with_match_to_bytes() {
1197 let bytes = two_page_doc().to_bytes().expect("to_bytes succeeds");
1198 let mut via_io = Vec::new();
1199 two_page_doc()
1200 .write_into(&mut via_io)
1201 .expect("write_into succeeds");
1202 assert_eq!(via_io, bytes);
1203 let via_sink = pdfboss_core::block_on(two_page_doc().write_into_with(Vec::new()))
1204 .expect("write_into_with succeeds");
1205 assert_eq!(via_sink, bytes);
1206 }
1207
1208 #[test]
1209 fn zero_page_document_is_an_error() {
1210 let err = Pdf::default()
1211 .to_bytes()
1212 .expect_err("a page-less document must not serialize");
1213 assert!(err.to_string().contains("at least one page"), "{err}");
1214 }
1215}