1pub mod certify;
30pub mod merge;
31pub mod redaction;
32pub(crate) mod tagged;
33pub(crate) mod xmp;
34
35use std::collections::{HashMap, HashSet};
36use std::fmt::Write as FmtWrite; use std::io::Write as IoWrite; use crate::error::FormeError;
40use crate::font::subset::subset_ttf;
41use crate::font::{FontContext, FontData, FontKey};
42use crate::layout::*;
43use crate::model::*;
44use crate::style::{Color, FontStyle, Overflow, TextDecoration, TransformOp};
45use crate::svg::SvgCommand;
46use miniz_oxide::deflate::compress_to_vec_zlib;
47
48const DEFAULT_ATTACHMENT_MOD_DATE: &str = "D:20000101000000Z";
52
53pub(crate) const PRODUCER: &str = "Forme";
63
64pub(crate) const CREATOR_TOOL: &str = "Forme";
67
68struct LinkAnnotation {
70 x: f64,
71 y: f64,
72 width: f64,
73 height: f64,
74 href: String,
75}
76
77struct InlineLinkSpan {
81 href: String,
82 x0: f64,
83 x1: f64,
84}
85
86struct PdfBookmark {
88 title: String,
89 page_obj_id: usize,
90 y_pdf: f64,
91}
92
93struct FormFieldData {
95 field_type: FormFieldType,
96 name: String,
97 x: f64,
98 y: f64,
99 width: f64,
100 height: f64,
101 page_idx: usize,
102}
103
104pub struct PdfWriter;
105
106fn record_glyph_text(glyph_to_text: &mut HashMap<u16, String>, glyph: &PositionedGlyph) {
109 let text = glyph_mapping(glyph);
110 match glyph_to_text.entry(glyph.glyph_id) {
111 std::collections::hash_map::Entry::Vacant(slot) => {
112 slot.insert(text);
113 }
114 std::collections::hash_map::Entry::Occupied(mut slot) => {
115 if slot.get().chars().nth(1).is_some() && text.chars().nth(1).is_none() {
116 slot.insert(text);
117 }
118 }
119 }
120}
121
122fn extraction_text(glyph: &PositionedGlyph) -> String {
123 glyph.extraction_text.clone().unwrap_or_else(|| {
124 if glyph.ligature {
125 glyph
126 .cluster_text
127 .clone()
128 .unwrap_or_else(|| glyph.char_value.to_string())
129 } else {
130 glyph.char_value.to_string()
131 }
132 })
133}
134
135fn glyph_mapping(glyph: &PositionedGlyph) -> String {
136 let text = extraction_text(glyph);
137 if text.chars().nth(1).is_some()
138 && text
139 .chars()
140 .any(|ch| unicode_bidi::bidi_class(ch) == unicode_bidi::BidiClass::NSM)
141 {
142 text.chars()
143 .find(|&ch| unicode_bidi::bidi_class(ch) != unicode_bidi::BidiClass::NSM)
144 .or_else(|| text.chars().next())
145 .unwrap()
146 .to_string()
147 } else {
148 text
149 }
150}
151
152#[allow(dead_code)]
154struct CustomFontEmbedData {
155 ttf_data: Vec<u8>,
156 gid_remap: HashMap<u16, u16>,
158 glyph_to_cid: HashMap<(u16, String), u16>,
159 glyph_to_text: HashMap<u16, String>,
161 char_to_gid: HashMap<char, u16>,
163 pdf_widths: HashMap<u16, f64>,
167 default_width: u32,
168 units_per_em: u16,
169 ascender: i16,
170 descender: i16,
171}
172
173#[derive(Clone, Default)]
175struct FontUsage {
176 chars: HashSet<char>,
178 glyph_ids: HashSet<u16>,
180 glyph_to_text: HashMap<u16, String>,
183 glyph_texts: HashSet<(u16, String)>,
184 carrier_chars: HashSet<char>,
185}
186
187struct PdfBuilder {
189 objects: Vec<PdfObject>,
190 font_objects: Vec<(FontKey, usize)>,
192 custom_font_data: HashMap<FontKey, CustomFontEmbedData>,
194 embedded_standard_fonts: std::collections::HashSet<FontKey>,
199 image_objects: Vec<usize>,
202 image_index_map: HashMap<(usize, usize), usize>,
205 page_background_image_map: HashMap<usize, (usize, u32, u32)>,
210 page_background_url_cache: HashMap<String, (usize, u32, u32)>,
214 ext_gstate_map: HashMap<u64, (usize, String)>,
217 shading_map: HashMap<(usize, usize), (usize, String)>,
221 warnings: Vec<String>,
225 missing_glyphs: std::cell::RefCell<std::collections::BTreeSet<char>>,
233 pdf_version: crate::model::PdfVersion,
236}
237
238pub(crate) struct PdfObject {
239 #[allow(dead_code)]
240 pub(crate) id: usize,
241 pub(crate) data: Vec<u8>,
242}
243
244impl Default for PdfWriter {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250impl PdfWriter {
251 pub fn new() -> Self {
252 Self
253 }
254
255 #[allow(clippy::too_many_arguments)]
277 pub fn write(
278 &self,
279 pages: &[LayoutPage],
280 metadata: &Metadata,
281 font_context: &FontContext,
282 tagged: bool,
283 pdfa: Option<&PdfAConformance>,
284 pdf_ua: bool,
285 embedded_data: Option<&str>,
286 attachments: &[Attachment],
287 zugferd: Option<&ZugferdMeta>,
288 flatten_forms: bool,
289 pdf_version: crate::model::PdfVersion,
290 pdf_ua2: bool,
291 ) -> Result<(Vec<u8>, Vec<String>), FormeError> {
292 if let Some(level) = pdfa {
300 if !level.allows_attachments() && (embedded_data.is_some() || !attachments.is_empty()) {
301 use crate::model::PdfAConformance as L;
302 let (family, clause, remedy) = match level {
303 L::A4 => ("PDF/A-4", "ISO 19005-4", "pdfa: \"4f\""),
304 _ => (
305 "PDF/A-2",
306 "ISO 19005-2, 6.8",
307 "a PDF/A-3 level — e.g. pdfa: \"3b\"",
308 ),
309 };
310 return Err(FormeError::RenderError(format!(
311 "{family} forbids embedded files that are not themselves PDF/A \
312 ({clause}), which the engine cannot verify. Use {remedy}, which \
313 permits arbitrary attachments, or remove the attachment / embedData."
314 )));
315 }
316 if matches!(level, crate::model::PdfAConformance::A4f)
321 && embedded_data.is_none()
322 && attachments.is_empty()
323 {
324 return Err(FormeError::RenderError(
325 "pdfa: \"4f\" requires at least one embedded file (ISO 19005-4, \
326 Annex A; veraPDF 6.9-t5 — the EmbeddedFiles name tree must exist). \
327 Add an attachment or embedData, or claim pdfa: \"4\" instead."
328 .to_string(),
329 ));
330 }
331 }
332 let zugferd_filename: Option<String> = if let Some(z) = zugferd {
336 const LEVELS: [&str; 6] = [
337 "MINIMUM",
338 "BASIC WL",
339 "BASIC",
340 "EN 16931",
341 "EXTENDED",
342 "XRECHNUNG",
343 ];
344 if !LEVELS.contains(&z.conformance_level.as_str()) {
345 return Err(FormeError::RenderError(format!(
346 "zugferd.conformanceLevel {:?} is not a Factur-X profile — expected one of \
347 MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, XRECHNUNG (exact spelling, \
348 spaces included).",
349 z.conformance_level
350 )));
351 }
352 if !pdfa.is_some_and(|l| l.allows_attachments()) {
353 return Err(FormeError::RenderError(
354 "Factur-X/ZUGFeRD (zugferd) requires a PDF/A-3 conformance level — set \
355 pdfa: \"3b\" (or \"3a\"/\"3u\"). The e-invoice XML is an embedded file, \
356 which only PDF/A-3 permits."
357 .to_string(),
358 ));
359 }
360 let filename = z.document_file_name.clone().unwrap_or_else(|| {
361 if z.conformance_level == "XRECHNUNG" {
362 "xrechnung.xml".to_string()
363 } else {
364 "factur-x.xml".to_string()
365 }
366 });
367 if !attachments.iter().any(|a| a.name == filename) {
368 return Err(FormeError::RenderError(format!(
369 "zugferd is set but no attachment is named {filename:?} — the XMP would \
370 point at a file that isn't embedded. Attach the invoice XML with name: \
371 {filename:?}, or set zugferd.documentFileName to the attachment's name."
372 )));
373 }
374 Some(filename)
375 } else {
376 None
377 };
378 let mut builder = PdfBuilder {
379 objects: Vec::new(),
380 font_objects: Vec::new(),
381 custom_font_data: HashMap::new(),
382 embedded_standard_fonts: std::collections::HashSet::new(),
383 image_objects: Vec::new(),
384 image_index_map: HashMap::new(),
385 page_background_image_map: HashMap::new(),
386 page_background_url_cache: HashMap::new(),
387 ext_gstate_map: HashMap::new(),
388 shading_map: HashMap::new(),
389 warnings: Vec::new(),
390 missing_glyphs: Default::default(),
391 pdf_version: Default::default(),
392 };
393
394 builder.objects.push(PdfObject {
400 id: 0,
401 data: vec![],
402 });
403 builder.objects.push(PdfObject {
404 id: 1,
405 data: vec![],
406 });
407 builder.objects.push(PdfObject {
408 id: 2,
409 data: vec![],
410 });
411
412 builder.pdf_version = pdf_version;
414 self.register_fonts(
415 &mut builder,
416 pages,
417 font_context,
418 pdf_ua,
419 pdfa.is_some(),
420 pdf_version,
421 )?;
422
423 if pdfa.is_some() {
429 for (key, _) in &builder.font_objects {
430 if !builder.custom_font_data.contains_key(key)
431 && !builder.embedded_standard_fonts.contains(key)
432 {
433 return Err(FormeError::RenderError(format!(
434 "PDF/A requires all fonts to be embedded, but '{}' is not. Register a \
435 metric-compatible font — install @formepdf/fonts-standard and register \
436 its fonts (`for (const f of standardFonts()) Font.register(f)`), or supply \
437 your own via Font.register().",
438 key.family
439 )));
440 }
441 }
442 }
443
444 self.register_images(&mut builder, pages);
446
447 self.register_page_background_images(&mut builder, pages);
451
452 self.register_ext_gstates(&mut builder, pages);
454
455 self.register_shadings(&mut builder, pages);
457
458 let mut tag_builder = if tagged {
461 Some(tagged::TagBuilder::new(pages.len(), pdf_ua2))
462 } else {
463 None
464 };
465
466 let mut page_obj_ids: Vec<usize> = Vec::new();
470 let mut all_bookmarks: Vec<PdfBookmark> = Vec::new();
471 let mut per_page_content_obj_ids: Vec<usize> = Vec::new();
472 let mut per_page_annotations: Vec<Vec<LinkAnnotation>> = Vec::new();
473 let mut per_page_resources: Vec<String> = Vec::new();
474 let mut all_form_fields: Vec<FormFieldData> = Vec::new();
475
476 for (page_idx, page) in pages.iter().enumerate() {
478 let content = self.build_content_stream_for_page(
479 page,
480 page_idx,
481 &builder,
482 page_idx + 1,
483 pages.len(),
484 tag_builder.as_mut(),
485 flatten_forms,
486 );
487 let compressed = compress_to_vec_zlib(content.as_bytes(), 6);
488
489 let content_obj_id = builder.objects.len();
490 let mut content_data: Vec<u8> = Vec::new();
491 let _ = write!(
492 content_data,
493 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
494 compressed.len()
495 );
496 content_data.extend_from_slice(&compressed);
497 content_data.extend_from_slice(b"\nendstream");
498 builder.objects.push(PdfObject {
499 id: content_obj_id,
500 data: content_data,
501 });
502 per_page_content_obj_ids.push(content_obj_id);
503
504 let mut annotations: Vec<LinkAnnotation> = Vec::new();
506 Self::collect_link_annotations(&page.elements, page.height, &mut annotations);
507 Self::warn_nested_links(&page.elements, None, &mut builder.warnings);
508 per_page_annotations.push(annotations);
509
510 Self::collect_form_fields(&page.elements, page.height, page_idx, &mut all_form_fields);
512
513 let page_obj_id = builder.objects.len();
515 builder.objects.push(PdfObject {
516 id: page_obj_id,
517 data: vec![],
518 });
519
520 let font_resources = self.build_font_resource_dict(&builder.font_objects);
522 let xobject_resources = self.build_xobject_resource_dict(page_idx, &builder);
523 let ext_gstate_resources = self.build_ext_gstate_resource_dict(&builder);
524 let shading_resources = self.build_shading_resource_dict(page_idx, &builder);
525 let mut resources = format!("/Font << {} >>", font_resources);
526 if !xobject_resources.is_empty() {
527 let _ = write!(resources, " /XObject << {} >>", xobject_resources);
528 }
529 if !ext_gstate_resources.is_empty() {
530 let _ = write!(resources, " /ExtGState << {} >>", ext_gstate_resources);
531 }
532 if !shading_resources.is_empty() {
533 let _ = write!(resources, " /Shading << {} >>", shading_resources);
534 }
535 per_page_resources.push(resources);
536
537 Self::collect_bookmarks(&page.elements, page.height, page_obj_id, &mut all_bookmarks);
539
540 page_obj_ids.push(page_obj_id);
541 }
542
543 for (page_idx, annotations) in per_page_annotations.iter().enumerate() {
545 let mut annot_obj_ids: Vec<usize> = Vec::new();
546 for annot in annotations {
547 let rect = format!(
548 "[{:.2} {:.2} {:.2} {:.2}]",
549 annot.x,
550 annot.y,
551 annot.x + annot.width,
552 annot.y + annot.height
553 );
554
555 if let Some(anchor) = annot.href.strip_prefix('#') {
556 if let Some(bm) = all_bookmarks.iter().find(|b| b.title == anchor) {
558 let annot_obj_id = builder.objects.len();
559 let sp_str = tag_builder
563 .as_mut()
564 .and_then(|tb| {
565 tb.connect_link_annotation(page_idx, &annot.href, annot_obj_id)
566 })
567 .map(|sp| format!(" /StructParent {}", sp))
568 .unwrap_or_default();
569 let contents = Self::encode_text_string(&format!("Link to {anchor}"));
572 let wants_sd = tag_builder
579 .as_mut()
580 .map(|tb| tb.request_struct_destination(anchor, annot_obj_id))
581 .unwrap_or(false);
582 let sd_str = if wants_sd {
583 format!(" /SD [999999999 0 R /XYZ 0 {:.2} null]", bm.y_pdf)
584 } else {
585 String::new()
586 };
587 let annot_dict = format!(
588 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
589 /F 4 /Contents {}{} \
590 /A << /S /GoTo /D [{} 0 R /XYZ 0 {:.2} null]{} >> >>",
591 rect, contents, sp_str, bm.page_obj_id, bm.y_pdf, sd_str
592 );
593 builder.objects.push(PdfObject {
594 id: annot_obj_id,
595 data: annot_dict.into_bytes(),
596 });
597 annot_obj_ids.push(annot_obj_id);
598 }
599 } else {
601 let annot_obj_id = builder.objects.len();
603 let sp_str = tag_builder
604 .as_mut()
605 .and_then(|tb| {
606 tb.connect_link_annotation(page_idx, &annot.href, annot_obj_id)
607 })
608 .map(|sp| format!(" /StructParent {}", sp))
609 .unwrap_or_default();
610 let contents = Self::encode_text_string(&annot.href);
614 let href_esc = Self::escape_pdf_string(&annot.href);
615 let annot_dict = format!(
616 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
617 /F 4 /Contents {}{} \
618 /A << /Type /Action /S /URI /URI ({}) >> >>",
619 rect, contents, sp_str, href_esc
620 );
621 builder.objects.push(PdfObject {
622 id: annot_obj_id,
623 data: annot_dict.into_bytes(),
624 });
625 annot_obj_ids.push(annot_obj_id);
626 }
627 }
628
629 let annots_str = if annot_obj_ids.is_empty() {
630 String::new()
631 } else {
632 let refs: String = annot_obj_ids
633 .iter()
634 .map(|id| format!("{} 0 R", id))
635 .collect::<Vec<_>>()
636 .join(" ");
637 format!(" /Annots [{}]", refs)
638 };
639
640 let page_obj_id = page_obj_ids[page_idx];
641 let content_obj_id = per_page_content_obj_ids[page_idx];
642 let struct_parents_str = if tagged {
643 format!(" /StructParents {} /Tabs /S", page_idx)
644 } else {
645 String::new()
646 };
647 let page_dict = format!(
648 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {:.2} {:.2}] \
649 /Contents {} 0 R /Resources << {} >>{}{} >>",
650 pages[page_idx].width,
651 pages[page_idx].height,
652 content_obj_id,
653 per_page_resources[page_idx],
654 annots_str,
655 struct_parents_str
656 );
657 builder.objects[page_obj_id].data = page_dict.into_bytes();
658 }
659
660 let outlines_obj_id = if !all_bookmarks.is_empty() {
662 Some(self.write_outline_tree(&mut builder, &all_bookmarks, tag_builder.as_mut()))
663 } else {
664 None
665 };
666
667 let struct_tree_root_id = if let Some(ref tb) = tag_builder {
669 let (root_id, _parent_tree_id, sd_patches) = tb.write_objects(
670 &mut builder.objects,
671 &page_obj_ids,
672 metadata.lang.as_deref(),
673 pdf_version == crate::model::PdfVersion::V2_0,
674 );
675 for (annot_obj_id, elem_obj_id) in sd_patches {
679 let data = std::mem::take(&mut builder.objects[annot_obj_id].data);
680 let patched = String::from_utf8(data)
681 .expect("annotation dicts are ASCII")
682 .replacen("999999999 0 R", &format!("{} 0 R", elem_obj_id), 1);
683 builder.objects[annot_obj_id].data = patched.into_bytes();
684 }
685 Some(root_id)
686 } else {
687 None
688 };
689
690 let xmp_metadata_id =
695 if pdfa.is_some() || pdf_ua || pdf_version == crate::model::PdfVersion::V2_0 {
696 let xmp_xml = xmp::generate_xmp(metadata, pdfa, pdf_ua, pdf_ua2, zugferd);
697 let xmp_bytes = xmp_xml.as_bytes();
698 let xmp_obj_id = builder.objects.len();
699 let xmp_data = format!(
701 "<< /Type /Metadata /Subtype /XML /Length {} >>\nstream\n",
702 xmp_bytes.len()
703 );
704 let mut xmp_obj_data: Vec<u8> = xmp_data.into_bytes();
705 xmp_obj_data.extend_from_slice(xmp_bytes);
706 xmp_obj_data.extend_from_slice(b"\nendstream");
707 builder.objects.push(PdfObject {
708 id: xmp_obj_id,
709 data: xmp_obj_data,
710 });
711 Some(xmp_obj_id)
712 } else {
713 None
714 };
715
716 let output_intent_id = if pdfa.is_some() {
717 static SRGB_ICC: &[u8] = include_bytes!("sRGB.icc");
719 let compressed_icc = compress_to_vec_zlib(SRGB_ICC, 6);
720
721 let icc_obj_id = builder.objects.len();
722 let mut icc_data: Vec<u8> = Vec::new();
723 let _ = write!(
724 icc_data,
725 "<< /N 3 /Length {} /Filter /FlateDecode >>\nstream\n",
726 compressed_icc.len()
727 );
728 icc_data.extend_from_slice(&compressed_icc);
729 icc_data.extend_from_slice(b"\nendstream");
730 builder.objects.push(PdfObject {
731 id: icc_obj_id,
732 data: icc_data,
733 });
734
735 let oi_obj_id = builder.objects.len();
737 let oi_data = format!(
738 "<< /Type /OutputIntent /S /GTS_PDFA1 \
739 /OutputConditionIdentifier (sRGB IEC61966-2.1) \
740 /RegistryName (http://www.color.org) \
741 /DestOutputProfile {} 0 R >>",
742 icc_obj_id
743 );
744 builder.objects.push(PdfObject {
745 id: oi_obj_id,
746 data: oi_data.into_bytes(),
747 });
748 Some(oi_obj_id)
749 } else {
750 None
751 };
752
753 let mut name_tree_entries: Vec<(String, usize)> = Vec::new();
760 let mut af_filespec_ids: Vec<usize> = Vec::new();
761 if let Some(data) = embedded_data {
762 let compressed = compress_to_vec_zlib(data.as_bytes(), 6);
763
764 let ef_obj_id = builder.objects.len();
766 let ef_data = format!(
767 "<< /Type /EmbeddedFile /Subtype /application#2Fjson /Length {} /Filter /FlateDecode >>\nstream\n",
768 compressed.len()
769 );
770 let mut ef_bytes = ef_data.into_bytes();
771 ef_bytes.extend_from_slice(&compressed);
772 ef_bytes.extend_from_slice(b"\nendstream");
773 builder.objects.push(PdfObject {
774 id: ef_obj_id,
775 data: ef_bytes,
776 });
777
778 let fs_obj_id = builder.objects.len();
780 let desc = if builder.pdf_version == crate::model::PdfVersion::V2_0 {
781 " /Desc (forme-data.json)"
783 } else {
784 ""
785 };
786 let fs_data = format!(
787 "<< /Type /Filespec /F (forme-data.json) /UF (forme-data.json) /EF << /F {} 0 R >> /AFRelationship /Data{} >>",
788 ef_obj_id, desc
789 );
790 builder.objects.push(PdfObject {
791 id: fs_obj_id,
792 data: fs_data.into_bytes(),
793 });
794 name_tree_entries.push(("forme-data.json".to_string(), fs_obj_id));
795 if pdfa.is_some_and(|l| l.allows_attachments()) {
798 af_filespec_ids.push(fs_obj_id);
799 }
800 }
801 for att in attachments {
802 let bytes = Self::decode_attachment_src(&att.src)?;
803 let compressed = compress_to_vec_zlib(&bytes, 6);
804 let mime = att
805 .mime_type
806 .as_deref()
807 .unwrap_or("application/octet-stream");
808 let mod_date = att
809 .mod_date
810 .as_deref()
811 .unwrap_or(DEFAULT_ATTACHMENT_MOD_DATE);
812
813 let ef_obj_id = builder.objects.len();
814 let ef_head = format!(
815 "<< /Type /EmbeddedFile /Subtype /{} /Length {} /Filter /FlateDecode \
816 /Params << /Size {} /ModDate ({}) >> >>\nstream\n",
817 Self::mime_to_pdf_name(mime),
818 compressed.len(),
819 bytes.len(),
820 Self::escape_pdf_string(mod_date),
821 );
822 let mut ef_bytes = ef_head.into_bytes();
823 ef_bytes.extend_from_slice(&compressed);
824 ef_bytes.extend_from_slice(b"\nendstream");
825 builder.objects.push(PdfObject {
826 id: ef_obj_id,
827 data: ef_bytes,
828 });
829
830 let relationship = att.relationship.unwrap_or_else(|| {
835 if zugferd_filename.as_deref() == Some(att.name.as_str()) {
836 match zugferd.map(|z| z.conformance_level.as_str()) {
837 Some("MINIMUM") | Some("BASIC WL") => AfRelationship::Data,
838 _ => AfRelationship::Alternative,
839 }
840 } else {
841 AfRelationship::Unspecified
842 }
843 });
844
845 let fs_obj_id = builder.objects.len();
846 let mut fs_data = format!(
847 "<< /Type /Filespec /F ({name}) /UF {uf} /EF << /F {ef} 0 R >> /AFRelationship /{rel}",
848 name = Self::escape_pdf_string(&att.name),
851 uf = Self::encode_text_string(&att.name),
852 ef = ef_obj_id,
853 rel = relationship.pdf_name(),
854 );
855 if let Some(desc) = &att.description {
856 let _ = write!(fs_data, " /Desc {}", Self::encode_text_string(desc));
857 } else if builder.pdf_version == crate::model::PdfVersion::V2_0 {
858 let _ = write!(fs_data, " /Desc {}", Self::encode_text_string(&att.name));
863 }
864 fs_data.push_str(" >>");
865 builder.objects.push(PdfObject {
866 id: fs_obj_id,
867 data: fs_data.into_bytes(),
868 });
869 name_tree_entries.push((att.name.clone(), fs_obj_id));
870 af_filespec_ids.push(fs_obj_id);
871 }
872 let embedded_names_id = if name_tree_entries.is_empty() {
873 None
874 } else {
875 name_tree_entries.sort_by(|a, b| a.0.cmp(&b.0));
877 let names_obj_id = builder.objects.len();
878 let pairs = name_tree_entries
879 .iter()
880 .map(|(name, id)| format!("({}) {} 0 R", Self::escape_pdf_string(name), id))
881 .collect::<Vec<_>>()
882 .join(" ");
883 let names_data = format!("<< /Names [{}] >>", pairs);
884 builder.objects.push(PdfObject {
885 id: names_obj_id,
886 data: names_data.into_bytes(),
887 });
888 Some(names_obj_id)
889 };
890
891 let acroform_obj_id = if !all_form_fields.is_empty() && !flatten_forms {
893 let helv_obj_id = builder
895 .font_objects
896 .iter()
897 .find(|(key, _)| key.family == "Helvetica" && key.weight == 400 && !key.italic)
898 .map(|(_, id)| *id);
899
900 let mut radio_groups: HashMap<String, Vec<usize>> = HashMap::new(); let mut non_radio_indices: Vec<usize> = Vec::new();
903 for (i, field) in all_form_fields.iter().enumerate() {
904 if matches!(field.field_type, FormFieldType::RadioButton { .. }) {
905 radio_groups.entry(field.name.clone()).or_default().push(i);
906 } else {
907 non_radio_indices.push(i);
908 }
909 }
910
911 let mut radio_parent_ids: HashMap<String, usize> = HashMap::new();
913 for group_name in radio_groups.keys() {
914 let parent_id = builder.objects.len();
915 builder.objects.push(PdfObject {
916 id: parent_id,
917 data: vec![], });
919 radio_parent_ids.insert(group_name.clone(), parent_id);
920 }
921
922 let checkbox_yes_stream_id = builder.objects.len();
925 {
926 let stream_content =
927 b"0.2 0.2 0.2 rg\n2 6 m 5.5 2 l 12 11 l 11 12 l 5.5 4.5 l 3 7 l 2 6 l f\n";
928 let mut data: Vec<u8> = Vec::new();
929 let _ = write!(
930 data,
931 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
932 stream_content.len()
933 );
934 data.extend_from_slice(stream_content);
935 data.extend_from_slice(b"\nendstream");
936 builder.objects.push(PdfObject {
937 id: checkbox_yes_stream_id,
938 data,
939 });
940 }
941 let checkbox_off_stream_id = builder.objects.len();
943 {
944 let stream_content = b"";
945 let mut data: Vec<u8> = Vec::new();
946 let _ = write!(
947 data,
948 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
949 stream_content.len()
950 );
951 data.extend_from_slice(stream_content);
952 data.extend_from_slice(b"\nendstream");
953 builder.objects.push(PdfObject {
954 id: checkbox_off_stream_id,
955 data,
956 });
957 }
958 let radio_on_stream_id = builder.objects.len();
960 {
961 let k = 2.761; let stream_content = format!(
964 "0.2 0.2 0.2 rg\n\
965 7 12 m {:.2} 12 12 {:.2} 12 7 c\n\
966 12 {:.2} {:.2} 2 7 2 c\n\
967 {:.2} 2 2 {:.2} 2 7 c\n\
968 2 {:.2} {:.2} 12 7 12 c f\n",
969 7.0 + k,
970 7.0 + k, 7.0 - k,
972 7.0 - k, 7.0 - k,
974 7.0 - k, 7.0 + k,
976 7.0 + k, );
978 let stream_bytes = stream_content.as_bytes();
979 let mut data: Vec<u8> = Vec::new();
980 let _ = write!(
981 data,
982 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
983 stream_bytes.len()
984 );
985 data.extend_from_slice(stream_bytes);
986 data.extend_from_slice(b"\nendstream");
987 builder.objects.push(PdfObject {
988 id: radio_on_stream_id,
989 data,
990 });
991 }
992 let radio_off_stream_id = builder.objects.len();
994 {
995 let stream_content = b"";
996 let mut data: Vec<u8> = Vec::new();
997 let _ = write!(
998 data,
999 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
1000 stream_content.len()
1001 );
1002 data.extend_from_slice(stream_content);
1003 data.extend_from_slice(b"\nendstream");
1004 builder.objects.push(PdfObject {
1005 id: radio_off_stream_id,
1006 data,
1007 });
1008 }
1009
1010 let mut acroform_field_ids: Vec<usize> = Vec::new();
1012 let mut per_page_widget_ids: Vec<Vec<usize>> = vec![Vec::new(); pages.len()];
1013 let mut radio_kid_ids: HashMap<String, Vec<usize>> = HashMap::new();
1014
1015 for field in all_form_fields.iter() {
1016 let rect = format!(
1017 "[{:.2} {:.2} {:.2} {:.2}]",
1018 field.x,
1019 field.y,
1020 field.x + field.width,
1021 field.y + field.height
1022 );
1023 let page_ref = format!("{} 0 R", page_obj_ids[field.page_idx]);
1024
1025 match &field.field_type {
1026 FormFieldType::TextField {
1027 value,
1028 multiline,
1029 password,
1030 read_only,
1031 max_length,
1032 font_size,
1033 ..
1034 } => {
1035 let mut flags: u32 = 0;
1036 if *multiline {
1037 flags |= 1 << 12; }
1039 if *password {
1040 flags |= 1 << 13; }
1042 if *read_only {
1043 flags |= 1; }
1045 let da = if let Some(helv_id) = helv_obj_id {
1046 let _ = helv_id; format!("/Helv {} Tf 0 g", font_size)
1048 } else {
1049 format!("/Helv {} Tf 0 g", font_size)
1050 };
1051 let v_str = if let Some(ref v) = value {
1052 format!(
1053 " /V {} /DV {}",
1054 Self::encode_text_string(v),
1055 Self::encode_text_string(v)
1056 )
1057 } else {
1058 String::new()
1059 };
1060 let max_len_str = if let Some(ml) = max_length {
1061 format!(" /MaxLen {}", ml)
1062 } else {
1063 String::new()
1064 };
1065 let ap_w = field.width;
1067 let ap_h = field.height;
1068 let text_y = if *multiline {
1069 ap_h - *font_size - 2.0
1070 } else {
1071 (ap_h - *font_size) / 2.0
1072 };
1073 let ap_content = if let Some(ref v) = value {
1074 format!(
1075 "1 1 1 rg 0 0 {} {} re f \
1076 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S \
1077 BT /Helv {} Tf 0 g 2 {} Td ({}) Tj ET",
1078 ap_w,
1079 ap_h,
1080 ap_w,
1081 ap_h,
1082 font_size,
1083 text_y,
1084 Self::escape_pdf_string(v)
1085 )
1086 } else {
1087 format!(
1088 "1 1 1 rg 0 0 {} {} re f \
1089 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S",
1090 ap_w, ap_h, ap_w, ap_h
1091 )
1092 };
1093 let ap_stream_id = builder.objects.len();
1094 let ap_stream = format!(
1095 "<< /Type /XObject /Subtype /Form /BBox [0 0 {} {}] \
1096 /Resources << /Font << /Helv {} 0 R >> >> /Length {} >>\nstream\n{}\nendstream",
1097 ap_w, ap_h,
1098 helv_obj_id.unwrap_or(0),
1099 ap_content.len(),
1100 ap_content
1101 );
1102 builder.objects.push(PdfObject {
1103 id: ap_stream_id,
1104 data: ap_stream.into_bytes(),
1105 });
1106
1107 let widget_obj_id = builder.objects.len();
1108 let widget_dict = format!(
1109 "<< /Type /Annot /Subtype /Widget /FT /Tx \
1110 /T {} /Rect {} /P {}\
1111 {} /DA ({}) /Ff {}{} \
1112 /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> \
1113 /AP << /N {} 0 R >> >>",
1114 Self::encode_text_string(&field.name),
1115 rect,
1116 page_ref,
1117 v_str,
1118 da,
1119 flags,
1120 max_len_str,
1121 ap_stream_id
1122 );
1123 builder.objects.push(PdfObject {
1124 id: widget_obj_id,
1125 data: widget_dict.into_bytes(),
1126 });
1127 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1128 acroform_field_ids.push(widget_obj_id);
1129 }
1130
1131 FormFieldType::Checkbox {
1132 checked, read_only, ..
1133 } => {
1134 let state = if *checked { "Yes" } else { "Off" };
1135 let mut flags: u32 = 0;
1136 if *read_only {
1137 flags |= 1;
1138 }
1139 let ff_str = if flags > 0 {
1140 format!(" /Ff {}", flags)
1141 } else {
1142 String::new()
1143 };
1144 let widget_obj_id = builder.objects.len();
1145 let widget_dict = format!(
1146 "<< /Type /Annot /Subtype /Widget /FT /Btn \
1147 /T {} /Rect {} /P {} \
1148 /V /{} /AS /{}{} \
1149 /MK << /BC [0.6 0.6 0.6] /CA (4) >> \
1150 /AP << /N << /Yes {} 0 R /Off {} 0 R >> >> >>",
1151 Self::encode_text_string(&field.name),
1152 rect,
1153 page_ref,
1154 state,
1155 state,
1156 ff_str,
1157 checkbox_yes_stream_id,
1158 checkbox_off_stream_id,
1159 );
1160 builder.objects.push(PdfObject {
1161 id: widget_obj_id,
1162 data: widget_dict.into_bytes(),
1163 });
1164 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1165 acroform_field_ids.push(widget_obj_id);
1166 }
1167
1168 FormFieldType::Dropdown {
1169 options,
1170 value,
1171 read_only,
1172 font_size,
1173 ..
1174 } => {
1175 let mut flags: u32 = 1 << 17; if *read_only {
1177 flags |= 1;
1178 }
1179 let opts_str: String = options
1180 .iter()
1181 .map(|o| Self::encode_text_string(o))
1182 .collect::<Vec<_>>()
1183 .join(" ");
1184 let v_str = if let Some(ref v) = value {
1185 format!(" /V {}", Self::encode_text_string(v))
1186 } else {
1187 String::new()
1188 };
1189 let ap_w = field.width;
1191 let ap_h = field.height;
1192 let text_y = (ap_h - *font_size) / 2.0;
1193 let ap_content = if let Some(ref v) = value {
1194 format!(
1195 "1 1 1 rg 0 0 {} {} re f \
1196 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S \
1197 BT /Helv {} Tf 0 g 2 {} Td ({}) Tj ET",
1198 ap_w,
1199 ap_h,
1200 ap_w,
1201 ap_h,
1202 font_size,
1203 text_y,
1204 Self::escape_pdf_string(v)
1205 )
1206 } else {
1207 format!(
1208 "1 1 1 rg 0 0 {} {} re f \
1209 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S",
1210 ap_w, ap_h, ap_w, ap_h
1211 )
1212 };
1213 let ap_stream_id = builder.objects.len();
1214 let ap_stream = format!(
1215 "<< /Type /XObject /Subtype /Form /BBox [0 0 {} {}] \
1216 /Resources << /Font << /Helv {} 0 R >> >> /Length {} >>\nstream\n{}\nendstream",
1217 ap_w, ap_h,
1218 helv_obj_id.unwrap_or(0),
1219 ap_content.len(),
1220 ap_content
1221 );
1222 builder.objects.push(PdfObject {
1223 id: ap_stream_id,
1224 data: ap_stream.into_bytes(),
1225 });
1226
1227 let widget_obj_id = builder.objects.len();
1228 let widget_dict = format!(
1229 "<< /Type /Annot /Subtype /Widget /FT /Ch \
1230 /T {} /Rect {} /P {} \
1231 /Opt [{}]{} \
1232 /DA (/Helv {} Tf 0 g) /Ff {} \
1233 /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> \
1234 /AP << /N {} 0 R >> >>",
1235 Self::encode_text_string(&field.name),
1236 rect,
1237 page_ref,
1238 opts_str,
1239 v_str,
1240 font_size,
1241 flags,
1242 ap_stream_id
1243 );
1244 builder.objects.push(PdfObject {
1245 id: widget_obj_id,
1246 data: widget_dict.into_bytes(),
1247 });
1248 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1249 acroform_field_ids.push(widget_obj_id);
1250 }
1251
1252 FormFieldType::RadioButton {
1253 value,
1254 checked,
1255 read_only: _,
1256 } => {
1257 let parent_id = radio_parent_ids[&field.name];
1259 let as_value = if *checked { value.as_str() } else { "Off" };
1260 let widget_obj_id = builder.objects.len();
1261 let widget_dict = format!(
1262 "<< /Type /Annot /Subtype /Widget \
1263 /Parent {} 0 R \
1264 /Rect {} /P {} \
1265 /AS /{} \
1266 /AP << /N << /{} {} 0 R /Off {} 0 R >> >> \
1267 /MK << /BC [0.6 0.6 0.6] >> >>",
1268 parent_id,
1269 rect,
1270 page_ref,
1271 Self::escape_pdf_string(as_value),
1272 Self::escape_pdf_string(value),
1273 radio_on_stream_id,
1274 radio_off_stream_id,
1275 );
1276 builder.objects.push(PdfObject {
1277 id: widget_obj_id,
1278 data: widget_dict.into_bytes(),
1279 });
1280 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1281 radio_kid_ids
1283 .entry(field.name.clone())
1284 .or_default()
1285 .push(widget_obj_id);
1286 }
1287 }
1288 }
1289
1290 for (group_name, kid_indices) in &radio_kid_ids {
1292 let parent_id = radio_parent_ids[group_name];
1293 let checked_value = all_form_fields
1295 .iter()
1296 .filter(|f| f.name == *group_name)
1297 .find_map(|f| {
1298 if let FormFieldType::RadioButton {
1299 ref value, checked, ..
1300 } = f.field_type
1301 {
1302 if checked {
1303 Some(value.clone())
1304 } else {
1305 None
1306 }
1307 } else {
1308 None
1309 }
1310 })
1311 .unwrap_or_else(|| "Off".to_string());
1312
1313 let kids_refs: String = kid_indices
1314 .iter()
1315 .map(|id| format!("{} 0 R", id))
1316 .collect::<Vec<_>>()
1317 .join(" ");
1318
1319 let mut flags: u32 = (1 << 14) | (1 << 15); let is_read_only = all_form_fields
1322 .iter()
1323 .filter(|f| f.name == *group_name)
1324 .any(|f| {
1325 matches!(
1326 f.field_type,
1327 FormFieldType::RadioButton {
1328 read_only: true,
1329 ..
1330 }
1331 )
1332 });
1333 if is_read_only {
1334 flags |= 1;
1335 }
1336
1337 let parent_dict = format!(
1338 "<< /FT /Btn /T {} /Ff {} /Kids [{}] /V /{} >>",
1339 Self::encode_text_string(group_name),
1340 flags,
1341 kids_refs,
1342 Self::escape_pdf_string(&checked_value),
1343 );
1344 builder.objects[parent_id].data = parent_dict.into_bytes();
1345 acroform_field_ids.push(parent_id);
1346 }
1347
1348 for (page_idx, widget_ids) in per_page_widget_ids.iter().enumerate() {
1352 if widget_ids.is_empty() {
1353 continue;
1354 }
1355 let page_obj_id = page_obj_ids[page_idx];
1356 let existing_page_data =
1357 String::from_utf8_lossy(&builder.objects[page_obj_id].data).to_string();
1358
1359 let new_refs: String = widget_ids
1361 .iter()
1362 .map(|id| format!("{} 0 R", id))
1363 .collect::<Vec<_>>()
1364 .join(" ");
1365
1366 let updated = if let Some(pos) = existing_page_data.find("/Annots [") {
1367 let bracket_end = existing_page_data[pos..].find(']').unwrap() + pos;
1369 format!(
1370 "{} {}{}",
1371 &existing_page_data[..bracket_end],
1372 new_refs,
1373 &existing_page_data[bracket_end..]
1374 )
1375 } else {
1376 let end = existing_page_data.rfind(">>").unwrap();
1378 format!(
1379 "{} /Annots [{}]{}",
1380 &existing_page_data[..end],
1381 new_refs,
1382 &existing_page_data[end..]
1383 )
1384 };
1385 builder.objects[page_obj_id].data = updated.into_bytes();
1386 }
1387
1388 let acroform_id = builder.objects.len();
1390 let fields_refs: String = acroform_field_ids
1391 .iter()
1392 .map(|id| format!("{} 0 R", id))
1393 .collect::<Vec<_>>()
1394 .join(" ");
1395 let dr_str = if let Some(helv_id) = helv_obj_id {
1396 format!(" /DR << /Font << /Helv {} 0 R >> >>", helv_id)
1397 } else {
1398 String::new()
1399 };
1400 let acroform_dict = format!(
1407 "<< /Fields [{}]{} /DA (/Helv 0 Tf 0 g) >>",
1408 fields_refs, dr_str
1409 );
1410 builder.objects.push(PdfObject {
1411 id: acroform_id,
1412 data: acroform_dict.into_bytes(),
1413 });
1414 Some(acroform_id)
1415 } else {
1416 None
1417 };
1418
1419 let mut catalog = String::from("<< /Type /Catalog /Pages 2 0 R");
1421 if let Some(acroform_id) = acroform_obj_id {
1422 write!(catalog, " /AcroForm {} 0 R", acroform_id).unwrap();
1423 }
1424 if let Some(outlines_id) = outlines_obj_id {
1425 write!(
1426 catalog,
1427 " /Outlines {} 0 R /PageMode /UseOutlines",
1428 outlines_id
1429 )
1430 .unwrap();
1431 }
1432 if let Some(ref lang) = metadata.lang {
1433 write!(catalog, " /Lang ({})", Self::escape_pdf_string(lang)).unwrap();
1434 }
1435 if let Some(struct_root_id) = struct_tree_root_id {
1436 write!(
1437 catalog,
1438 " /MarkInfo << /Marked true >> /StructTreeRoot {} 0 R",
1439 struct_root_id
1440 )
1441 .unwrap();
1442 }
1443 if let Some(xmp_id) = xmp_metadata_id {
1444 write!(catalog, " /Metadata {} 0 R", xmp_id).unwrap();
1445 }
1446 if let Some(oi_id) = output_intent_id {
1447 write!(catalog, " /OutputIntents [{} 0 R]", oi_id).unwrap();
1448 }
1449 if let Some(names_id) = embedded_names_id {
1450 write!(catalog, " /Names << /EmbeddedFiles {} 0 R >>", names_id).unwrap();
1451 }
1452 if !af_filespec_ids.is_empty() {
1453 let refs = af_filespec_ids
1456 .iter()
1457 .map(|id| format!("{} 0 R", id))
1458 .collect::<Vec<_>>()
1459 .join(" ");
1460 write!(catalog, " /AF [{}]", refs).unwrap();
1461 }
1462 if pdf_ua || pdf_ua2 {
1463 catalog.push_str(" /ViewerPreferences << /DisplayDocTitle true >>");
1464 }
1465 catalog.push_str(" >>");
1466 builder.objects[1].data = catalog.into_bytes();
1467
1468 let kids: String = page_obj_ids
1470 .iter()
1471 .map(|id| format!("{} 0 R", id))
1472 .collect::<Vec<_>>()
1473 .join(" ");
1474 builder.objects[2].data = format!(
1475 "<< /Type /Pages /Kids [{}] /Count {} >>",
1476 kids,
1477 page_obj_ids.len()
1478 )
1479 .into_bytes();
1480
1481 let info_obj_id = if pdf_version == crate::model::PdfVersion::V1_7
1489 && (metadata.title.is_some() || metadata.author.is_some() || metadata.subject.is_some())
1490 {
1491 let id = builder.objects.len();
1492 let mut info = String::from("<< ");
1493 if let Some(ref title) = metadata.title {
1494 let _ = write!(info, "/Title {} ", Self::encode_text_string(title));
1495 }
1496 if let Some(ref author) = metadata.author {
1497 let _ = write!(info, "/Author {} ", Self::encode_text_string(author));
1498 }
1499 if let Some(ref subject) = metadata.subject {
1500 let _ = write!(info, "/Subject {} ", Self::encode_text_string(subject));
1501 }
1502 let _ = write!(
1503 info,
1504 "/Producer {} /Creator {} >>",
1505 Self::encode_text_string(PRODUCER),
1506 Self::encode_text_string(CREATOR_TOOL)
1507 );
1508 builder.objects.push(PdfObject {
1509 id,
1510 data: info.into_bytes(),
1511 });
1512 Some(id)
1513 } else {
1514 None
1515 };
1516
1517 let pdf = self.serialize(&builder, info_obj_id);
1518 let mut warnings = builder.warnings;
1523 for ch in builder.missing_glyphs.into_inner() {
1524 warnings.push(format!(
1525 "render defect: \"{ch}\" (U+{:04X}) is not covered by any available font and was rendered as \"?\" — register a font containing it (Font.register, the Document fonts prop, or @font-face on the HTML path)",
1526 ch as u32
1527 ));
1528 }
1529 Ok((pdf, warnings))
1530 }
1531
1532 #[allow(clippy::too_many_arguments)]
1534 fn build_content_stream_for_page(
1535 &self,
1536 page: &LayoutPage,
1537 page_idx: usize,
1538 builder: &PdfBuilder,
1539 page_number: usize,
1540 total_pages: usize,
1541 mut tag_builder: Option<&mut tagged::TagBuilder>,
1542 flatten_forms: bool,
1543 ) -> String {
1544 let mut stream = String::new();
1545 let page_height = page.height;
1546 let mut element_counter = 0usize;
1547 let mut gradient_counter = 0usize;
1548
1549 if let Some(&img_idx) = builder.page_background_image_map.get(&page_idx) {
1555 self.write_page_background(&mut stream, page, img_idx, builder);
1556 }
1557
1558 let clip_x = page.config.clip_content_x;
1562 if clip_x {
1563 let x = page.config.margin.left;
1564 let w = page.width - page.config.margin.left - page.config.margin.right;
1565 stream.push_str(&format!(
1566 "q\n{:.2} 0 {:.2} {:.2} re W n\n",
1567 x, w, page.height
1568 ));
1569 }
1570
1571 for element in &page.elements {
1572 self.write_element(
1573 &mut stream,
1574 element,
1575 page_height,
1576 builder,
1577 page_idx,
1578 &mut element_counter,
1579 &mut gradient_counter,
1580 page_number,
1581 total_pages,
1582 tag_builder.as_deref_mut(),
1583 flatten_forms,
1584 );
1585 }
1586
1587 if clip_x {
1588 stream.push_str("Q\n");
1589 }
1590
1591 stream
1592 }
1593
1594 #[allow(clippy::too_many_arguments)]
1596 #[allow(clippy::too_many_arguments)]
1597 fn write_element(
1598 &self,
1599 stream: &mut String,
1600 element: &LayoutElement,
1601 page_height: f64,
1602 builder: &PdfBuilder,
1603 page_idx: usize,
1604 element_counter: &mut usize,
1605 gradient_counter: &mut usize,
1606 page_number: usize,
1607 total_pages: usize,
1608 mut tag_builder: Option<&mut tagged::TagBuilder>,
1609 flatten_forms: bool,
1610 ) {
1611 let mut is_artifact = false;
1614 let mut artifact_own_draw = false;
1619 let mut inline_links: std::collections::VecDeque<usize> = std::collections::VecDeque::new();
1623 let mut bdc_role: Option<&'static str> = None;
1624 let tagged_mcid = if let Some(ref mut tb) = tag_builder {
1625 if let Some(ref nt) = element.node_type {
1626 if nt == "Watermark" {
1627 let _ = writeln!(stream, "/Artifact BMC");
1629 is_artifact = true;
1630 None
1631 } else {
1632 let is_header = element.is_header_row;
1633 let href = element.href.as_deref();
1634 let wrapper = !element.children.is_empty()
1638 && matches!(element.draw, DrawCommand::None | DrawCommand::Rect { .. });
1639 let mcid = tb.begin_element_as(
1640 nt,
1641 is_header,
1642 element.alt.as_deref(),
1643 page_idx,
1644 href,
1645 element.col_span,
1646 element.list_numbering,
1647 element.actual_text.as_deref(),
1648 wrapper,
1649 );
1650 if let Some(ref bm) = element.bookmark {
1654 tb.note_bookmark(bm);
1655 }
1656 if href.is_none() && !tb.inside_link() {
1664 if let DrawCommand::Text { ref lines, .. } = element.draw {
1665 for line in lines {
1666 for span in Self::inline_link_spans(line) {
1667 inline_links
1668 .push_back(tb.add_inline_link(page_idx, &span.href));
1669 }
1670 }
1671 }
1672 }
1673 match mcid {
1674 Some(mcid) => {
1675 let role = if href.is_some() {
1678 "Link"
1679 } else {
1680 tb.map_role_public(nt, is_header)
1681 };
1682 let _ = writeln!(stream, "/{} <</MCID {}>> BDC", role, mcid);
1683 bdc_role = Some(role);
1684 Some(mcid)
1685 }
1686 None => {
1687 artifact_own_draw = true;
1688 None
1689 }
1690 }
1691 }
1692 } else if !matches!(element.draw, DrawCommand::None) {
1693 let _ = writeln!(stream, "/Artifact BMC");
1695 is_artifact = true;
1696 None
1697 } else {
1698 None
1699 }
1700 } else {
1701 None
1702 };
1703
1704 let needs_element_opacity = element.opacity < 1.0;
1712 if needs_element_opacity {
1713 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&element.opacity.to_bits()) {
1714 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1715 }
1716 }
1717
1718 let transform_ops: &[TransformOp] = element
1724 .resolved_style
1725 .as_ref()
1726 .map(|s| s.transform.as_slice())
1727 .unwrap_or(&[]);
1728 let has_transform = !transform_ops.is_empty();
1729 if has_transform {
1730 let rs = element.resolved_style.as_ref().unwrap();
1731 let pdf_x = element.x;
1732 let pdf_y_bottom = page_height - element.y - element.height;
1733 let (ox_frac, oy_frac) = rs.transform_origin;
1734 let origin_x = pdf_x + element.width * ox_frac;
1735 let origin_y = pdf_y_bottom + (1.0 - oy_frac) * element.height;
1738
1739 let _ = writeln!(stream, "q");
1740 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", -origin_x, -origin_y);
1742 for op in transform_ops.iter().rev() {
1750 match op {
1751 TransformOp::Rotate { deg } => {
1752 let theta = (-deg).to_radians();
1757 let c = theta.cos();
1758 let s = theta.sin();
1759 let _ = writeln!(stream, "{:.6} {:.6} {:.6} {:.6} 0 0 cm", c, s, -s, c);
1760 }
1761 TransformOp::Scale { x, y } => {
1762 let _ = writeln!(stream, "{:.6} 0 0 {:.6} 0 0 cm", x, y);
1763 }
1764 TransformOp::Translate { x, y } => {
1765 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", x, -y);
1767 }
1768 }
1769 }
1770 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", origin_x, origin_y);
1772 }
1773
1774 let wrap_own_draw_as_artifact =
1781 artifact_own_draw && !matches!(element.draw, DrawCommand::None);
1782 if wrap_own_draw_as_artifact {
1783 let _ = writeln!(stream, "/Artifact BMC");
1784 }
1785
1786 match &element.draw {
1787 DrawCommand::None => {}
1788
1789 DrawCommand::Rect {
1790 background,
1791 border_width,
1792 border_color,
1793 border_style,
1794 border_radius,
1795 opacity,
1796 box_shadow,
1797 background_gradient,
1798 } => {
1799 let x = element.x;
1800 let y = page_height - element.y - element.height;
1801 let w = element.width;
1802 let h = element.height;
1803
1804 let needs_opacity = *opacity < 1.0;
1806 if needs_opacity {
1807 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1808 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1809 }
1810 }
1811
1812 if let Some(shadow) = box_shadow {
1818 if shadow.color.a > 0.0 {
1819 let sx = x + shadow.offset_x;
1824 let sy = y - shadow.offset_y;
1825 let needs_shadow_alpha = shadow.color.a < 1.0;
1826 if needs_shadow_alpha {
1827 if let Some((_, gs_name)) =
1828 builder.ext_gstate_map.get(&shadow.color.a.to_bits())
1829 {
1830 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1831 } else {
1832 let _ = writeln!(stream, "q");
1833 }
1834 } else {
1835 let _ = writeln!(stream, "q");
1836 }
1837 let _ = writeln!(
1838 stream,
1839 "{:.3} {:.3} {:.3} rg",
1840 shadow.color.r, shadow.color.g, shadow.color.b
1841 );
1842 if border_radius.top_left > 0.0
1843 || border_radius.top_right > 0.0
1844 || border_radius.bottom_right > 0.0
1845 || border_radius.bottom_left > 0.0
1846 {
1847 self.write_rounded_rect(stream, sx, sy, w, h, border_radius);
1848 } else {
1849 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", sx, sy, w, h);
1850 }
1851 let _ = writeln!(stream, "f\nQ");
1852 }
1853 }
1854
1855 if background_gradient.is_some() {
1861 let key = (page_idx, *gradient_counter);
1862 *gradient_counter += 1;
1863 if let Some((_, sh_name)) = builder.shading_map.get(&key) {
1864 let _ = writeln!(stream, "q");
1865 if border_radius.top_left > 0.0
1867 || border_radius.top_right > 0.0
1868 || border_radius.bottom_right > 0.0
1869 || border_radius.bottom_left > 0.0
1870 {
1871 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1872 let _ = writeln!(stream, "W n");
1873 } else {
1874 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re W n", x, y, w, h);
1875 }
1876 let _ =
1879 writeln!(stream, "1 0 0 1 {:.3} {:.3} cm\n/{} sh\nQ", x, y, sh_name);
1880 }
1881 } else if let Some(bg) = background {
1882 if bg.a > 0.0 {
1883 let _ = writeln!(stream, "q\n{:.3} {:.3} {:.3} rg", bg.r, bg.g, bg.b);
1884
1885 if border_radius.top_left > 0.0 {
1886 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1887 } else {
1888 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1889 }
1890
1891 let _ = writeln!(stream, "f\nQ");
1892 }
1893 }
1894
1895 let bw = border_width;
1896 if bw.top > 0.0 || bw.right > 0.0 || bw.bottom > 0.0 || bw.left > 0.0 {
1897 use crate::style::BorderStyle::Solid;
1898 let all_solid = border_style.top == Solid
1899 && border_style.right == Solid
1900 && border_style.bottom == Solid
1901 && border_style.left == Solid;
1902 if all_solid
1908 && (bw.top - bw.right).abs() < 0.001
1909 && (bw.right - bw.bottom).abs() < 0.001
1910 && (bw.bottom - bw.left).abs() < 0.001
1911 {
1912 let bc = &border_color.top;
1913 let _ = writeln!(
1914 stream,
1915 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w",
1916 bc.r, bc.g, bc.b, bw.top
1917 );
1918
1919 if border_radius.top_left > 0.0 {
1920 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1921 } else {
1922 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1923 }
1924
1925 let _ = writeln!(stream, "S\nQ");
1926 } else {
1927 self.write_border_sides(stream, x, y, w, h, bw, border_color, border_style);
1928 }
1929 }
1930
1931 if needs_opacity {
1932 let _ = writeln!(stream, "Q");
1933 }
1934 }
1935
1936 DrawCommand::Text {
1937 lines,
1938 color,
1939 text_decoration,
1940 opacity,
1941 } => {
1942 let needs_opacity = *opacity < 1.0;
1944 if needs_opacity {
1945 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1946 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1947 }
1948 }
1949
1950 let mut tw_set = false;
1956 let tag_links = bdc_role.is_some() && !inline_links.is_empty();
1963 for line in lines {
1964 if line.glyphs.is_empty() {
1965 continue;
1966 }
1967
1968 let absolute_text = line
1969 .glyphs
1970 .iter()
1971 .any(|g| glyph_mapping(g) != extraction_text(g));
1972
1973 let groups = Self::group_glyphs(&line.glyphs, tag_links);
1976 let group_links = if tag_links {
1977 Self::group_link_runs(&groups)
1978 } else {
1979 vec![None; groups.len()]
1980 };
1981 let mut open_link: Option<&str> = None;
1982 let pdf_y = page_height - line.y;
1983
1984 let _ = writeln!(stream, "BT");
1985
1986 if line.word_spacing.abs() > 0.001 {
1988 let _ = writeln!(stream, "{:.4} Tw", line.word_spacing);
1989 tw_set = true;
1990 } else if tw_set {
1991 let _ = writeln!(stream, "0 Tw");
1992 tw_set = false;
1993 }
1994
1995 let mut tm_x = 0.0_f64;
1997 let mut tm_y = 0.0_f64;
1998 let mut x_cursor = line.x;
1999
2000 let mut group_spans: Vec<(f64, f64, TextDecoration, Color)> = Vec::new();
2002
2003 for (gi, group) in groups.iter().enumerate() {
2004 let want = group_links[gi];
2005 if want != open_link {
2006 let _ = writeln!(stream, "ET\nEMC");
2007 let role = bdc_role.unwrap_or("Span");
2008 let tb = tag_builder.as_mut().expect("tag_links implies tagging");
2009 let link = want.and_then(|_| inline_links.pop_front());
2010 match link {
2011 Some(idx) => {
2012 let mcid = tb.attach_inline_link(idx, page_idx);
2013 let _ = writeln!(stream, "/Link <</MCID {}>> BDC", mcid);
2014 open_link = want;
2015 }
2016 None => {
2017 let mcid = tb.continue_current(page_idx);
2018 let _ = writeln!(stream, "/{} <</MCID {}>> BDC", role, mcid);
2019 open_link = None;
2020 }
2021 }
2022 let _ = writeln!(stream, "BT");
2024 tm_x = 0.0;
2025 tm_y = 0.0;
2026 }
2027 let first = &group[0];
2028 let glyph_color = first.color.unwrap_or(*color);
2029
2030 let idx = self.font_index(
2031 &first.font_family,
2032 first.font_weight,
2033 first.font_style,
2034 &builder.font_objects,
2035 );
2036 let italic =
2037 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
2038 let font_key = FontKey {
2039 family: first.font_family.to_string(),
2040 weight: first.font_weight,
2041 italic,
2042 };
2043 let font_name = format!("F{}", idx);
2044
2045 if (builder.custom_font_data.contains_key(&font_key)
2053 && !has_placeholder_group(group))
2054 || line.word_spacing.abs() > 0.001
2055 {
2056 x_cursor = line.x + first.x_offset;
2057 }
2058 let dx = x_cursor - tm_x;
2060 let dy = pdf_y - tm_y;
2061 if absolute_text {
2062 let _ = writeln!(
2063 stream,
2064 "{:.3} {:.3} {:.3} rg\n/{} {:.1} Tf\n{:.2} Tc\n1 0 0 1 {} {} Tm",
2065 glyph_color.r,
2066 glyph_color.g,
2067 glyph_color.b,
2068 font_name,
2069 first.font_size,
2070 first.letter_spacing,
2071 pdf_number(x_cursor),
2072 pdf_number(pdf_y)
2073 );
2074 } else {
2075 let _ = writeln!(
2076 stream,
2077 "{:.3} {:.3} {:.3} rg\n/{} {:.1} Tf\n{:.2} Tc\n{:.2} {:.2} Td",
2078 glyph_color.r,
2079 glyph_color.g,
2080 glyph_color.b,
2081 font_name,
2082 first.font_size,
2083 first.letter_spacing,
2084 dx,
2085 dy
2086 );
2087 }
2088 tm_x = x_cursor;
2089 tm_y = pdf_y;
2090
2091 let actual_text =
2092 Self::begin_extraction_span(stream, group, page_number, total_pages);
2093
2094 let raw_text: String = group.iter().map(|g| g.char_value).collect();
2096 let has_placeholder = raw_text.contains(PAGE_NUMBER_SENTINEL)
2097 || raw_text.contains(TOTAL_PAGES_SENTINEL);
2098
2099 let is_custom = builder.custom_font_data.contains_key(&font_key);
2100
2101 if is_custom {
2102 if let Some(embed_data) = builder.custom_font_data.get(&font_key) {
2103 let mut hex = String::new();
2104 let mut continue_after_show = false;
2106 if has_placeholder {
2107 let pn = PAGE_NUMBER_SENTINEL.to_string();
2109 let tp = TOTAL_PAGES_SENTINEL.to_string();
2110 let text_after = raw_text
2111 .replace(&pn, &page_number.to_string())
2112 .replace(&tp, &total_pages.to_string());
2113 for ch in text_after.chars() {
2114 let gid =
2115 embed_data.char_to_gid.get(&ch).copied().unwrap_or(0);
2116 let _ = write!(hex, "{:04X}", gid);
2117 }
2118 } else {
2119 let gids: Vec<u16> = group
2121 .iter()
2122 .map(|g| {
2123 embed_data
2124 .glyph_to_cid
2125 .get(&(g.glyph_id, glyph_mapping(g)))
2126 .copied()
2127 .unwrap_or_else(|| {
2128 embed_data
2130 .char_to_gid
2131 .get(&g.char_value)
2132 .copied()
2133 .unwrap_or(0)
2134 })
2135 })
2136 .collect();
2137 if let Some(tj) = Self::positioned_tj(
2138 group,
2139 &gids,
2140 embed_data,
2141 first.letter_spacing,
2142 x_cursor,
2143 pdf_y,
2144 ) {
2145 let _ = writeln!(stream, "{}", tj);
2146 continue_after_show = true;
2147 } else {
2148 for gid in &gids {
2149 let _ = write!(hex, "{:04X}", gid);
2150 }
2151 }
2152 }
2153 if !continue_after_show {
2154 let _ = writeln!(stream, "<{}> Tj", hex);
2155 }
2156 } else {
2157 let _ = writeln!(stream, "<> Tj");
2158 }
2159 } else {
2160 let pn = PAGE_NUMBER_SENTINEL.to_string();
2161 let tp = TOTAL_PAGES_SENTINEL.to_string();
2162 let text_after = raw_text
2163 .replace(&pn, &page_number.to_string())
2164 .replace(&tp, &total_pages.to_string());
2165 let mut text_str = String::new();
2166 for ch in text_after.chars() {
2167 let b = Self::unicode_to_winansi(ch).unwrap_or_else(|| {
2168 builder.missing_glyphs.borrow_mut().insert(ch);
2169 b'?'
2170 });
2171 match b {
2172 b'\\' => text_str.push_str("\\\\"),
2173 b'(' => text_str.push_str("\\("),
2174 b')' => text_str.push_str("\\)"),
2175 0x20..=0x7E => text_str.push(b as char),
2176 _ => {
2177 let _ = write!(text_str, "\\{:03o}", b);
2178 }
2179 }
2180 }
2181 let _ = writeln!(stream, "({}) Tj", text_str);
2182 }
2183
2184 if actual_text {
2185 stream.push_str("EMC\n");
2186 }
2187
2188 let group_start_x = x_cursor;
2190
2191 if let Some(last) = group.last() {
2198 x_cursor = line.x + last.x_offset + last.x_advance;
2199 }
2200
2201 let group_dec = first.text_decoration;
2203 if !matches!(group_dec, TextDecoration::None) {
2204 group_spans.push((group_start_x, x_cursor, group_dec, glyph_color));
2205 }
2206 }
2207
2208 let _ = writeln!(stream, "ET");
2209 if open_link.is_some() {
2213 let tb = tag_builder.as_mut().expect("tag_links implies tagging");
2214 let mcid = tb.continue_current(page_idx);
2215 let _ = writeln!(
2216 stream,
2217 "EMC\n/{} <</MCID {}>> BDC",
2218 bdc_role.unwrap_or("Span"),
2219 mcid
2220 );
2221 }
2222
2223 for (span_x, span_end_x, dec, dec_color) in &group_spans {
2225 match dec {
2226 TextDecoration::Underline => {
2227 let underline_y = pdf_y - 1.5;
2228 let _ = write!(
2229 stream,
2230 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2231 dec_color.r, dec_color.g, dec_color.b,
2232 span_x, underline_y,
2233 span_end_x, underline_y
2234 );
2235 }
2236 TextDecoration::LineThrough => {
2237 let first_size =
2238 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
2239 let strikethrough_y = pdf_y + first_size * 0.3;
2240 let _ = write!(
2241 stream,
2242 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2243 dec_color.r, dec_color.g, dec_color.b,
2244 span_x, strikethrough_y,
2245 span_end_x, strikethrough_y
2246 );
2247 }
2248 TextDecoration::None => {}
2249 }
2250 }
2251
2252 if group_spans.is_empty() {
2254 if matches!(text_decoration, TextDecoration::Underline) {
2255 let underline_y = pdf_y - 1.5;
2256 let _ = write!(
2257 stream,
2258 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2259 color.r, color.g, color.b,
2260 line.x, underline_y,
2261 line.x + line.width, underline_y
2262 );
2263 }
2264 if matches!(text_decoration, TextDecoration::LineThrough) {
2265 let first_size =
2266 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
2267 let strikethrough_y = pdf_y + first_size * 0.3;
2268 let _ = write!(
2269 stream,
2270 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2271 color.r, color.g, color.b,
2272 line.x, strikethrough_y,
2273 line.x + line.width, strikethrough_y
2274 );
2275 }
2276 }
2277 }
2278
2279 if tw_set {
2283 let _ = writeln!(stream, "0 Tw");
2284 }
2285 if let Some(tb) = tag_builder.as_mut() {
2289 while let Some(idx) = inline_links.pop_front() {
2290 tb.attach_inline_link_without_content(idx);
2291 }
2292 }
2293
2294 if needs_opacity {
2295 let _ = writeln!(stream, "Q");
2296 }
2297 }
2298
2299 DrawCommand::Image { .. } => {
2300 let elem_idx = *element_counter;
2301 *element_counter += 1;
2302 if let Some(&img_idx) = builder.image_index_map.get(&(page_idx, elem_idx)) {
2303 let x = element.x;
2304 let y = page_height - element.y - element.height;
2305 let _ = write!(
2306 stream,
2307 "q\n{:.4} 0 0 {:.4} {:.2} {:.2} cm\n/Im{} Do\nQ\n",
2308 element.width, element.height, x, y, img_idx
2309 );
2310 } else {
2311 let x = element.x;
2313 let y = page_height - element.y - element.height;
2314 let _ = write!(
2315 stream,
2316 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
2317 x, y, element.width, element.height
2318 );
2319 }
2320 if tagged_mcid.is_some() {
2321 let _ = writeln!(stream, "EMC");
2322 if let Some(ref mut tb) = tag_builder {
2323 tb.end_element();
2324 }
2325 } else if is_artifact {
2326 let _ = writeln!(stream, "EMC");
2327 } else if wrap_own_draw_as_artifact {
2328 let _ = writeln!(stream, "EMC");
2332 if let Some(ref mut tb) = tag_builder {
2333 tb.end_element();
2334 }
2335 }
2336 return; }
2338
2339 DrawCommand::ImagePlaceholder => {
2340 *element_counter += 1;
2341 let x = element.x;
2342 let y = page_height - element.y - element.height;
2343 let _ = write!(
2344 stream,
2345 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
2346 x, y, element.width, element.height
2347 );
2348 if tagged_mcid.is_some() {
2349 let _ = writeln!(stream, "EMC");
2350 if let Some(ref mut tb) = tag_builder {
2351 tb.end_element();
2352 }
2353 } else if is_artifact {
2354 let _ = writeln!(stream, "EMC");
2355 } else if wrap_own_draw_as_artifact {
2356 let _ = writeln!(stream, "EMC");
2360 if let Some(ref mut tb) = tag_builder {
2361 tb.end_element();
2362 }
2363 }
2364 return;
2365 }
2366
2367 DrawCommand::Svg {
2368 commands,
2369 width: _svg_w,
2370 height: _svg_h,
2371 viewbox_min_x,
2372 viewbox_min_y,
2373 viewbox_width,
2374 viewbox_height,
2375 clip,
2376 } => {
2377 let x = element.x;
2378 let y = page_height - element.y - element.height;
2379
2380 let _ = writeln!(stream, "q");
2382 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", x, y);
2383
2384 if *viewbox_width > 0.0 && *viewbox_height > 0.0 {
2390 let raw_sx = element.width / *viewbox_width;
2391 let raw_sy = element.height / *viewbox_height;
2392 let s = raw_sx.min(raw_sy);
2393 let tx = (element.width - s * *viewbox_width) / 2.0;
2394 let ty = (element.height - s * *viewbox_height) / 2.0;
2395 let _ = writeln!(stream, "{:.4} 0 0 {:.4} {:.2} {:.2} cm", s, s, tx, ty);
2396 }
2397
2398 let _ = writeln!(stream, "1 0 0 -1 0 {:.2} cm", *viewbox_height);
2401
2402 if *viewbox_min_x != 0.0 || *viewbox_min_y != 0.0 {
2404 let _ = writeln!(
2405 stream,
2406 "1 0 0 1 {:.2} {:.2} cm",
2407 -*viewbox_min_x, -*viewbox_min_y
2408 );
2409 }
2410
2411 if *clip {
2413 let _ = writeln!(
2414 stream,
2415 "{:.2} {:.2} {:.2} {:.2} re W n",
2416 *viewbox_min_x, *viewbox_min_y, *viewbox_width, *viewbox_height
2417 );
2418 }
2419
2420 Self::write_svg_commands(stream, commands, &builder.ext_gstate_map);
2421
2422 let _ = writeln!(stream, "Q");
2423 if tagged_mcid.is_some() {
2424 let _ = writeln!(stream, "EMC");
2425 if let Some(ref mut tb) = tag_builder {
2426 tb.end_element();
2427 }
2428 } else if is_artifact {
2429 let _ = writeln!(stream, "EMC");
2430 } else if wrap_own_draw_as_artifact {
2431 let _ = writeln!(stream, "EMC");
2435 if let Some(ref mut tb) = tag_builder {
2436 tb.end_element();
2437 }
2438 }
2439 return;
2440 }
2441
2442 DrawCommand::Barcode {
2443 bars,
2444 bar_width,
2445 height,
2446 color,
2447 } => {
2448 *element_counter += 1;
2449 let _ = writeln!(stream, "q");
2450 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2451 for (i, &bar) in bars.iter().enumerate() {
2452 if bar == 1 {
2453 let bx = element.x + i as f64 * bar_width;
2454 let by = page_height - element.y - height;
2455 let _ = writeln!(
2456 stream,
2457 "{:.2} {:.2} {:.2} {:.2} re",
2458 bx, by, bar_width, height
2459 );
2460 }
2461 }
2462 let _ = writeln!(stream, "f\nQ");
2463 if tagged_mcid.is_some() {
2464 let _ = writeln!(stream, "EMC");
2465 if let Some(ref mut tb) = tag_builder {
2466 tb.end_element();
2467 }
2468 } else if is_artifact {
2469 let _ = writeln!(stream, "EMC");
2470 } else if wrap_own_draw_as_artifact {
2471 let _ = writeln!(stream, "EMC");
2475 if let Some(ref mut tb) = tag_builder {
2476 tb.end_element();
2477 }
2478 }
2479 return;
2480 }
2481
2482 DrawCommand::QrCode {
2483 modules,
2484 module_size,
2485 color,
2486 } => {
2487 *element_counter += 1;
2488 let _ = writeln!(stream, "q");
2489 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2490 for (row_idx, row) in modules.iter().enumerate() {
2491 for (col_idx, &dark) in row.iter().enumerate() {
2492 if dark {
2493 let mx = element.x + col_idx as f64 * module_size;
2494 let my = page_height - element.y - (row_idx as f64 + 1.0) * module_size;
2495 let _ = writeln!(
2496 stream,
2497 "{:.2} {:.2} {:.2} {:.2} re",
2498 mx, my, module_size, module_size
2499 );
2500 }
2501 }
2502 }
2503 let _ = writeln!(stream, "f\nQ");
2504 if tagged_mcid.is_some() {
2505 let _ = writeln!(stream, "EMC");
2506 if let Some(ref mut tb) = tag_builder {
2507 tb.end_element();
2508 }
2509 } else if is_artifact {
2510 let _ = writeln!(stream, "EMC");
2511 } else if wrap_own_draw_as_artifact {
2512 let _ = writeln!(stream, "EMC");
2516 if let Some(ref mut tb) = tag_builder {
2517 tb.end_element();
2518 }
2519 }
2520 return;
2521 }
2522
2523 DrawCommand::Chart { primitives } => {
2524 *element_counter += 1;
2525 let _ = writeln!(stream, "q");
2526 let _ = writeln!(
2528 stream,
2529 "1 0 0 -1 {:.4} {:.4} cm",
2530 element.x,
2531 page_height - element.y
2532 );
2533
2534 for prim in primitives {
2535 write_chart_primitive(stream, prim, element.height, builder);
2536 }
2537
2538 let _ = writeln!(stream, "Q");
2539 if tagged_mcid.is_some() {
2540 let _ = writeln!(stream, "EMC");
2541 if let Some(ref mut tb) = tag_builder {
2542 tb.end_element();
2543 }
2544 } else if is_artifact {
2545 let _ = writeln!(stream, "EMC");
2546 } else if wrap_own_draw_as_artifact {
2547 let _ = writeln!(stream, "EMC");
2551 if let Some(ref mut tb) = tag_builder {
2552 tb.end_element();
2553 }
2554 }
2555 return;
2556 }
2557
2558 DrawCommand::Watermark {
2559 lines,
2560 color,
2561 opacity,
2562 angle_rad,
2563 font_family: _,
2564 } => {
2565 let _ = writeln!(stream, "q");
2566 if *opacity < 1.0 {
2568 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
2569 let _ = writeln!(stream, "/{} gs", gs_name);
2570 }
2571 }
2572 let pdf_cx = element.x;
2574 let pdf_cy = page_height - element.y;
2575 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", pdf_cx, pdf_cy);
2576 let cos_a = angle_rad.cos();
2578 let sin_a = angle_rad.sin();
2579 let _ = writeln!(
2580 stream,
2581 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
2582 cos_a, sin_a, -sin_a, cos_a
2583 );
2584 let _ = writeln!(stream, "BT");
2586 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2587 if let Some(line) = lines.first() {
2588 let groups = Self::group_glyphs_by_style(&line.glyphs);
2589 let text_width = line.width;
2590 let cap_height = line.height * 0.7;
2591 let _ = writeln!(
2592 stream,
2593 "{:.2} {:.2} Td",
2594 -text_width / 2.0,
2595 -cap_height / 2.0
2596 );
2597 for group in &groups {
2598 let first = &group[0];
2599 let italic =
2600 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
2601 let fk = FontKey {
2602 family: first.font_family.to_string(),
2603 weight: first.font_weight,
2604 italic,
2605 };
2606 let idx = self.font_index(
2607 &first.font_family,
2608 first.font_weight,
2609 first.font_style,
2610 &builder.font_objects,
2611 );
2612 let font_name = format!("F{}", idx);
2613 let _ = writeln!(stream, "/{} {:.1} Tf", font_name, first.font_size);
2614 let is_custom = builder.custom_font_data.contains_key(&fk);
2615 if is_custom {
2616 if let Some(embed_data) = builder.custom_font_data.get(&fk) {
2617 let gids: Vec<_> = group
2618 .iter()
2619 .map(|g| {
2620 embed_data
2621 .glyph_to_cid
2622 .get(&(g.glyph_id, glyph_mapping(g)))
2623 .copied()
2624 .unwrap_or(0)
2625 })
2626 .collect();
2627 let actual_text = Self::begin_extraction_span(
2628 stream,
2629 group,
2630 page_number,
2631 total_pages,
2632 );
2633 let positioned = if actual_text {
2634 let x = -text_width / 2.0 + first.x_offset;
2635 let y = -cap_height / 2.0;
2636 let _ = writeln!(
2637 stream,
2638 "1 0 0 1 {} {} Tm",
2639 pdf_number(x),
2640 pdf_number(y)
2641 );
2642 Self::positioned_tj(group, &gids, embed_data, 0.0, x, y)
2643 } else {
2644 None
2645 };
2646 if let Some(tj) = positioned {
2647 let _ = writeln!(stream, "{}", tj);
2648 } else {
2649 let hex: String =
2650 gids.iter().map(|gid| format!("{:04X}", gid)).collect();
2651 let _ = writeln!(stream, "<{}> Tj", hex);
2652 }
2653 if actual_text {
2654 stream.push_str("EMC\n");
2655 }
2656 }
2657 } else {
2658 let hex_str: String = group
2659 .iter()
2660 .map(|g| format!("{:02X}", g.glyph_id as u8))
2661 .collect();
2662 let _ = writeln!(stream, "<{}> Tj", hex_str);
2663 }
2664 }
2665 }
2666 let _ = writeln!(stream, "ET");
2667 let _ = writeln!(stream, "Q");
2668 if tagged_mcid.is_some() {
2669 let _ = writeln!(stream, "EMC");
2670 if let Some(ref mut tb) = tag_builder {
2671 tb.end_element();
2672 }
2673 } else if is_artifact {
2674 let _ = writeln!(stream, "EMC");
2675 } else if wrap_own_draw_as_artifact {
2676 let _ = writeln!(stream, "EMC");
2680 if let Some(ref mut tb) = tag_builder {
2681 tb.end_element();
2682 }
2683 }
2684 return;
2685 }
2686
2687 DrawCommand::FormField { field_type, .. } => {
2688 let pdf_x = element.x;
2692 let pdf_y = page_height - element.y - element.height;
2693 let w = element.width;
2694 let h = element.height;
2695 let _ = writeln!(stream, "q");
2696 match field_type {
2697 FormFieldType::Checkbox { checked, .. } => {
2698 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
2701 let _ =
2702 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
2703 if *checked {
2704 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
2706 let sx = w / 14.0;
2707 let sy = h / 14.0;
2708 let _ = writeln!(
2709 stream,
2710 "{:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l f",
2711 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
2712 pdf_x + 5.5 * sx, pdf_y + 2.0 * sy,
2713 pdf_x + 12.0 * sx, pdf_y + 11.0 * sy,
2714 pdf_x + 11.0 * sx, pdf_y + 12.0 * sy,
2715 pdf_x + 5.5 * sx, pdf_y + 4.5 * sy,
2716 pdf_x + 3.0 * sx, pdf_y + 7.0 * sy,
2717 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
2718 );
2719 }
2720 }
2721 FormFieldType::RadioButton { checked, .. } => {
2722 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
2725 let _ =
2726 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
2727 if *checked {
2728 let cx = pdf_x + w / 2.0;
2730 let cy = pdf_y + h / 2.0;
2731 let r = (w.min(h) / 2.0) * 0.6;
2732 let k = r * 0.5523;
2733 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
2734 let _ = writeln!(
2735 stream,
2736 "{:.2} {:.2} m {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c f",
2737 cx, cy + r,
2738 cx + k, cy + r, cx + r, cy + k, cx + r, cy,
2739 cx + r, cy - k, cx + k, cy - r, cx, cy - r,
2740 cx - k, cy - r, cx - r, cy - k, cx - r, cy,
2741 cx - r, cy + k, cx - k, cy + r, cx, cy + r,
2742 );
2743 }
2744 }
2745 FormFieldType::TextField {
2746 value,
2747 placeholder,
2748 font_size,
2749 multiline,
2750 password,
2751 ..
2752 } => {
2753 let _ = writeln!(stream, "1 1 1 rg");
2755 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2756 let _ = writeln!(stream, "0.5 w");
2757 let _ =
2758 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2759 if flatten_forms {
2761 let has_value = value.as_ref().is_some_and(|v| !v.is_empty());
2762 if has_value {
2763 let val = value.as_ref().unwrap();
2764 let display_text = if *password {
2765 "\u{2022}".repeat(val.len())
2766 } else {
2767 val.clone()
2768 };
2769 let font_idx = builder
2770 .font_objects
2771 .iter()
2772 .enumerate()
2773 .find(|(_, (key, _))| {
2774 key.family == "Helvetica"
2775 && key.weight == 400
2776 && !key.italic
2777 })
2778 .map(|(i, _)| i)
2779 .unwrap_or(0);
2780 if *multiline {
2781 let metrics = crate::font::StandardFont::Helvetica.metrics();
2783 let max_w = w - 4.0;
2784 let mut lines: Vec<String> = Vec::new();
2785 for paragraph in display_text.split('\n') {
2786 let mut line = String::new();
2787 let mut line_w = 0.0;
2788 for word in paragraph.split_whitespace() {
2789 let word_w =
2790 metrics.measure_string(word, *font_size, 0.0);
2791 let space_w = if line.is_empty() {
2792 0.0
2793 } else {
2794 metrics.measure_string(" ", *font_size, 0.0)
2795 };
2796 if word_w > max_w {
2798 let mut char_line = String::new();
2799 let mut char_w = 0.0;
2800 for ch in word.chars() {
2801 let cw = metrics.char_width(ch, *font_size);
2802 if !char_line.is_empty() && char_w + cw > max_w
2803 {
2804 if !line.is_empty() {
2805 lines.push(line.clone());
2806 line.clear();
2807 line_w = 0.0;
2808 }
2809 lines.push(char_line.clone());
2810 char_line.clear();
2811 char_w = 0.0;
2812 }
2813 char_line.push(ch);
2814 char_w += cw;
2815 }
2816 if !char_line.is_empty() {
2818 if !line.is_empty() {
2819 line.push(' ');
2820 line_w += metrics
2821 .measure_string(" ", *font_size, 0.0);
2822 }
2823 line.push_str(&char_line);
2824 line_w += char_w;
2825 }
2826 continue;
2827 }
2828 if !line.is_empty() && line_w + space_w + word_w > max_w
2829 {
2830 lines.push(line.clone());
2831 line.clear();
2832 line_w = 0.0;
2833 }
2834 if !line.is_empty() {
2835 line.push(' ');
2836 line_w += space_w;
2837 }
2838 line.push_str(word);
2839 line_w += word_w;
2840 }
2841 if !line.is_empty() {
2842 lines.push(line);
2843 }
2844 }
2845 let text_y = pdf_y + h - font_size - 2.0;
2846 for (i, line_text) in lines.iter().enumerate() {
2847 let ly = text_y - (i as f64) * (font_size * 1.2);
2848 if ly < pdf_y {
2849 break;
2850 }
2851 let esc = Self::encode_winansi_text(builder, line_text);
2852 let _ = writeln!(
2853 stream,
2854 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2855 font_idx,
2856 font_size,
2857 pdf_x + 2.0,
2858 ly,
2859 esc
2860 );
2861 }
2862 } else {
2863 let escaped = Self::encode_winansi_text(builder, &display_text);
2864 let text_y = pdf_y + (h - font_size) / 2.0;
2865 let _ = writeln!(
2866 stream,
2867 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2868 font_idx,
2869 font_size,
2870 pdf_x + 2.0,
2871 text_y,
2872 escaped
2873 );
2874 }
2875 } else if let Some(ref ph) = placeholder {
2876 if !ph.is_empty() {
2877 let font_idx = builder
2879 .font_objects
2880 .iter()
2881 .enumerate()
2882 .find(|(_, (key, _))| {
2883 key.family == "Helvetica"
2884 && key.weight == 400
2885 && !key.italic
2886 })
2887 .map(|(i, _)| i)
2888 .unwrap_or(0);
2889 let escaped = Self::encode_winansi_text(builder, ph);
2890 let text_y = pdf_y + (h - font_size) / 2.0;
2891 let _ = writeln!(
2892 stream,
2893 "BT /F{} {:.1} Tf 0.6 g {:.2} {:.2} Td ({}) Tj ET",
2894 font_idx,
2895 font_size,
2896 pdf_x + 2.0,
2897 text_y,
2898 escaped
2899 );
2900 }
2901 }
2902 }
2903 }
2904 FormFieldType::Dropdown {
2905 value, font_size, ..
2906 } => {
2907 let _ = writeln!(stream, "1 1 1 rg");
2909 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2910 let _ = writeln!(stream, "0.5 w");
2911 let _ =
2912 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2913 if flatten_forms {
2915 if let Some(ref val) = value {
2916 if !val.is_empty() {
2917 let font_idx = builder
2918 .font_objects
2919 .iter()
2920 .enumerate()
2921 .find(|(_, (key, _))| {
2922 key.family == "Helvetica"
2923 && key.weight == 400
2924 && !key.italic
2925 })
2926 .map(|(i, _)| i)
2927 .unwrap_or(0);
2928 let escaped = Self::encode_winansi_text(builder, val);
2929 let text_y = pdf_y + (h - font_size) / 2.0;
2930 let _ = writeln!(
2931 stream,
2932 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2933 font_idx,
2934 font_size,
2935 pdf_x + 2.0,
2936 text_y,
2937 escaped
2938 );
2939 }
2940 }
2941 }
2942 }
2943 }
2944 let _ = writeln!(stream, "Q");
2945 }
2946 }
2947
2948 if wrap_own_draw_as_artifact {
2951 let _ = writeln!(stream, "EMC");
2952 }
2953
2954 let clip_overflow = matches!(element.overflow, Overflow::Hidden);
2959 if clip_overflow {
2960 let clip_x = element.x;
2961 let clip_y = page_height - element.y - element.height;
2962 let clip_w = element.width;
2963 let clip_h = element.height;
2964 let radius = if let DrawCommand::Rect { border_radius, .. } = &element.draw {
2968 Some(border_radius)
2969 } else {
2970 None
2971 };
2972 let has_rounded_corners = radius.is_some_and(|r| {
2973 r.top_left > 0.0 || r.top_right > 0.0 || r.bottom_right > 0.0 || r.bottom_left > 0.0
2974 });
2975 let _ = writeln!(stream, "q");
2976 if has_rounded_corners {
2977 self.write_rounded_rect(stream, clip_x, clip_y, clip_w, clip_h, radius.unwrap());
2978 let _ = writeln!(stream, "W n");
2979 } else {
2980 let _ = writeln!(
2981 stream,
2982 "{:.2} {:.2} {:.2} {:.2} re W n",
2983 clip_x, clip_y, clip_w, clip_h
2984 );
2985 }
2986 }
2987
2988 for child in &element.children {
2989 self.write_element(
2990 stream,
2991 child,
2992 page_height,
2993 builder,
2994 page_idx,
2995 element_counter,
2996 gradient_counter,
2997 page_number,
2998 total_pages,
2999 tag_builder.as_deref_mut(),
3000 flatten_forms,
3001 );
3002 }
3003
3004 if clip_overflow {
3005 let _ = writeln!(stream, "Q");
3006 }
3007
3008 if has_transform {
3010 let _ = writeln!(stream, "Q");
3011 }
3012
3013 if needs_element_opacity {
3016 let _ = writeln!(stream, "Q");
3017 }
3018
3019 if tagged_mcid.is_some() {
3021 let _ = writeln!(stream, "EMC");
3022 if let Some(ref mut tb) = tag_builder {
3023 tb.end_element();
3024 }
3025 } else if is_artifact {
3026 let _ = writeln!(stream, "EMC");
3027 } else if artifact_own_draw {
3028 if let Some(ref mut tb) = tag_builder {
3031 tb.end_element();
3032 }
3033 }
3034 }
3035
3036 fn write_rounded_rect(
3037 &self,
3038 stream: &mut String,
3039 x: f64,
3040 y: f64,
3041 w: f64,
3042 h: f64,
3043 r: &crate::style::CornerValues,
3044 ) {
3045 let k = 0.5522847498;
3046
3047 let tl = r.top_left.min(w / 2.0).min(h / 2.0);
3048 let tr = r.top_right.min(w / 2.0).min(h / 2.0);
3049 let br = r.bottom_right.min(w / 2.0).min(h / 2.0);
3050 let bl = r.bottom_left.min(w / 2.0).min(h / 2.0);
3051
3052 let _ = writeln!(stream, "{:.2} {:.2} m", x + bl, y);
3053
3054 let _ = writeln!(stream, "{:.2} {:.2} l", x + w - br, y);
3055 if br > 0.0 {
3056 let _ = writeln!(
3057 stream,
3058 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3059 x + w - br + br * k,
3060 y,
3061 x + w,
3062 y + br - br * k,
3063 x + w,
3064 y + br
3065 );
3066 }
3067
3068 let _ = writeln!(stream, "{:.2} {:.2} l", x + w, y + h - tr);
3069 if tr > 0.0 {
3070 let _ = writeln!(
3071 stream,
3072 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3073 x + w,
3074 y + h - tr + tr * k,
3075 x + w - tr + tr * k,
3076 y + h,
3077 x + w - tr,
3078 y + h
3079 );
3080 }
3081
3082 let _ = writeln!(stream, "{:.2} {:.2} l", x + tl, y + h);
3083 if tl > 0.0 {
3084 let _ = writeln!(
3085 stream,
3086 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3087 x + tl - tl * k,
3088 y + h,
3089 x,
3090 y + h - tl + tl * k,
3091 x,
3092 y + h - tl
3093 );
3094 }
3095
3096 let _ = writeln!(stream, "{:.2} {:.2} l", x, y + bl);
3097 if bl > 0.0 {
3098 let _ = writeln!(
3099 stream,
3100 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3101 x,
3102 y + bl - bl * k,
3103 x + bl - bl * k,
3104 y,
3105 x + bl,
3106 y
3107 );
3108 }
3109
3110 let _ = writeln!(stream, "h");
3111 }
3112
3113 #[allow(clippy::too_many_arguments)]
3114 fn write_border_sides(
3115 &self,
3116 stream: &mut String,
3117 x: f64,
3118 y: f64,
3119 w: f64,
3120 h: f64,
3121 bw: &Edges,
3122 bc: &crate::style::EdgeValues<Color>,
3123 bs: &crate::style::EdgeValues<crate::style::BorderStyle>,
3124 ) {
3125 fn dash_ops(style: crate::style::BorderStyle, width: f64) -> String {
3130 use crate::style::BorderStyle::*;
3131 match style {
3132 Solid => String::new(),
3133 Dashed => format!("[{:.2} {:.2}] 0 d\n", width * 2.0, width),
3134 Dotted => format!("1 J\n[0 {:.2}] 0 d\n", width * 2.0),
3135 }
3136 }
3137 let sides = [
3139 (bc.top, bw.top, bs.top, x, y + h, x + w, y + h),
3140 (bc.bottom, bw.bottom, bs.bottom, x, y, x + w, y),
3141 (bc.left, bw.left, bs.left, x, y, x, y + h),
3142 (bc.right, bw.right, bs.right, x + w, y, x + w, y + h),
3143 ];
3144 for (color, width, style, x0, y0, x1, y1) in sides {
3145 if width <= 0.0 {
3146 continue;
3147 }
3148 let _ = write!(
3149 stream,
3150 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{}{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
3151 color.r,
3152 color.g,
3153 color.b,
3154 width,
3155 dash_ops(style, width),
3156 x0,
3157 y0,
3158 x1,
3159 y1
3160 );
3161 }
3162 }
3163
3164 fn emit_pdfua_embedded_standard(
3176 builder: &mut PdfBuilder,
3177 key: &FontKey,
3178 std_font: &crate::font::StandardFont,
3179 metrics: &crate::font::StandardFontMetrics,
3180 font_context: &FontContext,
3181 ) -> bool {
3182 let lib_family = match std_font.liberation_family() {
3183 Some(f) => f,
3184 None => return false, };
3186 let lib_bytes: &[u8] = match font_context.resolve(lib_family, key.weight, key.italic) {
3189 FontData::Custom { data, .. } => data,
3190 FontData::Standard(_) => return false,
3191 };
3192 let face = match ttf_parser::Face::parse(lib_bytes, 0) {
3193 Ok(f) => f,
3194 Err(_) => return false,
3195 };
3196 let scale = 1000.0 / face.units_per_em() as f64;
3197 let bbox = face.global_bounding_box();
3198 let pdf_name = Self::sanitize_font_name(lib_family, key.weight, key.italic);
3199
3200 let compressed = compress_to_vec_zlib(lib_bytes, 6);
3202 let fontfile2_id = builder.objects.len();
3203 let mut ff2: Vec<u8> = Vec::new();
3204 let _ = write!(
3205 ff2,
3206 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
3207 compressed.len(),
3208 lib_bytes.len()
3209 );
3210 ff2.extend_from_slice(&compressed);
3211 ff2.extend_from_slice(b"\nendstream");
3212 builder.objects.push(PdfObject {
3213 id: fontfile2_id,
3214 data: ff2,
3215 });
3216
3217 let fd_id = builder.objects.len();
3219 let cap_height =
3220 (face.capital_height().unwrap_or_else(|| face.ascender()) as f64 * scale) as i32;
3221 let fd = format!(
3222 "<< /Type /FontDescriptor /FontName /{name} /Flags {flags} \
3223 /FontBBox [{x0} {y0} {x1} {y1}] /ItalicAngle {ia} \
3224 /Ascent {asc} /Descent {desc} /CapHeight {cap} /StemV {stem} \
3225 /FontFile2 {ff2} 0 R >>",
3226 name = pdf_name,
3227 flags = std_font.descriptor_flags(),
3228 x0 = (bbox.x_min as f64 * scale) as i32,
3229 y0 = (bbox.y_min as f64 * scale) as i32,
3230 x1 = (bbox.x_max as f64 * scale) as i32,
3231 y1 = (bbox.y_max as f64 * scale) as i32,
3232 ia = if key.italic { -12 } else { 0 },
3233 asc = (face.ascender() as f64 * scale) as i32,
3234 desc = (face.descender() as f64 * scale) as i32,
3235 cap = cap_height,
3236 stem = if key.weight >= 700 { 120 } else { 80 },
3237 ff2 = fontfile2_id,
3238 );
3239 builder.objects.push(PdfObject {
3240 id: fd_id,
3241 data: fd.into_bytes(),
3242 });
3243
3244 let declared_widths: Vec<u16> = metrics
3258 .widths
3259 .iter()
3260 .enumerate()
3261 .map(|(i, &afm)| {
3262 let code = 32u8.wrapping_add(i as u8); if let Some(ch) = crate::font::winansi_to_char(code) {
3264 if let Some(gid) = face.glyph_index(ch) {
3265 if let Some(adv) = face.glyph_hor_advance(gid) {
3266 let hmtx = (adv as f64 * scale).round() as u16;
3267 if (hmtx as i32 - afm as i32).abs() > 1 {
3268 return hmtx;
3269 }
3270 }
3271 }
3272 }
3273 afm
3274 })
3275 .collect();
3276 let widths_str: String = declared_widths
3277 .iter()
3278 .map(|w| w.to_string())
3279 .collect::<Vec<_>>()
3280 .join(" ");
3281 let obj_id = builder.objects.len();
3282 let font_dict = format!(
3283 "<< /Type /Font /Subtype /TrueType /BaseFont /{name} \
3284 /Encoding /WinAnsiEncoding \
3285 /FirstChar 32 /LastChar 255 /Widths [{w}] \
3286 /FontDescriptor {fd} 0 R >>",
3287 name = pdf_name,
3288 w = widths_str,
3289 fd = fd_id,
3290 );
3291 builder.objects.push(PdfObject {
3292 id: obj_id,
3293 data: font_dict.into_bytes(),
3294 });
3295 builder.font_objects.push((key.clone(), obj_id));
3296 builder.embedded_standard_fonts.insert(key.clone());
3300 true
3301 }
3302
3303 #[allow(clippy::too_many_arguments)]
3304 fn register_fonts(
3305 &self,
3306 builder: &mut PdfBuilder,
3307 pages: &[LayoutPage],
3308 font_context: &FontContext,
3309 pdf_ua: bool,
3310 pdfa: bool,
3311 pdf_version: crate::model::PdfVersion,
3312 ) -> Result<(), FormeError> {
3313 let mut font_usage_map: HashMap<FontKey, FontUsage> = HashMap::new();
3315
3316 for page in pages {
3317 Self::collect_font_usage(&page.elements, &mut font_usage_map);
3318 }
3319
3320 let mut keys: Vec<FontKey> = font_usage_map.keys().cloned().collect();
3321
3322 keys.sort_by(|a, b| {
3324 a.family
3325 .cmp(&b.family)
3326 .then(a.weight.cmp(&b.weight))
3327 .then(a.italic.cmp(&b.italic))
3328 });
3329 keys.dedup();
3330
3331 if keys.is_empty() {
3333 keys.push(FontKey {
3334 family: "Helvetica".to_string(),
3335 weight: 400,
3336 italic: false,
3337 });
3338 }
3339
3340 for key in &keys {
3341 let font_data = font_context.resolve(&key.family, key.weight, key.italic);
3342
3343 match font_data {
3344 FontData::Standard(std_font) => {
3345 let metrics = std_font.metrics();
3346
3347 if pdf_ua || pdfa || pdf_version == crate::model::PdfVersion::V2_0 {
3362 if Self::emit_pdfua_embedded_standard(
3363 builder,
3364 key,
3365 std_font,
3366 &metrics,
3367 font_context,
3368 ) {
3369 continue;
3370 }
3371 if pdf_version == crate::model::PdfVersion::V2_0 {
3377 let remedy = match std_font.liberation_family() {
3378 Some(lib) => format!(
3379 "install @formepdf/fonts-standard and register its fonts (`for (const f of standardFonts()) Font.register(f)`) — Forme will embed the metric-compatible {lib} in its place"
3380 ),
3381 None => "register an embeddable TrueType font for this text (Symbol/ZapfDingbats have no metric-compatible substitute)"
3382 .to_string(),
3383 };
3384 return Err(FormeError::FontError(format!(
3385 "pdfVersion \"2.0\": font '{}' is not embedded. ISO 32000-2 removes the standard-14 provision, so every font must be embedded — {}.",
3386 std_font.pdf_name(),
3387 remedy,
3388 )));
3389 }
3390 if let Some(lib) = std_font.liberation_family() {
3396 builder.warnings.push(format!(
3397 "pdfUa: font '{}' is not embedded, so the PDF will not conform to \
3398 PDF/UA (all fonts must be embedded). Install \
3399 @formepdf/fonts-standard and register its fonts \
3400 (`for (const f of standardFonts()) Font.register(f)`) — Forme \
3401 will then embed the metric-compatible {} in its place.",
3402 std_font.pdf_name(),
3403 lib,
3404 ));
3405 }
3406 }
3407
3408 let obj_id = builder.objects.len();
3409 let widths_str: String = metrics
3412 .widths
3413 .iter()
3414 .map(|w| w.to_string())
3415 .collect::<Vec<_>>()
3416 .join(" ");
3417 let font_dict = format!(
3418 "<< /Type /Font /Subtype /Type1 /BaseFont /{} \
3419 /Encoding /WinAnsiEncoding \
3420 /FirstChar 32 /LastChar 255 /Widths [{}] >>",
3421 std_font.pdf_name(),
3422 widths_str,
3423 );
3424 builder.objects.push(PdfObject {
3425 id: obj_id,
3426 data: font_dict.into_bytes(),
3427 });
3428 builder.font_objects.push((key.clone(), obj_id));
3429 }
3430 FontData::Custom { data, .. } => {
3431 let type0_obj_id = Self::write_custom_font_objects(
3432 builder,
3433 key,
3434 data,
3435 font_usage_map.get(key).cloned().unwrap_or_default(),
3436 )?;
3437 builder.font_objects.push((key.clone(), type0_obj_id));
3438 }
3439 }
3440 }
3441
3442 Ok(())
3443 }
3444
3445 fn collect_font_usage(
3447 elements: &[LayoutElement],
3448 font_usage: &mut HashMap<FontKey, FontUsage>,
3449 ) {
3450 for element in elements {
3451 let lines_opt = match &element.draw {
3452 DrawCommand::Text { lines, .. } => Some(lines),
3453 DrawCommand::Watermark { lines, .. } => Some(lines),
3454 _ => None,
3455 };
3456 if let Some(lines) = lines_opt {
3457 for line in lines {
3458 for glyph in &line.glyphs {
3459 let italic =
3460 matches!(glyph.font_style, FontStyle::Italic | FontStyle::Oblique);
3461 let key = FontKey {
3462 family: glyph.font_family.to_string(),
3463 weight: glyph.font_weight,
3464 italic,
3465 };
3466 let usage = font_usage.entry(key).or_insert_with(|| FontUsage {
3467 chars: HashSet::new(),
3468 glyph_ids: HashSet::new(),
3469 glyph_to_text: HashMap::new(),
3470 glyph_texts: HashSet::new(),
3471 carrier_chars: HashSet::new(),
3472 });
3473 usage.chars.insert(glyph.char_value);
3474 if glyph.char_value == PAGE_NUMBER_SENTINEL
3479 || glyph.char_value == TOTAL_PAGES_SENTINEL
3480 {
3481 usage.chars.extend('0'..='9');
3482 }
3483 usage.glyph_ids.insert(glyph.glyph_id);
3484 record_glyph_text(&mut usage.glyph_to_text, glyph);
3485 usage
3486 .glyph_texts
3487 .insert((glyph.glyph_id, glyph_mapping(glyph)));
3488 if glyph_mapping(glyph) != extraction_text(glyph) {
3489 usage.carrier_chars.extend(extraction_text(glyph).chars());
3490 usage.chars.extend(extraction_text(glyph).chars());
3491 }
3492 }
3493 }
3494 }
3495 Self::collect_font_usage(&element.children, font_usage);
3496 }
3497 }
3498
3499 fn register_shadings(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3505 for (page_idx, page) in pages.iter().enumerate() {
3506 let mut counter = 0usize;
3507 Self::collect_shadings_recursive(&page.elements, page_idx, &mut counter, builder);
3508 }
3509 }
3510
3511 fn collect_shadings_recursive(
3512 elements: &[LayoutElement],
3513 page_idx: usize,
3514 counter: &mut usize,
3515 builder: &mut PdfBuilder,
3516 ) {
3517 for element in elements {
3518 if let DrawCommand::Rect {
3519 background_gradient: Some(gradient),
3520 ..
3521 } = &element.draw
3522 {
3523 let ordinal = *counter;
3524 *counter += 1;
3525 let (obj_id, name) =
3526 Self::write_shading_objects(builder, gradient, element, ordinal);
3527 builder
3528 .shading_map
3529 .insert((page_idx, ordinal), (obj_id, name));
3530 }
3531 Self::collect_shadings_recursive(&element.children, page_idx, counter, builder);
3532 }
3533 }
3534
3535 fn write_shading_objects(
3541 builder: &mut PdfBuilder,
3542 gradient: &crate::style::Background,
3543 element: &LayoutElement,
3544 ordinal: usize,
3545 ) -> (usize, String) {
3546 use crate::style::Background;
3547 use crate::style::GradientStop;
3548
3549 let black = Color {
3553 r: 0.0,
3554 g: 0.0,
3555 b: 0.0,
3556 a: 1.0,
3557 };
3558 let stops: Vec<GradientStop> = match gradient {
3559 Background::Color(c) => vec![
3560 GradientStop {
3561 position: 0.0,
3562 color: *c,
3563 },
3564 GradientStop {
3565 position: 1.0,
3566 color: *c,
3567 },
3568 ],
3569 Background::Linear(g) => normalize_gradient_stops(&g.stops, black),
3570 Background::Radial(g) => normalize_gradient_stops(&g.stops, black),
3571 };
3572
3573 let function_id = if stops.len() <= 2 {
3577 let c0 = stops.first().map(|s| s.color).unwrap_or(black);
3578 let c1 = stops.last().map(|s| s.color).unwrap_or(c0);
3579 let id = builder.objects.len();
3580 let data = format!(
3581 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
3582 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
3583 );
3584 builder.objects.push(PdfObject {
3585 id,
3586 data: data.into_bytes(),
3587 });
3588 id
3589 } else {
3590 let mut sub_ids: Vec<usize> = Vec::with_capacity(stops.len() - 1);
3592 for window in stops.windows(2) {
3593 let c0 = window[0].color;
3594 let c1 = window[1].color;
3595 let id = builder.objects.len();
3596 let data = format!(
3597 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
3598 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
3599 );
3600 builder.objects.push(PdfObject {
3601 id,
3602 data: data.into_bytes(),
3603 });
3604 sub_ids.push(id);
3605 }
3606 let bounds: Vec<String> = stops[1..stops.len() - 1]
3610 .iter()
3611 .map(|s| format!("{:.4}", s.position))
3612 .collect();
3613 let encode: Vec<&str> = (0..sub_ids.len()).map(|_| "0 1").collect();
3614 let functions: Vec<String> = sub_ids.iter().map(|i| format!("{} 0 R", i)).collect();
3615 let id = builder.objects.len();
3616 let data = format!(
3617 "<< /FunctionType 3 /Domain [0 1] /Functions [{}] /Bounds [{}] /Encode [{}] >>",
3618 functions.join(" "),
3619 bounds.join(" "),
3620 encode.join(" "),
3621 );
3622 builder.objects.push(PdfObject {
3623 id,
3624 data: data.into_bytes(),
3625 });
3626 id
3627 };
3628
3629 let _ = element.x;
3633 let _ = element.y;
3634 let w = element.width;
3635 let h = element.height;
3636
3637 let shading_id = builder.objects.len();
3638 let shading_data = match gradient {
3639 Background::Linear(g) => {
3640 let theta = g.angle_deg.to_radians();
3650 let dx = theta.sin();
3651 let dy = theta.cos();
3652 let axis_len = w * dx.abs() + h * dy.abs();
3655 let cx_rel = w / 2.0;
3658 let cy_rel = h / 2.0;
3659 let half = axis_len / 2.0;
3660 let x0 = cx_rel - dx * half;
3661 let y0 = cy_rel - dy * half;
3662 let x1 = cx_rel + dx * half;
3663 let y1 = cy_rel + dy * half;
3664 format!(
3665 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
3666 x0, y0, x1, y1, function_id,
3667 )
3668 }
3669 Background::Radial(_) => {
3670 let cx_rel = w / 2.0;
3673 let cy_rel = h / 2.0;
3674 let r_outer = (w / 2.0).max(h / 2.0);
3675 format!(
3676 "<< /ShadingType 3 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} 0 {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
3677 cx_rel, cy_rel, cx_rel, cy_rel, r_outer, function_id,
3678 )
3679 }
3680 Background::Color(_) => {
3681 format!(
3685 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 0 0] /Function {} 0 R /Extend [true true] >>",
3686 function_id,
3687 )
3688 }
3689 };
3690 builder.objects.push(PdfObject {
3691 id: shading_id,
3692 data: shading_data.into_bytes(),
3693 });
3694 (shading_id, format!("Sh{}", ordinal))
3695 }
3696
3697 fn register_page_background_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3701 for (page_idx, page) in pages.iter().enumerate() {
3702 let Some(src) = &page.config.background_image else {
3703 continue;
3704 };
3705 if let Some(&entry) = builder.page_background_url_cache.get(src) {
3707 builder.page_background_image_map.insert(page_idx, entry);
3708 continue;
3709 }
3710 match crate::image_loader::load_image(src) {
3713 Ok(image_data) => {
3714 let img_idx = builder.image_objects.len();
3715 let dims = (img_idx, image_data.width_px, image_data.height_px);
3716 let xobj_id = Self::write_image_xobject(builder, &image_data);
3717 builder.image_objects.push(xobj_id);
3718 builder.page_background_image_map.insert(page_idx, dims);
3719 builder.page_background_url_cache.insert(src.clone(), dims);
3720 }
3721 Err(e) => {
3722 eprintln!("[forme] page background image failed to load: {}", e);
3723 }
3724 }
3725 }
3726 }
3727
3728 fn write_page_background(
3733 &self,
3734 stream: &mut String,
3735 page: &LayoutPage,
3736 page_bg: (usize, u32, u32),
3737 builder: &PdfBuilder,
3738 ) {
3739 use crate::model::{BackgroundPosition, BackgroundSize};
3740 let (img_idx, iw_px, ih_px) = page_bg;
3741 let page_w = page.width;
3742 let page_h = page.height;
3743 let iw = iw_px as f64;
3744 let ih = ih_px as f64;
3745
3746 let size = page.config.background_size.unwrap_or_default();
3747 let (dest_w, dest_h) = match size {
3748 BackgroundSize::Fill => (page_w, page_h),
3749 BackgroundSize::Cover => {
3750 let s = (page_w / iw).max(page_h / ih);
3751 (iw * s, ih * s)
3752 }
3753 BackgroundSize::Contain => {
3754 let s = (page_w / iw).min(page_h / ih);
3755 (iw * s, ih * s)
3756 }
3757 };
3758
3759 let position = page.config.background_position.unwrap_or_default();
3763 let (dest_x, dest_y) = match position {
3766 BackgroundPosition::TopLeft => (0.0, page_h - dest_h),
3767 BackgroundPosition::TopRight => (page_w - dest_w, page_h - dest_h),
3768 BackgroundPosition::BottomLeft => (0.0, 0.0),
3769 BackgroundPosition::BottomRight => (page_w - dest_w, 0.0),
3770 BackgroundPosition::Center => ((page_w - dest_w) / 2.0, (page_h - dest_h) / 2.0),
3771 };
3772
3773 let opacity = page.config.background_opacity.unwrap_or(1.0);
3775 let needs_opacity = opacity < 1.0;
3776 if needs_opacity {
3777 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
3778 let _ = writeln!(stream, "q\n/{} gs", gs_name);
3779 } else {
3780 let _ = writeln!(stream, "q");
3781 }
3782 } else {
3783 let _ = writeln!(stream, "q");
3784 }
3785 let _ = writeln!(
3788 stream,
3789 "{:.2} 0 0 {:.2} {:.2} {:.2} cm\n/Im{} Do\nQ",
3790 dest_w, dest_h, dest_x, dest_y, img_idx,
3791 );
3792 }
3793
3794 fn register_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3796 for (page_idx, page) in pages.iter().enumerate() {
3797 let mut element_counter = 0usize;
3798 Self::collect_images_recursive(&page.elements, page_idx, &mut element_counter, builder);
3799 }
3800 }
3801
3802 fn collect_images_recursive(
3803 elements: &[LayoutElement],
3804 page_idx: usize,
3805 element_counter: &mut usize,
3806 builder: &mut PdfBuilder,
3807 ) {
3808 for element in elements {
3809 match &element.draw {
3810 DrawCommand::Image { image_data } => {
3811 let elem_idx = *element_counter;
3812 *element_counter += 1;
3813
3814 let img_idx = builder.image_objects.len();
3815 let xobj_id = Self::write_image_xobject(builder, image_data);
3816 builder.image_objects.push(xobj_id);
3817 builder
3818 .image_index_map
3819 .insert((page_idx, elem_idx), img_idx);
3820 }
3821 DrawCommand::ImagePlaceholder => {
3822 *element_counter += 1;
3823 }
3824 _ => {
3825 Self::collect_images_recursive(
3826 &element.children,
3827 page_idx,
3828 element_counter,
3829 builder,
3830 );
3831 }
3832 }
3833 }
3834 }
3835
3836 fn register_ext_gstates(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3838 let mut unique_opacities: Vec<f64> = Vec::new();
3839 for page in pages {
3840 Self::collect_opacities_recursive(&page.elements, &mut unique_opacities);
3841 if let Some(o) = page.config.background_opacity {
3843 if o < 1.0 {
3844 unique_opacities.push(o);
3845 }
3846 }
3847 }
3848 unique_opacities.sort_by(|a, b| a.partial_cmp(b).unwrap());
3849 unique_opacities.dedup();
3850
3851 for (idx, &opacity) in unique_opacities.iter().enumerate() {
3852 let obj_id = builder.objects.len();
3853 let gs_name = format!("GS{}", idx);
3854 let obj_data = format!(
3855 "<< /Type /ExtGState /ca {:.4} /CA {:.4} >>",
3856 opacity, opacity
3857 );
3858 builder.objects.push(PdfObject {
3859 id: obj_id,
3860 data: obj_data.into_bytes(),
3861 });
3862 let key = opacity.to_bits();
3863 builder.ext_gstate_map.insert(key, (obj_id, gs_name));
3864 }
3865 }
3866
3867 fn collect_opacities_recursive(elements: &[LayoutElement], opacities: &mut Vec<f64>) {
3868 for element in elements {
3869 if element.opacity < 1.0 {
3876 opacities.push(element.opacity);
3877 }
3878 if let DrawCommand::Rect {
3882 box_shadow: Some(shadow),
3883 ..
3884 } = &element.draw
3885 {
3886 if shadow.color.a < 1.0 {
3887 opacities.push(shadow.color.a);
3888 }
3889 }
3890 match &element.draw {
3891 DrawCommand::Rect { opacity, .. }
3892 | DrawCommand::Text { opacity, .. }
3893 | DrawCommand::Watermark { opacity, .. }
3894 if *opacity < 1.0 =>
3895 {
3896 opacities.push(*opacity);
3897 }
3898 DrawCommand::Chart { primitives } => {
3899 for prim in primitives {
3900 if let crate::chart::ChartPrimitive::FilledPath { opacity, .. } = prim {
3901 if *opacity < 1.0 {
3902 opacities.push(*opacity);
3903 }
3904 }
3905 }
3906 }
3907 DrawCommand::Svg { commands, .. } => {
3908 for cmd in commands {
3909 if let crate::svg::SvgCommand::SetOpacity(opacity) = cmd {
3910 if *opacity < 1.0 {
3911 opacities.push(*opacity);
3912 }
3913 }
3914 }
3915 }
3916 _ => {}
3917 }
3918 Self::collect_opacities_recursive(&element.children, opacities);
3919 }
3920 }
3921
3922 fn build_ext_gstate_resource_dict(&self, builder: &PdfBuilder) -> String {
3924 if builder.ext_gstate_map.is_empty() {
3925 return String::new();
3926 }
3927 let mut entries: Vec<(&String, usize)> = builder
3928 .ext_gstate_map
3929 .values()
3930 .map(|(obj_id, name)| (name, *obj_id))
3931 .collect();
3932 entries.sort_by_key(|(name, _)| (*name).clone());
3933 entries
3934 .iter()
3935 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3936 .collect::<Vec<_>>()
3937 .join(" ")
3938 }
3939
3940 fn write_image_xobject(
3943 builder: &mut PdfBuilder,
3944 image: &crate::image_loader::LoadedImage,
3945 ) -> usize {
3946 use crate::image_loader::{ImagePixelData, JpegColorSpace};
3947
3948 match &image.pixel_data {
3949 ImagePixelData::Jpeg { data, color_space } => {
3950 let color_space_str = match color_space {
3951 JpegColorSpace::DeviceRGB => "/DeviceRGB",
3952 JpegColorSpace::DeviceGray => "/DeviceGray",
3953 };
3954
3955 let obj_id = builder.objects.len();
3956 let mut obj_data: Vec<u8> = Vec::new();
3957 let _ = write!(
3958 obj_data,
3959 "<< /Type /XObject /Subtype /Image \
3960 /Width {} /Height {} \
3961 /ColorSpace {} \
3962 /BitsPerComponent 8 \
3963 /Filter /DCTDecode \
3964 /Length {} >>\nstream\n",
3965 image.width_px,
3966 image.height_px,
3967 color_space_str,
3968 data.len()
3969 );
3970 obj_data.extend_from_slice(data);
3971 obj_data.extend_from_slice(b"\nendstream");
3972 builder.objects.push(PdfObject {
3973 id: obj_id,
3974 data: obj_data,
3975 });
3976 obj_id
3977 }
3978
3979 ImagePixelData::Decoded { rgb, alpha } => {
3980 let smask_id = alpha.as_ref().map(|alpha_data| {
3982 let compressed_alpha = compress_to_vec_zlib(alpha_data, 6);
3983 let smask_obj_id = builder.objects.len();
3984 let mut smask_data: Vec<u8> = Vec::new();
3985 let _ = write!(
3986 smask_data,
3987 "<< /Type /XObject /Subtype /Image \
3988 /Width {} /Height {} \
3989 /ColorSpace /DeviceGray \
3990 /BitsPerComponent 8 \
3991 /Filter /FlateDecode \
3992 /Length {} >>\nstream\n",
3993 image.width_px,
3994 image.height_px,
3995 compressed_alpha.len()
3996 );
3997 smask_data.extend_from_slice(&compressed_alpha);
3998 smask_data.extend_from_slice(b"\nendstream");
3999 builder.objects.push(PdfObject {
4000 id: smask_obj_id,
4001 data: smask_data,
4002 });
4003 smask_obj_id
4004 });
4005
4006 let compressed_rgb = compress_to_vec_zlib(rgb, 6);
4008 let obj_id = builder.objects.len();
4009 let mut obj_data: Vec<u8> = Vec::new();
4010
4011 let smask_ref = smask_id
4012 .map(|id| format!(" /SMask {} 0 R", id))
4013 .unwrap_or_default();
4014
4015 let _ = write!(
4016 obj_data,
4017 "<< /Type /XObject /Subtype /Image \
4018 /Width {} /Height {} \
4019 /ColorSpace /DeviceRGB \
4020 /BitsPerComponent 8 \
4021 /Filter /FlateDecode \
4022 /Length {}{} >>\nstream\n",
4023 image.width_px,
4024 image.height_px,
4025 compressed_rgb.len(),
4026 smask_ref
4027 );
4028 obj_data.extend_from_slice(&compressed_rgb);
4029 obj_data.extend_from_slice(b"\nendstream");
4030 builder.objects.push(PdfObject {
4031 id: obj_id,
4032 data: obj_data,
4033 });
4034 obj_id
4035 }
4036 }
4037 }
4038
4039 fn build_shading_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
4043 let mut entries: Vec<(String, usize)> = builder
4044 .shading_map
4045 .iter()
4046 .filter(|(&(p, _), _)| p == page_idx)
4047 .map(|(_, (obj_id, name))| (name.clone(), *obj_id))
4048 .collect();
4049 if entries.is_empty() {
4050 return String::new();
4051 }
4052 entries.sort_by(|a, b| a.0.cmp(&b.0));
4053 entries
4054 .iter()
4055 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
4056 .collect::<Vec<_>>()
4057 .join(" ")
4058 }
4059
4060 fn build_xobject_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
4061 let mut entries: Vec<(usize, usize)> = Vec::new();
4062 for (&(pidx, _), &img_idx) in &builder.image_index_map {
4063 if pidx == page_idx {
4064 let obj_id = builder.image_objects[img_idx];
4065 entries.push((img_idx, obj_id));
4066 }
4067 }
4068 if let Some(&(img_idx, _, _)) = builder.page_background_image_map.get(&page_idx) {
4071 let obj_id = builder.image_objects[img_idx];
4072 entries.push((img_idx, obj_id));
4073 }
4074 if entries.is_empty() {
4075 return String::new();
4076 }
4077 entries.sort_by_key(|(idx, _)| *idx);
4078 entries.dedup();
4079 entries
4080 .iter()
4081 .map(|(idx, obj_id)| format!("/Im{} {} 0 R", idx, obj_id))
4082 .collect::<Vec<_>>()
4083 .join(" ")
4084 }
4085
4086 fn write_custom_font_objects(
4093 builder: &mut PdfBuilder,
4094 key: &FontKey,
4095 ttf_data: &[u8],
4096 usage: FontUsage,
4097 ) -> Result<usize, FormeError> {
4098 let FontUsage {
4099 glyph_ids: used_glyph_ids,
4100 chars: used_chars,
4101 glyph_to_text: glyph_to_text_map,
4102 mut glyph_texts,
4103 carrier_chars,
4104 } = usage;
4105 let face = ttf_parser::Face::parse(ttf_data, 0).map_err(|e| {
4106 FormeError::FontError(format!(
4107 "Failed to parse TTF data for font '{}': {}",
4108 key.family, e
4109 ))
4110 })?;
4111
4112 let units_per_em = face.units_per_em();
4113 let ascender = face.ascender();
4114 let descender = face.descender();
4115
4116 let mut char_to_orig_gid: HashMap<char, u16> = HashMap::new();
4118 for &ch in &used_chars {
4119 if let Some(gid) = face.glyph_index(ch) {
4120 char_to_orig_gid.insert(ch, gid.0);
4121 }
4122 }
4123
4124 for &ch in &carrier_chars {
4125 char_to_orig_gid.entry(ch).or_insert(0);
4126 }
4127
4128 let mut all_orig_gids: HashSet<u16> = used_glyph_ids.clone();
4132 for &gid in char_to_orig_gid.values() {
4133 all_orig_gids.insert(gid);
4134 }
4135
4136 let (embed_ttf, gid_remap) = match subset_ttf(ttf_data, &all_orig_gids) {
4138 Ok(subset_result) => (subset_result.ttf_data, subset_result.gid_remap),
4139 Err(_) => {
4140 let identity: HashMap<u16, u16> =
4142 all_orig_gids.iter().map(|&gid| (gid, gid)).collect();
4143 (ttf_data.to_vec(), identity)
4144 }
4145 };
4146
4147 let char_to_gid: HashMap<char, u16> = char_to_orig_gid
4149 .iter()
4150 .filter_map(|(&ch, &orig_gid)| gid_remap.get(&orig_gid).map(|&new_gid| (ch, new_gid)))
4151 .collect();
4152
4153 let gid_remap_for_embed = gid_remap.clone();
4155
4156 let mut new_gid_to_text: HashMap<u16, String> = HashMap::new();
4158 for (orig_gid, text) in &glyph_to_text_map {
4160 if let Some(&new_gid) = gid_remap.get(orig_gid) {
4161 new_gid_to_text
4162 .entry(new_gid)
4163 .or_insert_with(|| text.clone());
4164 }
4165 }
4166 for (&ch, &new_gid) in &char_to_gid {
4168 new_gid_to_text
4169 .entry(new_gid)
4170 .or_insert_with(|| ch.to_string());
4171 }
4172
4173 for ch in carrier_chars {
4174 let gid = char_to_orig_gid.get(&ch).copied().ok_or_else(|| {
4175 FormeError::FontError(format!("No glyph for extraction character {ch:?}"))
4176 })?;
4177 glyph_texts.insert((gid, ch.to_string()));
4178 }
4179 let mut cid_to_gid: HashMap<u16, u16> = gid_remap.values().map(|&gid| (gid, gid)).collect();
4182 let mut next_cid = cid_to_gid.keys().copied().max().unwrap_or(0) as u32 + 1;
4183 let mut glyph_to_cid = HashMap::new();
4184 let mut pairs: Vec<_> = glyph_texts.into_iter().collect();
4185 pairs.sort();
4186 for (orig_gid, text) in pairs {
4187 let gid = gid_remap.get(&orig_gid).copied().unwrap_or(0);
4188 let cid = if new_gid_to_text.get(&gid) == Some(&text) {
4189 gid
4190 } else {
4191 let cid = u16::try_from(next_cid).map_err(|_| {
4192 FormeError::FontError("Too many character mappings".to_string())
4193 })?;
4194 next_cid += 1;
4195 cid_to_gid.insert(cid, gid);
4196 new_gid_to_text.insert(cid, text.clone());
4197 cid
4198 };
4199 glyph_to_cid.insert((orig_gid, text), cid);
4200 }
4201
4202 let char_to_gid = char_to_orig_gid
4203 .iter()
4204 .filter_map(|(&ch, &gid)| {
4205 glyph_to_cid
4206 .get(&(gid, ch.to_string()))
4207 .copied()
4208 .or_else(|| gid_remap.get(&gid).copied())
4209 .map(|cid| (ch, cid))
4210 })
4211 .collect();
4212
4213 let pdf_font_name = Self::sanitize_font_name(&key.family, key.weight, key.italic);
4214
4215 let compressed_ttf = compress_to_vec_zlib(&embed_ttf, 6);
4217 let fontfile2_id = builder.objects.len();
4218 let mut fontfile2_data: Vec<u8> = Vec::new();
4219 let _ = write!(
4220 fontfile2_data,
4221 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
4222 compressed_ttf.len(),
4223 embed_ttf.len()
4224 );
4225 fontfile2_data.extend_from_slice(&compressed_ttf);
4226 fontfile2_data.extend_from_slice(b"\nendstream");
4227 builder.objects.push(PdfObject {
4228 id: fontfile2_id,
4229 data: fontfile2_data,
4230 });
4231
4232 let subset_face = ttf_parser::Face::parse(&embed_ttf, 0).unwrap_or_else(|_| face.clone());
4234 let subset_upem = subset_face.units_per_em();
4235
4236 let font_descriptor_id = builder.objects.len();
4238 let bbox = face.global_bounding_box();
4239 let scale = 1000.0 / units_per_em as f64;
4240 let bbox_str = format!(
4241 "[{} {} {} {}]",
4242 (bbox.x_min as f64 * scale) as i32,
4243 (bbox.y_min as f64 * scale) as i32,
4244 (bbox.x_max as f64 * scale) as i32,
4245 (bbox.y_max as f64 * scale) as i32,
4246 );
4247
4248 let flags = 4u32;
4249 let cap_height = face.capital_height().unwrap_or(ascender) as f64 * scale;
4250 let stem_v = if key.weight >= 700 { 120 } else { 80 };
4251
4252 let font_descriptor_dict = format!(
4253 "<< /Type /FontDescriptor /FontName /{} /Flags {} \
4254 /FontBBox {} /ItalicAngle {} \
4255 /Ascent {} /Descent {} /CapHeight {} /StemV {} \
4256 /FontFile2 {} 0 R >>",
4257 pdf_font_name,
4258 flags,
4259 bbox_str,
4260 if key.italic { -12 } else { 0 },
4261 (ascender as f64 * scale) as i32,
4262 (descender as f64 * scale) as i32,
4263 cap_height as i32,
4264 stem_v,
4265 fontfile2_id,
4266 );
4267 builder.objects.push(PdfObject {
4268 id: font_descriptor_id,
4269 data: font_descriptor_dict.into_bytes(),
4270 });
4271
4272 let cid_map = if cid_to_gid.iter().all(|(cid, gid)| cid == gid) {
4274 "/Identity".to_string()
4275 } else {
4276 let map_id = builder.objects.len();
4277 let mut bytes = vec![0u8; next_cid as usize * 2];
4278 for (&cid, &gid) in &cid_to_gid {
4279 bytes[cid as usize * 2..cid as usize * 2 + 2].copy_from_slice(&gid.to_be_bytes());
4280 }
4281 let compressed = compress_to_vec_zlib(&bytes, 6);
4282 let mut data = format!(
4283 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
4284 compressed.len()
4285 )
4286 .into_bytes();
4287 data.extend_from_slice(&compressed);
4288 data.extend_from_slice(b"\nendstream");
4289 builder.objects.push(PdfObject { id: map_id, data });
4290 format!("{} 0 R", map_id)
4291 };
4292 let cidfont_id = builder.objects.len();
4293 let (w_array, pdf_widths) =
4295 Self::build_w_array_from_gids(&cid_to_gid, &subset_face, subset_upem);
4296 let default_width = subset_face
4297 .glyph_hor_advance(ttf_parser::GlyphId(0))
4298 .map(|adv| (adv as f64 * 1000.0 / subset_upem as f64) as u32)
4299 .unwrap_or(1000);
4300 let cidfont_dict = format!(
4301 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{} \
4302 /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> \
4303 /FontDescriptor {} 0 R /DW {} /W {} \
4304 /CIDToGIDMap {} >>",
4305 pdf_font_name, font_descriptor_id, default_width, w_array, cid_map,
4306 );
4307 builder.objects.push(PdfObject {
4308 id: cidfont_id,
4309 data: cidfont_dict.into_bytes(),
4310 });
4311
4312 let tounicode_id = builder.objects.len();
4314 let cmap_content = Self::build_tounicode_cmap_from_gids(&new_gid_to_text, &pdf_font_name);
4315 let compressed_cmap = compress_to_vec_zlib(cmap_content.as_bytes(), 6);
4316 let mut tounicode_data: Vec<u8> = Vec::new();
4317 let _ = write!(
4318 tounicode_data,
4319 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
4320 compressed_cmap.len()
4321 );
4322 tounicode_data.extend_from_slice(&compressed_cmap);
4323 tounicode_data.extend_from_slice(b"\nendstream");
4324 builder.objects.push(PdfObject {
4325 id: tounicode_id,
4326 data: tounicode_data,
4327 });
4328
4329 let type0_id = builder.objects.len();
4331 let type0_dict = format!(
4332 "<< /Type /Font /Subtype /Type0 /BaseFont /{} \
4333 /Encoding /Identity-H \
4334 /DescendantFonts [{} 0 R] \
4335 /ToUnicode {} 0 R >>",
4336 pdf_font_name, cidfont_id, tounicode_id,
4337 );
4338 builder.objects.push(PdfObject {
4339 id: type0_id,
4340 data: type0_dict.into_bytes(),
4341 });
4342
4343 builder.custom_font_data.insert(
4345 key.clone(),
4346 CustomFontEmbedData {
4347 ttf_data: embed_ttf,
4348 gid_remap: gid_remap_for_embed,
4349 glyph_to_cid,
4350 glyph_to_text: glyph_to_text_map,
4351 char_to_gid,
4352 pdf_widths,
4353 default_width,
4354 units_per_em,
4355 ascender,
4356 descender,
4357 },
4358 );
4359
4360 Ok(type0_id)
4361 }
4362
4363 fn build_w_array_from_gids(
4365 gid_remap: &HashMap<u16, u16>,
4366 face: &ttf_parser::Face,
4367 units_per_em: u16,
4368 ) -> (String, HashMap<u16, f64>) {
4369 let scale = 1000.0 / units_per_em as f64;
4370
4371 let mut entries: Vec<(u16, f64)> = Vec::new();
4372 let mut seen_gids: HashSet<u16> = HashSet::new();
4373
4374 for (&cid, &new_gid) in gid_remap {
4375 if seen_gids.contains(&cid) {
4376 continue;
4377 }
4378 seen_gids.insert(cid);
4379 let advance = face
4380 .glyph_hor_advance(ttf_parser::GlyphId(new_gid))
4381 .unwrap_or(0);
4382 let width = advance as f64 * scale;
4386 entries.push((cid, width));
4387 }
4388
4389 entries.sort_by_key(|(gid, _)| *gid);
4390
4391 let mut result = String::from("[");
4393 for (gid, width) in &entries {
4394 let _ = write!(result, " {} [{}]", gid, pdf_number(*width));
4395 }
4396 result.push_str(" ]");
4397 (result, entries.into_iter().collect())
4398 }
4399
4400 fn build_tounicode_cmap_from_gids(
4407 gid_to_text: &HashMap<u16, String>,
4408 font_name: &str,
4409 ) -> String {
4410 let mut gid_to_unicode: Vec<(u16, String)> = gid_to_text
4411 .iter()
4412 .filter(|(_, text)| !text.is_empty())
4413 .map(|(&gid, text)| {
4414 let hex: String = text
4415 .encode_utf16()
4416 .map(|unit| format!("{:04X}", unit))
4417 .collect();
4418 (gid, hex)
4419 })
4420 .collect();
4421 gid_to_unicode.sort_by_key(|(gid, _)| *gid);
4422
4423 let mut cmap = String::new();
4424 let _ = writeln!(cmap, "/CIDInit /ProcSet findresource begin");
4425 let _ = writeln!(cmap, "12 dict begin");
4426 let _ = writeln!(cmap, "begincmap");
4427 let _ = writeln!(cmap, "/CIDSystemInfo");
4428 let _ = writeln!(
4429 cmap,
4430 "<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def"
4431 );
4432 let _ = writeln!(cmap, "/CMapName /{}-UTF16 def", font_name);
4433 let _ = writeln!(cmap, "/CMapType 2 def");
4434 let _ = writeln!(cmap, "1 begincodespacerange");
4435 let _ = writeln!(cmap, "<0000> <FFFF>");
4436 let _ = writeln!(cmap, "endcodespacerange");
4437
4438 for chunk in gid_to_unicode.chunks(100) {
4440 let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
4441 for (gid, unicode) in chunk {
4442 let _ = writeln!(cmap, "<{:04X}> <{}>", gid, unicode);
4443 }
4444 let _ = writeln!(cmap, "endbfchar");
4445 }
4446
4447 let _ = writeln!(cmap, "endcmap");
4448 let _ = writeln!(cmap, "CMapName currentdict /CMap defineresource pop");
4449 let _ = writeln!(cmap, "end");
4450 let _ = writeln!(cmap, "end");
4451
4452 cmap
4453 }
4454
4455 fn sanitize_font_name(family: &str, weight: u32, italic: bool) -> String {
4458 let mut name: String = family
4459 .chars()
4460 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
4461 .collect();
4462
4463 if weight >= 700 {
4464 name.push_str("-Bold");
4465 }
4466 if italic {
4467 name.push_str("-Italic");
4468 }
4469
4470 if name.is_empty() {
4472 name = "CustomFont".to_string();
4473 }
4474
4475 name
4476 }
4477
4478 fn build_font_resource_dict(&self, font_objects: &[(FontKey, usize)]) -> String {
4479 font_objects
4480 .iter()
4481 .enumerate()
4482 .map(|(i, (_, obj_id))| format!("/F{} {} 0 R", i, obj_id))
4483 .collect::<Vec<_>>()
4484 .join(" ")
4485 }
4486
4487 fn font_index(
4489 &self,
4490 family: &str,
4491 weight: u32,
4492 font_style: FontStyle,
4493 font_objects: &[(FontKey, usize)],
4494 ) -> usize {
4495 let italic = matches!(font_style, FontStyle::Italic | FontStyle::Oblique);
4496
4497 for (i, (key, _)) in font_objects.iter().enumerate() {
4499 if key.family == family && key.weight == weight && key.italic == italic {
4500 return i;
4501 }
4502 }
4503
4504 let snapped = if weight >= 600 { 700 } else { 400 };
4506 for (i, (key, _)) in font_objects.iter().enumerate() {
4507 if key.family == family && key.weight == snapped && key.italic == italic {
4508 return i;
4509 }
4510 }
4511
4512 for (i, (key, _)) in font_objects.iter().enumerate() {
4514 if key.family == "Helvetica" && key.weight == snapped && key.italic == italic {
4515 return i;
4516 }
4517 }
4518
4519 0
4521 }
4522
4523 fn positioned_tj(
4538 group: &[&PositionedGlyph],
4539 gids: &[u16],
4540 embed: &CustomFontEmbedData,
4541 char_spacing: f64,
4542 origin_x: f64,
4543 origin_y: f64,
4544 ) -> Option<String> {
4545 let size = group.first()?.font_size;
4546 if size <= 0.0 {
4547 return None;
4548 }
4549 let mut adjustments = vec![0.0_f64; group.len()];
4550 let mut any = false;
4551 for i in 0..group.len().saturating_sub(1) {
4552 let width = embed
4553 .pdf_widths
4554 .get(&gids[i])
4555 .copied()
4556 .unwrap_or(embed.default_width as f64);
4557 let drawn = width * size / 1000.0 + char_spacing;
4558 let wanted = group[i + 1].x_offset - group[i].x_offset;
4559 let delta = drawn - wanted;
4560 if delta.abs() > 0.001 {
4561 adjustments[i] = delta * 1000.0 / size;
4562 any = true;
4563 }
4564 }
4565 let rises: Vec<f64> = group
4566 .iter()
4567 .map(|g| {
4568 if g.y_offset.abs() > 0.001 {
4569 g.y_offset
4570 } else {
4571 0.0
4572 }
4573 })
4574 .collect();
4575 if !any
4576 && rises.iter().all(|r| *r == 0.0)
4577 && group.iter().all(|g| glyph_mapping(g) == extraction_text(g))
4578 {
4579 return None;
4580 }
4581 let mut out = String::new();
4586 let mut rise = 0.0_f64;
4587 let mut open = false;
4588 for (i, gid) in gids.iter().enumerate() {
4589 if rises[i] != rise {
4590 if open {
4591 out.push_str("] TJ\n");
4592 open = false;
4593 }
4594 let _ = writeln!(out, "{} Ts", pdf_number(rises[i]));
4595 rise = rises[i];
4596 }
4597 if !open {
4598 out.push('[');
4599 open = true;
4600 }
4601 let text = extraction_text(group[i]);
4602 let mapping = glyph_mapping(group[i]);
4603 let parts = if text != mapping {
4604 let split = text.find(&mapping).unwrap();
4605 Some((&text[..split], &text[split + mapping.len()..]))
4606 } else {
4607 None
4608 };
4609 if let Some((before, _)) = parts {
4610 Self::write_text_carriers(
4611 &mut out,
4612 before,
4613 embed,
4614 char_spacing,
4615 origin_x + group[i].x_offset - group[0].x_offset,
4616 origin_y,
4617 );
4618 }
4619 let _ = write!(out, "<{:04X}>", gid);
4620 if let Some((_, after)) = parts {
4621 let width = embed
4622 .pdf_widths
4623 .get(gid)
4624 .copied()
4625 .unwrap_or(embed.default_width as f64);
4626 Self::write_text_carriers(
4627 &mut out,
4628 after,
4629 embed,
4630 char_spacing,
4631 origin_x + group[i].x_offset - group[0].x_offset
4632 + width * size / 1000.0
4633 + char_spacing,
4634 origin_y,
4635 );
4636 }
4637
4638 if adjustments[i] != 0.0 && i + 1 < gids.len() {
4639 let _ = write!(out, " {:.2} ", adjustments[i]);
4640 }
4641 }
4642 out.push_str("] TJ");
4643 if rise != 0.0 {
4644 out.push_str("\n0 Ts");
4645 }
4646 Some(out)
4647 }
4648
4649 fn begin_extraction_span(
4650 stream: &mut String,
4651 group: &[&PositionedGlyph],
4652 page_number: usize,
4653 total_pages: usize,
4654 ) -> bool {
4655 let needed = group.iter().any(|g| {
4656 (g.extraction_text.is_some() && !g.ligature) || glyph_mapping(g) != extraction_text(g)
4657 });
4658 if needed {
4659 let text: String = group
4662 .iter()
4663 .map(|g| extraction_text(g))
4664 .filter(|t| t != "\u{200B}")
4665 .collect();
4666 let text = text
4667 .replace(PAGE_NUMBER_SENTINEL, &page_number.to_string())
4668 .replace(TOTAL_PAGES_SENTINEL, &total_pages.to_string());
4669 let _ = writeln!(
4670 stream,
4671 "/Span << /ActualText {} >> BDC",
4672 Self::encode_text_string(&text)
4673 );
4674 }
4675 needed
4676 }
4677
4678 fn write_text_carriers(
4679 out: &mut String,
4680 text: &str,
4681 embed: &CustomFontEmbedData,
4682 char_spacing: f64,
4683 x: f64,
4684 y: f64,
4685 ) {
4686 if text.is_empty() {
4687 return;
4688 }
4689 out.push_str("] TJ\n3 Tr\n0 Tc\n");
4694 for ch in text.chars() {
4695 if let Some(&cid) = embed.char_to_gid.get(&ch) {
4696 let _ = writeln!(
4697 out,
4698 "<{:04X}> Tj\n1 0 0 1 {} {} Tm",
4699 cid,
4700 pdf_number(x),
4701 pdf_number(y)
4702 );
4703 }
4704 }
4705 let _ = write!(out, "0 Tr\n{} Tc\n[", pdf_number(char_spacing));
4706 }
4707
4708 fn group_glyphs_by_style(glyphs: &[PositionedGlyph]) -> Vec<Vec<&PositionedGlyph>> {
4709 Self::group_glyphs(glyphs, false)
4710 }
4711
4712 fn group_glyphs(glyphs: &[PositionedGlyph], split_on_href: bool) -> Vec<Vec<&PositionedGlyph>> {
4715 if glyphs.is_empty() {
4716 return vec![];
4717 }
4718
4719 let mut groups: Vec<Vec<&PositionedGlyph>> = Vec::new();
4720 let mut current_group: Vec<&PositionedGlyph> = vec![&glyphs[0]];
4721
4722 for glyph in &glyphs[1..] {
4723 let prev = current_group.last().unwrap();
4724 let same_style = glyph.font_family == prev.font_family
4725 && glyph.font_weight == prev.font_weight
4726 && std::mem::discriminant(&glyph.font_style)
4727 == std::mem::discriminant(&prev.font_style)
4728 && (glyph.font_size - prev.font_size).abs() < 0.01
4729 && Self::colors_equal(&glyph.color, &prev.color)
4730 && std::mem::discriminant(&glyph.text_decoration)
4731 == std::mem::discriminant(&prev.text_decoration)
4732 && (!split_on_href || glyph.href == prev.href);
4733
4734 if same_style {
4735 current_group.push(glyph);
4736 } else {
4737 groups.push(current_group);
4738 current_group = vec![glyph];
4739 }
4740 }
4741 groups.push(current_group);
4742 groups
4743 }
4744
4745 fn group_link_runs<'a>(groups: &[Vec<&'a PositionedGlyph>]) -> Vec<Option<&'a str>> {
4750 let hrefs: Vec<Option<&'a str>> = groups
4751 .iter()
4752 .map(|g| g[0].href.as_deref().filter(|h| !h.is_empty()))
4753 .collect();
4754 let mut out = vec![None; groups.len()];
4755 let mut i = 0;
4756 while i < groups.len() {
4757 let mut j = i + 1;
4758 while j < groups.len() && hrefs[j] == hrefs[i] {
4759 j += 1;
4760 }
4761 let inked = groups[i..j]
4762 .iter()
4763 .any(|g| g.iter().any(|gl| !gl.char_value.is_whitespace()));
4764 if hrefs[i].is_some() && inked {
4765 for slot in &mut out[i..j] {
4766 *slot = hrefs[i];
4767 }
4768 }
4769 i = j;
4770 }
4771 out
4772 }
4773
4774 fn colors_equal(a: &Option<Color>, b: &Option<Color>) -> bool {
4775 match (a, b) {
4776 (None, None) => true,
4777 (Some(ca), Some(cb)) => {
4778 (ca.r - cb.r).abs() < 0.001
4779 && (ca.g - cb.g).abs() < 0.001
4780 && (ca.b - cb.b).abs() < 0.001
4781 && (ca.a - cb.a).abs() < 0.001
4782 }
4783 _ => false,
4784 }
4785 }
4786
4787 fn warn_nested_links(
4796 elements: &[LayoutElement],
4797 outer: Option<&str>,
4798 warnings: &mut Vec<String>,
4799 ) {
4800 fn report(warnings: &mut Vec<String>, inner: &str, outer: &str) {
4801 let msg = format!(
4802 "render defect: a link to \"{inner}\" is inside a link to \"{outer}\". Links cannot be nested (HTML forbids it), so the outer link covers the whole area and the inner one was dropped"
4803 );
4804 if !warnings.contains(&msg) {
4805 warnings.push(msg);
4806 }
4807 }
4808 for el in elements {
4809 let own = el.href.as_deref().filter(|h| !h.is_empty());
4810 if let (Some(o), Some(h)) = (outer, own) {
4811 if h != o {
4812 report(warnings, h, o);
4813 }
4814 }
4815 let enclosing = outer.or(own);
4816 if let Some(o) = enclosing {
4817 if let DrawCommand::Text { ref lines, .. } = el.draw {
4818 for g in lines.iter().flat_map(|l| l.glyphs.iter()) {
4819 if let Some(h) = g.href.as_deref().filter(|h| !h.is_empty()) {
4820 if h != o {
4821 report(warnings, h, o);
4822 }
4823 }
4824 }
4825 }
4826 }
4827 Self::warn_nested_links(&el.children, enclosing, warnings);
4828 }
4829 }
4830
4831 fn collect_link_annotations(
4832 elements: &[LayoutElement],
4833 page_height: f64,
4834 annotations: &mut Vec<LinkAnnotation>,
4835 ) {
4836 for element in elements {
4837 if let Some(ref href) = element.href {
4838 if !href.is_empty() {
4839 let pdf_y = page_height - element.y - element.height;
4840 annotations.push(LinkAnnotation {
4841 x: element.x,
4842 y: pdf_y,
4843 width: element.width,
4844 height: element.height,
4845 href: href.clone(),
4846 });
4847 continue;
4849 }
4850 }
4851 if let DrawCommand::Text { ref lines, .. } = element.draw {
4856 for line in lines {
4857 let spans = Self::inline_link_spans(line);
4858 if spans.is_empty() {
4859 continue;
4860 }
4861 let (top, height) = if lines.len() == 1 {
4865 (element.y, element.height)
4866 } else {
4867 let fs = line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
4868 (line.y - fs * 0.8 - (line.height - fs) / 2.0, line.height)
4869 };
4870 for span in spans {
4871 annotations.push(LinkAnnotation {
4872 x: span.x0,
4873 y: page_height - top - height,
4874 width: span.x1 - span.x0,
4875 height,
4876 href: span.href,
4877 });
4878 }
4879 }
4880 }
4881 Self::collect_link_annotations(&element.children, page_height, annotations);
4882 }
4883 }
4884
4885 fn inline_link_spans(line: &TextLine) -> Vec<InlineLinkSpan> {
4892 let mut spans: Vec<InlineLinkSpan> = Vec::new();
4893 if !line.glyphs.iter().any(|g| g.href.is_some()) {
4894 return spans;
4895 }
4896 let mut prev_href: Option<&str> = None;
4897 for g in &line.glyphs {
4898 let x0 = line.x + g.x_offset;
4899 let x1 = x0 + g.x_advance;
4900 let href = g.href.as_deref().filter(|h| !h.is_empty());
4901 if let Some(h) = href {
4902 if prev_href != Some(h) {
4903 spans.push(InlineLinkSpan {
4904 href: h.to_string(),
4905 x0: f64::INFINITY,
4906 x1: f64::NEG_INFINITY,
4907 });
4908 }
4909 if !g.char_value.is_whitespace() {
4913 if let Some(last) = spans.last_mut() {
4914 last.x0 = last.x0.min(x0);
4915 last.x1 = last.x1.max(x1);
4916 }
4917 }
4918 }
4919 prev_href = href;
4920 }
4921 spans.retain(|s| s.x1 > s.x0);
4923 spans
4924 }
4925
4926 fn collect_form_fields(
4928 elements: &[LayoutElement],
4929 page_height: f64,
4930 page_idx: usize,
4931 fields: &mut Vec<FormFieldData>,
4932 ) {
4933 for element in elements {
4934 if let DrawCommand::FormField {
4935 ref field_type,
4936 ref name,
4937 } = element.draw
4938 {
4939 let pdf_y = page_height - element.y - element.height;
4940 fields.push(FormFieldData {
4941 field_type: field_type.clone(),
4942 name: name.clone(),
4943 x: element.x,
4944 y: pdf_y,
4945 width: element.width,
4946 height: element.height,
4947 page_idx,
4948 });
4949 }
4950 Self::collect_form_fields(&element.children, page_height, page_idx, fields);
4951 }
4952 }
4953
4954 fn collect_bookmarks(
4956 elements: &[LayoutElement],
4957 page_height: f64,
4958 page_obj_id: usize,
4959 bookmarks: &mut Vec<PdfBookmark>,
4960 ) {
4961 for element in elements {
4962 if let Some(ref title) = element.bookmark {
4963 let y_pdf = page_height - element.y;
4964 bookmarks.push(PdfBookmark {
4965 title: title.clone(),
4966 page_obj_id,
4967 y_pdf,
4968 });
4969 }
4970 Self::collect_bookmarks(&element.children, page_height, page_obj_id, bookmarks);
4971 }
4972 }
4973
4974 fn write_outline_tree(
4977 &self,
4978 builder: &mut PdfBuilder,
4979 bookmarks: &[PdfBookmark],
4980 mut tag_builder: Option<&mut tagged::TagBuilder>,
4981 ) -> usize {
4982 let outlines_id = builder.objects.len();
4984 builder.objects.push(PdfObject {
4985 id: outlines_id,
4986 data: vec![],
4987 });
4988
4989 let mut item_ids: Vec<usize> = Vec::new();
4991 for _bm in bookmarks {
4992 let item_id = builder.objects.len();
4993 builder.objects.push(PdfObject {
4994 id: item_id,
4995 data: vec![],
4996 });
4997 item_ids.push(item_id);
4998 }
4999
5000 for (i, (bm, &item_id)) in bookmarks.iter().zip(item_ids.iter()).enumerate() {
5002 let wants_sd = tag_builder
5009 .as_mut()
5010 .map(|tb| tb.request_struct_destination(&bm.title, item_id))
5011 .unwrap_or(false);
5012 let dest = if wants_sd {
5013 format!("/SD [999999999 0 R /XYZ 0 {:.2} null]", bm.y_pdf)
5014 } else {
5015 format!("/Dest [{} 0 R /XYZ 0 {:.2} null]", bm.page_obj_id, bm.y_pdf)
5016 };
5017 let mut dict = format!(
5018 "<< /Title {} /Parent {} 0 R {}",
5019 Self::encode_text_string(&bm.title),
5020 outlines_id,
5021 dest,
5022 );
5023 if i > 0 {
5024 let _ = write!(dict, " /Prev {} 0 R", item_ids[i - 1]);
5025 }
5026 if i + 1 < item_ids.len() {
5027 let _ = write!(dict, " /Next {} 0 R", item_ids[i + 1]);
5028 }
5029 dict.push_str(" >>");
5030 builder.objects[item_id].data = dict.into_bytes();
5031 }
5032
5033 let first_id = item_ids.first().copied().unwrap_or(0);
5035 let last_id = item_ids.last().copied().unwrap_or(0);
5036 let outlines_dict = format!(
5037 "<< /Type /Outlines /First {} 0 R /Last {} 0 R /Count {} >>",
5038 first_id,
5039 last_id,
5040 bookmarks.len()
5041 );
5042 builder.objects[outlines_id].data = outlines_dict.into_bytes();
5043
5044 outlines_id
5045 }
5046
5047 fn write_svg_commands(
5049 stream: &mut String,
5050 commands: &[SvgCommand],
5051 ext_gstate_map: &HashMap<u64, (usize, String)>,
5052 ) {
5053 for cmd in commands {
5054 match cmd {
5055 SvgCommand::MoveTo(x, y) => {
5056 let _ = writeln!(stream, "{:.2} {:.2} m", x, y);
5057 }
5058 SvgCommand::LineTo(x, y) => {
5059 let _ = writeln!(stream, "{:.2} {:.2} l", x, y);
5060 }
5061 SvgCommand::CurveTo(x1, y1, x2, y2, x3, y3) => {
5062 let _ = writeln!(
5063 stream,
5064 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5065 x1, y1, x2, y2, x3, y3
5066 );
5067 }
5068 SvgCommand::ClosePath => {
5069 let _ = writeln!(stream, "h");
5070 }
5071 SvgCommand::SetFill(r, g, b) => {
5072 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", r, g, b);
5073 }
5074 SvgCommand::SetFillNone => {
5075 }
5077 SvgCommand::SetStroke(r, g, b) => {
5078 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", r, g, b);
5079 }
5080 SvgCommand::SetStrokeNone => {
5081 }
5083 SvgCommand::SetStrokeWidth(w) => {
5084 let _ = writeln!(stream, "{:.2} w", w);
5085 }
5086 SvgCommand::Fill => {
5087 let _ = writeln!(stream, "f");
5088 }
5089 SvgCommand::Stroke => {
5090 let _ = writeln!(stream, "S");
5091 }
5092 SvgCommand::FillAndStroke => {
5093 let _ = writeln!(stream, "B");
5094 }
5095 SvgCommand::SetLineCap(cap) => {
5096 let _ = writeln!(stream, "{} J", cap);
5097 }
5098 SvgCommand::SetLineJoin(join) => {
5099 let _ = writeln!(stream, "{} j", join);
5100 }
5101 SvgCommand::SaveState => {
5102 let _ = writeln!(stream, "q");
5103 }
5104 SvgCommand::RestoreState => {
5105 let _ = writeln!(stream, "Q");
5106 }
5107 SvgCommand::SetOpacity(opacity) => {
5108 if let Some((_, gs_name)) = ext_gstate_map.get(&opacity.to_bits()) {
5109 let _ = writeln!(stream, "/{} gs", gs_name);
5110 }
5111 }
5112 }
5113 }
5114 }
5115
5116 pub(crate) fn escape_pdf_string(s: &str) -> String {
5118 s.replace('\\', "\\\\")
5119 .replace('(', "\\(")
5120 .replace(')', "\\)")
5121 }
5122
5123 pub(crate) fn encode_text_string(s: &str) -> String {
5136 if s.bytes().all(|b| (0x20..=0x7E).contains(&b)) {
5137 return format!("({})", Self::escape_pdf_string(s));
5138 }
5139 let mut out = String::with_capacity(6 + s.len() * 4);
5140 out.push_str("<FEFF");
5141 for unit in s.encode_utf16() {
5142 let _ = write!(out, "{unit:04X}");
5143 }
5144 out.push('>');
5145 out
5146 }
5147
5148 fn decode_attachment_src(src: &str) -> Result<Vec<u8>, FormeError> {
5151 use base64::Engine as _;
5152 let b64 = src.rsplit_once(";base64,").map(|(_, d)| d).unwrap_or(src);
5153 base64::engine::general_purpose::STANDARD
5154 .decode(b64.trim())
5155 .map_err(|e| {
5156 FormeError::RenderError(format!(
5157 "attachment src is not valid base64 (expected base64 bytes or a data: URI): {e}"
5158 ))
5159 })
5160 }
5161
5162 fn mime_to_pdf_name(mime: &str) -> String {
5165 let mut out = String::with_capacity(mime.len() + 2);
5166 for b in mime.bytes() {
5167 let regular =
5168 b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'+' | b'\'' | b'"');
5169 if regular {
5170 out.push(b as char);
5171 } else {
5172 let _ = write!(out, "#{:02X}", b);
5173 }
5174 }
5175 out
5176 }
5177
5178 fn encode_winansi_text(builder: &PdfBuilder, s: &str) -> String {
5182 let mut result = String::with_capacity(s.len());
5183 for ch in s.chars() {
5184 let b = Self::unicode_to_winansi(ch).unwrap_or_else(|| {
5185 builder.missing_glyphs.borrow_mut().insert(ch);
5186 b'?'
5187 });
5188 match b {
5189 b'\\' => result.push_str("\\\\"),
5190 b'(' => result.push_str("\\("),
5191 b')' => result.push_str("\\)"),
5192 0x20..=0x7E => result.push(b as char),
5193 _ => {
5194 let _ = write!(result, "\\{:03o}", b);
5195 }
5196 }
5197 }
5198 result
5199 }
5200
5201 fn unicode_to_winansi(ch: char) -> Option<u8> {
5203 crate::font::unicode_to_winansi(ch)
5204 }
5205
5206 fn serialize(&self, builder: &PdfBuilder, info_obj_id: Option<usize>) -> Vec<u8> {
5208 let mut output: Vec<u8> = Vec::new();
5209 let mut offsets: Vec<usize> = vec![0; builder.objects.len()];
5210
5211 output.extend_from_slice(match builder.pdf_version {
5213 crate::model::PdfVersion::V1_7 => b"%PDF-1.7\n".as_slice(),
5214 crate::model::PdfVersion::V2_0 => b"%PDF-2.0\n".as_slice(),
5215 });
5216 output.extend_from_slice(b"%\xe2\xe3\xcf\xd3\n");
5217
5218 for (i, obj) in builder.objects.iter().enumerate().skip(1) {
5219 offsets[i] = output.len();
5220 let header = format!("{} 0 obj\n", i);
5221 output.extend_from_slice(header.as_bytes());
5222 output.extend_from_slice(&obj.data);
5223 output.extend_from_slice(b"\nendobj\n\n");
5224 }
5225
5226 let xref_offset = output.len();
5227 let _ = writeln!(output, "xref\n0 {}", builder.objects.len());
5228 let _ = writeln!(output, "0000000000 65535 f ");
5229 for offset in offsets.iter().skip(1) {
5230 let _ = writeln!(output, "{:010} 00000 n ", offset);
5231 }
5232
5233 let _ = write!(
5234 output,
5235 "trailer\n<< /Size {} /Root 1 0 R",
5236 builder.objects.len()
5237 );
5238 if let Some(info_id) = info_obj_id {
5239 let _ = write!(output, " /Info {} 0 R", info_id);
5240 }
5241 {
5247 use sha2::Digest as _;
5248 let digest = sha2::Sha256::digest(&output);
5249 let mut id_hex = String::with_capacity(32);
5250 for b in &digest[..16] {
5251 let _ = write!(id_hex, "{:02X}", b);
5252 }
5253 let _ = write!(output, " /ID [<{id_hex}> <{id_hex}>]");
5254 }
5255 let _ = writeln!(output, " >>\nstartxref\n{}\n%%EOF", xref_offset);
5256
5257 output
5258 }
5259}
5260
5261fn write_chart_primitive(
5266 stream: &mut String,
5267 prim: &crate::chart::ChartPrimitive,
5268 _chart_height: f64,
5269 builder: &PdfBuilder,
5270) {
5271 use crate::chart::{ChartPrimitive, TextAnchor};
5272 use crate::font::metrics::unicode_to_winansi;
5273
5274 match prim {
5275 ChartPrimitive::Rect { x, y, w, h, fill } => {
5276 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
5277 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re f", x, y, w, h);
5278 }
5279
5280 ChartPrimitive::Line {
5281 x1,
5282 y1,
5283 x2,
5284 y2,
5285 stroke,
5286 width,
5287 } => {
5288 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
5289 let _ = writeln!(stream, "{:.2} w", width);
5290 let _ = writeln!(stream, "{:.2} {:.2} m {:.2} {:.2} l S", x1, y1, x2, y2);
5291 }
5292
5293 ChartPrimitive::Polyline {
5294 points,
5295 stroke,
5296 width,
5297 } => {
5298 if points.len() < 2 {
5299 return;
5300 }
5301 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
5302 let _ = writeln!(stream, "{:.2} w", width);
5303 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
5304 for &(px, py) in &points[1..] {
5305 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
5306 }
5307 let _ = writeln!(stream, "S");
5308 }
5309
5310 ChartPrimitive::FilledPath {
5311 points,
5312 fill,
5313 opacity,
5314 } => {
5315 if points.len() < 3 {
5316 return;
5317 }
5318 let _ = writeln!(stream, "q");
5319 if *opacity < 1.0 {
5321 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
5322 let _ = writeln!(stream, "/{} gs", gs_name);
5323 }
5324 }
5325 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
5326 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
5327 for &(px, py) in &points[1..] {
5328 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
5329 }
5330 let _ = writeln!(stream, "h f");
5331 let _ = writeln!(stream, "Q");
5332 }
5333
5334 ChartPrimitive::Circle { cx, cy, r, fill } => {
5335 let kappa: f64 = 0.5523;
5337 let kr = kappa * r;
5338 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
5339 let _ = writeln!(stream, "{:.2} {:.2} m", cx + r, cy);
5340 let _ = writeln!(
5341 stream,
5342 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5343 cx + r,
5344 cy + kr,
5345 cx + kr,
5346 cy + r,
5347 cx,
5348 cy + r
5349 );
5350 let _ = writeln!(
5351 stream,
5352 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5353 cx - kr,
5354 cy + r,
5355 cx - r,
5356 cy + kr,
5357 cx - r,
5358 cy
5359 );
5360 let _ = writeln!(
5361 stream,
5362 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5363 cx - r,
5364 cy - kr,
5365 cx - kr,
5366 cy - r,
5367 cx,
5368 cy - r
5369 );
5370 let _ = writeln!(
5371 stream,
5372 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5373 cx + kr,
5374 cy - r,
5375 cx + r,
5376 cy - kr,
5377 cx + r,
5378 cy
5379 );
5380 let _ = writeln!(stream, "f");
5381 }
5382
5383 ChartPrimitive::ArcSector {
5384 cx,
5385 cy,
5386 r,
5387 start_angle,
5388 end_angle,
5389 fill,
5390 } => {
5391 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
5392 let _ = writeln!(stream, "{:.2} {:.2} m", cx, cy);
5394 let sx = cx + r * start_angle.cos();
5396 let sy = cy + r * start_angle.sin();
5397 let _ = writeln!(stream, "{:.2} {:.2} l", sx, sy);
5398
5399 let mut angle = *start_angle;
5401 let total = end_angle - start_angle;
5402 let segments = ((total.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
5403 let step = total / segments as f64;
5404
5405 for _ in 0..segments {
5406 let a1 = angle;
5407 let a2 = angle + step;
5408 let alpha = 4.0 / 3.0 * ((a2 - a1) / 4.0).tan();
5409
5410 let p1x = cx + r * a1.cos();
5411 let p1y = cy + r * a1.sin();
5412 let p2x = cx + r * a2.cos();
5413 let p2y = cy + r * a2.sin();
5414
5415 let cp1x = p1x - alpha * r * a1.sin();
5416 let cp1y = p1y + alpha * r * a1.cos();
5417 let cp2x = p2x + alpha * r * a2.sin();
5418 let cp2y = p2y - alpha * r * a2.cos();
5419
5420 let _ = writeln!(
5421 stream,
5422 "{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c",
5423 cp1x, cp1y, cp2x, cp2y, p2x, p2y
5424 );
5425 angle = a2;
5426 }
5427
5428 let _ = writeln!(stream, "h f");
5430 }
5431
5432 ChartPrimitive::Label {
5433 text,
5434 x,
5435 y,
5436 font_size,
5437 color,
5438 anchor,
5439 } => {
5440 let metrics = crate::font::StandardFont::Helvetica.metrics();
5442 let text_width = metrics.measure_string(text, *font_size, 0.0);
5443 let x_offset = match anchor {
5444 TextAnchor::Left => 0.0,
5445 TextAnchor::Center => -text_width / 2.0,
5446 TextAnchor::Right => -text_width,
5447 };
5448
5449 let font_idx = builder
5451 .font_objects
5452 .iter()
5453 .enumerate()
5454 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
5455 .map(|(i, _)| i)
5456 .unwrap_or(0);
5457
5458 let encoded: String = text
5460 .chars()
5461 .map(|ch| {
5462 if let Some(code) = unicode_to_winansi(ch) {
5463 code as char
5464 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
5465 ch
5466 } else {
5467 builder.missing_glyphs.borrow_mut().insert(ch);
5468 '?'
5469 }
5470 })
5471 .collect();
5472 let escaped = pdf_escape_string(&encoded);
5473
5474 let _ = writeln!(stream, "q");
5476 let _ = writeln!(stream, "1 0 0 -1 {:.4} {:.4} cm", x + x_offset, *y);
5477 let _ = writeln!(
5478 stream,
5479 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
5480 font_idx, font_size, color.r, color.g, color.b, escaped
5481 );
5482 let _ = writeln!(stream, "Q");
5483 }
5484 ChartPrimitive::VerticalLabel {
5485 text,
5486 x,
5487 y,
5488 font_size,
5489 color,
5490 } => {
5491 let metrics = crate::font::StandardFont::Helvetica.metrics();
5492 let text_width = metrics.measure_string(text, *font_size, 0.0);
5493 let font_idx = builder
5494 .font_objects
5495 .iter()
5496 .enumerate()
5497 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
5498 .map(|(i, _)| i)
5499 .unwrap_or(0);
5500 let encoded: String = text
5501 .chars()
5502 .map(|ch| {
5503 if let Some(code) = unicode_to_winansi(ch) {
5504 code as char
5505 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
5506 ch
5507 } else {
5508 builder.missing_glyphs.borrow_mut().insert(ch);
5509 '?'
5510 }
5511 })
5512 .collect();
5513 let escaped = pdf_escape_string(&encoded);
5514
5515 let _ = writeln!(stream, "q");
5520 let _ = writeln!(
5521 stream,
5522 "0 -1 -1 0 {:.4} {:.4} cm",
5523 x + font_size * 0.35,
5524 y + text_width / 2.0
5525 );
5526 let _ = writeln!(
5527 stream,
5528 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
5529 font_idx, font_size, color.r, color.g, color.b, escaped
5530 );
5531 let _ = writeln!(stream, "Q");
5532 }
5533 }
5534}
5535
5536fn normalize_gradient_stops(
5543 stops: &[crate::style::GradientStop],
5544 fallback: Color,
5545) -> Vec<crate::style::GradientStop> {
5546 use crate::style::GradientStop;
5547 if stops.is_empty() {
5548 return vec![
5549 GradientStop {
5550 position: 0.0,
5551 color: fallback,
5552 },
5553 GradientStop {
5554 position: 1.0,
5555 color: fallback,
5556 },
5557 ];
5558 }
5559 let mut sorted: Vec<GradientStop> = stops
5560 .iter()
5561 .map(|s| GradientStop {
5562 position: s.position.clamp(0.0, 1.0),
5563 color: s.color,
5564 })
5565 .collect();
5566 sorted.sort_by(|a, b| {
5567 a.position
5568 .partial_cmp(&b.position)
5569 .unwrap_or(std::cmp::Ordering::Equal)
5570 });
5571 if sorted[0].position > 0.0 {
5572 sorted.insert(
5573 0,
5574 GradientStop {
5575 position: 0.0,
5576 color: sorted[0].color,
5577 },
5578 );
5579 }
5580 if sorted[sorted.len() - 1].position < 1.0 {
5581 let last = sorted[sorted.len() - 1].color;
5582 sorted.push(GradientStop {
5583 position: 1.0,
5584 color: last,
5585 });
5586 }
5587 sorted
5588}
5589
5590fn pdf_escape_string(s: &str) -> String {
5591 let mut out = String::with_capacity(s.len());
5592 for ch in s.chars() {
5593 match ch {
5594 '(' => out.push_str("\\("),
5595 ')' => out.push_str("\\)"),
5596 '\\' => out.push_str("\\\\"),
5597 _ => out.push(ch),
5598 }
5599 }
5600 out
5601}
5602
5603fn has_placeholder_group(group: &[&PositionedGlyph]) -> bool {
5606 group
5607 .iter()
5608 .any(|g| g.char_value == PAGE_NUMBER_SENTINEL || g.char_value == TOTAL_PAGES_SENTINEL)
5609}
5610
5611fn pdf_number(v: f64) -> String {
5614 let s = format!("{:.3}", v);
5615 let s = s.trim_end_matches('0').trim_end_matches('.');
5616 if s.is_empty() || s == "-" {
5617 "0".to_string()
5618 } else {
5619 s.to_string()
5620 }
5621}
5622
5623#[cfg(test)]
5624mod tests {
5625 use super::*;
5626 use crate::font::FontContext;
5627
5628 #[test]
5635 fn test_embedded_srgb_is_a_valid_icc_profile() {
5636 let icc: &[u8] = include_bytes!("sRGB.icc");
5637 assert!(
5638 icc.len() >= 128,
5639 "ICC shorter than its 128-byte header: {}",
5640 icc.len()
5641 );
5642 assert_ne!(
5644 icc[0], b'<',
5645 "embedded ICC starts with '<' — looks like HTML, not a profile"
5646 );
5647 assert_eq!(&icc[36..40], b"acsp", "missing ICC 'acsp' signature");
5649 let device_class = &icc[12..16];
5651 assert!(
5652 device_class == b"mntr" || device_class == b"prtr",
5653 "ICC device class {:?} is not mntr/prtr (PDF/A 6.2.3)",
5654 String::from_utf8_lossy(device_class),
5655 );
5656 assert_eq!(&icc[16..20], b"RGB ", "ICC data colour space is not RGB");
5658 }
5659
5660 #[test]
5661 fn test_escape_pdf_string() {
5662 assert_eq!(
5663 PdfWriter::escape_pdf_string("Hello (World)"),
5664 "Hello \\(World\\)"
5665 );
5666 assert_eq!(PdfWriter::escape_pdf_string("back\\slash"), "back\\\\slash");
5667 }
5668
5669 #[test]
5670 fn test_encode_text_string() {
5671 assert_eq!(PdfWriter::encode_text_string("A (b)"), "(A \\(b\\))");
5673 assert_eq!(PdfWriter::encode_text_string(""), "()");
5674 assert_eq!(PdfWriter::encode_text_string("Üb"), "<FEFF00DC0062>");
5676 assert_eq!(PdfWriter::encode_text_string("𝄞"), "<FEFFD834DD1E>");
5678 assert_eq!(PdfWriter::encode_text_string("a\nb"), "<FEFF0061000A0062>");
5680 }
5681
5682 #[test]
5683 fn test_empty_document_produces_valid_pdf() {
5684 let writer = PdfWriter::new();
5685 let font_context = FontContext::new();
5686 let pages = vec![LayoutPage {
5687 width: 595.28,
5688 height: 841.89,
5689 elements: vec![],
5690 fixed_header: vec![],
5691 fixed_footer: vec![],
5692 watermarks: vec![],
5693 config: PageConfig::default(),
5694 page_name: None,
5695 }];
5696 let metadata = Metadata::default();
5697 let (bytes, _warnings) = writer
5698 .write(
5699 &pages,
5700 &metadata,
5701 &font_context,
5702 false,
5703 None,
5704 false,
5705 None,
5706 &[],
5707 None,
5708 false,
5709 crate::model::PdfVersion::V1_7,
5710 false,
5711 )
5712 .unwrap();
5713
5714 assert!(bytes.starts_with(b"%PDF-1.7"));
5715 assert!(bytes.windows(5).any(|w| w == b"%%EOF"));
5716 assert!(bytes.windows(4).any(|w| w == b"xref"));
5717 assert!(bytes.windows(7).any(|w| w == b"trailer"));
5718 }
5719
5720 #[test]
5721 fn test_metadata_in_pdf() {
5722 let writer = PdfWriter::new();
5723 let font_context = FontContext::new();
5724 let pages = vec![LayoutPage {
5725 width: 595.28,
5726 height: 841.89,
5727 elements: vec![],
5728 fixed_header: vec![],
5729 fixed_footer: vec![],
5730 watermarks: vec![],
5731 config: PageConfig::default(),
5732 page_name: None,
5733 }];
5734 let metadata = Metadata {
5735 title: Some("Test Document".to_string()),
5736 author: Some("Forme".to_string()),
5737 subject: None,
5738 creator: None,
5739 lang: None,
5740 };
5741 let (bytes, _warnings) = writer
5742 .write(
5743 &pages,
5744 &metadata,
5745 &font_context,
5746 false,
5747 None,
5748 false,
5749 None,
5750 &[],
5751 None,
5752 false,
5753 crate::model::PdfVersion::V1_7,
5754 false,
5755 )
5756 .unwrap();
5757 let text = String::from_utf8_lossy(&bytes);
5758
5759 assert!(text.contains("/Title (Test Document)"));
5760 assert!(text.contains("/Author (Forme)"));
5761 }
5762
5763 #[test]
5764 fn test_bold_font_registered_separately() {
5765 let writer = PdfWriter::new();
5766 let font_context = FontContext::new();
5767
5768 let pages = vec![LayoutPage {
5770 width: 595.28,
5771 height: 841.89,
5772 elements: vec![
5773 LayoutElement {
5774 x: 54.0,
5775 y: 54.0,
5776 width: 100.0,
5777 height: 16.8,
5778 draw: DrawCommand::Text {
5779 lines: vec![TextLine {
5780 x: 54.0,
5781 y: 66.0,
5782 width: 50.0,
5783 height: 16.8,
5784 glyphs: vec![PositionedGlyph {
5785 glyph_id: 65,
5786 x_offset: 0.0,
5787 y_offset: 0.0,
5788 x_advance: 8.0,
5789 font_size: 12.0,
5790 font_family: "Helvetica".into(),
5791 font_weight: 400,
5792 font_style: FontStyle::Normal,
5793 char_value: 'A',
5794 color: None,
5795 href: None,
5796 text_decoration: TextDecoration::None,
5797 letter_spacing: 0.0,
5798 cluster_text: None,
5799 extraction_text: None,
5800 ligature: false,
5801 }],
5802 word_spacing: 0.0,
5803 }],
5804 color: Color::BLACK,
5805 text_decoration: TextDecoration::None,
5806 opacity: 1.0,
5807 },
5808 children: vec![],
5809 node_type: None,
5810 resolved_style: None,
5811 source_location: None,
5812 href: None,
5813 bookmark: None,
5814 alt: None,
5815 is_header_row: false,
5816 actual_text: None,
5817 list_numbering: None,
5818 col_span: 1,
5819 overflow: Overflow::default(),
5820 opacity: 1.0,
5821 },
5822 LayoutElement {
5823 x: 54.0,
5824 y: 74.0,
5825 width: 100.0,
5826 height: 16.8,
5827 draw: DrawCommand::Text {
5828 lines: vec![TextLine {
5829 x: 54.0,
5830 y: 86.0,
5831 width: 50.0,
5832 height: 16.8,
5833 glyphs: vec![PositionedGlyph {
5834 glyph_id: 65,
5835 x_offset: 0.0,
5836 y_offset: 0.0,
5837 x_advance: 8.0,
5838 font_size: 12.0,
5839 font_family: "Helvetica".into(),
5840 font_weight: 700,
5841 font_style: FontStyle::Normal,
5842 char_value: 'A',
5843 color: None,
5844 href: None,
5845 text_decoration: TextDecoration::None,
5846 letter_spacing: 0.0,
5847 cluster_text: None,
5848 extraction_text: None,
5849 ligature: false,
5850 }],
5851 word_spacing: 0.0,
5852 }],
5853 color: Color::BLACK,
5854 text_decoration: TextDecoration::None,
5855 opacity: 1.0,
5856 },
5857 children: vec![],
5858 node_type: None,
5859 resolved_style: None,
5860 source_location: None,
5861 href: None,
5862 bookmark: None,
5863 alt: None,
5864 is_header_row: false,
5865 actual_text: None,
5866 list_numbering: None,
5867 col_span: 1,
5868 overflow: Overflow::default(),
5869 opacity: 1.0,
5870 },
5871 ],
5872 fixed_header: vec![],
5873 fixed_footer: vec![],
5874 watermarks: vec![],
5875 config: PageConfig::default(),
5876 page_name: None,
5877 }];
5878
5879 let metadata = Metadata::default();
5880 let (bytes, _warnings) = writer
5881 .write(
5882 &pages,
5883 &metadata,
5884 &font_context,
5885 false,
5886 None,
5887 false,
5888 None,
5889 &[],
5890 None,
5891 false,
5892 crate::model::PdfVersion::V1_7,
5893 false,
5894 )
5895 .unwrap();
5896 let text = String::from_utf8_lossy(&bytes);
5897
5898 assert!(
5900 text.contains("Helvetica"),
5901 "Should contain regular Helvetica"
5902 );
5903 assert!(
5904 text.contains("Helvetica-Bold"),
5905 "Should contain Helvetica-Bold"
5906 );
5907 }
5908
5909 #[test]
5910 fn test_sanitize_font_name() {
5911 assert_eq!(PdfWriter::sanitize_font_name("Inter", 400, false), "Inter");
5912 assert_eq!(
5913 PdfWriter::sanitize_font_name("Inter", 700, false),
5914 "Inter-Bold"
5915 );
5916 assert_eq!(
5917 PdfWriter::sanitize_font_name("Inter", 400, true),
5918 "Inter-Italic"
5919 );
5920 assert_eq!(
5921 PdfWriter::sanitize_font_name("Inter", 700, true),
5922 "Inter-Bold-Italic"
5923 );
5924 assert_eq!(
5925 PdfWriter::sanitize_font_name("Noto Sans", 400, false),
5926 "NotoSans"
5927 );
5928 assert_eq!(
5929 PdfWriter::sanitize_font_name("Font (Display)", 400, false),
5930 "FontDisplay"
5931 );
5932 }
5933
5934 #[test]
5935 fn test_tounicode_cmap_format() {
5936 let mut glyph_to_char = HashMap::new();
5938 glyph_to_char.insert(36u16, "A".to_string());
5939 glyph_to_char.insert(37u16, "B".to_string());
5940
5941 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&glyph_to_char, "TestFont");
5942
5943 assert!(cmap.contains("begincmap"), "CMap should contain begincmap");
5944 assert!(cmap.contains("endcmap"), "CMap should contain endcmap");
5945 assert!(
5946 cmap.contains("beginbfchar"),
5947 "CMap should contain beginbfchar"
5948 );
5949 assert!(cmap.contains("endbfchar"), "CMap should contain endbfchar");
5950 assert!(
5951 cmap.contains("<0024> <0041>"),
5952 "Should map gid 0x0024 to Unicode 'A' 0x0041"
5953 );
5954 assert!(
5955 cmap.contains("<0025> <0042>"),
5956 "Should map gid 0x0025 to Unicode 'B' 0x0042"
5957 );
5958 assert!(
5959 cmap.contains("begincodespacerange"),
5960 "Should define codespace range"
5961 );
5962 assert!(
5963 cmap.contains("<0000> <FFFF>"),
5964 "Codespace should be 0000-FFFF"
5965 );
5966 }
5967
5968 #[test]
5973 fn test_tounicode_cmap_multi_char_and_non_bmp_destinations() {
5974 let mut gid_to_text = HashMap::new();
5975 gid_to_text.insert(5u16, "ffi".to_string());
5976 gid_to_text.insert(6u16, "Th".to_string());
5977 gid_to_text.insert(7u16, "\u{1F600}".to_string());
5978
5979 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&gid_to_text, "TestFont");
5980
5981 assert!(cmap.contains("<0005> <006600660069>"), "{cmap}");
5982 assert!(cmap.contains("<0006> <00540068>"), "{cmap}");
5983 assert!(cmap.contains("<0007> <D83DDE00>"), "{cmap}");
5984 }
5985
5986 fn text_glyph(
5987 glyph_id: u16,
5988 ch: char,
5989 cluster: Option<&str>,
5990 ligature: bool,
5991 ) -> PositionedGlyph {
5992 PositionedGlyph {
5993 glyph_id,
5994 x_offset: 0.0,
5995 y_offset: 0.0,
5996 x_advance: 5.0,
5997 font_size: 12.0,
5998 font_family: "Lig".into(),
5999 font_weight: 400,
6000 font_style: FontStyle::Normal,
6001 char_value: ch,
6002 color: None,
6003 href: None,
6004 text_decoration: TextDecoration::None,
6005 letter_spacing: 0.0,
6006 cluster_text: cluster.map(str::to_string),
6007 extraction_text: None,
6008 ligature,
6009 }
6010 }
6011
6012 #[test]
6013 fn test_record_glyph_text_ligature_maps_whole_cluster() {
6014 let mut map = HashMap::new();
6015 record_glyph_text(&mut map, &text_glyph(9, 'f', Some("ffi"), true));
6016 assert_eq!(map[&9], "ffi");
6017 }
6018
6019 #[test]
6023 fn test_record_glyph_text_shared_cluster_glyph_keeps_its_char() {
6024 let mut map = HashMap::new();
6025 record_glyph_text(&mut map, &text_glyph(9, 'k', Some("ki"), false));
6026 assert_eq!(map[&9], "k");
6027 }
6028
6029 #[test]
6033 fn test_record_glyph_text_single_char_beats_multi_char() {
6034 let mut first_multi = HashMap::new();
6035 record_glyph_text(
6036 &mut first_multi,
6037 &text_glyph(9, 'e', Some("e\u{FE0F}"), true),
6038 );
6039 record_glyph_text(&mut first_multi, &text_glyph(9, 'e', None, false));
6040 assert_eq!(first_multi[&9], "e");
6041
6042 let mut first_single = HashMap::new();
6043 record_glyph_text(&mut first_single, &text_glyph(9, 'e', None, false));
6044 record_glyph_text(
6045 &mut first_single,
6046 &text_glyph(9, 'e', Some("e\u{FE0F}"), true),
6047 );
6048 assert_eq!(first_single[&9], "e");
6049
6050 let mut two_multi = HashMap::new();
6052 record_glyph_text(&mut two_multi, &text_glyph(9, 'f', Some("fi"), true));
6053 record_glyph_text(&mut two_multi, &text_glyph(9, 'f', Some("ffi"), true));
6054 assert_eq!(two_multi[&9], "fi");
6055 }
6056
6057 #[test]
6058 fn test_w_array_format() {
6059 let mut char_to_gid = HashMap::new();
6060 char_to_gid.insert('A', 36u16);
6061
6062 let w_array_str = "[ 36 [600] ]";
6065 assert!(w_array_str.starts_with('['));
6066 assert!(w_array_str.ends_with(']'));
6067 }
6068
6069 #[test]
6070 fn test_hex_glyph_encoding() {
6071 let gid: u16 = 0x0041;
6073 let hex = format!("{:04X}", gid);
6074 assert_eq!(hex, "0041");
6075
6076 let gids = [0x0041u16, 0x0042, 0x0043];
6077 let hex_str: String = gids.iter().map(|g| format!("{:04X}", g)).collect();
6078 assert_eq!(hex_str, "004100420043");
6079 }
6080
6081 #[test]
6082 fn test_standard_font_still_uses_text_string() {
6083 let writer = PdfWriter::new();
6084 let font_context = FontContext::new();
6085
6086 let pages = vec![LayoutPage {
6087 width: 595.28,
6088 height: 841.89,
6089 elements: vec![LayoutElement {
6090 x: 54.0,
6091 y: 54.0,
6092 width: 100.0,
6093 height: 16.8,
6094 draw: DrawCommand::Text {
6095 lines: vec![TextLine {
6096 x: 54.0,
6097 y: 66.0,
6098 width: 50.0,
6099 height: 16.8,
6100 glyphs: vec![PositionedGlyph {
6101 glyph_id: 65,
6102 x_offset: 0.0,
6103 y_offset: 0.0,
6104 x_advance: 8.0,
6105 font_size: 12.0,
6106 font_family: "Helvetica".into(),
6107 font_weight: 400,
6108 font_style: FontStyle::Normal,
6109 char_value: 'H',
6110 color: None,
6111 href: None,
6112 text_decoration: TextDecoration::None,
6113 letter_spacing: 0.0,
6114 cluster_text: None,
6115 extraction_text: None,
6116 ligature: false,
6117 }],
6118 word_spacing: 0.0,
6119 }],
6120 color: Color::BLACK,
6121 text_decoration: TextDecoration::None,
6122 opacity: 1.0,
6123 },
6124 children: vec![],
6125 node_type: None,
6126 resolved_style: None,
6127 source_location: None,
6128 href: None,
6129 bookmark: None,
6130 alt: None,
6131 is_header_row: false,
6132 actual_text: None,
6133 list_numbering: None,
6134 col_span: 1,
6135 overflow: Overflow::default(),
6136 opacity: 1.0,
6137 }],
6138 fixed_header: vec![],
6139 fixed_footer: vec![],
6140 watermarks: vec![],
6141 config: PageConfig::default(),
6142 page_name: None,
6143 }];
6144
6145 let metadata = Metadata::default();
6146 let (bytes, _warnings) = writer
6147 .write(
6148 &pages,
6149 &metadata,
6150 &font_context,
6151 false,
6152 None,
6153 false,
6154 None,
6155 &[],
6156 None,
6157 false,
6158 crate::model::PdfVersion::V1_7,
6159 false,
6160 )
6161 .unwrap();
6162 let text = String::from_utf8_lossy(&bytes);
6163
6164 assert!(
6166 text.contains("/Type1"),
6167 "Standard font should use Type1 subtype"
6168 );
6169 assert!(
6170 !text.contains("CIDFontType2"),
6171 "Standard font should not use CIDFontType2"
6172 );
6173 }
6174}