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)
2049 && !has_placeholder_group(group)
2050 {
2051 x_cursor = line.x + first.x_offset;
2052 }
2053 let dx = x_cursor - tm_x;
2055 let dy = pdf_y - tm_y;
2056 if absolute_text {
2057 let _ = writeln!(
2058 stream,
2059 "{:.3} {:.3} {:.3} rg\n/{} {:.1} Tf\n{:.2} Tc\n1 0 0 1 {} {} Tm",
2060 glyph_color.r,
2061 glyph_color.g,
2062 glyph_color.b,
2063 font_name,
2064 first.font_size,
2065 first.letter_spacing,
2066 pdf_number(x_cursor),
2067 pdf_number(pdf_y)
2068 );
2069 } else {
2070 let _ = writeln!(
2071 stream,
2072 "{:.3} {:.3} {:.3} rg\n/{} {:.1} Tf\n{:.2} Tc\n{:.2} {:.2} Td",
2073 glyph_color.r,
2074 glyph_color.g,
2075 glyph_color.b,
2076 font_name,
2077 first.font_size,
2078 first.letter_spacing,
2079 dx,
2080 dy
2081 );
2082 }
2083 tm_x = x_cursor;
2084 tm_y = pdf_y;
2085
2086 let actual_text =
2087 Self::begin_extraction_span(stream, group, page_number, total_pages);
2088
2089 let raw_text: String = group.iter().map(|g| g.char_value).collect();
2091 let has_placeholder = raw_text.contains(PAGE_NUMBER_SENTINEL)
2092 || raw_text.contains(TOTAL_PAGES_SENTINEL);
2093
2094 let is_custom = builder.custom_font_data.contains_key(&font_key);
2095
2096 if is_custom {
2097 if let Some(embed_data) = builder.custom_font_data.get(&font_key) {
2098 let mut hex = String::new();
2099 let mut continue_after_show = false;
2101 if has_placeholder {
2102 let pn = PAGE_NUMBER_SENTINEL.to_string();
2104 let tp = TOTAL_PAGES_SENTINEL.to_string();
2105 let text_after = raw_text
2106 .replace(&pn, &page_number.to_string())
2107 .replace(&tp, &total_pages.to_string());
2108 for ch in text_after.chars() {
2109 let gid =
2110 embed_data.char_to_gid.get(&ch).copied().unwrap_or(0);
2111 let _ = write!(hex, "{:04X}", gid);
2112 }
2113 } else {
2114 let gids: Vec<u16> = group
2116 .iter()
2117 .map(|g| {
2118 embed_data
2119 .glyph_to_cid
2120 .get(&(g.glyph_id, glyph_mapping(g)))
2121 .copied()
2122 .unwrap_or_else(|| {
2123 embed_data
2125 .char_to_gid
2126 .get(&g.char_value)
2127 .copied()
2128 .unwrap_or(0)
2129 })
2130 })
2131 .collect();
2132 if let Some(tj) = Self::positioned_tj(
2133 group,
2134 &gids,
2135 embed_data,
2136 first.letter_spacing,
2137 x_cursor,
2138 pdf_y,
2139 ) {
2140 let _ = writeln!(stream, "{}", tj);
2141 continue_after_show = true;
2142 } else {
2143 for gid in &gids {
2144 let _ = write!(hex, "{:04X}", gid);
2145 }
2146 }
2147 }
2148 if !continue_after_show {
2149 let _ = writeln!(stream, "<{}> Tj", hex);
2150 }
2151 } else {
2152 let _ = writeln!(stream, "<> Tj");
2153 }
2154 } else {
2155 let pn = PAGE_NUMBER_SENTINEL.to_string();
2156 let tp = TOTAL_PAGES_SENTINEL.to_string();
2157 let text_after = raw_text
2158 .replace(&pn, &page_number.to_string())
2159 .replace(&tp, &total_pages.to_string());
2160 let mut text_str = String::new();
2161 for ch in text_after.chars() {
2162 let b = Self::unicode_to_winansi(ch).unwrap_or_else(|| {
2163 builder.missing_glyphs.borrow_mut().insert(ch);
2164 b'?'
2165 });
2166 match b {
2167 b'\\' => text_str.push_str("\\\\"),
2168 b'(' => text_str.push_str("\\("),
2169 b')' => text_str.push_str("\\)"),
2170 0x20..=0x7E => text_str.push(b as char),
2171 _ => {
2172 let _ = write!(text_str, "\\{:03o}", b);
2173 }
2174 }
2175 }
2176 let _ = writeln!(stream, "({}) Tj", text_str);
2177 }
2178
2179 if actual_text {
2180 stream.push_str("EMC\n");
2181 }
2182
2183 let group_start_x = x_cursor;
2185
2186 if let Some(last) = group.last() {
2193 x_cursor = line.x + last.x_offset + last.x_advance;
2194 }
2195
2196 let group_dec = first.text_decoration;
2198 if !matches!(group_dec, TextDecoration::None) {
2199 group_spans.push((group_start_x, x_cursor, group_dec, glyph_color));
2200 }
2201 }
2202
2203 let _ = writeln!(stream, "ET");
2204 if open_link.is_some() {
2208 let tb = tag_builder.as_mut().expect("tag_links implies tagging");
2209 let mcid = tb.continue_current(page_idx);
2210 let _ = writeln!(
2211 stream,
2212 "EMC\n/{} <</MCID {}>> BDC",
2213 bdc_role.unwrap_or("Span"),
2214 mcid
2215 );
2216 }
2217
2218 for (span_x, span_end_x, dec, dec_color) in &group_spans {
2220 match dec {
2221 TextDecoration::Underline => {
2222 let underline_y = pdf_y - 1.5;
2223 let _ = write!(
2224 stream,
2225 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2226 dec_color.r, dec_color.g, dec_color.b,
2227 span_x, underline_y,
2228 span_end_x, underline_y
2229 );
2230 }
2231 TextDecoration::LineThrough => {
2232 let first_size =
2233 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
2234 let strikethrough_y = pdf_y + first_size * 0.3;
2235 let _ = write!(
2236 stream,
2237 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2238 dec_color.r, dec_color.g, dec_color.b,
2239 span_x, strikethrough_y,
2240 span_end_x, strikethrough_y
2241 );
2242 }
2243 TextDecoration::None => {}
2244 }
2245 }
2246
2247 if group_spans.is_empty() {
2249 if matches!(text_decoration, TextDecoration::Underline) {
2250 let underline_y = pdf_y - 1.5;
2251 let _ = write!(
2252 stream,
2253 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2254 color.r, color.g, color.b,
2255 line.x, underline_y,
2256 line.x + line.width, underline_y
2257 );
2258 }
2259 if matches!(text_decoration, TextDecoration::LineThrough) {
2260 let first_size =
2261 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
2262 let strikethrough_y = pdf_y + first_size * 0.3;
2263 let _ = write!(
2264 stream,
2265 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2266 color.r, color.g, color.b,
2267 line.x, strikethrough_y,
2268 line.x + line.width, strikethrough_y
2269 );
2270 }
2271 }
2272 }
2273
2274 if tw_set {
2278 let _ = writeln!(stream, "0 Tw");
2279 }
2280 if let Some(tb) = tag_builder.as_mut() {
2284 while let Some(idx) = inline_links.pop_front() {
2285 tb.attach_inline_link_without_content(idx);
2286 }
2287 }
2288
2289 if needs_opacity {
2290 let _ = writeln!(stream, "Q");
2291 }
2292 }
2293
2294 DrawCommand::Image { .. } => {
2295 let elem_idx = *element_counter;
2296 *element_counter += 1;
2297 if let Some(&img_idx) = builder.image_index_map.get(&(page_idx, elem_idx)) {
2298 let x = element.x;
2299 let y = page_height - element.y - element.height;
2300 let _ = write!(
2301 stream,
2302 "q\n{:.4} 0 0 {:.4} {:.2} {:.2} cm\n/Im{} Do\nQ\n",
2303 element.width, element.height, x, y, img_idx
2304 );
2305 } else {
2306 let x = element.x;
2308 let y = page_height - element.y - element.height;
2309 let _ = write!(
2310 stream,
2311 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
2312 x, y, element.width, element.height
2313 );
2314 }
2315 if tagged_mcid.is_some() {
2316 let _ = writeln!(stream, "EMC");
2317 if let Some(ref mut tb) = tag_builder {
2318 tb.end_element();
2319 }
2320 } else if is_artifact {
2321 let _ = writeln!(stream, "EMC");
2322 } else if wrap_own_draw_as_artifact {
2323 let _ = writeln!(stream, "EMC");
2327 if let Some(ref mut tb) = tag_builder {
2328 tb.end_element();
2329 }
2330 }
2331 return; }
2333
2334 DrawCommand::ImagePlaceholder => {
2335 *element_counter += 1;
2336 let x = element.x;
2337 let y = page_height - element.y - element.height;
2338 let _ = write!(
2339 stream,
2340 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
2341 x, y, element.width, element.height
2342 );
2343 if tagged_mcid.is_some() {
2344 let _ = writeln!(stream, "EMC");
2345 if let Some(ref mut tb) = tag_builder {
2346 tb.end_element();
2347 }
2348 } else if is_artifact {
2349 let _ = writeln!(stream, "EMC");
2350 } else if wrap_own_draw_as_artifact {
2351 let _ = writeln!(stream, "EMC");
2355 if let Some(ref mut tb) = tag_builder {
2356 tb.end_element();
2357 }
2358 }
2359 return;
2360 }
2361
2362 DrawCommand::Svg {
2363 commands,
2364 width: _svg_w,
2365 height: _svg_h,
2366 viewbox_min_x,
2367 viewbox_min_y,
2368 viewbox_width,
2369 viewbox_height,
2370 clip,
2371 } => {
2372 let x = element.x;
2373 let y = page_height - element.y - element.height;
2374
2375 let _ = writeln!(stream, "q");
2377 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", x, y);
2378
2379 if *viewbox_width > 0.0 && *viewbox_height > 0.0 {
2385 let raw_sx = element.width / *viewbox_width;
2386 let raw_sy = element.height / *viewbox_height;
2387 let s = raw_sx.min(raw_sy);
2388 let tx = (element.width - s * *viewbox_width) / 2.0;
2389 let ty = (element.height - s * *viewbox_height) / 2.0;
2390 let _ = writeln!(stream, "{:.4} 0 0 {:.4} {:.2} {:.2} cm", s, s, tx, ty);
2391 }
2392
2393 let _ = writeln!(stream, "1 0 0 -1 0 {:.2} cm", *viewbox_height);
2396
2397 if *viewbox_min_x != 0.0 || *viewbox_min_y != 0.0 {
2399 let _ = writeln!(
2400 stream,
2401 "1 0 0 1 {:.2} {:.2} cm",
2402 -*viewbox_min_x, -*viewbox_min_y
2403 );
2404 }
2405
2406 if *clip {
2408 let _ = writeln!(
2409 stream,
2410 "{:.2} {:.2} {:.2} {:.2} re W n",
2411 *viewbox_min_x, *viewbox_min_y, *viewbox_width, *viewbox_height
2412 );
2413 }
2414
2415 Self::write_svg_commands(stream, commands, &builder.ext_gstate_map);
2416
2417 let _ = writeln!(stream, "Q");
2418 if tagged_mcid.is_some() {
2419 let _ = writeln!(stream, "EMC");
2420 if let Some(ref mut tb) = tag_builder {
2421 tb.end_element();
2422 }
2423 } else if is_artifact {
2424 let _ = writeln!(stream, "EMC");
2425 } else if wrap_own_draw_as_artifact {
2426 let _ = writeln!(stream, "EMC");
2430 if let Some(ref mut tb) = tag_builder {
2431 tb.end_element();
2432 }
2433 }
2434 return;
2435 }
2436
2437 DrawCommand::Barcode {
2438 bars,
2439 bar_width,
2440 height,
2441 color,
2442 } => {
2443 *element_counter += 1;
2444 let _ = writeln!(stream, "q");
2445 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2446 for (i, &bar) in bars.iter().enumerate() {
2447 if bar == 1 {
2448 let bx = element.x + i as f64 * bar_width;
2449 let by = page_height - element.y - height;
2450 let _ = writeln!(
2451 stream,
2452 "{:.2} {:.2} {:.2} {:.2} re",
2453 bx, by, bar_width, height
2454 );
2455 }
2456 }
2457 let _ = writeln!(stream, "f\nQ");
2458 if tagged_mcid.is_some() {
2459 let _ = writeln!(stream, "EMC");
2460 if let Some(ref mut tb) = tag_builder {
2461 tb.end_element();
2462 }
2463 } else if is_artifact {
2464 let _ = writeln!(stream, "EMC");
2465 } else if wrap_own_draw_as_artifact {
2466 let _ = writeln!(stream, "EMC");
2470 if let Some(ref mut tb) = tag_builder {
2471 tb.end_element();
2472 }
2473 }
2474 return;
2475 }
2476
2477 DrawCommand::QrCode {
2478 modules,
2479 module_size,
2480 color,
2481 } => {
2482 *element_counter += 1;
2483 let _ = writeln!(stream, "q");
2484 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2485 for (row_idx, row) in modules.iter().enumerate() {
2486 for (col_idx, &dark) in row.iter().enumerate() {
2487 if dark {
2488 let mx = element.x + col_idx as f64 * module_size;
2489 let my = page_height - element.y - (row_idx as f64 + 1.0) * module_size;
2490 let _ = writeln!(
2491 stream,
2492 "{:.2} {:.2} {:.2} {:.2} re",
2493 mx, my, module_size, module_size
2494 );
2495 }
2496 }
2497 }
2498 let _ = writeln!(stream, "f\nQ");
2499 if tagged_mcid.is_some() {
2500 let _ = writeln!(stream, "EMC");
2501 if let Some(ref mut tb) = tag_builder {
2502 tb.end_element();
2503 }
2504 } else if is_artifact {
2505 let _ = writeln!(stream, "EMC");
2506 } else if wrap_own_draw_as_artifact {
2507 let _ = writeln!(stream, "EMC");
2511 if let Some(ref mut tb) = tag_builder {
2512 tb.end_element();
2513 }
2514 }
2515 return;
2516 }
2517
2518 DrawCommand::Chart { primitives } => {
2519 *element_counter += 1;
2520 let _ = writeln!(stream, "q");
2521 let _ = writeln!(
2523 stream,
2524 "1 0 0 -1 {:.4} {:.4} cm",
2525 element.x,
2526 page_height - element.y
2527 );
2528
2529 for prim in primitives {
2530 write_chart_primitive(stream, prim, element.height, builder);
2531 }
2532
2533 let _ = writeln!(stream, "Q");
2534 if tagged_mcid.is_some() {
2535 let _ = writeln!(stream, "EMC");
2536 if let Some(ref mut tb) = tag_builder {
2537 tb.end_element();
2538 }
2539 } else if is_artifact {
2540 let _ = writeln!(stream, "EMC");
2541 } else if wrap_own_draw_as_artifact {
2542 let _ = writeln!(stream, "EMC");
2546 if let Some(ref mut tb) = tag_builder {
2547 tb.end_element();
2548 }
2549 }
2550 return;
2551 }
2552
2553 DrawCommand::Watermark {
2554 lines,
2555 color,
2556 opacity,
2557 angle_rad,
2558 font_family: _,
2559 } => {
2560 let _ = writeln!(stream, "q");
2561 if *opacity < 1.0 {
2563 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
2564 let _ = writeln!(stream, "/{} gs", gs_name);
2565 }
2566 }
2567 let pdf_cx = element.x;
2569 let pdf_cy = page_height - element.y;
2570 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", pdf_cx, pdf_cy);
2571 let cos_a = angle_rad.cos();
2573 let sin_a = angle_rad.sin();
2574 let _ = writeln!(
2575 stream,
2576 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
2577 cos_a, sin_a, -sin_a, cos_a
2578 );
2579 let _ = writeln!(stream, "BT");
2581 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2582 if let Some(line) = lines.first() {
2583 let groups = Self::group_glyphs_by_style(&line.glyphs);
2584 let text_width = line.width;
2585 let cap_height = line.height * 0.7;
2586 let _ = writeln!(
2587 stream,
2588 "{:.2} {:.2} Td",
2589 -text_width / 2.0,
2590 -cap_height / 2.0
2591 );
2592 for group in &groups {
2593 let first = &group[0];
2594 let italic =
2595 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
2596 let fk = FontKey {
2597 family: first.font_family.to_string(),
2598 weight: first.font_weight,
2599 italic,
2600 };
2601 let idx = self.font_index(
2602 &first.font_family,
2603 first.font_weight,
2604 first.font_style,
2605 &builder.font_objects,
2606 );
2607 let font_name = format!("F{}", idx);
2608 let _ = writeln!(stream, "/{} {:.1} Tf", font_name, first.font_size);
2609 let is_custom = builder.custom_font_data.contains_key(&fk);
2610 if is_custom {
2611 if let Some(embed_data) = builder.custom_font_data.get(&fk) {
2612 let gids: Vec<_> = group
2613 .iter()
2614 .map(|g| {
2615 embed_data
2616 .glyph_to_cid
2617 .get(&(g.glyph_id, glyph_mapping(g)))
2618 .copied()
2619 .unwrap_or(0)
2620 })
2621 .collect();
2622 let actual_text = Self::begin_extraction_span(
2623 stream,
2624 group,
2625 page_number,
2626 total_pages,
2627 );
2628 let positioned = if actual_text {
2629 let x = -text_width / 2.0 + first.x_offset;
2630 let y = -cap_height / 2.0;
2631 let _ = writeln!(
2632 stream,
2633 "1 0 0 1 {} {} Tm",
2634 pdf_number(x),
2635 pdf_number(y)
2636 );
2637 Self::positioned_tj(group, &gids, embed_data, 0.0, x, y)
2638 } else {
2639 None
2640 };
2641 if let Some(tj) = positioned {
2642 let _ = writeln!(stream, "{}", tj);
2643 } else {
2644 let hex: String =
2645 gids.iter().map(|gid| format!("{:04X}", gid)).collect();
2646 let _ = writeln!(stream, "<{}> Tj", hex);
2647 }
2648 if actual_text {
2649 stream.push_str("EMC\n");
2650 }
2651 }
2652 } else {
2653 let hex_str: String = group
2654 .iter()
2655 .map(|g| format!("{:02X}", g.glyph_id as u8))
2656 .collect();
2657 let _ = writeln!(stream, "<{}> Tj", hex_str);
2658 }
2659 }
2660 }
2661 let _ = writeln!(stream, "ET");
2662 let _ = writeln!(stream, "Q");
2663 if tagged_mcid.is_some() {
2664 let _ = writeln!(stream, "EMC");
2665 if let Some(ref mut tb) = tag_builder {
2666 tb.end_element();
2667 }
2668 } else if is_artifact {
2669 let _ = writeln!(stream, "EMC");
2670 } else if wrap_own_draw_as_artifact {
2671 let _ = writeln!(stream, "EMC");
2675 if let Some(ref mut tb) = tag_builder {
2676 tb.end_element();
2677 }
2678 }
2679 return;
2680 }
2681
2682 DrawCommand::FormField { field_type, .. } => {
2683 let pdf_x = element.x;
2687 let pdf_y = page_height - element.y - element.height;
2688 let w = element.width;
2689 let h = element.height;
2690 let _ = writeln!(stream, "q");
2691 match field_type {
2692 FormFieldType::Checkbox { checked, .. } => {
2693 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
2696 let _ =
2697 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
2698 if *checked {
2699 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
2701 let sx = w / 14.0;
2702 let sy = h / 14.0;
2703 let _ = writeln!(
2704 stream,
2705 "{:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l f",
2706 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
2707 pdf_x + 5.5 * sx, pdf_y + 2.0 * sy,
2708 pdf_x + 12.0 * sx, pdf_y + 11.0 * sy,
2709 pdf_x + 11.0 * sx, pdf_y + 12.0 * sy,
2710 pdf_x + 5.5 * sx, pdf_y + 4.5 * sy,
2711 pdf_x + 3.0 * sx, pdf_y + 7.0 * sy,
2712 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
2713 );
2714 }
2715 }
2716 FormFieldType::RadioButton { checked, .. } => {
2717 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
2720 let _ =
2721 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
2722 if *checked {
2723 let cx = pdf_x + w / 2.0;
2725 let cy = pdf_y + h / 2.0;
2726 let r = (w.min(h) / 2.0) * 0.6;
2727 let k = r * 0.5523;
2728 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
2729 let _ = writeln!(
2730 stream,
2731 "{:.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",
2732 cx, cy + r,
2733 cx + k, cy + r, cx + r, cy + k, cx + r, cy,
2734 cx + r, cy - k, cx + k, cy - r, cx, cy - r,
2735 cx - k, cy - r, cx - r, cy - k, cx - r, cy,
2736 cx - r, cy + k, cx - k, cy + r, cx, cy + r,
2737 );
2738 }
2739 }
2740 FormFieldType::TextField {
2741 value,
2742 placeholder,
2743 font_size,
2744 multiline,
2745 password,
2746 ..
2747 } => {
2748 let _ = writeln!(stream, "1 1 1 rg");
2750 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2751 let _ = writeln!(stream, "0.5 w");
2752 let _ =
2753 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2754 if flatten_forms {
2756 let has_value = value.as_ref().is_some_and(|v| !v.is_empty());
2757 if has_value {
2758 let val = value.as_ref().unwrap();
2759 let display_text = if *password {
2760 "\u{2022}".repeat(val.len())
2761 } else {
2762 val.clone()
2763 };
2764 let font_idx = builder
2765 .font_objects
2766 .iter()
2767 .enumerate()
2768 .find(|(_, (key, _))| {
2769 key.family == "Helvetica"
2770 && key.weight == 400
2771 && !key.italic
2772 })
2773 .map(|(i, _)| i)
2774 .unwrap_or(0);
2775 if *multiline {
2776 let metrics = crate::font::StandardFont::Helvetica.metrics();
2778 let max_w = w - 4.0;
2779 let mut lines: Vec<String> = Vec::new();
2780 for paragraph in display_text.split('\n') {
2781 let mut line = String::new();
2782 let mut line_w = 0.0;
2783 for word in paragraph.split_whitespace() {
2784 let word_w =
2785 metrics.measure_string(word, *font_size, 0.0);
2786 let space_w = if line.is_empty() {
2787 0.0
2788 } else {
2789 metrics.measure_string(" ", *font_size, 0.0)
2790 };
2791 if word_w > max_w {
2793 let mut char_line = String::new();
2794 let mut char_w = 0.0;
2795 for ch in word.chars() {
2796 let cw = metrics.char_width(ch, *font_size);
2797 if !char_line.is_empty() && char_w + cw > max_w
2798 {
2799 if !line.is_empty() {
2800 lines.push(line.clone());
2801 line.clear();
2802 line_w = 0.0;
2803 }
2804 lines.push(char_line.clone());
2805 char_line.clear();
2806 char_w = 0.0;
2807 }
2808 char_line.push(ch);
2809 char_w += cw;
2810 }
2811 if !char_line.is_empty() {
2813 if !line.is_empty() {
2814 line.push(' ');
2815 line_w += metrics
2816 .measure_string(" ", *font_size, 0.0);
2817 }
2818 line.push_str(&char_line);
2819 line_w += char_w;
2820 }
2821 continue;
2822 }
2823 if !line.is_empty() && line_w + space_w + word_w > max_w
2824 {
2825 lines.push(line.clone());
2826 line.clear();
2827 line_w = 0.0;
2828 }
2829 if !line.is_empty() {
2830 line.push(' ');
2831 line_w += space_w;
2832 }
2833 line.push_str(word);
2834 line_w += word_w;
2835 }
2836 if !line.is_empty() {
2837 lines.push(line);
2838 }
2839 }
2840 let text_y = pdf_y + h - font_size - 2.0;
2841 for (i, line_text) in lines.iter().enumerate() {
2842 let ly = text_y - (i as f64) * (font_size * 1.2);
2843 if ly < pdf_y {
2844 break;
2845 }
2846 let esc = Self::encode_winansi_text(builder, line_text);
2847 let _ = writeln!(
2848 stream,
2849 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2850 font_idx,
2851 font_size,
2852 pdf_x + 2.0,
2853 ly,
2854 esc
2855 );
2856 }
2857 } else {
2858 let escaped = Self::encode_winansi_text(builder, &display_text);
2859 let text_y = pdf_y + (h - font_size) / 2.0;
2860 let _ = writeln!(
2861 stream,
2862 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2863 font_idx,
2864 font_size,
2865 pdf_x + 2.0,
2866 text_y,
2867 escaped
2868 );
2869 }
2870 } else if let Some(ref ph) = placeholder {
2871 if !ph.is_empty() {
2872 let font_idx = builder
2874 .font_objects
2875 .iter()
2876 .enumerate()
2877 .find(|(_, (key, _))| {
2878 key.family == "Helvetica"
2879 && key.weight == 400
2880 && !key.italic
2881 })
2882 .map(|(i, _)| i)
2883 .unwrap_or(0);
2884 let escaped = Self::encode_winansi_text(builder, ph);
2885 let text_y = pdf_y + (h - font_size) / 2.0;
2886 let _ = writeln!(
2887 stream,
2888 "BT /F{} {:.1} Tf 0.6 g {:.2} {:.2} Td ({}) Tj ET",
2889 font_idx,
2890 font_size,
2891 pdf_x + 2.0,
2892 text_y,
2893 escaped
2894 );
2895 }
2896 }
2897 }
2898 }
2899 FormFieldType::Dropdown {
2900 value, font_size, ..
2901 } => {
2902 let _ = writeln!(stream, "1 1 1 rg");
2904 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2905 let _ = writeln!(stream, "0.5 w");
2906 let _ =
2907 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2908 if flatten_forms {
2910 if let Some(ref val) = value {
2911 if !val.is_empty() {
2912 let font_idx = builder
2913 .font_objects
2914 .iter()
2915 .enumerate()
2916 .find(|(_, (key, _))| {
2917 key.family == "Helvetica"
2918 && key.weight == 400
2919 && !key.italic
2920 })
2921 .map(|(i, _)| i)
2922 .unwrap_or(0);
2923 let escaped = Self::encode_winansi_text(builder, val);
2924 let text_y = pdf_y + (h - font_size) / 2.0;
2925 let _ = writeln!(
2926 stream,
2927 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2928 font_idx,
2929 font_size,
2930 pdf_x + 2.0,
2931 text_y,
2932 escaped
2933 );
2934 }
2935 }
2936 }
2937 }
2938 }
2939 let _ = writeln!(stream, "Q");
2940 }
2941 }
2942
2943 if wrap_own_draw_as_artifact {
2946 let _ = writeln!(stream, "EMC");
2947 }
2948
2949 let clip_overflow = matches!(element.overflow, Overflow::Hidden);
2954 if clip_overflow {
2955 let clip_x = element.x;
2956 let clip_y = page_height - element.y - element.height;
2957 let clip_w = element.width;
2958 let clip_h = element.height;
2959 let radius = if let DrawCommand::Rect { border_radius, .. } = &element.draw {
2963 Some(border_radius)
2964 } else {
2965 None
2966 };
2967 let has_rounded_corners = radius.is_some_and(|r| {
2968 r.top_left > 0.0 || r.top_right > 0.0 || r.bottom_right > 0.0 || r.bottom_left > 0.0
2969 });
2970 let _ = writeln!(stream, "q");
2971 if has_rounded_corners {
2972 self.write_rounded_rect(stream, clip_x, clip_y, clip_w, clip_h, radius.unwrap());
2973 let _ = writeln!(stream, "W n");
2974 } else {
2975 let _ = writeln!(
2976 stream,
2977 "{:.2} {:.2} {:.2} {:.2} re W n",
2978 clip_x, clip_y, clip_w, clip_h
2979 );
2980 }
2981 }
2982
2983 for child in &element.children {
2984 self.write_element(
2985 stream,
2986 child,
2987 page_height,
2988 builder,
2989 page_idx,
2990 element_counter,
2991 gradient_counter,
2992 page_number,
2993 total_pages,
2994 tag_builder.as_deref_mut(),
2995 flatten_forms,
2996 );
2997 }
2998
2999 if clip_overflow {
3000 let _ = writeln!(stream, "Q");
3001 }
3002
3003 if has_transform {
3005 let _ = writeln!(stream, "Q");
3006 }
3007
3008 if needs_element_opacity {
3011 let _ = writeln!(stream, "Q");
3012 }
3013
3014 if tagged_mcid.is_some() {
3016 let _ = writeln!(stream, "EMC");
3017 if let Some(ref mut tb) = tag_builder {
3018 tb.end_element();
3019 }
3020 } else if is_artifact {
3021 let _ = writeln!(stream, "EMC");
3022 } else if artifact_own_draw {
3023 if let Some(ref mut tb) = tag_builder {
3026 tb.end_element();
3027 }
3028 }
3029 }
3030
3031 fn write_rounded_rect(
3032 &self,
3033 stream: &mut String,
3034 x: f64,
3035 y: f64,
3036 w: f64,
3037 h: f64,
3038 r: &crate::style::CornerValues,
3039 ) {
3040 let k = 0.5522847498;
3041
3042 let tl = r.top_left.min(w / 2.0).min(h / 2.0);
3043 let tr = r.top_right.min(w / 2.0).min(h / 2.0);
3044 let br = r.bottom_right.min(w / 2.0).min(h / 2.0);
3045 let bl = r.bottom_left.min(w / 2.0).min(h / 2.0);
3046
3047 let _ = writeln!(stream, "{:.2} {:.2} m", x + bl, y);
3048
3049 let _ = writeln!(stream, "{:.2} {:.2} l", x + w - br, y);
3050 if br > 0.0 {
3051 let _ = writeln!(
3052 stream,
3053 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3054 x + w - br + br * k,
3055 y,
3056 x + w,
3057 y + br - br * k,
3058 x + w,
3059 y + br
3060 );
3061 }
3062
3063 let _ = writeln!(stream, "{:.2} {:.2} l", x + w, y + h - tr);
3064 if tr > 0.0 {
3065 let _ = writeln!(
3066 stream,
3067 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3068 x + w,
3069 y + h - tr + tr * k,
3070 x + w - tr + tr * k,
3071 y + h,
3072 x + w - tr,
3073 y + h
3074 );
3075 }
3076
3077 let _ = writeln!(stream, "{:.2} {:.2} l", x + tl, y + h);
3078 if tl > 0.0 {
3079 let _ = writeln!(
3080 stream,
3081 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3082 x + tl - tl * k,
3083 y + h,
3084 x,
3085 y + h - tl + tl * k,
3086 x,
3087 y + h - tl
3088 );
3089 }
3090
3091 let _ = writeln!(stream, "{:.2} {:.2} l", x, y + bl);
3092 if bl > 0.0 {
3093 let _ = writeln!(
3094 stream,
3095 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3096 x,
3097 y + bl - bl * k,
3098 x + bl - bl * k,
3099 y,
3100 x + bl,
3101 y
3102 );
3103 }
3104
3105 let _ = writeln!(stream, "h");
3106 }
3107
3108 #[allow(clippy::too_many_arguments)]
3109 fn write_border_sides(
3110 &self,
3111 stream: &mut String,
3112 x: f64,
3113 y: f64,
3114 w: f64,
3115 h: f64,
3116 bw: &Edges,
3117 bc: &crate::style::EdgeValues<Color>,
3118 bs: &crate::style::EdgeValues<crate::style::BorderStyle>,
3119 ) {
3120 fn dash_ops(style: crate::style::BorderStyle, width: f64) -> String {
3125 use crate::style::BorderStyle::*;
3126 match style {
3127 Solid => String::new(),
3128 Dashed => format!("[{:.2} {:.2}] 0 d\n", width * 2.0, width),
3129 Dotted => format!("1 J\n[0 {:.2}] 0 d\n", width * 2.0),
3130 }
3131 }
3132 let sides = [
3134 (bc.top, bw.top, bs.top, x, y + h, x + w, y + h),
3135 (bc.bottom, bw.bottom, bs.bottom, x, y, x + w, y),
3136 (bc.left, bw.left, bs.left, x, y, x, y + h),
3137 (bc.right, bw.right, bs.right, x + w, y, x + w, y + h),
3138 ];
3139 for (color, width, style, x0, y0, x1, y1) in sides {
3140 if width <= 0.0 {
3141 continue;
3142 }
3143 let _ = write!(
3144 stream,
3145 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{}{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
3146 color.r,
3147 color.g,
3148 color.b,
3149 width,
3150 dash_ops(style, width),
3151 x0,
3152 y0,
3153 x1,
3154 y1
3155 );
3156 }
3157 }
3158
3159 fn emit_pdfua_embedded_standard(
3171 builder: &mut PdfBuilder,
3172 key: &FontKey,
3173 std_font: &crate::font::StandardFont,
3174 metrics: &crate::font::StandardFontMetrics,
3175 font_context: &FontContext,
3176 ) -> bool {
3177 let lib_family = match std_font.liberation_family() {
3178 Some(f) => f,
3179 None => return false, };
3181 let lib_bytes: &[u8] = match font_context.resolve(lib_family, key.weight, key.italic) {
3184 FontData::Custom { data, .. } => data,
3185 FontData::Standard(_) => return false,
3186 };
3187 let face = match ttf_parser::Face::parse(lib_bytes, 0) {
3188 Ok(f) => f,
3189 Err(_) => return false,
3190 };
3191 let scale = 1000.0 / face.units_per_em() as f64;
3192 let bbox = face.global_bounding_box();
3193 let pdf_name = Self::sanitize_font_name(lib_family, key.weight, key.italic);
3194
3195 let compressed = compress_to_vec_zlib(lib_bytes, 6);
3197 let fontfile2_id = builder.objects.len();
3198 let mut ff2: Vec<u8> = Vec::new();
3199 let _ = write!(
3200 ff2,
3201 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
3202 compressed.len(),
3203 lib_bytes.len()
3204 );
3205 ff2.extend_from_slice(&compressed);
3206 ff2.extend_from_slice(b"\nendstream");
3207 builder.objects.push(PdfObject {
3208 id: fontfile2_id,
3209 data: ff2,
3210 });
3211
3212 let fd_id = builder.objects.len();
3214 let cap_height =
3215 (face.capital_height().unwrap_or_else(|| face.ascender()) as f64 * scale) as i32;
3216 let fd = format!(
3217 "<< /Type /FontDescriptor /FontName /{name} /Flags {flags} \
3218 /FontBBox [{x0} {y0} {x1} {y1}] /ItalicAngle {ia} \
3219 /Ascent {asc} /Descent {desc} /CapHeight {cap} /StemV {stem} \
3220 /FontFile2 {ff2} 0 R >>",
3221 name = pdf_name,
3222 flags = std_font.descriptor_flags(),
3223 x0 = (bbox.x_min as f64 * scale) as i32,
3224 y0 = (bbox.y_min as f64 * scale) as i32,
3225 x1 = (bbox.x_max as f64 * scale) as i32,
3226 y1 = (bbox.y_max as f64 * scale) as i32,
3227 ia = if key.italic { -12 } else { 0 },
3228 asc = (face.ascender() as f64 * scale) as i32,
3229 desc = (face.descender() as f64 * scale) as i32,
3230 cap = cap_height,
3231 stem = if key.weight >= 700 { 120 } else { 80 },
3232 ff2 = fontfile2_id,
3233 );
3234 builder.objects.push(PdfObject {
3235 id: fd_id,
3236 data: fd.into_bytes(),
3237 });
3238
3239 let declared_widths: Vec<u16> = metrics
3253 .widths
3254 .iter()
3255 .enumerate()
3256 .map(|(i, &afm)| {
3257 let code = 32u8.wrapping_add(i as u8); if let Some(ch) = crate::font::winansi_to_char(code) {
3259 if let Some(gid) = face.glyph_index(ch) {
3260 if let Some(adv) = face.glyph_hor_advance(gid) {
3261 let hmtx = (adv as f64 * scale).round() as u16;
3262 if (hmtx as i32 - afm as i32).abs() > 1 {
3263 return hmtx;
3264 }
3265 }
3266 }
3267 }
3268 afm
3269 })
3270 .collect();
3271 let widths_str: String = declared_widths
3272 .iter()
3273 .map(|w| w.to_string())
3274 .collect::<Vec<_>>()
3275 .join(" ");
3276 let obj_id = builder.objects.len();
3277 let font_dict = format!(
3278 "<< /Type /Font /Subtype /TrueType /BaseFont /{name} \
3279 /Encoding /WinAnsiEncoding \
3280 /FirstChar 32 /LastChar 255 /Widths [{w}] \
3281 /FontDescriptor {fd} 0 R >>",
3282 name = pdf_name,
3283 w = widths_str,
3284 fd = fd_id,
3285 );
3286 builder.objects.push(PdfObject {
3287 id: obj_id,
3288 data: font_dict.into_bytes(),
3289 });
3290 builder.font_objects.push((key.clone(), obj_id));
3291 builder.embedded_standard_fonts.insert(key.clone());
3295 true
3296 }
3297
3298 #[allow(clippy::too_many_arguments)]
3299 fn register_fonts(
3300 &self,
3301 builder: &mut PdfBuilder,
3302 pages: &[LayoutPage],
3303 font_context: &FontContext,
3304 pdf_ua: bool,
3305 pdfa: bool,
3306 pdf_version: crate::model::PdfVersion,
3307 ) -> Result<(), FormeError> {
3308 let mut font_usage_map: HashMap<FontKey, FontUsage> = HashMap::new();
3310
3311 for page in pages {
3312 Self::collect_font_usage(&page.elements, &mut font_usage_map);
3313 }
3314
3315 let mut keys: Vec<FontKey> = font_usage_map.keys().cloned().collect();
3316
3317 keys.sort_by(|a, b| {
3319 a.family
3320 .cmp(&b.family)
3321 .then(a.weight.cmp(&b.weight))
3322 .then(a.italic.cmp(&b.italic))
3323 });
3324 keys.dedup();
3325
3326 if keys.is_empty() {
3328 keys.push(FontKey {
3329 family: "Helvetica".to_string(),
3330 weight: 400,
3331 italic: false,
3332 });
3333 }
3334
3335 for key in &keys {
3336 let font_data = font_context.resolve(&key.family, key.weight, key.italic);
3337
3338 match font_data {
3339 FontData::Standard(std_font) => {
3340 let metrics = std_font.metrics();
3341
3342 if pdf_ua || pdfa || pdf_version == crate::model::PdfVersion::V2_0 {
3357 if Self::emit_pdfua_embedded_standard(
3358 builder,
3359 key,
3360 std_font,
3361 &metrics,
3362 font_context,
3363 ) {
3364 continue;
3365 }
3366 if pdf_version == crate::model::PdfVersion::V2_0 {
3372 let remedy = match std_font.liberation_family() {
3373 Some(lib) => format!(
3374 "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"
3375 ),
3376 None => "register an embeddable TrueType font for this text (Symbol/ZapfDingbats have no metric-compatible substitute)"
3377 .to_string(),
3378 };
3379 return Err(FormeError::FontError(format!(
3380 "pdfVersion \"2.0\": font '{}' is not embedded. ISO 32000-2 removes the standard-14 provision, so every font must be embedded — {}.",
3381 std_font.pdf_name(),
3382 remedy,
3383 )));
3384 }
3385 if let Some(lib) = std_font.liberation_family() {
3391 builder.warnings.push(format!(
3392 "pdfUa: font '{}' is not embedded, so the PDF will not conform to \
3393 PDF/UA (all fonts must be embedded). Install \
3394 @formepdf/fonts-standard and register its fonts \
3395 (`for (const f of standardFonts()) Font.register(f)`) — Forme \
3396 will then embed the metric-compatible {} in its place.",
3397 std_font.pdf_name(),
3398 lib,
3399 ));
3400 }
3401 }
3402
3403 let obj_id = builder.objects.len();
3404 let widths_str: String = metrics
3407 .widths
3408 .iter()
3409 .map(|w| w.to_string())
3410 .collect::<Vec<_>>()
3411 .join(" ");
3412 let font_dict = format!(
3413 "<< /Type /Font /Subtype /Type1 /BaseFont /{} \
3414 /Encoding /WinAnsiEncoding \
3415 /FirstChar 32 /LastChar 255 /Widths [{}] >>",
3416 std_font.pdf_name(),
3417 widths_str,
3418 );
3419 builder.objects.push(PdfObject {
3420 id: obj_id,
3421 data: font_dict.into_bytes(),
3422 });
3423 builder.font_objects.push((key.clone(), obj_id));
3424 }
3425 FontData::Custom { data, .. } => {
3426 let type0_obj_id = Self::write_custom_font_objects(
3427 builder,
3428 key,
3429 data,
3430 font_usage_map.get(key).cloned().unwrap_or_default(),
3431 )?;
3432 builder.font_objects.push((key.clone(), type0_obj_id));
3433 }
3434 }
3435 }
3436
3437 Ok(())
3438 }
3439
3440 fn collect_font_usage(
3442 elements: &[LayoutElement],
3443 font_usage: &mut HashMap<FontKey, FontUsage>,
3444 ) {
3445 for element in elements {
3446 let lines_opt = match &element.draw {
3447 DrawCommand::Text { lines, .. } => Some(lines),
3448 DrawCommand::Watermark { lines, .. } => Some(lines),
3449 _ => None,
3450 };
3451 if let Some(lines) = lines_opt {
3452 for line in lines {
3453 for glyph in &line.glyphs {
3454 let italic =
3455 matches!(glyph.font_style, FontStyle::Italic | FontStyle::Oblique);
3456 let key = FontKey {
3457 family: glyph.font_family.to_string(),
3458 weight: glyph.font_weight,
3459 italic,
3460 };
3461 let usage = font_usage.entry(key).or_insert_with(|| FontUsage {
3462 chars: HashSet::new(),
3463 glyph_ids: HashSet::new(),
3464 glyph_to_text: HashMap::new(),
3465 glyph_texts: HashSet::new(),
3466 carrier_chars: HashSet::new(),
3467 });
3468 usage.chars.insert(glyph.char_value);
3469 if glyph.char_value == PAGE_NUMBER_SENTINEL
3474 || glyph.char_value == TOTAL_PAGES_SENTINEL
3475 {
3476 usage.chars.extend('0'..='9');
3477 }
3478 usage.glyph_ids.insert(glyph.glyph_id);
3479 record_glyph_text(&mut usage.glyph_to_text, glyph);
3480 usage
3481 .glyph_texts
3482 .insert((glyph.glyph_id, glyph_mapping(glyph)));
3483 if glyph_mapping(glyph) != extraction_text(glyph) {
3484 usage.carrier_chars.extend(extraction_text(glyph).chars());
3485 usage.chars.extend(extraction_text(glyph).chars());
3486 }
3487 }
3488 }
3489 }
3490 Self::collect_font_usage(&element.children, font_usage);
3491 }
3492 }
3493
3494 fn register_shadings(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3500 for (page_idx, page) in pages.iter().enumerate() {
3501 let mut counter = 0usize;
3502 Self::collect_shadings_recursive(&page.elements, page_idx, &mut counter, builder);
3503 }
3504 }
3505
3506 fn collect_shadings_recursive(
3507 elements: &[LayoutElement],
3508 page_idx: usize,
3509 counter: &mut usize,
3510 builder: &mut PdfBuilder,
3511 ) {
3512 for element in elements {
3513 if let DrawCommand::Rect {
3514 background_gradient: Some(gradient),
3515 ..
3516 } = &element.draw
3517 {
3518 let ordinal = *counter;
3519 *counter += 1;
3520 let (obj_id, name) =
3521 Self::write_shading_objects(builder, gradient, element, ordinal);
3522 builder
3523 .shading_map
3524 .insert((page_idx, ordinal), (obj_id, name));
3525 }
3526 Self::collect_shadings_recursive(&element.children, page_idx, counter, builder);
3527 }
3528 }
3529
3530 fn write_shading_objects(
3536 builder: &mut PdfBuilder,
3537 gradient: &crate::style::Background,
3538 element: &LayoutElement,
3539 ordinal: usize,
3540 ) -> (usize, String) {
3541 use crate::style::Background;
3542 use crate::style::GradientStop;
3543
3544 let black = Color {
3548 r: 0.0,
3549 g: 0.0,
3550 b: 0.0,
3551 a: 1.0,
3552 };
3553 let stops: Vec<GradientStop> = match gradient {
3554 Background::Color(c) => vec![
3555 GradientStop {
3556 position: 0.0,
3557 color: *c,
3558 },
3559 GradientStop {
3560 position: 1.0,
3561 color: *c,
3562 },
3563 ],
3564 Background::Linear(g) => normalize_gradient_stops(&g.stops, black),
3565 Background::Radial(g) => normalize_gradient_stops(&g.stops, black),
3566 };
3567
3568 let function_id = if stops.len() <= 2 {
3572 let c0 = stops.first().map(|s| s.color).unwrap_or(black);
3573 let c1 = stops.last().map(|s| s.color).unwrap_or(c0);
3574 let id = builder.objects.len();
3575 let data = format!(
3576 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
3577 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
3578 );
3579 builder.objects.push(PdfObject {
3580 id,
3581 data: data.into_bytes(),
3582 });
3583 id
3584 } else {
3585 let mut sub_ids: Vec<usize> = Vec::with_capacity(stops.len() - 1);
3587 for window in stops.windows(2) {
3588 let c0 = window[0].color;
3589 let c1 = window[1].color;
3590 let id = builder.objects.len();
3591 let data = format!(
3592 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
3593 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
3594 );
3595 builder.objects.push(PdfObject {
3596 id,
3597 data: data.into_bytes(),
3598 });
3599 sub_ids.push(id);
3600 }
3601 let bounds: Vec<String> = stops[1..stops.len() - 1]
3605 .iter()
3606 .map(|s| format!("{:.4}", s.position))
3607 .collect();
3608 let encode: Vec<&str> = (0..sub_ids.len()).map(|_| "0 1").collect();
3609 let functions: Vec<String> = sub_ids.iter().map(|i| format!("{} 0 R", i)).collect();
3610 let id = builder.objects.len();
3611 let data = format!(
3612 "<< /FunctionType 3 /Domain [0 1] /Functions [{}] /Bounds [{}] /Encode [{}] >>",
3613 functions.join(" "),
3614 bounds.join(" "),
3615 encode.join(" "),
3616 );
3617 builder.objects.push(PdfObject {
3618 id,
3619 data: data.into_bytes(),
3620 });
3621 id
3622 };
3623
3624 let _ = element.x;
3628 let _ = element.y;
3629 let w = element.width;
3630 let h = element.height;
3631
3632 let shading_id = builder.objects.len();
3633 let shading_data = match gradient {
3634 Background::Linear(g) => {
3635 let theta = g.angle_deg.to_radians();
3645 let dx = theta.sin();
3646 let dy = theta.cos();
3647 let axis_len = w * dx.abs() + h * dy.abs();
3650 let cx_rel = w / 2.0;
3653 let cy_rel = h / 2.0;
3654 let half = axis_len / 2.0;
3655 let x0 = cx_rel - dx * half;
3656 let y0 = cy_rel - dy * half;
3657 let x1 = cx_rel + dx * half;
3658 let y1 = cy_rel + dy * half;
3659 format!(
3660 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
3661 x0, y0, x1, y1, function_id,
3662 )
3663 }
3664 Background::Radial(_) => {
3665 let cx_rel = w / 2.0;
3668 let cy_rel = h / 2.0;
3669 let r_outer = (w / 2.0).max(h / 2.0);
3670 format!(
3671 "<< /ShadingType 3 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} 0 {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
3672 cx_rel, cy_rel, cx_rel, cy_rel, r_outer, function_id,
3673 )
3674 }
3675 Background::Color(_) => {
3676 format!(
3680 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 0 0] /Function {} 0 R /Extend [true true] >>",
3681 function_id,
3682 )
3683 }
3684 };
3685 builder.objects.push(PdfObject {
3686 id: shading_id,
3687 data: shading_data.into_bytes(),
3688 });
3689 (shading_id, format!("Sh{}", ordinal))
3690 }
3691
3692 fn register_page_background_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3696 for (page_idx, page) in pages.iter().enumerate() {
3697 let Some(src) = &page.config.background_image else {
3698 continue;
3699 };
3700 if let Some(&entry) = builder.page_background_url_cache.get(src) {
3702 builder.page_background_image_map.insert(page_idx, entry);
3703 continue;
3704 }
3705 match crate::image_loader::load_image(src) {
3708 Ok(image_data) => {
3709 let img_idx = builder.image_objects.len();
3710 let dims = (img_idx, image_data.width_px, image_data.height_px);
3711 let xobj_id = Self::write_image_xobject(builder, &image_data);
3712 builder.image_objects.push(xobj_id);
3713 builder.page_background_image_map.insert(page_idx, dims);
3714 builder.page_background_url_cache.insert(src.clone(), dims);
3715 }
3716 Err(e) => {
3717 eprintln!("[forme] page background image failed to load: {}", e);
3718 }
3719 }
3720 }
3721 }
3722
3723 fn write_page_background(
3728 &self,
3729 stream: &mut String,
3730 page: &LayoutPage,
3731 page_bg: (usize, u32, u32),
3732 builder: &PdfBuilder,
3733 ) {
3734 use crate::model::{BackgroundPosition, BackgroundSize};
3735 let (img_idx, iw_px, ih_px) = page_bg;
3736 let page_w = page.width;
3737 let page_h = page.height;
3738 let iw = iw_px as f64;
3739 let ih = ih_px as f64;
3740
3741 let size = page.config.background_size.unwrap_or_default();
3742 let (dest_w, dest_h) = match size {
3743 BackgroundSize::Fill => (page_w, page_h),
3744 BackgroundSize::Cover => {
3745 let s = (page_w / iw).max(page_h / ih);
3746 (iw * s, ih * s)
3747 }
3748 BackgroundSize::Contain => {
3749 let s = (page_w / iw).min(page_h / ih);
3750 (iw * s, ih * s)
3751 }
3752 };
3753
3754 let position = page.config.background_position.unwrap_or_default();
3758 let (dest_x, dest_y) = match position {
3761 BackgroundPosition::TopLeft => (0.0, page_h - dest_h),
3762 BackgroundPosition::TopRight => (page_w - dest_w, page_h - dest_h),
3763 BackgroundPosition::BottomLeft => (0.0, 0.0),
3764 BackgroundPosition::BottomRight => (page_w - dest_w, 0.0),
3765 BackgroundPosition::Center => ((page_w - dest_w) / 2.0, (page_h - dest_h) / 2.0),
3766 };
3767
3768 let opacity = page.config.background_opacity.unwrap_or(1.0);
3770 let needs_opacity = opacity < 1.0;
3771 if needs_opacity {
3772 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
3773 let _ = writeln!(stream, "q\n/{} gs", gs_name);
3774 } else {
3775 let _ = writeln!(stream, "q");
3776 }
3777 } else {
3778 let _ = writeln!(stream, "q");
3779 }
3780 let _ = writeln!(
3783 stream,
3784 "{:.2} 0 0 {:.2} {:.2} {:.2} cm\n/Im{} Do\nQ",
3785 dest_w, dest_h, dest_x, dest_y, img_idx,
3786 );
3787 }
3788
3789 fn register_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3791 for (page_idx, page) in pages.iter().enumerate() {
3792 let mut element_counter = 0usize;
3793 Self::collect_images_recursive(&page.elements, page_idx, &mut element_counter, builder);
3794 }
3795 }
3796
3797 fn collect_images_recursive(
3798 elements: &[LayoutElement],
3799 page_idx: usize,
3800 element_counter: &mut usize,
3801 builder: &mut PdfBuilder,
3802 ) {
3803 for element in elements {
3804 match &element.draw {
3805 DrawCommand::Image { image_data } => {
3806 let elem_idx = *element_counter;
3807 *element_counter += 1;
3808
3809 let img_idx = builder.image_objects.len();
3810 let xobj_id = Self::write_image_xobject(builder, image_data);
3811 builder.image_objects.push(xobj_id);
3812 builder
3813 .image_index_map
3814 .insert((page_idx, elem_idx), img_idx);
3815 }
3816 DrawCommand::ImagePlaceholder => {
3817 *element_counter += 1;
3818 }
3819 _ => {
3820 Self::collect_images_recursive(
3821 &element.children,
3822 page_idx,
3823 element_counter,
3824 builder,
3825 );
3826 }
3827 }
3828 }
3829 }
3830
3831 fn register_ext_gstates(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3833 let mut unique_opacities: Vec<f64> = Vec::new();
3834 for page in pages {
3835 Self::collect_opacities_recursive(&page.elements, &mut unique_opacities);
3836 if let Some(o) = page.config.background_opacity {
3838 if o < 1.0 {
3839 unique_opacities.push(o);
3840 }
3841 }
3842 }
3843 unique_opacities.sort_by(|a, b| a.partial_cmp(b).unwrap());
3844 unique_opacities.dedup();
3845
3846 for (idx, &opacity) in unique_opacities.iter().enumerate() {
3847 let obj_id = builder.objects.len();
3848 let gs_name = format!("GS{}", idx);
3849 let obj_data = format!(
3850 "<< /Type /ExtGState /ca {:.4} /CA {:.4} >>",
3851 opacity, opacity
3852 );
3853 builder.objects.push(PdfObject {
3854 id: obj_id,
3855 data: obj_data.into_bytes(),
3856 });
3857 let key = opacity.to_bits();
3858 builder.ext_gstate_map.insert(key, (obj_id, gs_name));
3859 }
3860 }
3861
3862 fn collect_opacities_recursive(elements: &[LayoutElement], opacities: &mut Vec<f64>) {
3863 for element in elements {
3864 if element.opacity < 1.0 {
3871 opacities.push(element.opacity);
3872 }
3873 if let DrawCommand::Rect {
3877 box_shadow: Some(shadow),
3878 ..
3879 } = &element.draw
3880 {
3881 if shadow.color.a < 1.0 {
3882 opacities.push(shadow.color.a);
3883 }
3884 }
3885 match &element.draw {
3886 DrawCommand::Rect { opacity, .. }
3887 | DrawCommand::Text { opacity, .. }
3888 | DrawCommand::Watermark { opacity, .. }
3889 if *opacity < 1.0 =>
3890 {
3891 opacities.push(*opacity);
3892 }
3893 DrawCommand::Chart { primitives } => {
3894 for prim in primitives {
3895 if let crate::chart::ChartPrimitive::FilledPath { opacity, .. } = prim {
3896 if *opacity < 1.0 {
3897 opacities.push(*opacity);
3898 }
3899 }
3900 }
3901 }
3902 DrawCommand::Svg { commands, .. } => {
3903 for cmd in commands {
3904 if let crate::svg::SvgCommand::SetOpacity(opacity) = cmd {
3905 if *opacity < 1.0 {
3906 opacities.push(*opacity);
3907 }
3908 }
3909 }
3910 }
3911 _ => {}
3912 }
3913 Self::collect_opacities_recursive(&element.children, opacities);
3914 }
3915 }
3916
3917 fn build_ext_gstate_resource_dict(&self, builder: &PdfBuilder) -> String {
3919 if builder.ext_gstate_map.is_empty() {
3920 return String::new();
3921 }
3922 let mut entries: Vec<(&String, usize)> = builder
3923 .ext_gstate_map
3924 .values()
3925 .map(|(obj_id, name)| (name, *obj_id))
3926 .collect();
3927 entries.sort_by_key(|(name, _)| (*name).clone());
3928 entries
3929 .iter()
3930 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3931 .collect::<Vec<_>>()
3932 .join(" ")
3933 }
3934
3935 fn write_image_xobject(
3938 builder: &mut PdfBuilder,
3939 image: &crate::image_loader::LoadedImage,
3940 ) -> usize {
3941 use crate::image_loader::{ImagePixelData, JpegColorSpace};
3942
3943 match &image.pixel_data {
3944 ImagePixelData::Jpeg { data, color_space } => {
3945 let color_space_str = match color_space {
3946 JpegColorSpace::DeviceRGB => "/DeviceRGB",
3947 JpegColorSpace::DeviceGray => "/DeviceGray",
3948 };
3949
3950 let obj_id = builder.objects.len();
3951 let mut obj_data: Vec<u8> = Vec::new();
3952 let _ = write!(
3953 obj_data,
3954 "<< /Type /XObject /Subtype /Image \
3955 /Width {} /Height {} \
3956 /ColorSpace {} \
3957 /BitsPerComponent 8 \
3958 /Filter /DCTDecode \
3959 /Length {} >>\nstream\n",
3960 image.width_px,
3961 image.height_px,
3962 color_space_str,
3963 data.len()
3964 );
3965 obj_data.extend_from_slice(data);
3966 obj_data.extend_from_slice(b"\nendstream");
3967 builder.objects.push(PdfObject {
3968 id: obj_id,
3969 data: obj_data,
3970 });
3971 obj_id
3972 }
3973
3974 ImagePixelData::Decoded { rgb, alpha } => {
3975 let smask_id = alpha.as_ref().map(|alpha_data| {
3977 let compressed_alpha = compress_to_vec_zlib(alpha_data, 6);
3978 let smask_obj_id = builder.objects.len();
3979 let mut smask_data: Vec<u8> = Vec::new();
3980 let _ = write!(
3981 smask_data,
3982 "<< /Type /XObject /Subtype /Image \
3983 /Width {} /Height {} \
3984 /ColorSpace /DeviceGray \
3985 /BitsPerComponent 8 \
3986 /Filter /FlateDecode \
3987 /Length {} >>\nstream\n",
3988 image.width_px,
3989 image.height_px,
3990 compressed_alpha.len()
3991 );
3992 smask_data.extend_from_slice(&compressed_alpha);
3993 smask_data.extend_from_slice(b"\nendstream");
3994 builder.objects.push(PdfObject {
3995 id: smask_obj_id,
3996 data: smask_data,
3997 });
3998 smask_obj_id
3999 });
4000
4001 let compressed_rgb = compress_to_vec_zlib(rgb, 6);
4003 let obj_id = builder.objects.len();
4004 let mut obj_data: Vec<u8> = Vec::new();
4005
4006 let smask_ref = smask_id
4007 .map(|id| format!(" /SMask {} 0 R", id))
4008 .unwrap_or_default();
4009
4010 let _ = write!(
4011 obj_data,
4012 "<< /Type /XObject /Subtype /Image \
4013 /Width {} /Height {} \
4014 /ColorSpace /DeviceRGB \
4015 /BitsPerComponent 8 \
4016 /Filter /FlateDecode \
4017 /Length {}{} >>\nstream\n",
4018 image.width_px,
4019 image.height_px,
4020 compressed_rgb.len(),
4021 smask_ref
4022 );
4023 obj_data.extend_from_slice(&compressed_rgb);
4024 obj_data.extend_from_slice(b"\nendstream");
4025 builder.objects.push(PdfObject {
4026 id: obj_id,
4027 data: obj_data,
4028 });
4029 obj_id
4030 }
4031 }
4032 }
4033
4034 fn build_shading_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
4038 let mut entries: Vec<(String, usize)> = builder
4039 .shading_map
4040 .iter()
4041 .filter(|(&(p, _), _)| p == page_idx)
4042 .map(|(_, (obj_id, name))| (name.clone(), *obj_id))
4043 .collect();
4044 if entries.is_empty() {
4045 return String::new();
4046 }
4047 entries.sort_by(|a, b| a.0.cmp(&b.0));
4048 entries
4049 .iter()
4050 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
4051 .collect::<Vec<_>>()
4052 .join(" ")
4053 }
4054
4055 fn build_xobject_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
4056 let mut entries: Vec<(usize, usize)> = Vec::new();
4057 for (&(pidx, _), &img_idx) in &builder.image_index_map {
4058 if pidx == page_idx {
4059 let obj_id = builder.image_objects[img_idx];
4060 entries.push((img_idx, obj_id));
4061 }
4062 }
4063 if let Some(&(img_idx, _, _)) = builder.page_background_image_map.get(&page_idx) {
4066 let obj_id = builder.image_objects[img_idx];
4067 entries.push((img_idx, obj_id));
4068 }
4069 if entries.is_empty() {
4070 return String::new();
4071 }
4072 entries.sort_by_key(|(idx, _)| *idx);
4073 entries.dedup();
4074 entries
4075 .iter()
4076 .map(|(idx, obj_id)| format!("/Im{} {} 0 R", idx, obj_id))
4077 .collect::<Vec<_>>()
4078 .join(" ")
4079 }
4080
4081 fn write_custom_font_objects(
4088 builder: &mut PdfBuilder,
4089 key: &FontKey,
4090 ttf_data: &[u8],
4091 usage: FontUsage,
4092 ) -> Result<usize, FormeError> {
4093 let FontUsage {
4094 glyph_ids: used_glyph_ids,
4095 chars: used_chars,
4096 glyph_to_text: glyph_to_text_map,
4097 mut glyph_texts,
4098 carrier_chars,
4099 } = usage;
4100 let face = ttf_parser::Face::parse(ttf_data, 0).map_err(|e| {
4101 FormeError::FontError(format!(
4102 "Failed to parse TTF data for font '{}': {}",
4103 key.family, e
4104 ))
4105 })?;
4106
4107 let units_per_em = face.units_per_em();
4108 let ascender = face.ascender();
4109 let descender = face.descender();
4110
4111 let mut char_to_orig_gid: HashMap<char, u16> = HashMap::new();
4113 for &ch in &used_chars {
4114 if let Some(gid) = face.glyph_index(ch) {
4115 char_to_orig_gid.insert(ch, gid.0);
4116 }
4117 }
4118
4119 for &ch in &carrier_chars {
4120 char_to_orig_gid.entry(ch).or_insert(0);
4121 }
4122
4123 let mut all_orig_gids: HashSet<u16> = used_glyph_ids.clone();
4127 for &gid in char_to_orig_gid.values() {
4128 all_orig_gids.insert(gid);
4129 }
4130
4131 let (embed_ttf, gid_remap) = match subset_ttf(ttf_data, &all_orig_gids) {
4133 Ok(subset_result) => (subset_result.ttf_data, subset_result.gid_remap),
4134 Err(_) => {
4135 let identity: HashMap<u16, u16> =
4137 all_orig_gids.iter().map(|&gid| (gid, gid)).collect();
4138 (ttf_data.to_vec(), identity)
4139 }
4140 };
4141
4142 let char_to_gid: HashMap<char, u16> = char_to_orig_gid
4144 .iter()
4145 .filter_map(|(&ch, &orig_gid)| gid_remap.get(&orig_gid).map(|&new_gid| (ch, new_gid)))
4146 .collect();
4147
4148 let gid_remap_for_embed = gid_remap.clone();
4150
4151 let mut new_gid_to_text: HashMap<u16, String> = HashMap::new();
4153 for (orig_gid, text) in &glyph_to_text_map {
4155 if let Some(&new_gid) = gid_remap.get(orig_gid) {
4156 new_gid_to_text
4157 .entry(new_gid)
4158 .or_insert_with(|| text.clone());
4159 }
4160 }
4161 for (&ch, &new_gid) in &char_to_gid {
4163 new_gid_to_text
4164 .entry(new_gid)
4165 .or_insert_with(|| ch.to_string());
4166 }
4167
4168 for ch in carrier_chars {
4169 let gid = char_to_orig_gid.get(&ch).copied().ok_or_else(|| {
4170 FormeError::FontError(format!("No glyph for extraction character {ch:?}"))
4171 })?;
4172 glyph_texts.insert((gid, ch.to_string()));
4173 }
4174 let mut cid_to_gid: HashMap<u16, u16> = gid_remap.values().map(|&gid| (gid, gid)).collect();
4177 let mut next_cid = cid_to_gid.keys().copied().max().unwrap_or(0) as u32 + 1;
4178 let mut glyph_to_cid = HashMap::new();
4179 let mut pairs: Vec<_> = glyph_texts.into_iter().collect();
4180 pairs.sort();
4181 for (orig_gid, text) in pairs {
4182 let gid = gid_remap.get(&orig_gid).copied().unwrap_or(0);
4183 let cid = if new_gid_to_text.get(&gid) == Some(&text) {
4184 gid
4185 } else {
4186 let cid = u16::try_from(next_cid).map_err(|_| {
4187 FormeError::FontError("Too many character mappings".to_string())
4188 })?;
4189 next_cid += 1;
4190 cid_to_gid.insert(cid, gid);
4191 new_gid_to_text.insert(cid, text.clone());
4192 cid
4193 };
4194 glyph_to_cid.insert((orig_gid, text), cid);
4195 }
4196
4197 let char_to_gid = char_to_orig_gid
4198 .iter()
4199 .filter_map(|(&ch, &gid)| {
4200 glyph_to_cid
4201 .get(&(gid, ch.to_string()))
4202 .copied()
4203 .or_else(|| gid_remap.get(&gid).copied())
4204 .map(|cid| (ch, cid))
4205 })
4206 .collect();
4207
4208 let pdf_font_name = Self::sanitize_font_name(&key.family, key.weight, key.italic);
4209
4210 let compressed_ttf = compress_to_vec_zlib(&embed_ttf, 6);
4212 let fontfile2_id = builder.objects.len();
4213 let mut fontfile2_data: Vec<u8> = Vec::new();
4214 let _ = write!(
4215 fontfile2_data,
4216 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
4217 compressed_ttf.len(),
4218 embed_ttf.len()
4219 );
4220 fontfile2_data.extend_from_slice(&compressed_ttf);
4221 fontfile2_data.extend_from_slice(b"\nendstream");
4222 builder.objects.push(PdfObject {
4223 id: fontfile2_id,
4224 data: fontfile2_data,
4225 });
4226
4227 let subset_face = ttf_parser::Face::parse(&embed_ttf, 0).unwrap_or_else(|_| face.clone());
4229 let subset_upem = subset_face.units_per_em();
4230
4231 let font_descriptor_id = builder.objects.len();
4233 let bbox = face.global_bounding_box();
4234 let scale = 1000.0 / units_per_em as f64;
4235 let bbox_str = format!(
4236 "[{} {} {} {}]",
4237 (bbox.x_min as f64 * scale) as i32,
4238 (bbox.y_min as f64 * scale) as i32,
4239 (bbox.x_max as f64 * scale) as i32,
4240 (bbox.y_max as f64 * scale) as i32,
4241 );
4242
4243 let flags = 4u32;
4244 let cap_height = face.capital_height().unwrap_or(ascender) as f64 * scale;
4245 let stem_v = if key.weight >= 700 { 120 } else { 80 };
4246
4247 let font_descriptor_dict = format!(
4248 "<< /Type /FontDescriptor /FontName /{} /Flags {} \
4249 /FontBBox {} /ItalicAngle {} \
4250 /Ascent {} /Descent {} /CapHeight {} /StemV {} \
4251 /FontFile2 {} 0 R >>",
4252 pdf_font_name,
4253 flags,
4254 bbox_str,
4255 if key.italic { -12 } else { 0 },
4256 (ascender as f64 * scale) as i32,
4257 (descender as f64 * scale) as i32,
4258 cap_height as i32,
4259 stem_v,
4260 fontfile2_id,
4261 );
4262 builder.objects.push(PdfObject {
4263 id: font_descriptor_id,
4264 data: font_descriptor_dict.into_bytes(),
4265 });
4266
4267 let cid_map = if cid_to_gid.iter().all(|(cid, gid)| cid == gid) {
4269 "/Identity".to_string()
4270 } else {
4271 let map_id = builder.objects.len();
4272 let mut bytes = vec![0u8; next_cid as usize * 2];
4273 for (&cid, &gid) in &cid_to_gid {
4274 bytes[cid as usize * 2..cid as usize * 2 + 2].copy_from_slice(&gid.to_be_bytes());
4275 }
4276 let compressed = compress_to_vec_zlib(&bytes, 6);
4277 let mut data = format!(
4278 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
4279 compressed.len()
4280 )
4281 .into_bytes();
4282 data.extend_from_slice(&compressed);
4283 data.extend_from_slice(b"\nendstream");
4284 builder.objects.push(PdfObject { id: map_id, data });
4285 format!("{} 0 R", map_id)
4286 };
4287 let cidfont_id = builder.objects.len();
4288 let (w_array, pdf_widths) =
4290 Self::build_w_array_from_gids(&cid_to_gid, &subset_face, subset_upem);
4291 let default_width = subset_face
4292 .glyph_hor_advance(ttf_parser::GlyphId(0))
4293 .map(|adv| (adv as f64 * 1000.0 / subset_upem as f64) as u32)
4294 .unwrap_or(1000);
4295 let cidfont_dict = format!(
4296 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{} \
4297 /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> \
4298 /FontDescriptor {} 0 R /DW {} /W {} \
4299 /CIDToGIDMap {} >>",
4300 pdf_font_name, font_descriptor_id, default_width, w_array, cid_map,
4301 );
4302 builder.objects.push(PdfObject {
4303 id: cidfont_id,
4304 data: cidfont_dict.into_bytes(),
4305 });
4306
4307 let tounicode_id = builder.objects.len();
4309 let cmap_content = Self::build_tounicode_cmap_from_gids(&new_gid_to_text, &pdf_font_name);
4310 let compressed_cmap = compress_to_vec_zlib(cmap_content.as_bytes(), 6);
4311 let mut tounicode_data: Vec<u8> = Vec::new();
4312 let _ = write!(
4313 tounicode_data,
4314 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
4315 compressed_cmap.len()
4316 );
4317 tounicode_data.extend_from_slice(&compressed_cmap);
4318 tounicode_data.extend_from_slice(b"\nendstream");
4319 builder.objects.push(PdfObject {
4320 id: tounicode_id,
4321 data: tounicode_data,
4322 });
4323
4324 let type0_id = builder.objects.len();
4326 let type0_dict = format!(
4327 "<< /Type /Font /Subtype /Type0 /BaseFont /{} \
4328 /Encoding /Identity-H \
4329 /DescendantFonts [{} 0 R] \
4330 /ToUnicode {} 0 R >>",
4331 pdf_font_name, cidfont_id, tounicode_id,
4332 );
4333 builder.objects.push(PdfObject {
4334 id: type0_id,
4335 data: type0_dict.into_bytes(),
4336 });
4337
4338 builder.custom_font_data.insert(
4340 key.clone(),
4341 CustomFontEmbedData {
4342 ttf_data: embed_ttf,
4343 gid_remap: gid_remap_for_embed,
4344 glyph_to_cid,
4345 glyph_to_text: glyph_to_text_map,
4346 char_to_gid,
4347 pdf_widths,
4348 default_width,
4349 units_per_em,
4350 ascender,
4351 descender,
4352 },
4353 );
4354
4355 Ok(type0_id)
4356 }
4357
4358 fn build_w_array_from_gids(
4360 gid_remap: &HashMap<u16, u16>,
4361 face: &ttf_parser::Face,
4362 units_per_em: u16,
4363 ) -> (String, HashMap<u16, f64>) {
4364 let scale = 1000.0 / units_per_em as f64;
4365
4366 let mut entries: Vec<(u16, f64)> = Vec::new();
4367 let mut seen_gids: HashSet<u16> = HashSet::new();
4368
4369 for (&cid, &new_gid) in gid_remap {
4370 if seen_gids.contains(&cid) {
4371 continue;
4372 }
4373 seen_gids.insert(cid);
4374 let advance = face
4375 .glyph_hor_advance(ttf_parser::GlyphId(new_gid))
4376 .unwrap_or(0);
4377 let width = advance as f64 * scale;
4381 entries.push((cid, width));
4382 }
4383
4384 entries.sort_by_key(|(gid, _)| *gid);
4385
4386 let mut result = String::from("[");
4388 for (gid, width) in &entries {
4389 let _ = write!(result, " {} [{}]", gid, pdf_number(*width));
4390 }
4391 result.push_str(" ]");
4392 (result, entries.into_iter().collect())
4393 }
4394
4395 fn build_tounicode_cmap_from_gids(
4402 gid_to_text: &HashMap<u16, String>,
4403 font_name: &str,
4404 ) -> String {
4405 let mut gid_to_unicode: Vec<(u16, String)> = gid_to_text
4406 .iter()
4407 .filter(|(_, text)| !text.is_empty())
4408 .map(|(&gid, text)| {
4409 let hex: String = text
4410 .encode_utf16()
4411 .map(|unit| format!("{:04X}", unit))
4412 .collect();
4413 (gid, hex)
4414 })
4415 .collect();
4416 gid_to_unicode.sort_by_key(|(gid, _)| *gid);
4417
4418 let mut cmap = String::new();
4419 let _ = writeln!(cmap, "/CIDInit /ProcSet findresource begin");
4420 let _ = writeln!(cmap, "12 dict begin");
4421 let _ = writeln!(cmap, "begincmap");
4422 let _ = writeln!(cmap, "/CIDSystemInfo");
4423 let _ = writeln!(
4424 cmap,
4425 "<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def"
4426 );
4427 let _ = writeln!(cmap, "/CMapName /{}-UTF16 def", font_name);
4428 let _ = writeln!(cmap, "/CMapType 2 def");
4429 let _ = writeln!(cmap, "1 begincodespacerange");
4430 let _ = writeln!(cmap, "<0000> <FFFF>");
4431 let _ = writeln!(cmap, "endcodespacerange");
4432
4433 for chunk in gid_to_unicode.chunks(100) {
4435 let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
4436 for (gid, unicode) in chunk {
4437 let _ = writeln!(cmap, "<{:04X}> <{}>", gid, unicode);
4438 }
4439 let _ = writeln!(cmap, "endbfchar");
4440 }
4441
4442 let _ = writeln!(cmap, "endcmap");
4443 let _ = writeln!(cmap, "CMapName currentdict /CMap defineresource pop");
4444 let _ = writeln!(cmap, "end");
4445 let _ = writeln!(cmap, "end");
4446
4447 cmap
4448 }
4449
4450 fn sanitize_font_name(family: &str, weight: u32, italic: bool) -> String {
4453 let mut name: String = family
4454 .chars()
4455 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
4456 .collect();
4457
4458 if weight >= 700 {
4459 name.push_str("-Bold");
4460 }
4461 if italic {
4462 name.push_str("-Italic");
4463 }
4464
4465 if name.is_empty() {
4467 name = "CustomFont".to_string();
4468 }
4469
4470 name
4471 }
4472
4473 fn build_font_resource_dict(&self, font_objects: &[(FontKey, usize)]) -> String {
4474 font_objects
4475 .iter()
4476 .enumerate()
4477 .map(|(i, (_, obj_id))| format!("/F{} {} 0 R", i, obj_id))
4478 .collect::<Vec<_>>()
4479 .join(" ")
4480 }
4481
4482 fn font_index(
4484 &self,
4485 family: &str,
4486 weight: u32,
4487 font_style: FontStyle,
4488 font_objects: &[(FontKey, usize)],
4489 ) -> usize {
4490 let italic = matches!(font_style, FontStyle::Italic | FontStyle::Oblique);
4491
4492 for (i, (key, _)) in font_objects.iter().enumerate() {
4494 if key.family == family && key.weight == weight && key.italic == italic {
4495 return i;
4496 }
4497 }
4498
4499 let snapped = if weight >= 600 { 700 } else { 400 };
4501 for (i, (key, _)) in font_objects.iter().enumerate() {
4502 if key.family == family && key.weight == snapped && key.italic == italic {
4503 return i;
4504 }
4505 }
4506
4507 for (i, (key, _)) in font_objects.iter().enumerate() {
4509 if key.family == "Helvetica" && key.weight == snapped && key.italic == italic {
4510 return i;
4511 }
4512 }
4513
4514 0
4516 }
4517
4518 fn positioned_tj(
4533 group: &[&PositionedGlyph],
4534 gids: &[u16],
4535 embed: &CustomFontEmbedData,
4536 char_spacing: f64,
4537 origin_x: f64,
4538 origin_y: f64,
4539 ) -> Option<String> {
4540 let size = group.first()?.font_size;
4541 if size <= 0.0 {
4542 return None;
4543 }
4544 let mut adjustments = vec![0.0_f64; group.len()];
4545 let mut any = false;
4546 for i in 0..group.len().saturating_sub(1) {
4547 let width = embed
4548 .pdf_widths
4549 .get(&gids[i])
4550 .copied()
4551 .unwrap_or(embed.default_width as f64);
4552 let drawn = width * size / 1000.0 + char_spacing;
4553 let wanted = group[i + 1].x_offset - group[i].x_offset;
4554 let delta = drawn - wanted;
4555 if delta.abs() > 0.001 {
4556 adjustments[i] = delta * 1000.0 / size;
4557 any = true;
4558 }
4559 }
4560 let rises: Vec<f64> = group
4561 .iter()
4562 .map(|g| {
4563 if g.y_offset.abs() > 0.001 {
4564 g.y_offset
4565 } else {
4566 0.0
4567 }
4568 })
4569 .collect();
4570 if !any
4571 && rises.iter().all(|r| *r == 0.0)
4572 && group.iter().all(|g| glyph_mapping(g) == extraction_text(g))
4573 {
4574 return None;
4575 }
4576 let mut out = String::new();
4581 let mut rise = 0.0_f64;
4582 let mut open = false;
4583 for (i, gid) in gids.iter().enumerate() {
4584 if rises[i] != rise {
4585 if open {
4586 out.push_str("] TJ\n");
4587 open = false;
4588 }
4589 let _ = writeln!(out, "{} Ts", pdf_number(rises[i]));
4590 rise = rises[i];
4591 }
4592 if !open {
4593 out.push('[');
4594 open = true;
4595 }
4596 let text = extraction_text(group[i]);
4597 let mapping = glyph_mapping(group[i]);
4598 let parts = if text != mapping {
4599 let split = text.find(&mapping).unwrap();
4600 Some((&text[..split], &text[split + mapping.len()..]))
4601 } else {
4602 None
4603 };
4604 if let Some((before, _)) = parts {
4605 Self::write_text_carriers(
4606 &mut out,
4607 before,
4608 embed,
4609 char_spacing,
4610 origin_x + group[i].x_offset - group[0].x_offset,
4611 origin_y,
4612 );
4613 }
4614 let _ = write!(out, "<{:04X}>", gid);
4615 if let Some((_, after)) = parts {
4616 let width = embed
4617 .pdf_widths
4618 .get(gid)
4619 .copied()
4620 .unwrap_or(embed.default_width as f64);
4621 Self::write_text_carriers(
4622 &mut out,
4623 after,
4624 embed,
4625 char_spacing,
4626 origin_x + group[i].x_offset - group[0].x_offset
4627 + width * size / 1000.0
4628 + char_spacing,
4629 origin_y,
4630 );
4631 }
4632
4633 if adjustments[i] != 0.0 && i + 1 < gids.len() {
4634 let _ = write!(out, " {:.2} ", adjustments[i]);
4635 }
4636 }
4637 out.push_str("] TJ");
4638 if rise != 0.0 {
4639 out.push_str("\n0 Ts");
4640 }
4641 Some(out)
4642 }
4643
4644 fn begin_extraction_span(
4645 stream: &mut String,
4646 group: &[&PositionedGlyph],
4647 page_number: usize,
4648 total_pages: usize,
4649 ) -> bool {
4650 let needed = group.iter().any(|g| {
4651 (g.extraction_text.is_some() && !g.ligature) || glyph_mapping(g) != extraction_text(g)
4652 });
4653 if needed {
4654 let text: String = group
4657 .iter()
4658 .map(|g| extraction_text(g))
4659 .filter(|t| t != "\u{200B}")
4660 .collect();
4661 let text = text
4662 .replace(PAGE_NUMBER_SENTINEL, &page_number.to_string())
4663 .replace(TOTAL_PAGES_SENTINEL, &total_pages.to_string());
4664 let _ = writeln!(
4665 stream,
4666 "/Span << /ActualText {} >> BDC",
4667 Self::encode_text_string(&text)
4668 );
4669 }
4670 needed
4671 }
4672
4673 fn write_text_carriers(
4674 out: &mut String,
4675 text: &str,
4676 embed: &CustomFontEmbedData,
4677 char_spacing: f64,
4678 x: f64,
4679 y: f64,
4680 ) {
4681 if text.is_empty() {
4682 return;
4683 }
4684 out.push_str("] TJ\n3 Tr\n0 Tc\n");
4689 for ch in text.chars() {
4690 if let Some(&cid) = embed.char_to_gid.get(&ch) {
4691 let _ = writeln!(
4692 out,
4693 "<{:04X}> Tj\n1 0 0 1 {} {} Tm",
4694 cid,
4695 pdf_number(x),
4696 pdf_number(y)
4697 );
4698 }
4699 }
4700 let _ = write!(out, "0 Tr\n{} Tc\n[", pdf_number(char_spacing));
4701 }
4702
4703 fn group_glyphs_by_style(glyphs: &[PositionedGlyph]) -> Vec<Vec<&PositionedGlyph>> {
4704 Self::group_glyphs(glyphs, false)
4705 }
4706
4707 fn group_glyphs(glyphs: &[PositionedGlyph], split_on_href: bool) -> Vec<Vec<&PositionedGlyph>> {
4710 if glyphs.is_empty() {
4711 return vec![];
4712 }
4713
4714 let mut groups: Vec<Vec<&PositionedGlyph>> = Vec::new();
4715 let mut current_group: Vec<&PositionedGlyph> = vec![&glyphs[0]];
4716
4717 for glyph in &glyphs[1..] {
4718 let prev = current_group.last().unwrap();
4719 let same_style = glyph.font_family == prev.font_family
4720 && glyph.font_weight == prev.font_weight
4721 && std::mem::discriminant(&glyph.font_style)
4722 == std::mem::discriminant(&prev.font_style)
4723 && (glyph.font_size - prev.font_size).abs() < 0.01
4724 && Self::colors_equal(&glyph.color, &prev.color)
4725 && std::mem::discriminant(&glyph.text_decoration)
4726 == std::mem::discriminant(&prev.text_decoration)
4727 && (!split_on_href || glyph.href == prev.href);
4728
4729 if same_style {
4730 current_group.push(glyph);
4731 } else {
4732 groups.push(current_group);
4733 current_group = vec![glyph];
4734 }
4735 }
4736 groups.push(current_group);
4737 groups
4738 }
4739
4740 fn group_link_runs<'a>(groups: &[Vec<&'a PositionedGlyph>]) -> Vec<Option<&'a str>> {
4745 let hrefs: Vec<Option<&'a str>> = groups
4746 .iter()
4747 .map(|g| g[0].href.as_deref().filter(|h| !h.is_empty()))
4748 .collect();
4749 let mut out = vec![None; groups.len()];
4750 let mut i = 0;
4751 while i < groups.len() {
4752 let mut j = i + 1;
4753 while j < groups.len() && hrefs[j] == hrefs[i] {
4754 j += 1;
4755 }
4756 let inked = groups[i..j]
4757 .iter()
4758 .any(|g| g.iter().any(|gl| !gl.char_value.is_whitespace()));
4759 if hrefs[i].is_some() && inked {
4760 for slot in &mut out[i..j] {
4761 *slot = hrefs[i];
4762 }
4763 }
4764 i = j;
4765 }
4766 out
4767 }
4768
4769 fn colors_equal(a: &Option<Color>, b: &Option<Color>) -> bool {
4770 match (a, b) {
4771 (None, None) => true,
4772 (Some(ca), Some(cb)) => {
4773 (ca.r - cb.r).abs() < 0.001
4774 && (ca.g - cb.g).abs() < 0.001
4775 && (ca.b - cb.b).abs() < 0.001
4776 && (ca.a - cb.a).abs() < 0.001
4777 }
4778 _ => false,
4779 }
4780 }
4781
4782 fn warn_nested_links(
4791 elements: &[LayoutElement],
4792 outer: Option<&str>,
4793 warnings: &mut Vec<String>,
4794 ) {
4795 fn report(warnings: &mut Vec<String>, inner: &str, outer: &str) {
4796 let msg = format!(
4797 "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"
4798 );
4799 if !warnings.contains(&msg) {
4800 warnings.push(msg);
4801 }
4802 }
4803 for el in elements {
4804 let own = el.href.as_deref().filter(|h| !h.is_empty());
4805 if let (Some(o), Some(h)) = (outer, own) {
4806 if h != o {
4807 report(warnings, h, o);
4808 }
4809 }
4810 let enclosing = outer.or(own);
4811 if let Some(o) = enclosing {
4812 if let DrawCommand::Text { ref lines, .. } = el.draw {
4813 for g in lines.iter().flat_map(|l| l.glyphs.iter()) {
4814 if let Some(h) = g.href.as_deref().filter(|h| !h.is_empty()) {
4815 if h != o {
4816 report(warnings, h, o);
4817 }
4818 }
4819 }
4820 }
4821 }
4822 Self::warn_nested_links(&el.children, enclosing, warnings);
4823 }
4824 }
4825
4826 fn collect_link_annotations(
4827 elements: &[LayoutElement],
4828 page_height: f64,
4829 annotations: &mut Vec<LinkAnnotation>,
4830 ) {
4831 for element in elements {
4832 if let Some(ref href) = element.href {
4833 if !href.is_empty() {
4834 let pdf_y = page_height - element.y - element.height;
4835 annotations.push(LinkAnnotation {
4836 x: element.x,
4837 y: pdf_y,
4838 width: element.width,
4839 height: element.height,
4840 href: href.clone(),
4841 });
4842 continue;
4844 }
4845 }
4846 if let DrawCommand::Text { ref lines, .. } = element.draw {
4851 for line in lines {
4852 let spans = Self::inline_link_spans(line);
4853 if spans.is_empty() {
4854 continue;
4855 }
4856 let (top, height) = if lines.len() == 1 {
4860 (element.y, element.height)
4861 } else {
4862 let fs = line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
4863 (line.y - fs * 0.8 - (line.height - fs) / 2.0, line.height)
4864 };
4865 for span in spans {
4866 annotations.push(LinkAnnotation {
4867 x: span.x0,
4868 y: page_height - top - height,
4869 width: span.x1 - span.x0,
4870 height,
4871 href: span.href,
4872 });
4873 }
4874 }
4875 }
4876 Self::collect_link_annotations(&element.children, page_height, annotations);
4877 }
4878 }
4879
4880 fn inline_link_spans(line: &TextLine) -> Vec<InlineLinkSpan> {
4887 let mut spans: Vec<InlineLinkSpan> = Vec::new();
4888 if !line.glyphs.iter().any(|g| g.href.is_some()) {
4889 return spans;
4890 }
4891 let mut prev_href: Option<&str> = None;
4892 for g in &line.glyphs {
4893 let x0 = line.x + g.x_offset;
4894 let x1 = x0 + g.x_advance;
4895 let href = g.href.as_deref().filter(|h| !h.is_empty());
4896 if let Some(h) = href {
4897 if prev_href != Some(h) {
4898 spans.push(InlineLinkSpan {
4899 href: h.to_string(),
4900 x0: f64::INFINITY,
4901 x1: f64::NEG_INFINITY,
4902 });
4903 }
4904 if !g.char_value.is_whitespace() {
4908 if let Some(last) = spans.last_mut() {
4909 last.x0 = last.x0.min(x0);
4910 last.x1 = last.x1.max(x1);
4911 }
4912 }
4913 }
4914 prev_href = href;
4915 }
4916 spans.retain(|s| s.x1 > s.x0);
4918 spans
4919 }
4920
4921 fn collect_form_fields(
4923 elements: &[LayoutElement],
4924 page_height: f64,
4925 page_idx: usize,
4926 fields: &mut Vec<FormFieldData>,
4927 ) {
4928 for element in elements {
4929 if let DrawCommand::FormField {
4930 ref field_type,
4931 ref name,
4932 } = element.draw
4933 {
4934 let pdf_y = page_height - element.y - element.height;
4935 fields.push(FormFieldData {
4936 field_type: field_type.clone(),
4937 name: name.clone(),
4938 x: element.x,
4939 y: pdf_y,
4940 width: element.width,
4941 height: element.height,
4942 page_idx,
4943 });
4944 }
4945 Self::collect_form_fields(&element.children, page_height, page_idx, fields);
4946 }
4947 }
4948
4949 fn collect_bookmarks(
4951 elements: &[LayoutElement],
4952 page_height: f64,
4953 page_obj_id: usize,
4954 bookmarks: &mut Vec<PdfBookmark>,
4955 ) {
4956 for element in elements {
4957 if let Some(ref title) = element.bookmark {
4958 let y_pdf = page_height - element.y;
4959 bookmarks.push(PdfBookmark {
4960 title: title.clone(),
4961 page_obj_id,
4962 y_pdf,
4963 });
4964 }
4965 Self::collect_bookmarks(&element.children, page_height, page_obj_id, bookmarks);
4966 }
4967 }
4968
4969 fn write_outline_tree(
4972 &self,
4973 builder: &mut PdfBuilder,
4974 bookmarks: &[PdfBookmark],
4975 mut tag_builder: Option<&mut tagged::TagBuilder>,
4976 ) -> usize {
4977 let outlines_id = builder.objects.len();
4979 builder.objects.push(PdfObject {
4980 id: outlines_id,
4981 data: vec![],
4982 });
4983
4984 let mut item_ids: Vec<usize> = Vec::new();
4986 for _bm in bookmarks {
4987 let item_id = builder.objects.len();
4988 builder.objects.push(PdfObject {
4989 id: item_id,
4990 data: vec![],
4991 });
4992 item_ids.push(item_id);
4993 }
4994
4995 for (i, (bm, &item_id)) in bookmarks.iter().zip(item_ids.iter()).enumerate() {
4997 let wants_sd = tag_builder
5004 .as_mut()
5005 .map(|tb| tb.request_struct_destination(&bm.title, item_id))
5006 .unwrap_or(false);
5007 let dest = if wants_sd {
5008 format!("/SD [999999999 0 R /XYZ 0 {:.2} null]", bm.y_pdf)
5009 } else {
5010 format!("/Dest [{} 0 R /XYZ 0 {:.2} null]", bm.page_obj_id, bm.y_pdf)
5011 };
5012 let mut dict = format!(
5013 "<< /Title {} /Parent {} 0 R {}",
5014 Self::encode_text_string(&bm.title),
5015 outlines_id,
5016 dest,
5017 );
5018 if i > 0 {
5019 let _ = write!(dict, " /Prev {} 0 R", item_ids[i - 1]);
5020 }
5021 if i + 1 < item_ids.len() {
5022 let _ = write!(dict, " /Next {} 0 R", item_ids[i + 1]);
5023 }
5024 dict.push_str(" >>");
5025 builder.objects[item_id].data = dict.into_bytes();
5026 }
5027
5028 let first_id = item_ids.first().copied().unwrap_or(0);
5030 let last_id = item_ids.last().copied().unwrap_or(0);
5031 let outlines_dict = format!(
5032 "<< /Type /Outlines /First {} 0 R /Last {} 0 R /Count {} >>",
5033 first_id,
5034 last_id,
5035 bookmarks.len()
5036 );
5037 builder.objects[outlines_id].data = outlines_dict.into_bytes();
5038
5039 outlines_id
5040 }
5041
5042 fn write_svg_commands(
5044 stream: &mut String,
5045 commands: &[SvgCommand],
5046 ext_gstate_map: &HashMap<u64, (usize, String)>,
5047 ) {
5048 for cmd in commands {
5049 match cmd {
5050 SvgCommand::MoveTo(x, y) => {
5051 let _ = writeln!(stream, "{:.2} {:.2} m", x, y);
5052 }
5053 SvgCommand::LineTo(x, y) => {
5054 let _ = writeln!(stream, "{:.2} {:.2} l", x, y);
5055 }
5056 SvgCommand::CurveTo(x1, y1, x2, y2, x3, y3) => {
5057 let _ = writeln!(
5058 stream,
5059 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5060 x1, y1, x2, y2, x3, y3
5061 );
5062 }
5063 SvgCommand::ClosePath => {
5064 let _ = writeln!(stream, "h");
5065 }
5066 SvgCommand::SetFill(r, g, b) => {
5067 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", r, g, b);
5068 }
5069 SvgCommand::SetFillNone => {
5070 }
5072 SvgCommand::SetStroke(r, g, b) => {
5073 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", r, g, b);
5074 }
5075 SvgCommand::SetStrokeNone => {
5076 }
5078 SvgCommand::SetStrokeWidth(w) => {
5079 let _ = writeln!(stream, "{:.2} w", w);
5080 }
5081 SvgCommand::Fill => {
5082 let _ = writeln!(stream, "f");
5083 }
5084 SvgCommand::Stroke => {
5085 let _ = writeln!(stream, "S");
5086 }
5087 SvgCommand::FillAndStroke => {
5088 let _ = writeln!(stream, "B");
5089 }
5090 SvgCommand::SetLineCap(cap) => {
5091 let _ = writeln!(stream, "{} J", cap);
5092 }
5093 SvgCommand::SetLineJoin(join) => {
5094 let _ = writeln!(stream, "{} j", join);
5095 }
5096 SvgCommand::SaveState => {
5097 let _ = writeln!(stream, "q");
5098 }
5099 SvgCommand::RestoreState => {
5100 let _ = writeln!(stream, "Q");
5101 }
5102 SvgCommand::SetOpacity(opacity) => {
5103 if let Some((_, gs_name)) = ext_gstate_map.get(&opacity.to_bits()) {
5104 let _ = writeln!(stream, "/{} gs", gs_name);
5105 }
5106 }
5107 }
5108 }
5109 }
5110
5111 pub(crate) fn escape_pdf_string(s: &str) -> String {
5113 s.replace('\\', "\\\\")
5114 .replace('(', "\\(")
5115 .replace(')', "\\)")
5116 }
5117
5118 pub(crate) fn encode_text_string(s: &str) -> String {
5131 if s.bytes().all(|b| (0x20..=0x7E).contains(&b)) {
5132 return format!("({})", Self::escape_pdf_string(s));
5133 }
5134 let mut out = String::with_capacity(6 + s.len() * 4);
5135 out.push_str("<FEFF");
5136 for unit in s.encode_utf16() {
5137 let _ = write!(out, "{unit:04X}");
5138 }
5139 out.push('>');
5140 out
5141 }
5142
5143 fn decode_attachment_src(src: &str) -> Result<Vec<u8>, FormeError> {
5146 use base64::Engine as _;
5147 let b64 = src.rsplit_once(";base64,").map(|(_, d)| d).unwrap_or(src);
5148 base64::engine::general_purpose::STANDARD
5149 .decode(b64.trim())
5150 .map_err(|e| {
5151 FormeError::RenderError(format!(
5152 "attachment src is not valid base64 (expected base64 bytes or a data: URI): {e}"
5153 ))
5154 })
5155 }
5156
5157 fn mime_to_pdf_name(mime: &str) -> String {
5160 let mut out = String::with_capacity(mime.len() + 2);
5161 for b in mime.bytes() {
5162 let regular =
5163 b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'+' | b'\'' | b'"');
5164 if regular {
5165 out.push(b as char);
5166 } else {
5167 let _ = write!(out, "#{:02X}", b);
5168 }
5169 }
5170 out
5171 }
5172
5173 fn encode_winansi_text(builder: &PdfBuilder, s: &str) -> String {
5177 let mut result = String::with_capacity(s.len());
5178 for ch in s.chars() {
5179 let b = Self::unicode_to_winansi(ch).unwrap_or_else(|| {
5180 builder.missing_glyphs.borrow_mut().insert(ch);
5181 b'?'
5182 });
5183 match b {
5184 b'\\' => result.push_str("\\\\"),
5185 b'(' => result.push_str("\\("),
5186 b')' => result.push_str("\\)"),
5187 0x20..=0x7E => result.push(b as char),
5188 _ => {
5189 let _ = write!(result, "\\{:03o}", b);
5190 }
5191 }
5192 }
5193 result
5194 }
5195
5196 fn unicode_to_winansi(ch: char) -> Option<u8> {
5198 crate::font::unicode_to_winansi(ch)
5199 }
5200
5201 fn serialize(&self, builder: &PdfBuilder, info_obj_id: Option<usize>) -> Vec<u8> {
5203 let mut output: Vec<u8> = Vec::new();
5204 let mut offsets: Vec<usize> = vec![0; builder.objects.len()];
5205
5206 output.extend_from_slice(match builder.pdf_version {
5208 crate::model::PdfVersion::V1_7 => b"%PDF-1.7\n".as_slice(),
5209 crate::model::PdfVersion::V2_0 => b"%PDF-2.0\n".as_slice(),
5210 });
5211 output.extend_from_slice(b"%\xe2\xe3\xcf\xd3\n");
5212
5213 for (i, obj) in builder.objects.iter().enumerate().skip(1) {
5214 offsets[i] = output.len();
5215 let header = format!("{} 0 obj\n", i);
5216 output.extend_from_slice(header.as_bytes());
5217 output.extend_from_slice(&obj.data);
5218 output.extend_from_slice(b"\nendobj\n\n");
5219 }
5220
5221 let xref_offset = output.len();
5222 let _ = writeln!(output, "xref\n0 {}", builder.objects.len());
5223 let _ = writeln!(output, "0000000000 65535 f ");
5224 for offset in offsets.iter().skip(1) {
5225 let _ = writeln!(output, "{:010} 00000 n ", offset);
5226 }
5227
5228 let _ = write!(
5229 output,
5230 "trailer\n<< /Size {} /Root 1 0 R",
5231 builder.objects.len()
5232 );
5233 if let Some(info_id) = info_obj_id {
5234 let _ = write!(output, " /Info {} 0 R", info_id);
5235 }
5236 {
5242 use sha2::Digest as _;
5243 let digest = sha2::Sha256::digest(&output);
5244 let mut id_hex = String::with_capacity(32);
5245 for b in &digest[..16] {
5246 let _ = write!(id_hex, "{:02X}", b);
5247 }
5248 let _ = write!(output, " /ID [<{id_hex}> <{id_hex}>]");
5249 }
5250 let _ = writeln!(output, " >>\nstartxref\n{}\n%%EOF", xref_offset);
5251
5252 output
5253 }
5254}
5255
5256fn write_chart_primitive(
5261 stream: &mut String,
5262 prim: &crate::chart::ChartPrimitive,
5263 _chart_height: f64,
5264 builder: &PdfBuilder,
5265) {
5266 use crate::chart::{ChartPrimitive, TextAnchor};
5267 use crate::font::metrics::unicode_to_winansi;
5268
5269 match prim {
5270 ChartPrimitive::Rect { x, y, w, h, fill } => {
5271 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
5272 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re f", x, y, w, h);
5273 }
5274
5275 ChartPrimitive::Line {
5276 x1,
5277 y1,
5278 x2,
5279 y2,
5280 stroke,
5281 width,
5282 } => {
5283 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
5284 let _ = writeln!(stream, "{:.2} w", width);
5285 let _ = writeln!(stream, "{:.2} {:.2} m {:.2} {:.2} l S", x1, y1, x2, y2);
5286 }
5287
5288 ChartPrimitive::Polyline {
5289 points,
5290 stroke,
5291 width,
5292 } => {
5293 if points.len() < 2 {
5294 return;
5295 }
5296 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
5297 let _ = writeln!(stream, "{:.2} w", width);
5298 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
5299 for &(px, py) in &points[1..] {
5300 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
5301 }
5302 let _ = writeln!(stream, "S");
5303 }
5304
5305 ChartPrimitive::FilledPath {
5306 points,
5307 fill,
5308 opacity,
5309 } => {
5310 if points.len() < 3 {
5311 return;
5312 }
5313 let _ = writeln!(stream, "q");
5314 if *opacity < 1.0 {
5316 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
5317 let _ = writeln!(stream, "/{} gs", gs_name);
5318 }
5319 }
5320 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
5321 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
5322 for &(px, py) in &points[1..] {
5323 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
5324 }
5325 let _ = writeln!(stream, "h f");
5326 let _ = writeln!(stream, "Q");
5327 }
5328
5329 ChartPrimitive::Circle { cx, cy, r, fill } => {
5330 let kappa: f64 = 0.5523;
5332 let kr = kappa * r;
5333 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
5334 let _ = writeln!(stream, "{:.2} {:.2} m", cx + r, cy);
5335 let _ = writeln!(
5336 stream,
5337 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5338 cx + r,
5339 cy + kr,
5340 cx + kr,
5341 cy + r,
5342 cx,
5343 cy + r
5344 );
5345 let _ = writeln!(
5346 stream,
5347 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5348 cx - kr,
5349 cy + r,
5350 cx - r,
5351 cy + kr,
5352 cx - r,
5353 cy
5354 );
5355 let _ = writeln!(
5356 stream,
5357 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5358 cx - r,
5359 cy - kr,
5360 cx - kr,
5361 cy - r,
5362 cx,
5363 cy - r
5364 );
5365 let _ = writeln!(
5366 stream,
5367 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5368 cx + kr,
5369 cy - r,
5370 cx + r,
5371 cy - kr,
5372 cx + r,
5373 cy
5374 );
5375 let _ = writeln!(stream, "f");
5376 }
5377
5378 ChartPrimitive::ArcSector {
5379 cx,
5380 cy,
5381 r,
5382 start_angle,
5383 end_angle,
5384 fill,
5385 } => {
5386 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
5387 let _ = writeln!(stream, "{:.2} {:.2} m", cx, cy);
5389 let sx = cx + r * start_angle.cos();
5391 let sy = cy + r * start_angle.sin();
5392 let _ = writeln!(stream, "{:.2} {:.2} l", sx, sy);
5393
5394 let mut angle = *start_angle;
5396 let total = end_angle - start_angle;
5397 let segments = ((total.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
5398 let step = total / segments as f64;
5399
5400 for _ in 0..segments {
5401 let a1 = angle;
5402 let a2 = angle + step;
5403 let alpha = 4.0 / 3.0 * ((a2 - a1) / 4.0).tan();
5404
5405 let p1x = cx + r * a1.cos();
5406 let p1y = cy + r * a1.sin();
5407 let p2x = cx + r * a2.cos();
5408 let p2y = cy + r * a2.sin();
5409
5410 let cp1x = p1x - alpha * r * a1.sin();
5411 let cp1y = p1y + alpha * r * a1.cos();
5412 let cp2x = p2x + alpha * r * a2.sin();
5413 let cp2y = p2y - alpha * r * a2.cos();
5414
5415 let _ = writeln!(
5416 stream,
5417 "{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c",
5418 cp1x, cp1y, cp2x, cp2y, p2x, p2y
5419 );
5420 angle = a2;
5421 }
5422
5423 let _ = writeln!(stream, "h f");
5425 }
5426
5427 ChartPrimitive::Label {
5428 text,
5429 x,
5430 y,
5431 font_size,
5432 color,
5433 anchor,
5434 } => {
5435 let metrics = crate::font::StandardFont::Helvetica.metrics();
5437 let text_width = metrics.measure_string(text, *font_size, 0.0);
5438 let x_offset = match anchor {
5439 TextAnchor::Left => 0.0,
5440 TextAnchor::Center => -text_width / 2.0,
5441 TextAnchor::Right => -text_width,
5442 };
5443
5444 let font_idx = builder
5446 .font_objects
5447 .iter()
5448 .enumerate()
5449 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
5450 .map(|(i, _)| i)
5451 .unwrap_or(0);
5452
5453 let encoded: String = text
5455 .chars()
5456 .map(|ch| {
5457 if let Some(code) = unicode_to_winansi(ch) {
5458 code as char
5459 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
5460 ch
5461 } else {
5462 builder.missing_glyphs.borrow_mut().insert(ch);
5463 '?'
5464 }
5465 })
5466 .collect();
5467 let escaped = pdf_escape_string(&encoded);
5468
5469 let _ = writeln!(stream, "q");
5471 let _ = writeln!(stream, "1 0 0 -1 {:.4} {:.4} cm", x + x_offset, *y);
5472 let _ = writeln!(
5473 stream,
5474 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
5475 font_idx, font_size, color.r, color.g, color.b, escaped
5476 );
5477 let _ = writeln!(stream, "Q");
5478 }
5479 ChartPrimitive::VerticalLabel {
5480 text,
5481 x,
5482 y,
5483 font_size,
5484 color,
5485 } => {
5486 let metrics = crate::font::StandardFont::Helvetica.metrics();
5487 let text_width = metrics.measure_string(text, *font_size, 0.0);
5488 let font_idx = builder
5489 .font_objects
5490 .iter()
5491 .enumerate()
5492 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
5493 .map(|(i, _)| i)
5494 .unwrap_or(0);
5495 let encoded: String = text
5496 .chars()
5497 .map(|ch| {
5498 if let Some(code) = unicode_to_winansi(ch) {
5499 code as char
5500 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
5501 ch
5502 } else {
5503 builder.missing_glyphs.borrow_mut().insert(ch);
5504 '?'
5505 }
5506 })
5507 .collect();
5508 let escaped = pdf_escape_string(&encoded);
5509
5510 let _ = writeln!(stream, "q");
5515 let _ = writeln!(
5516 stream,
5517 "0 -1 -1 0 {:.4} {:.4} cm",
5518 x + font_size * 0.35,
5519 y + text_width / 2.0
5520 );
5521 let _ = writeln!(
5522 stream,
5523 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
5524 font_idx, font_size, color.r, color.g, color.b, escaped
5525 );
5526 let _ = writeln!(stream, "Q");
5527 }
5528 }
5529}
5530
5531fn normalize_gradient_stops(
5538 stops: &[crate::style::GradientStop],
5539 fallback: Color,
5540) -> Vec<crate::style::GradientStop> {
5541 use crate::style::GradientStop;
5542 if stops.is_empty() {
5543 return vec![
5544 GradientStop {
5545 position: 0.0,
5546 color: fallback,
5547 },
5548 GradientStop {
5549 position: 1.0,
5550 color: fallback,
5551 },
5552 ];
5553 }
5554 let mut sorted: Vec<GradientStop> = stops
5555 .iter()
5556 .map(|s| GradientStop {
5557 position: s.position.clamp(0.0, 1.0),
5558 color: s.color,
5559 })
5560 .collect();
5561 sorted.sort_by(|a, b| {
5562 a.position
5563 .partial_cmp(&b.position)
5564 .unwrap_or(std::cmp::Ordering::Equal)
5565 });
5566 if sorted[0].position > 0.0 {
5567 sorted.insert(
5568 0,
5569 GradientStop {
5570 position: 0.0,
5571 color: sorted[0].color,
5572 },
5573 );
5574 }
5575 if sorted[sorted.len() - 1].position < 1.0 {
5576 let last = sorted[sorted.len() - 1].color;
5577 sorted.push(GradientStop {
5578 position: 1.0,
5579 color: last,
5580 });
5581 }
5582 sorted
5583}
5584
5585fn pdf_escape_string(s: &str) -> String {
5586 let mut out = String::with_capacity(s.len());
5587 for ch in s.chars() {
5588 match ch {
5589 '(' => out.push_str("\\("),
5590 ')' => out.push_str("\\)"),
5591 '\\' => out.push_str("\\\\"),
5592 _ => out.push(ch),
5593 }
5594 }
5595 out
5596}
5597
5598fn has_placeholder_group(group: &[&PositionedGlyph]) -> bool {
5601 group
5602 .iter()
5603 .any(|g| g.char_value == PAGE_NUMBER_SENTINEL || g.char_value == TOTAL_PAGES_SENTINEL)
5604}
5605
5606fn pdf_number(v: f64) -> String {
5609 let s = format!("{:.3}", v);
5610 let s = s.trim_end_matches('0').trim_end_matches('.');
5611 if s.is_empty() || s == "-" {
5612 "0".to_string()
5613 } else {
5614 s.to_string()
5615 }
5616}
5617
5618#[cfg(test)]
5619mod tests {
5620 use super::*;
5621 use crate::font::FontContext;
5622
5623 #[test]
5630 fn test_embedded_srgb_is_a_valid_icc_profile() {
5631 let icc: &[u8] = include_bytes!("sRGB.icc");
5632 assert!(
5633 icc.len() >= 128,
5634 "ICC shorter than its 128-byte header: {}",
5635 icc.len()
5636 );
5637 assert_ne!(
5639 icc[0], b'<',
5640 "embedded ICC starts with '<' — looks like HTML, not a profile"
5641 );
5642 assert_eq!(&icc[36..40], b"acsp", "missing ICC 'acsp' signature");
5644 let device_class = &icc[12..16];
5646 assert!(
5647 device_class == b"mntr" || device_class == b"prtr",
5648 "ICC device class {:?} is not mntr/prtr (PDF/A 6.2.3)",
5649 String::from_utf8_lossy(device_class),
5650 );
5651 assert_eq!(&icc[16..20], b"RGB ", "ICC data colour space is not RGB");
5653 }
5654
5655 #[test]
5656 fn test_escape_pdf_string() {
5657 assert_eq!(
5658 PdfWriter::escape_pdf_string("Hello (World)"),
5659 "Hello \\(World\\)"
5660 );
5661 assert_eq!(PdfWriter::escape_pdf_string("back\\slash"), "back\\\\slash");
5662 }
5663
5664 #[test]
5665 fn test_encode_text_string() {
5666 assert_eq!(PdfWriter::encode_text_string("A (b)"), "(A \\(b\\))");
5668 assert_eq!(PdfWriter::encode_text_string(""), "()");
5669 assert_eq!(PdfWriter::encode_text_string("Üb"), "<FEFF00DC0062>");
5671 assert_eq!(PdfWriter::encode_text_string("𝄞"), "<FEFFD834DD1E>");
5673 assert_eq!(PdfWriter::encode_text_string("a\nb"), "<FEFF0061000A0062>");
5675 }
5676
5677 #[test]
5678 fn test_empty_document_produces_valid_pdf() {
5679 let writer = PdfWriter::new();
5680 let font_context = FontContext::new();
5681 let pages = vec![LayoutPage {
5682 width: 595.28,
5683 height: 841.89,
5684 elements: vec![],
5685 fixed_header: vec![],
5686 fixed_footer: vec![],
5687 watermarks: vec![],
5688 config: PageConfig::default(),
5689 page_name: None,
5690 }];
5691 let metadata = Metadata::default();
5692 let (bytes, _warnings) = writer
5693 .write(
5694 &pages,
5695 &metadata,
5696 &font_context,
5697 false,
5698 None,
5699 false,
5700 None,
5701 &[],
5702 None,
5703 false,
5704 crate::model::PdfVersion::V1_7,
5705 false,
5706 )
5707 .unwrap();
5708
5709 assert!(bytes.starts_with(b"%PDF-1.7"));
5710 assert!(bytes.windows(5).any(|w| w == b"%%EOF"));
5711 assert!(bytes.windows(4).any(|w| w == b"xref"));
5712 assert!(bytes.windows(7).any(|w| w == b"trailer"));
5713 }
5714
5715 #[test]
5716 fn test_metadata_in_pdf() {
5717 let writer = PdfWriter::new();
5718 let font_context = FontContext::new();
5719 let pages = vec![LayoutPage {
5720 width: 595.28,
5721 height: 841.89,
5722 elements: vec![],
5723 fixed_header: vec![],
5724 fixed_footer: vec![],
5725 watermarks: vec![],
5726 config: PageConfig::default(),
5727 page_name: None,
5728 }];
5729 let metadata = Metadata {
5730 title: Some("Test Document".to_string()),
5731 author: Some("Forme".to_string()),
5732 subject: None,
5733 creator: None,
5734 lang: None,
5735 };
5736 let (bytes, _warnings) = writer
5737 .write(
5738 &pages,
5739 &metadata,
5740 &font_context,
5741 false,
5742 None,
5743 false,
5744 None,
5745 &[],
5746 None,
5747 false,
5748 crate::model::PdfVersion::V1_7,
5749 false,
5750 )
5751 .unwrap();
5752 let text = String::from_utf8_lossy(&bytes);
5753
5754 assert!(text.contains("/Title (Test Document)"));
5755 assert!(text.contains("/Author (Forme)"));
5756 }
5757
5758 #[test]
5759 fn test_bold_font_registered_separately() {
5760 let writer = PdfWriter::new();
5761 let font_context = FontContext::new();
5762
5763 let pages = vec![LayoutPage {
5765 width: 595.28,
5766 height: 841.89,
5767 elements: vec![
5768 LayoutElement {
5769 x: 54.0,
5770 y: 54.0,
5771 width: 100.0,
5772 height: 16.8,
5773 draw: DrawCommand::Text {
5774 lines: vec![TextLine {
5775 x: 54.0,
5776 y: 66.0,
5777 width: 50.0,
5778 height: 16.8,
5779 glyphs: vec![PositionedGlyph {
5780 glyph_id: 65,
5781 x_offset: 0.0,
5782 y_offset: 0.0,
5783 x_advance: 8.0,
5784 font_size: 12.0,
5785 font_family: "Helvetica".into(),
5786 font_weight: 400,
5787 font_style: FontStyle::Normal,
5788 char_value: 'A',
5789 color: None,
5790 href: None,
5791 text_decoration: TextDecoration::None,
5792 letter_spacing: 0.0,
5793 cluster_text: None,
5794 extraction_text: None,
5795 ligature: false,
5796 }],
5797 word_spacing: 0.0,
5798 }],
5799 color: Color::BLACK,
5800 text_decoration: TextDecoration::None,
5801 opacity: 1.0,
5802 },
5803 children: vec![],
5804 node_type: None,
5805 resolved_style: None,
5806 source_location: None,
5807 href: None,
5808 bookmark: None,
5809 alt: None,
5810 is_header_row: false,
5811 actual_text: None,
5812 list_numbering: None,
5813 col_span: 1,
5814 overflow: Overflow::default(),
5815 opacity: 1.0,
5816 },
5817 LayoutElement {
5818 x: 54.0,
5819 y: 74.0,
5820 width: 100.0,
5821 height: 16.8,
5822 draw: DrawCommand::Text {
5823 lines: vec![TextLine {
5824 x: 54.0,
5825 y: 86.0,
5826 width: 50.0,
5827 height: 16.8,
5828 glyphs: vec![PositionedGlyph {
5829 glyph_id: 65,
5830 x_offset: 0.0,
5831 y_offset: 0.0,
5832 x_advance: 8.0,
5833 font_size: 12.0,
5834 font_family: "Helvetica".into(),
5835 font_weight: 700,
5836 font_style: FontStyle::Normal,
5837 char_value: 'A',
5838 color: None,
5839 href: None,
5840 text_decoration: TextDecoration::None,
5841 letter_spacing: 0.0,
5842 cluster_text: None,
5843 extraction_text: None,
5844 ligature: false,
5845 }],
5846 word_spacing: 0.0,
5847 }],
5848 color: Color::BLACK,
5849 text_decoration: TextDecoration::None,
5850 opacity: 1.0,
5851 },
5852 children: vec![],
5853 node_type: None,
5854 resolved_style: None,
5855 source_location: None,
5856 href: None,
5857 bookmark: None,
5858 alt: None,
5859 is_header_row: false,
5860 actual_text: None,
5861 list_numbering: None,
5862 col_span: 1,
5863 overflow: Overflow::default(),
5864 opacity: 1.0,
5865 },
5866 ],
5867 fixed_header: vec![],
5868 fixed_footer: vec![],
5869 watermarks: vec![],
5870 config: PageConfig::default(),
5871 page_name: None,
5872 }];
5873
5874 let metadata = Metadata::default();
5875 let (bytes, _warnings) = writer
5876 .write(
5877 &pages,
5878 &metadata,
5879 &font_context,
5880 false,
5881 None,
5882 false,
5883 None,
5884 &[],
5885 None,
5886 false,
5887 crate::model::PdfVersion::V1_7,
5888 false,
5889 )
5890 .unwrap();
5891 let text = String::from_utf8_lossy(&bytes);
5892
5893 assert!(
5895 text.contains("Helvetica"),
5896 "Should contain regular Helvetica"
5897 );
5898 assert!(
5899 text.contains("Helvetica-Bold"),
5900 "Should contain Helvetica-Bold"
5901 );
5902 }
5903
5904 #[test]
5905 fn test_sanitize_font_name() {
5906 assert_eq!(PdfWriter::sanitize_font_name("Inter", 400, false), "Inter");
5907 assert_eq!(
5908 PdfWriter::sanitize_font_name("Inter", 700, false),
5909 "Inter-Bold"
5910 );
5911 assert_eq!(
5912 PdfWriter::sanitize_font_name("Inter", 400, true),
5913 "Inter-Italic"
5914 );
5915 assert_eq!(
5916 PdfWriter::sanitize_font_name("Inter", 700, true),
5917 "Inter-Bold-Italic"
5918 );
5919 assert_eq!(
5920 PdfWriter::sanitize_font_name("Noto Sans", 400, false),
5921 "NotoSans"
5922 );
5923 assert_eq!(
5924 PdfWriter::sanitize_font_name("Font (Display)", 400, false),
5925 "FontDisplay"
5926 );
5927 }
5928
5929 #[test]
5930 fn test_tounicode_cmap_format() {
5931 let mut glyph_to_char = HashMap::new();
5933 glyph_to_char.insert(36u16, "A".to_string());
5934 glyph_to_char.insert(37u16, "B".to_string());
5935
5936 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&glyph_to_char, "TestFont");
5937
5938 assert!(cmap.contains("begincmap"), "CMap should contain begincmap");
5939 assert!(cmap.contains("endcmap"), "CMap should contain endcmap");
5940 assert!(
5941 cmap.contains("beginbfchar"),
5942 "CMap should contain beginbfchar"
5943 );
5944 assert!(cmap.contains("endbfchar"), "CMap should contain endbfchar");
5945 assert!(
5946 cmap.contains("<0024> <0041>"),
5947 "Should map gid 0x0024 to Unicode 'A' 0x0041"
5948 );
5949 assert!(
5950 cmap.contains("<0025> <0042>"),
5951 "Should map gid 0x0025 to Unicode 'B' 0x0042"
5952 );
5953 assert!(
5954 cmap.contains("begincodespacerange"),
5955 "Should define codespace range"
5956 );
5957 assert!(
5958 cmap.contains("<0000> <FFFF>"),
5959 "Codespace should be 0000-FFFF"
5960 );
5961 }
5962
5963 #[test]
5968 fn test_tounicode_cmap_multi_char_and_non_bmp_destinations() {
5969 let mut gid_to_text = HashMap::new();
5970 gid_to_text.insert(5u16, "ffi".to_string());
5971 gid_to_text.insert(6u16, "Th".to_string());
5972 gid_to_text.insert(7u16, "\u{1F600}".to_string());
5973
5974 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&gid_to_text, "TestFont");
5975
5976 assert!(cmap.contains("<0005> <006600660069>"), "{cmap}");
5977 assert!(cmap.contains("<0006> <00540068>"), "{cmap}");
5978 assert!(cmap.contains("<0007> <D83DDE00>"), "{cmap}");
5979 }
5980
5981 fn text_glyph(
5982 glyph_id: u16,
5983 ch: char,
5984 cluster: Option<&str>,
5985 ligature: bool,
5986 ) -> PositionedGlyph {
5987 PositionedGlyph {
5988 glyph_id,
5989 x_offset: 0.0,
5990 y_offset: 0.0,
5991 x_advance: 5.0,
5992 font_size: 12.0,
5993 font_family: "Lig".into(),
5994 font_weight: 400,
5995 font_style: FontStyle::Normal,
5996 char_value: ch,
5997 color: None,
5998 href: None,
5999 text_decoration: TextDecoration::None,
6000 letter_spacing: 0.0,
6001 cluster_text: cluster.map(str::to_string),
6002 extraction_text: None,
6003 ligature,
6004 }
6005 }
6006
6007 #[test]
6008 fn test_record_glyph_text_ligature_maps_whole_cluster() {
6009 let mut map = HashMap::new();
6010 record_glyph_text(&mut map, &text_glyph(9, 'f', Some("ffi"), true));
6011 assert_eq!(map[&9], "ffi");
6012 }
6013
6014 #[test]
6018 fn test_record_glyph_text_shared_cluster_glyph_keeps_its_char() {
6019 let mut map = HashMap::new();
6020 record_glyph_text(&mut map, &text_glyph(9, 'k', Some("ki"), false));
6021 assert_eq!(map[&9], "k");
6022 }
6023
6024 #[test]
6028 fn test_record_glyph_text_single_char_beats_multi_char() {
6029 let mut first_multi = HashMap::new();
6030 record_glyph_text(
6031 &mut first_multi,
6032 &text_glyph(9, 'e', Some("e\u{FE0F}"), true),
6033 );
6034 record_glyph_text(&mut first_multi, &text_glyph(9, 'e', None, false));
6035 assert_eq!(first_multi[&9], "e");
6036
6037 let mut first_single = HashMap::new();
6038 record_glyph_text(&mut first_single, &text_glyph(9, 'e', None, false));
6039 record_glyph_text(
6040 &mut first_single,
6041 &text_glyph(9, 'e', Some("e\u{FE0F}"), true),
6042 );
6043 assert_eq!(first_single[&9], "e");
6044
6045 let mut two_multi = HashMap::new();
6047 record_glyph_text(&mut two_multi, &text_glyph(9, 'f', Some("fi"), true));
6048 record_glyph_text(&mut two_multi, &text_glyph(9, 'f', Some("ffi"), true));
6049 assert_eq!(two_multi[&9], "fi");
6050 }
6051
6052 #[test]
6053 fn test_w_array_format() {
6054 let mut char_to_gid = HashMap::new();
6055 char_to_gid.insert('A', 36u16);
6056
6057 let w_array_str = "[ 36 [600] ]";
6060 assert!(w_array_str.starts_with('['));
6061 assert!(w_array_str.ends_with(']'));
6062 }
6063
6064 #[test]
6065 fn test_hex_glyph_encoding() {
6066 let gid: u16 = 0x0041;
6068 let hex = format!("{:04X}", gid);
6069 assert_eq!(hex, "0041");
6070
6071 let gids = [0x0041u16, 0x0042, 0x0043];
6072 let hex_str: String = gids.iter().map(|g| format!("{:04X}", g)).collect();
6073 assert_eq!(hex_str, "004100420043");
6074 }
6075
6076 #[test]
6077 fn test_standard_font_still_uses_text_string() {
6078 let writer = PdfWriter::new();
6079 let font_context = FontContext::new();
6080
6081 let pages = vec![LayoutPage {
6082 width: 595.28,
6083 height: 841.89,
6084 elements: vec![LayoutElement {
6085 x: 54.0,
6086 y: 54.0,
6087 width: 100.0,
6088 height: 16.8,
6089 draw: DrawCommand::Text {
6090 lines: vec![TextLine {
6091 x: 54.0,
6092 y: 66.0,
6093 width: 50.0,
6094 height: 16.8,
6095 glyphs: vec![PositionedGlyph {
6096 glyph_id: 65,
6097 x_offset: 0.0,
6098 y_offset: 0.0,
6099 x_advance: 8.0,
6100 font_size: 12.0,
6101 font_family: "Helvetica".into(),
6102 font_weight: 400,
6103 font_style: FontStyle::Normal,
6104 char_value: 'H',
6105 color: None,
6106 href: None,
6107 text_decoration: TextDecoration::None,
6108 letter_spacing: 0.0,
6109 cluster_text: None,
6110 extraction_text: None,
6111 ligature: false,
6112 }],
6113 word_spacing: 0.0,
6114 }],
6115 color: Color::BLACK,
6116 text_decoration: TextDecoration::None,
6117 opacity: 1.0,
6118 },
6119 children: vec![],
6120 node_type: None,
6121 resolved_style: None,
6122 source_location: None,
6123 href: None,
6124 bookmark: None,
6125 alt: None,
6126 is_header_row: false,
6127 actual_text: None,
6128 list_numbering: None,
6129 col_span: 1,
6130 overflow: Overflow::default(),
6131 opacity: 1.0,
6132 }],
6133 fixed_header: vec![],
6134 fixed_footer: vec![],
6135 watermarks: vec![],
6136 config: PageConfig::default(),
6137 page_name: None,
6138 }];
6139
6140 let metadata = Metadata::default();
6141 let (bytes, _warnings) = writer
6142 .write(
6143 &pages,
6144 &metadata,
6145 &font_context,
6146 false,
6147 None,
6148 false,
6149 None,
6150 &[],
6151 None,
6152 false,
6153 crate::model::PdfVersion::V1_7,
6154 false,
6155 )
6156 .unwrap();
6157 let text = String::from_utf8_lossy(&bytes);
6158
6159 assert!(
6161 text.contains("/Type1"),
6162 "Standard font should use Type1 subtype"
6163 );
6164 assert!(
6165 !text.contains("CIDFontType2"),
6166 "Standard font should not use CIDFontType2"
6167 );
6168 }
6169}