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) {
122 let text = match &glyph.cluster_text {
123 Some(cluster) if glyph.ligature && !cluster.is_empty() => cluster.clone(),
124 _ => glyph.char_value.to_string(),
125 };
126 match glyph_to_text.entry(glyph.glyph_id) {
127 std::collections::hash_map::Entry::Vacant(slot) => {
128 slot.insert(text);
129 }
130 std::collections::hash_map::Entry::Occupied(mut slot) => {
131 let existing_is_multi = slot.get().chars().nth(1).is_some();
132 let new_is_single = text.chars().nth(1).is_none();
133 if existing_is_multi && new_is_single {
134 slot.insert(text);
135 }
136 }
137 }
138}
139
140#[allow(dead_code)]
142struct CustomFontEmbedData {
143 ttf_data: Vec<u8>,
144 gid_remap: HashMap<u16, u16>,
146 glyph_to_text: HashMap<u16, String>,
148 char_to_gid: HashMap<char, u16>,
150 pdf_widths: HashMap<u16, f64>,
154 default_width: u32,
155 units_per_em: u16,
156 ascender: i16,
157 descender: i16,
158}
159
160struct FontUsage {
162 chars: HashSet<char>,
164 glyph_ids: HashSet<u16>,
166 glyph_to_text: HashMap<u16, String>,
169}
170
171struct PdfBuilder {
173 objects: Vec<PdfObject>,
174 font_objects: Vec<(FontKey, usize)>,
176 custom_font_data: HashMap<FontKey, CustomFontEmbedData>,
178 embedded_standard_fonts: std::collections::HashSet<FontKey>,
183 image_objects: Vec<usize>,
186 image_index_map: HashMap<(usize, usize), usize>,
189 page_background_image_map: HashMap<usize, (usize, u32, u32)>,
194 page_background_url_cache: HashMap<String, (usize, u32, u32)>,
198 ext_gstate_map: HashMap<u64, (usize, String)>,
201 shading_map: HashMap<(usize, usize), (usize, String)>,
205 warnings: Vec<String>,
209 missing_glyphs: std::cell::RefCell<std::collections::BTreeSet<char>>,
217 pdf_version: crate::model::PdfVersion,
220}
221
222pub(crate) struct PdfObject {
223 #[allow(dead_code)]
224 pub(crate) id: usize,
225 pub(crate) data: Vec<u8>,
226}
227
228impl Default for PdfWriter {
229 fn default() -> Self {
230 Self::new()
231 }
232}
233
234impl PdfWriter {
235 pub fn new() -> Self {
236 Self
237 }
238
239 #[allow(clippy::too_many_arguments)]
261 pub fn write(
262 &self,
263 pages: &[LayoutPage],
264 metadata: &Metadata,
265 font_context: &FontContext,
266 tagged: bool,
267 pdfa: Option<&PdfAConformance>,
268 pdf_ua: bool,
269 embedded_data: Option<&str>,
270 attachments: &[Attachment],
271 zugferd: Option<&ZugferdMeta>,
272 flatten_forms: bool,
273 pdf_version: crate::model::PdfVersion,
274 pdf_ua2: bool,
275 ) -> Result<(Vec<u8>, Vec<String>), FormeError> {
276 if let Some(level) = pdfa {
284 if !level.allows_attachments() && (embedded_data.is_some() || !attachments.is_empty()) {
285 use crate::model::PdfAConformance as L;
286 let (family, clause, remedy) = match level {
287 L::A4 => ("PDF/A-4", "ISO 19005-4", "pdfa: \"4f\""),
288 _ => (
289 "PDF/A-2",
290 "ISO 19005-2, 6.8",
291 "a PDF/A-3 level — e.g. pdfa: \"3b\"",
292 ),
293 };
294 return Err(FormeError::RenderError(format!(
295 "{family} forbids embedded files that are not themselves PDF/A \
296 ({clause}), which the engine cannot verify. Use {remedy}, which \
297 permits arbitrary attachments, or remove the attachment / embedData."
298 )));
299 }
300 if matches!(level, crate::model::PdfAConformance::A4f)
305 && embedded_data.is_none()
306 && attachments.is_empty()
307 {
308 return Err(FormeError::RenderError(
309 "pdfa: \"4f\" requires at least one embedded file (ISO 19005-4, \
310 Annex A; veraPDF 6.9-t5 — the EmbeddedFiles name tree must exist). \
311 Add an attachment or embedData, or claim pdfa: \"4\" instead."
312 .to_string(),
313 ));
314 }
315 }
316 let zugferd_filename: Option<String> = if let Some(z) = zugferd {
320 const LEVELS: [&str; 6] = [
321 "MINIMUM",
322 "BASIC WL",
323 "BASIC",
324 "EN 16931",
325 "EXTENDED",
326 "XRECHNUNG",
327 ];
328 if !LEVELS.contains(&z.conformance_level.as_str()) {
329 return Err(FormeError::RenderError(format!(
330 "zugferd.conformanceLevel {:?} is not a Factur-X profile — expected one of \
331 MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, XRECHNUNG (exact spelling, \
332 spaces included).",
333 z.conformance_level
334 )));
335 }
336 if !pdfa.is_some_and(|l| l.allows_attachments()) {
337 return Err(FormeError::RenderError(
338 "Factur-X/ZUGFeRD (zugferd) requires a PDF/A-3 conformance level — set \
339 pdfa: \"3b\" (or \"3a\"/\"3u\"). The e-invoice XML is an embedded file, \
340 which only PDF/A-3 permits."
341 .to_string(),
342 ));
343 }
344 let filename = z.document_file_name.clone().unwrap_or_else(|| {
345 if z.conformance_level == "XRECHNUNG" {
346 "xrechnung.xml".to_string()
347 } else {
348 "factur-x.xml".to_string()
349 }
350 });
351 if !attachments.iter().any(|a| a.name == filename) {
352 return Err(FormeError::RenderError(format!(
353 "zugferd is set but no attachment is named {filename:?} — the XMP would \
354 point at a file that isn't embedded. Attach the invoice XML with name: \
355 {filename:?}, or set zugferd.documentFileName to the attachment's name."
356 )));
357 }
358 Some(filename)
359 } else {
360 None
361 };
362 let mut builder = PdfBuilder {
363 objects: Vec::new(),
364 font_objects: Vec::new(),
365 custom_font_data: HashMap::new(),
366 embedded_standard_fonts: std::collections::HashSet::new(),
367 image_objects: Vec::new(),
368 image_index_map: HashMap::new(),
369 page_background_image_map: HashMap::new(),
370 page_background_url_cache: HashMap::new(),
371 ext_gstate_map: HashMap::new(),
372 shading_map: HashMap::new(),
373 warnings: Vec::new(),
374 missing_glyphs: Default::default(),
375 pdf_version: Default::default(),
376 };
377
378 builder.objects.push(PdfObject {
384 id: 0,
385 data: vec![],
386 });
387 builder.objects.push(PdfObject {
388 id: 1,
389 data: vec![],
390 });
391 builder.objects.push(PdfObject {
392 id: 2,
393 data: vec![],
394 });
395
396 builder.pdf_version = pdf_version;
398 self.register_fonts(
399 &mut builder,
400 pages,
401 font_context,
402 pdf_ua,
403 pdfa.is_some(),
404 pdf_version,
405 )?;
406
407 if pdfa.is_some() {
413 for (key, _) in &builder.font_objects {
414 if !builder.custom_font_data.contains_key(key)
415 && !builder.embedded_standard_fonts.contains(key)
416 {
417 return Err(FormeError::RenderError(format!(
418 "PDF/A requires all fonts to be embedded, but '{}' is not. Register a \
419 metric-compatible font — install @formepdf/fonts-standard and register \
420 its fonts (`for (const f of standardFonts()) Font.register(f)`), or supply \
421 your own via Font.register().",
422 key.family
423 )));
424 }
425 }
426 }
427
428 self.register_images(&mut builder, pages);
430
431 self.register_page_background_images(&mut builder, pages);
435
436 self.register_ext_gstates(&mut builder, pages);
438
439 self.register_shadings(&mut builder, pages);
441
442 let mut tag_builder = if tagged {
445 Some(tagged::TagBuilder::new(pages.len(), pdf_ua2))
446 } else {
447 None
448 };
449
450 let mut page_obj_ids: Vec<usize> = Vec::new();
454 let mut all_bookmarks: Vec<PdfBookmark> = Vec::new();
455 let mut per_page_content_obj_ids: Vec<usize> = Vec::new();
456 let mut per_page_annotations: Vec<Vec<LinkAnnotation>> = Vec::new();
457 let mut per_page_resources: Vec<String> = Vec::new();
458 let mut all_form_fields: Vec<FormFieldData> = Vec::new();
459
460 for (page_idx, page) in pages.iter().enumerate() {
462 let content = self.build_content_stream_for_page(
463 page,
464 page_idx,
465 &builder,
466 page_idx + 1,
467 pages.len(),
468 tag_builder.as_mut(),
469 flatten_forms,
470 );
471 let compressed = compress_to_vec_zlib(content.as_bytes(), 6);
472
473 let content_obj_id = builder.objects.len();
474 let mut content_data: Vec<u8> = Vec::new();
475 let _ = write!(
476 content_data,
477 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
478 compressed.len()
479 );
480 content_data.extend_from_slice(&compressed);
481 content_data.extend_from_slice(b"\nendstream");
482 builder.objects.push(PdfObject {
483 id: content_obj_id,
484 data: content_data,
485 });
486 per_page_content_obj_ids.push(content_obj_id);
487
488 let mut annotations: Vec<LinkAnnotation> = Vec::new();
490 Self::collect_link_annotations(&page.elements, page.height, &mut annotations);
491 Self::warn_nested_links(&page.elements, None, &mut builder.warnings);
492 per_page_annotations.push(annotations);
493
494 Self::collect_form_fields(&page.elements, page.height, page_idx, &mut all_form_fields);
496
497 let page_obj_id = builder.objects.len();
499 builder.objects.push(PdfObject {
500 id: page_obj_id,
501 data: vec![],
502 });
503
504 let font_resources = self.build_font_resource_dict(&builder.font_objects);
506 let xobject_resources = self.build_xobject_resource_dict(page_idx, &builder);
507 let ext_gstate_resources = self.build_ext_gstate_resource_dict(&builder);
508 let shading_resources = self.build_shading_resource_dict(page_idx, &builder);
509 let mut resources = format!("/Font << {} >>", font_resources);
510 if !xobject_resources.is_empty() {
511 let _ = write!(resources, " /XObject << {} >>", xobject_resources);
512 }
513 if !ext_gstate_resources.is_empty() {
514 let _ = write!(resources, " /ExtGState << {} >>", ext_gstate_resources);
515 }
516 if !shading_resources.is_empty() {
517 let _ = write!(resources, " /Shading << {} >>", shading_resources);
518 }
519 per_page_resources.push(resources);
520
521 Self::collect_bookmarks(&page.elements, page.height, page_obj_id, &mut all_bookmarks);
523
524 page_obj_ids.push(page_obj_id);
525 }
526
527 for (page_idx, annotations) in per_page_annotations.iter().enumerate() {
529 let mut annot_obj_ids: Vec<usize> = Vec::new();
530 for annot in annotations {
531 let rect = format!(
532 "[{:.2} {:.2} {:.2} {:.2}]",
533 annot.x,
534 annot.y,
535 annot.x + annot.width,
536 annot.y + annot.height
537 );
538
539 if let Some(anchor) = annot.href.strip_prefix('#') {
540 if let Some(bm) = all_bookmarks.iter().find(|b| b.title == anchor) {
542 let annot_obj_id = builder.objects.len();
543 let sp_str = tag_builder
547 .as_mut()
548 .and_then(|tb| {
549 tb.connect_link_annotation(page_idx, &annot.href, annot_obj_id)
550 })
551 .map(|sp| format!(" /StructParent {}", sp))
552 .unwrap_or_default();
553 let contents = Self::encode_text_string(&format!("Link to {anchor}"));
556 let wants_sd = tag_builder
563 .as_mut()
564 .map(|tb| tb.request_struct_destination(anchor, annot_obj_id))
565 .unwrap_or(false);
566 let sd_str = if wants_sd {
567 format!(" /SD [999999999 0 R /XYZ 0 {:.2} null]", bm.y_pdf)
568 } else {
569 String::new()
570 };
571 let annot_dict = format!(
572 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
573 /F 4 /Contents {}{} \
574 /A << /S /GoTo /D [{} 0 R /XYZ 0 {:.2} null]{} >> >>",
575 rect, contents, sp_str, bm.page_obj_id, bm.y_pdf, sd_str
576 );
577 builder.objects.push(PdfObject {
578 id: annot_obj_id,
579 data: annot_dict.into_bytes(),
580 });
581 annot_obj_ids.push(annot_obj_id);
582 }
583 } else {
585 let annot_obj_id = builder.objects.len();
587 let sp_str = tag_builder
588 .as_mut()
589 .and_then(|tb| {
590 tb.connect_link_annotation(page_idx, &annot.href, annot_obj_id)
591 })
592 .map(|sp| format!(" /StructParent {}", sp))
593 .unwrap_or_default();
594 let contents = Self::encode_text_string(&annot.href);
598 let href_esc = Self::escape_pdf_string(&annot.href);
599 let annot_dict = format!(
600 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
601 /F 4 /Contents {}{} \
602 /A << /Type /Action /S /URI /URI ({}) >> >>",
603 rect, contents, sp_str, href_esc
604 );
605 builder.objects.push(PdfObject {
606 id: annot_obj_id,
607 data: annot_dict.into_bytes(),
608 });
609 annot_obj_ids.push(annot_obj_id);
610 }
611 }
612
613 let annots_str = if annot_obj_ids.is_empty() {
614 String::new()
615 } else {
616 let refs: String = annot_obj_ids
617 .iter()
618 .map(|id| format!("{} 0 R", id))
619 .collect::<Vec<_>>()
620 .join(" ");
621 format!(" /Annots [{}]", refs)
622 };
623
624 let page_obj_id = page_obj_ids[page_idx];
625 let content_obj_id = per_page_content_obj_ids[page_idx];
626 let struct_parents_str = if tagged {
627 format!(" /StructParents {} /Tabs /S", page_idx)
628 } else {
629 String::new()
630 };
631 let page_dict = format!(
632 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {:.2} {:.2}] \
633 /Contents {} 0 R /Resources << {} >>{}{} >>",
634 pages[page_idx].width,
635 pages[page_idx].height,
636 content_obj_id,
637 per_page_resources[page_idx],
638 annots_str,
639 struct_parents_str
640 );
641 builder.objects[page_obj_id].data = page_dict.into_bytes();
642 }
643
644 let outlines_obj_id = if !all_bookmarks.is_empty() {
646 Some(self.write_outline_tree(&mut builder, &all_bookmarks, tag_builder.as_mut()))
647 } else {
648 None
649 };
650
651 let struct_tree_root_id = if let Some(ref tb) = tag_builder {
653 let (root_id, _parent_tree_id, sd_patches) = tb.write_objects(
654 &mut builder.objects,
655 &page_obj_ids,
656 metadata.lang.as_deref(),
657 pdf_version == crate::model::PdfVersion::V2_0,
658 );
659 for (annot_obj_id, elem_obj_id) in sd_patches {
663 let data = std::mem::take(&mut builder.objects[annot_obj_id].data);
664 let patched = String::from_utf8(data)
665 .expect("annotation dicts are ASCII")
666 .replacen("999999999 0 R", &format!("{} 0 R", elem_obj_id), 1);
667 builder.objects[annot_obj_id].data = patched.into_bytes();
668 }
669 Some(root_id)
670 } else {
671 None
672 };
673
674 let xmp_metadata_id =
679 if pdfa.is_some() || pdf_ua || pdf_version == crate::model::PdfVersion::V2_0 {
680 let xmp_xml = xmp::generate_xmp(metadata, pdfa, pdf_ua, pdf_ua2, zugferd);
681 let xmp_bytes = xmp_xml.as_bytes();
682 let xmp_obj_id = builder.objects.len();
683 let xmp_data = format!(
685 "<< /Type /Metadata /Subtype /XML /Length {} >>\nstream\n",
686 xmp_bytes.len()
687 );
688 let mut xmp_obj_data: Vec<u8> = xmp_data.into_bytes();
689 xmp_obj_data.extend_from_slice(xmp_bytes);
690 xmp_obj_data.extend_from_slice(b"\nendstream");
691 builder.objects.push(PdfObject {
692 id: xmp_obj_id,
693 data: xmp_obj_data,
694 });
695 Some(xmp_obj_id)
696 } else {
697 None
698 };
699
700 let output_intent_id = if pdfa.is_some() {
701 static SRGB_ICC: &[u8] = include_bytes!("sRGB.icc");
703 let compressed_icc = compress_to_vec_zlib(SRGB_ICC, 6);
704
705 let icc_obj_id = builder.objects.len();
706 let mut icc_data: Vec<u8> = Vec::new();
707 let _ = write!(
708 icc_data,
709 "<< /N 3 /Length {} /Filter /FlateDecode >>\nstream\n",
710 compressed_icc.len()
711 );
712 icc_data.extend_from_slice(&compressed_icc);
713 icc_data.extend_from_slice(b"\nendstream");
714 builder.objects.push(PdfObject {
715 id: icc_obj_id,
716 data: icc_data,
717 });
718
719 let oi_obj_id = builder.objects.len();
721 let oi_data = format!(
722 "<< /Type /OutputIntent /S /GTS_PDFA1 \
723 /OutputConditionIdentifier (sRGB IEC61966-2.1) \
724 /RegistryName (http://www.color.org) \
725 /DestOutputProfile {} 0 R >>",
726 icc_obj_id
727 );
728 builder.objects.push(PdfObject {
729 id: oi_obj_id,
730 data: oi_data.into_bytes(),
731 });
732 Some(oi_obj_id)
733 } else {
734 None
735 };
736
737 let mut name_tree_entries: Vec<(String, usize)> = Vec::new();
744 let mut af_filespec_ids: Vec<usize> = Vec::new();
745 if let Some(data) = embedded_data {
746 let compressed = compress_to_vec_zlib(data.as_bytes(), 6);
747
748 let ef_obj_id = builder.objects.len();
750 let ef_data = format!(
751 "<< /Type /EmbeddedFile /Subtype /application#2Fjson /Length {} /Filter /FlateDecode >>\nstream\n",
752 compressed.len()
753 );
754 let mut ef_bytes = ef_data.into_bytes();
755 ef_bytes.extend_from_slice(&compressed);
756 ef_bytes.extend_from_slice(b"\nendstream");
757 builder.objects.push(PdfObject {
758 id: ef_obj_id,
759 data: ef_bytes,
760 });
761
762 let fs_obj_id = builder.objects.len();
764 let desc = if builder.pdf_version == crate::model::PdfVersion::V2_0 {
765 " /Desc (forme-data.json)"
767 } else {
768 ""
769 };
770 let fs_data = format!(
771 "<< /Type /Filespec /F (forme-data.json) /UF (forme-data.json) /EF << /F {} 0 R >> /AFRelationship /Data{} >>",
772 ef_obj_id, desc
773 );
774 builder.objects.push(PdfObject {
775 id: fs_obj_id,
776 data: fs_data.into_bytes(),
777 });
778 name_tree_entries.push(("forme-data.json".to_string(), fs_obj_id));
779 if pdfa.is_some_and(|l| l.allows_attachments()) {
782 af_filespec_ids.push(fs_obj_id);
783 }
784 }
785 for att in attachments {
786 let bytes = Self::decode_attachment_src(&att.src)?;
787 let compressed = compress_to_vec_zlib(&bytes, 6);
788 let mime = att
789 .mime_type
790 .as_deref()
791 .unwrap_or("application/octet-stream");
792 let mod_date = att
793 .mod_date
794 .as_deref()
795 .unwrap_or(DEFAULT_ATTACHMENT_MOD_DATE);
796
797 let ef_obj_id = builder.objects.len();
798 let ef_head = format!(
799 "<< /Type /EmbeddedFile /Subtype /{} /Length {} /Filter /FlateDecode \
800 /Params << /Size {} /ModDate ({}) >> >>\nstream\n",
801 Self::mime_to_pdf_name(mime),
802 compressed.len(),
803 bytes.len(),
804 Self::escape_pdf_string(mod_date),
805 );
806 let mut ef_bytes = ef_head.into_bytes();
807 ef_bytes.extend_from_slice(&compressed);
808 ef_bytes.extend_from_slice(b"\nendstream");
809 builder.objects.push(PdfObject {
810 id: ef_obj_id,
811 data: ef_bytes,
812 });
813
814 let relationship = att.relationship.unwrap_or_else(|| {
819 if zugferd_filename.as_deref() == Some(att.name.as_str()) {
820 match zugferd.map(|z| z.conformance_level.as_str()) {
821 Some("MINIMUM") | Some("BASIC WL") => AfRelationship::Data,
822 _ => AfRelationship::Alternative,
823 }
824 } else {
825 AfRelationship::Unspecified
826 }
827 });
828
829 let fs_obj_id = builder.objects.len();
830 let mut fs_data = format!(
831 "<< /Type /Filespec /F ({name}) /UF {uf} /EF << /F {ef} 0 R >> /AFRelationship /{rel}",
832 name = Self::escape_pdf_string(&att.name),
835 uf = Self::encode_text_string(&att.name),
836 ef = ef_obj_id,
837 rel = relationship.pdf_name(),
838 );
839 if let Some(desc) = &att.description {
840 let _ = write!(fs_data, " /Desc {}", Self::encode_text_string(desc));
841 } else if builder.pdf_version == crate::model::PdfVersion::V2_0 {
842 let _ = write!(fs_data, " /Desc {}", Self::encode_text_string(&att.name));
847 }
848 fs_data.push_str(" >>");
849 builder.objects.push(PdfObject {
850 id: fs_obj_id,
851 data: fs_data.into_bytes(),
852 });
853 name_tree_entries.push((att.name.clone(), fs_obj_id));
854 af_filespec_ids.push(fs_obj_id);
855 }
856 let embedded_names_id = if name_tree_entries.is_empty() {
857 None
858 } else {
859 name_tree_entries.sort_by(|a, b| a.0.cmp(&b.0));
861 let names_obj_id = builder.objects.len();
862 let pairs = name_tree_entries
863 .iter()
864 .map(|(name, id)| format!("({}) {} 0 R", Self::escape_pdf_string(name), id))
865 .collect::<Vec<_>>()
866 .join(" ");
867 let names_data = format!("<< /Names [{}] >>", pairs);
868 builder.objects.push(PdfObject {
869 id: names_obj_id,
870 data: names_data.into_bytes(),
871 });
872 Some(names_obj_id)
873 };
874
875 let acroform_obj_id = if !all_form_fields.is_empty() && !flatten_forms {
877 let helv_obj_id = builder
879 .font_objects
880 .iter()
881 .find(|(key, _)| key.family == "Helvetica" && key.weight == 400 && !key.italic)
882 .map(|(_, id)| *id);
883
884 let mut radio_groups: HashMap<String, Vec<usize>> = HashMap::new(); let mut non_radio_indices: Vec<usize> = Vec::new();
887 for (i, field) in all_form_fields.iter().enumerate() {
888 if matches!(field.field_type, FormFieldType::RadioButton { .. }) {
889 radio_groups.entry(field.name.clone()).or_default().push(i);
890 } else {
891 non_radio_indices.push(i);
892 }
893 }
894
895 let mut radio_parent_ids: HashMap<String, usize> = HashMap::new();
897 for group_name in radio_groups.keys() {
898 let parent_id = builder.objects.len();
899 builder.objects.push(PdfObject {
900 id: parent_id,
901 data: vec![], });
903 radio_parent_ids.insert(group_name.clone(), parent_id);
904 }
905
906 let checkbox_yes_stream_id = builder.objects.len();
909 {
910 let stream_content =
911 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";
912 let mut data: Vec<u8> = Vec::new();
913 let _ = write!(
914 data,
915 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
916 stream_content.len()
917 );
918 data.extend_from_slice(stream_content);
919 data.extend_from_slice(b"\nendstream");
920 builder.objects.push(PdfObject {
921 id: checkbox_yes_stream_id,
922 data,
923 });
924 }
925 let checkbox_off_stream_id = builder.objects.len();
927 {
928 let stream_content = b"";
929 let mut data: Vec<u8> = Vec::new();
930 let _ = write!(
931 data,
932 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
933 stream_content.len()
934 );
935 data.extend_from_slice(stream_content);
936 data.extend_from_slice(b"\nendstream");
937 builder.objects.push(PdfObject {
938 id: checkbox_off_stream_id,
939 data,
940 });
941 }
942 let radio_on_stream_id = builder.objects.len();
944 {
945 let k = 2.761; let stream_content = format!(
948 "0.2 0.2 0.2 rg\n\
949 7 12 m {:.2} 12 12 {:.2} 12 7 c\n\
950 12 {:.2} {:.2} 2 7 2 c\n\
951 {:.2} 2 2 {:.2} 2 7 c\n\
952 2 {:.2} {:.2} 12 7 12 c f\n",
953 7.0 + k,
954 7.0 + k, 7.0 - k,
956 7.0 - k, 7.0 - k,
958 7.0 - k, 7.0 + k,
960 7.0 + k, );
962 let stream_bytes = stream_content.as_bytes();
963 let mut data: Vec<u8> = Vec::new();
964 let _ = write!(
965 data,
966 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
967 stream_bytes.len()
968 );
969 data.extend_from_slice(stream_bytes);
970 data.extend_from_slice(b"\nendstream");
971 builder.objects.push(PdfObject {
972 id: radio_on_stream_id,
973 data,
974 });
975 }
976 let radio_off_stream_id = builder.objects.len();
978 {
979 let stream_content = b"";
980 let mut data: Vec<u8> = Vec::new();
981 let _ = write!(
982 data,
983 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
984 stream_content.len()
985 );
986 data.extend_from_slice(stream_content);
987 data.extend_from_slice(b"\nendstream");
988 builder.objects.push(PdfObject {
989 id: radio_off_stream_id,
990 data,
991 });
992 }
993
994 let mut acroform_field_ids: Vec<usize> = Vec::new();
996 let mut per_page_widget_ids: Vec<Vec<usize>> = vec![Vec::new(); pages.len()];
997 let mut radio_kid_ids: HashMap<String, Vec<usize>> = HashMap::new();
998
999 for field in all_form_fields.iter() {
1000 let rect = format!(
1001 "[{:.2} {:.2} {:.2} {:.2}]",
1002 field.x,
1003 field.y,
1004 field.x + field.width,
1005 field.y + field.height
1006 );
1007 let page_ref = format!("{} 0 R", page_obj_ids[field.page_idx]);
1008
1009 match &field.field_type {
1010 FormFieldType::TextField {
1011 value,
1012 multiline,
1013 password,
1014 read_only,
1015 max_length,
1016 font_size,
1017 ..
1018 } => {
1019 let mut flags: u32 = 0;
1020 if *multiline {
1021 flags |= 1 << 12; }
1023 if *password {
1024 flags |= 1 << 13; }
1026 if *read_only {
1027 flags |= 1; }
1029 let da = if let Some(helv_id) = helv_obj_id {
1030 let _ = helv_id; format!("/Helv {} Tf 0 g", font_size)
1032 } else {
1033 format!("/Helv {} Tf 0 g", font_size)
1034 };
1035 let v_str = if let Some(ref v) = value {
1036 format!(
1037 " /V {} /DV {}",
1038 Self::encode_text_string(v),
1039 Self::encode_text_string(v)
1040 )
1041 } else {
1042 String::new()
1043 };
1044 let max_len_str = if let Some(ml) = max_length {
1045 format!(" /MaxLen {}", ml)
1046 } else {
1047 String::new()
1048 };
1049 let ap_w = field.width;
1051 let ap_h = field.height;
1052 let text_y = if *multiline {
1053 ap_h - *font_size - 2.0
1054 } else {
1055 (ap_h - *font_size) / 2.0
1056 };
1057 let ap_content = if let Some(ref v) = value {
1058 format!(
1059 "1 1 1 rg 0 0 {} {} re f \
1060 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S \
1061 BT /Helv {} Tf 0 g 2 {} Td ({}) Tj ET",
1062 ap_w,
1063 ap_h,
1064 ap_w,
1065 ap_h,
1066 font_size,
1067 text_y,
1068 Self::escape_pdf_string(v)
1069 )
1070 } else {
1071 format!(
1072 "1 1 1 rg 0 0 {} {} re f \
1073 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S",
1074 ap_w, ap_h, ap_w, ap_h
1075 )
1076 };
1077 let ap_stream_id = builder.objects.len();
1078 let ap_stream = format!(
1079 "<< /Type /XObject /Subtype /Form /BBox [0 0 {} {}] \
1080 /Resources << /Font << /Helv {} 0 R >> >> /Length {} >>\nstream\n{}\nendstream",
1081 ap_w, ap_h,
1082 helv_obj_id.unwrap_or(0),
1083 ap_content.len(),
1084 ap_content
1085 );
1086 builder.objects.push(PdfObject {
1087 id: ap_stream_id,
1088 data: ap_stream.into_bytes(),
1089 });
1090
1091 let widget_obj_id = builder.objects.len();
1092 let widget_dict = format!(
1093 "<< /Type /Annot /Subtype /Widget /FT /Tx \
1094 /T {} /Rect {} /P {}\
1095 {} /DA ({}) /Ff {}{} \
1096 /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> \
1097 /AP << /N {} 0 R >> >>",
1098 Self::encode_text_string(&field.name),
1099 rect,
1100 page_ref,
1101 v_str,
1102 da,
1103 flags,
1104 max_len_str,
1105 ap_stream_id
1106 );
1107 builder.objects.push(PdfObject {
1108 id: widget_obj_id,
1109 data: widget_dict.into_bytes(),
1110 });
1111 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1112 acroform_field_ids.push(widget_obj_id);
1113 }
1114
1115 FormFieldType::Checkbox {
1116 checked, read_only, ..
1117 } => {
1118 let state = if *checked { "Yes" } else { "Off" };
1119 let mut flags: u32 = 0;
1120 if *read_only {
1121 flags |= 1;
1122 }
1123 let ff_str = if flags > 0 {
1124 format!(" /Ff {}", flags)
1125 } else {
1126 String::new()
1127 };
1128 let widget_obj_id = builder.objects.len();
1129 let widget_dict = format!(
1130 "<< /Type /Annot /Subtype /Widget /FT /Btn \
1131 /T {} /Rect {} /P {} \
1132 /V /{} /AS /{}{} \
1133 /MK << /BC [0.6 0.6 0.6] /CA (4) >> \
1134 /AP << /N << /Yes {} 0 R /Off {} 0 R >> >> >>",
1135 Self::encode_text_string(&field.name),
1136 rect,
1137 page_ref,
1138 state,
1139 state,
1140 ff_str,
1141 checkbox_yes_stream_id,
1142 checkbox_off_stream_id,
1143 );
1144 builder.objects.push(PdfObject {
1145 id: widget_obj_id,
1146 data: widget_dict.into_bytes(),
1147 });
1148 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1149 acroform_field_ids.push(widget_obj_id);
1150 }
1151
1152 FormFieldType::Dropdown {
1153 options,
1154 value,
1155 read_only,
1156 font_size,
1157 ..
1158 } => {
1159 let mut flags: u32 = 1 << 17; if *read_only {
1161 flags |= 1;
1162 }
1163 let opts_str: String = options
1164 .iter()
1165 .map(|o| Self::encode_text_string(o))
1166 .collect::<Vec<_>>()
1167 .join(" ");
1168 let v_str = if let Some(ref v) = value {
1169 format!(" /V {}", Self::encode_text_string(v))
1170 } else {
1171 String::new()
1172 };
1173 let ap_w = field.width;
1175 let ap_h = field.height;
1176 let text_y = (ap_h - *font_size) / 2.0;
1177 let ap_content = if let Some(ref v) = value {
1178 format!(
1179 "1 1 1 rg 0 0 {} {} re f \
1180 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S \
1181 BT /Helv {} Tf 0 g 2 {} Td ({}) Tj ET",
1182 ap_w,
1183 ap_h,
1184 ap_w,
1185 ap_h,
1186 font_size,
1187 text_y,
1188 Self::escape_pdf_string(v)
1189 )
1190 } else {
1191 format!(
1192 "1 1 1 rg 0 0 {} {} re f \
1193 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S",
1194 ap_w, ap_h, ap_w, ap_h
1195 )
1196 };
1197 let ap_stream_id = builder.objects.len();
1198 let ap_stream = format!(
1199 "<< /Type /XObject /Subtype /Form /BBox [0 0 {} {}] \
1200 /Resources << /Font << /Helv {} 0 R >> >> /Length {} >>\nstream\n{}\nendstream",
1201 ap_w, ap_h,
1202 helv_obj_id.unwrap_or(0),
1203 ap_content.len(),
1204 ap_content
1205 );
1206 builder.objects.push(PdfObject {
1207 id: ap_stream_id,
1208 data: ap_stream.into_bytes(),
1209 });
1210
1211 let widget_obj_id = builder.objects.len();
1212 let widget_dict = format!(
1213 "<< /Type /Annot /Subtype /Widget /FT /Ch \
1214 /T {} /Rect {} /P {} \
1215 /Opt [{}]{} \
1216 /DA (/Helv {} Tf 0 g) /Ff {} \
1217 /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> \
1218 /AP << /N {} 0 R >> >>",
1219 Self::encode_text_string(&field.name),
1220 rect,
1221 page_ref,
1222 opts_str,
1223 v_str,
1224 font_size,
1225 flags,
1226 ap_stream_id
1227 );
1228 builder.objects.push(PdfObject {
1229 id: widget_obj_id,
1230 data: widget_dict.into_bytes(),
1231 });
1232 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1233 acroform_field_ids.push(widget_obj_id);
1234 }
1235
1236 FormFieldType::RadioButton {
1237 value,
1238 checked,
1239 read_only: _,
1240 } => {
1241 let parent_id = radio_parent_ids[&field.name];
1243 let as_value = if *checked { value.as_str() } else { "Off" };
1244 let widget_obj_id = builder.objects.len();
1245 let widget_dict = format!(
1246 "<< /Type /Annot /Subtype /Widget \
1247 /Parent {} 0 R \
1248 /Rect {} /P {} \
1249 /AS /{} \
1250 /AP << /N << /{} {} 0 R /Off {} 0 R >> >> \
1251 /MK << /BC [0.6 0.6 0.6] >> >>",
1252 parent_id,
1253 rect,
1254 page_ref,
1255 Self::escape_pdf_string(as_value),
1256 Self::escape_pdf_string(value),
1257 radio_on_stream_id,
1258 radio_off_stream_id,
1259 );
1260 builder.objects.push(PdfObject {
1261 id: widget_obj_id,
1262 data: widget_dict.into_bytes(),
1263 });
1264 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1265 radio_kid_ids
1267 .entry(field.name.clone())
1268 .or_default()
1269 .push(widget_obj_id);
1270 }
1271 }
1272 }
1273
1274 for (group_name, kid_indices) in &radio_kid_ids {
1276 let parent_id = radio_parent_ids[group_name];
1277 let checked_value = all_form_fields
1279 .iter()
1280 .filter(|f| f.name == *group_name)
1281 .find_map(|f| {
1282 if let FormFieldType::RadioButton {
1283 ref value, checked, ..
1284 } = f.field_type
1285 {
1286 if checked {
1287 Some(value.clone())
1288 } else {
1289 None
1290 }
1291 } else {
1292 None
1293 }
1294 })
1295 .unwrap_or_else(|| "Off".to_string());
1296
1297 let kids_refs: String = kid_indices
1298 .iter()
1299 .map(|id| format!("{} 0 R", id))
1300 .collect::<Vec<_>>()
1301 .join(" ");
1302
1303 let mut flags: u32 = (1 << 14) | (1 << 15); let is_read_only = all_form_fields
1306 .iter()
1307 .filter(|f| f.name == *group_name)
1308 .any(|f| {
1309 matches!(
1310 f.field_type,
1311 FormFieldType::RadioButton {
1312 read_only: true,
1313 ..
1314 }
1315 )
1316 });
1317 if is_read_only {
1318 flags |= 1;
1319 }
1320
1321 let parent_dict = format!(
1322 "<< /FT /Btn /T {} /Ff {} /Kids [{}] /V /{} >>",
1323 Self::encode_text_string(group_name),
1324 flags,
1325 kids_refs,
1326 Self::escape_pdf_string(&checked_value),
1327 );
1328 builder.objects[parent_id].data = parent_dict.into_bytes();
1329 acroform_field_ids.push(parent_id);
1330 }
1331
1332 for (page_idx, widget_ids) in per_page_widget_ids.iter().enumerate() {
1336 if widget_ids.is_empty() {
1337 continue;
1338 }
1339 let page_obj_id = page_obj_ids[page_idx];
1340 let existing_page_data =
1341 String::from_utf8_lossy(&builder.objects[page_obj_id].data).to_string();
1342
1343 let new_refs: String = widget_ids
1345 .iter()
1346 .map(|id| format!("{} 0 R", id))
1347 .collect::<Vec<_>>()
1348 .join(" ");
1349
1350 let updated = if let Some(pos) = existing_page_data.find("/Annots [") {
1351 let bracket_end = existing_page_data[pos..].find(']').unwrap() + pos;
1353 format!(
1354 "{} {}{}",
1355 &existing_page_data[..bracket_end],
1356 new_refs,
1357 &existing_page_data[bracket_end..]
1358 )
1359 } else {
1360 let end = existing_page_data.rfind(">>").unwrap();
1362 format!(
1363 "{} /Annots [{}]{}",
1364 &existing_page_data[..end],
1365 new_refs,
1366 &existing_page_data[end..]
1367 )
1368 };
1369 builder.objects[page_obj_id].data = updated.into_bytes();
1370 }
1371
1372 let acroform_id = builder.objects.len();
1374 let fields_refs: String = acroform_field_ids
1375 .iter()
1376 .map(|id| format!("{} 0 R", id))
1377 .collect::<Vec<_>>()
1378 .join(" ");
1379 let dr_str = if let Some(helv_id) = helv_obj_id {
1380 format!(" /DR << /Font << /Helv {} 0 R >> >>", helv_id)
1381 } else {
1382 String::new()
1383 };
1384 let acroform_dict = format!(
1391 "<< /Fields [{}]{} /DA (/Helv 0 Tf 0 g) >>",
1392 fields_refs, dr_str
1393 );
1394 builder.objects.push(PdfObject {
1395 id: acroform_id,
1396 data: acroform_dict.into_bytes(),
1397 });
1398 Some(acroform_id)
1399 } else {
1400 None
1401 };
1402
1403 let mut catalog = String::from("<< /Type /Catalog /Pages 2 0 R");
1405 if let Some(acroform_id) = acroform_obj_id {
1406 write!(catalog, " /AcroForm {} 0 R", acroform_id).unwrap();
1407 }
1408 if let Some(outlines_id) = outlines_obj_id {
1409 write!(
1410 catalog,
1411 " /Outlines {} 0 R /PageMode /UseOutlines",
1412 outlines_id
1413 )
1414 .unwrap();
1415 }
1416 if let Some(ref lang) = metadata.lang {
1417 write!(catalog, " /Lang ({})", Self::escape_pdf_string(lang)).unwrap();
1418 }
1419 if let Some(struct_root_id) = struct_tree_root_id {
1420 write!(
1421 catalog,
1422 " /MarkInfo << /Marked true >> /StructTreeRoot {} 0 R",
1423 struct_root_id
1424 )
1425 .unwrap();
1426 }
1427 if let Some(xmp_id) = xmp_metadata_id {
1428 write!(catalog, " /Metadata {} 0 R", xmp_id).unwrap();
1429 }
1430 if let Some(oi_id) = output_intent_id {
1431 write!(catalog, " /OutputIntents [{} 0 R]", oi_id).unwrap();
1432 }
1433 if let Some(names_id) = embedded_names_id {
1434 write!(catalog, " /Names << /EmbeddedFiles {} 0 R >>", names_id).unwrap();
1435 }
1436 if !af_filespec_ids.is_empty() {
1437 let refs = af_filespec_ids
1440 .iter()
1441 .map(|id| format!("{} 0 R", id))
1442 .collect::<Vec<_>>()
1443 .join(" ");
1444 write!(catalog, " /AF [{}]", refs).unwrap();
1445 }
1446 if pdf_ua || pdf_ua2 {
1447 catalog.push_str(" /ViewerPreferences << /DisplayDocTitle true >>");
1448 }
1449 catalog.push_str(" >>");
1450 builder.objects[1].data = catalog.into_bytes();
1451
1452 let kids: String = page_obj_ids
1454 .iter()
1455 .map(|id| format!("{} 0 R", id))
1456 .collect::<Vec<_>>()
1457 .join(" ");
1458 builder.objects[2].data = format!(
1459 "<< /Type /Pages /Kids [{}] /Count {} >>",
1460 kids,
1461 page_obj_ids.len()
1462 )
1463 .into_bytes();
1464
1465 let info_obj_id = if pdf_version == crate::model::PdfVersion::V1_7
1473 && (metadata.title.is_some() || metadata.author.is_some() || metadata.subject.is_some())
1474 {
1475 let id = builder.objects.len();
1476 let mut info = String::from("<< ");
1477 if let Some(ref title) = metadata.title {
1478 let _ = write!(info, "/Title {} ", Self::encode_text_string(title));
1479 }
1480 if let Some(ref author) = metadata.author {
1481 let _ = write!(info, "/Author {} ", Self::encode_text_string(author));
1482 }
1483 if let Some(ref subject) = metadata.subject {
1484 let _ = write!(info, "/Subject {} ", Self::encode_text_string(subject));
1485 }
1486 let _ = write!(
1487 info,
1488 "/Producer {} /Creator {} >>",
1489 Self::encode_text_string(PRODUCER),
1490 Self::encode_text_string(CREATOR_TOOL)
1491 );
1492 builder.objects.push(PdfObject {
1493 id,
1494 data: info.into_bytes(),
1495 });
1496 Some(id)
1497 } else {
1498 None
1499 };
1500
1501 let pdf = self.serialize(&builder, info_obj_id);
1502 let mut warnings = builder.warnings;
1507 for ch in builder.missing_glyphs.into_inner() {
1508 warnings.push(format!(
1509 "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)",
1510 ch as u32
1511 ));
1512 }
1513 Ok((pdf, warnings))
1514 }
1515
1516 #[allow(clippy::too_many_arguments)]
1518 fn build_content_stream_for_page(
1519 &self,
1520 page: &LayoutPage,
1521 page_idx: usize,
1522 builder: &PdfBuilder,
1523 page_number: usize,
1524 total_pages: usize,
1525 mut tag_builder: Option<&mut tagged::TagBuilder>,
1526 flatten_forms: bool,
1527 ) -> String {
1528 let mut stream = String::new();
1529 let page_height = page.height;
1530 let mut element_counter = 0usize;
1531 let mut gradient_counter = 0usize;
1532
1533 if let Some(&img_idx) = builder.page_background_image_map.get(&page_idx) {
1539 self.write_page_background(&mut stream, page, img_idx, builder);
1540 }
1541
1542 let clip_x = page.config.clip_content_x;
1546 if clip_x {
1547 let x = page.config.margin.left;
1548 let w = page.width - page.config.margin.left - page.config.margin.right;
1549 stream.push_str(&format!(
1550 "q\n{:.2} 0 {:.2} {:.2} re W n\n",
1551 x, w, page.height
1552 ));
1553 }
1554
1555 for element in &page.elements {
1556 self.write_element(
1557 &mut stream,
1558 element,
1559 page_height,
1560 builder,
1561 page_idx,
1562 &mut element_counter,
1563 &mut gradient_counter,
1564 page_number,
1565 total_pages,
1566 tag_builder.as_deref_mut(),
1567 flatten_forms,
1568 );
1569 }
1570
1571 if clip_x {
1572 stream.push_str("Q\n");
1573 }
1574
1575 stream
1576 }
1577
1578 #[allow(clippy::too_many_arguments)]
1580 #[allow(clippy::too_many_arguments)]
1581 fn write_element(
1582 &self,
1583 stream: &mut String,
1584 element: &LayoutElement,
1585 page_height: f64,
1586 builder: &PdfBuilder,
1587 page_idx: usize,
1588 element_counter: &mut usize,
1589 gradient_counter: &mut usize,
1590 page_number: usize,
1591 total_pages: usize,
1592 mut tag_builder: Option<&mut tagged::TagBuilder>,
1593 flatten_forms: bool,
1594 ) {
1595 let mut is_artifact = false;
1598 let mut artifact_own_draw = false;
1603 let mut inline_links: std::collections::VecDeque<usize> = std::collections::VecDeque::new();
1607 let mut bdc_role: Option<&'static str> = None;
1608 let tagged_mcid = if let Some(ref mut tb) = tag_builder {
1609 if let Some(ref nt) = element.node_type {
1610 if nt == "Watermark" {
1611 let _ = writeln!(stream, "/Artifact BMC");
1613 is_artifact = true;
1614 None
1615 } else {
1616 let is_header = element.is_header_row;
1617 let href = element.href.as_deref();
1618 let wrapper = !element.children.is_empty()
1622 && matches!(element.draw, DrawCommand::None | DrawCommand::Rect { .. });
1623 let mcid = tb.begin_element_as(
1624 nt,
1625 is_header,
1626 element.alt.as_deref(),
1627 page_idx,
1628 href,
1629 element.col_span,
1630 element.list_numbering,
1631 element.actual_text.as_deref(),
1632 wrapper,
1633 );
1634 if let Some(ref bm) = element.bookmark {
1638 tb.note_bookmark(bm);
1639 }
1640 if href.is_none() && !tb.inside_link() {
1648 if let DrawCommand::Text { ref lines, .. } = element.draw {
1649 for line in lines {
1650 for span in Self::inline_link_spans(line) {
1651 inline_links
1652 .push_back(tb.add_inline_link(page_idx, &span.href));
1653 }
1654 }
1655 }
1656 }
1657 match mcid {
1658 Some(mcid) => {
1659 let role = if href.is_some() {
1662 "Link"
1663 } else {
1664 tb.map_role_public(nt, is_header)
1665 };
1666 let _ = writeln!(stream, "/{} <</MCID {}>> BDC", role, mcid);
1667 bdc_role = Some(role);
1668 Some(mcid)
1669 }
1670 None => {
1671 artifact_own_draw = true;
1672 None
1673 }
1674 }
1675 }
1676 } else if !matches!(element.draw, DrawCommand::None) {
1677 let _ = writeln!(stream, "/Artifact BMC");
1679 is_artifact = true;
1680 None
1681 } else {
1682 None
1683 }
1684 } else {
1685 None
1686 };
1687
1688 let needs_element_opacity = element.opacity < 1.0;
1696 if needs_element_opacity {
1697 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&element.opacity.to_bits()) {
1698 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1699 }
1700 }
1701
1702 let transform_ops: &[TransformOp] = element
1708 .resolved_style
1709 .as_ref()
1710 .map(|s| s.transform.as_slice())
1711 .unwrap_or(&[]);
1712 let has_transform = !transform_ops.is_empty();
1713 if has_transform {
1714 let rs = element.resolved_style.as_ref().unwrap();
1715 let pdf_x = element.x;
1716 let pdf_y_bottom = page_height - element.y - element.height;
1717 let (ox_frac, oy_frac) = rs.transform_origin;
1718 let origin_x = pdf_x + element.width * ox_frac;
1719 let origin_y = pdf_y_bottom + (1.0 - oy_frac) * element.height;
1722
1723 let _ = writeln!(stream, "q");
1724 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", -origin_x, -origin_y);
1726 for op in transform_ops.iter().rev() {
1734 match op {
1735 TransformOp::Rotate { deg } => {
1736 let theta = (-deg).to_radians();
1741 let c = theta.cos();
1742 let s = theta.sin();
1743 let _ = writeln!(stream, "{:.6} {:.6} {:.6} {:.6} 0 0 cm", c, s, -s, c);
1744 }
1745 TransformOp::Scale { x, y } => {
1746 let _ = writeln!(stream, "{:.6} 0 0 {:.6} 0 0 cm", x, y);
1747 }
1748 TransformOp::Translate { x, y } => {
1749 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", x, -y);
1751 }
1752 }
1753 }
1754 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", origin_x, origin_y);
1756 }
1757
1758 let wrap_own_draw_as_artifact =
1765 artifact_own_draw && !matches!(element.draw, DrawCommand::None);
1766 if wrap_own_draw_as_artifact {
1767 let _ = writeln!(stream, "/Artifact BMC");
1768 }
1769
1770 match &element.draw {
1771 DrawCommand::None => {}
1772
1773 DrawCommand::Rect {
1774 background,
1775 border_width,
1776 border_color,
1777 border_style,
1778 border_radius,
1779 opacity,
1780 box_shadow,
1781 background_gradient,
1782 } => {
1783 let x = element.x;
1784 let y = page_height - element.y - element.height;
1785 let w = element.width;
1786 let h = element.height;
1787
1788 let needs_opacity = *opacity < 1.0;
1790 if needs_opacity {
1791 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1792 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1793 }
1794 }
1795
1796 if let Some(shadow) = box_shadow {
1802 if shadow.color.a > 0.0 {
1803 let sx = x + shadow.offset_x;
1808 let sy = y - shadow.offset_y;
1809 let needs_shadow_alpha = shadow.color.a < 1.0;
1810 if needs_shadow_alpha {
1811 if let Some((_, gs_name)) =
1812 builder.ext_gstate_map.get(&shadow.color.a.to_bits())
1813 {
1814 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1815 } else {
1816 let _ = writeln!(stream, "q");
1817 }
1818 } else {
1819 let _ = writeln!(stream, "q");
1820 }
1821 let _ = writeln!(
1822 stream,
1823 "{:.3} {:.3} {:.3} rg",
1824 shadow.color.r, shadow.color.g, shadow.color.b
1825 );
1826 if border_radius.top_left > 0.0
1827 || border_radius.top_right > 0.0
1828 || border_radius.bottom_right > 0.0
1829 || border_radius.bottom_left > 0.0
1830 {
1831 self.write_rounded_rect(stream, sx, sy, w, h, border_radius);
1832 } else {
1833 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", sx, sy, w, h);
1834 }
1835 let _ = writeln!(stream, "f\nQ");
1836 }
1837 }
1838
1839 if background_gradient.is_some() {
1845 let key = (page_idx, *gradient_counter);
1846 *gradient_counter += 1;
1847 if let Some((_, sh_name)) = builder.shading_map.get(&key) {
1848 let _ = writeln!(stream, "q");
1849 if border_radius.top_left > 0.0
1851 || border_radius.top_right > 0.0
1852 || border_radius.bottom_right > 0.0
1853 || border_radius.bottom_left > 0.0
1854 {
1855 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1856 let _ = writeln!(stream, "W n");
1857 } else {
1858 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re W n", x, y, w, h);
1859 }
1860 let _ =
1863 writeln!(stream, "1 0 0 1 {:.3} {:.3} cm\n/{} sh\nQ", x, y, sh_name);
1864 }
1865 } else if let Some(bg) = background {
1866 if bg.a > 0.0 {
1867 let _ = writeln!(stream, "q\n{:.3} {:.3} {:.3} rg", bg.r, bg.g, bg.b);
1868
1869 if border_radius.top_left > 0.0 {
1870 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1871 } else {
1872 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1873 }
1874
1875 let _ = writeln!(stream, "f\nQ");
1876 }
1877 }
1878
1879 let bw = border_width;
1880 if bw.top > 0.0 || bw.right > 0.0 || bw.bottom > 0.0 || bw.left > 0.0 {
1881 use crate::style::BorderStyle::Solid;
1882 let all_solid = border_style.top == Solid
1883 && border_style.right == Solid
1884 && border_style.bottom == Solid
1885 && border_style.left == Solid;
1886 if all_solid
1892 && (bw.top - bw.right).abs() < 0.001
1893 && (bw.right - bw.bottom).abs() < 0.001
1894 && (bw.bottom - bw.left).abs() < 0.001
1895 {
1896 let bc = &border_color.top;
1897 let _ = writeln!(
1898 stream,
1899 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w",
1900 bc.r, bc.g, bc.b, bw.top
1901 );
1902
1903 if border_radius.top_left > 0.0 {
1904 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1905 } else {
1906 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1907 }
1908
1909 let _ = writeln!(stream, "S\nQ");
1910 } else {
1911 self.write_border_sides(stream, x, y, w, h, bw, border_color, border_style);
1912 }
1913 }
1914
1915 if needs_opacity {
1916 let _ = writeln!(stream, "Q");
1917 }
1918 }
1919
1920 DrawCommand::Text {
1921 lines,
1922 color,
1923 text_decoration,
1924 opacity,
1925 } => {
1926 let needs_opacity = *opacity < 1.0;
1928 if needs_opacity {
1929 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1930 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1931 }
1932 }
1933
1934 let mut tw_set = false;
1940 let tag_links = bdc_role.is_some() && !inline_links.is_empty();
1947 for line in lines {
1948 if line.glyphs.is_empty() {
1949 continue;
1950 }
1951
1952 let groups = Self::group_glyphs(&line.glyphs, tag_links);
1955 let group_links = if tag_links {
1956 Self::group_link_runs(&groups)
1957 } else {
1958 vec![None; groups.len()]
1959 };
1960 let mut open_link: Option<&str> = None;
1961 let pdf_y = page_height - line.y;
1962
1963 let _ = writeln!(stream, "BT");
1964
1965 if line.word_spacing.abs() > 0.001 {
1967 let _ = writeln!(stream, "{:.4} Tw", line.word_spacing);
1968 tw_set = true;
1969 } else if tw_set {
1970 let _ = writeln!(stream, "0 Tw");
1971 tw_set = false;
1972 }
1973
1974 let mut tm_x = 0.0_f64;
1976 let mut tm_y = 0.0_f64;
1977 let mut x_cursor = line.x;
1978
1979 let mut group_spans: Vec<(f64, f64, TextDecoration, Color)> = Vec::new();
1981
1982 for (gi, group) in groups.iter().enumerate() {
1983 let want = group_links[gi];
1984 if want != open_link {
1985 let _ = writeln!(stream, "ET\nEMC");
1986 let role = bdc_role.unwrap_or("Span");
1987 let tb = tag_builder.as_mut().expect("tag_links implies tagging");
1988 let link = want.and_then(|_| inline_links.pop_front());
1989 match link {
1990 Some(idx) => {
1991 let mcid = tb.attach_inline_link(idx, page_idx);
1992 let _ = writeln!(stream, "/Link <</MCID {}>> BDC", mcid);
1993 open_link = want;
1994 }
1995 None => {
1996 let mcid = tb.continue_current(page_idx);
1997 let _ = writeln!(stream, "/{} <</MCID {}>> BDC", role, mcid);
1998 open_link = None;
1999 }
2000 }
2001 let _ = writeln!(stream, "BT");
2003 tm_x = 0.0;
2004 tm_y = 0.0;
2005 }
2006 let first = &group[0];
2007 let glyph_color = first.color.unwrap_or(*color);
2008
2009 let idx = self.font_index(
2010 &first.font_family,
2011 first.font_weight,
2012 first.font_style,
2013 &builder.font_objects,
2014 );
2015 let italic =
2016 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
2017 let font_key = FontKey {
2018 family: first.font_family.to_string(),
2019 weight: first.font_weight,
2020 italic,
2021 };
2022 let font_name = format!("F{}", idx);
2023
2024 if builder.custom_font_data.contains_key(&font_key)
2028 && !has_placeholder_group(group)
2029 {
2030 x_cursor = line.x + first.x_offset;
2031 }
2032 let dx = x_cursor - tm_x;
2034 let dy = pdf_y - tm_y;
2035 let _ = writeln!(
2036 stream,
2037 "{:.3} {:.3} {:.3} rg\n/{} {:.1} Tf\n{:.2} Tc\n{:.2} {:.2} Td",
2038 glyph_color.r,
2039 glyph_color.g,
2040 glyph_color.b,
2041 font_name,
2042 first.font_size,
2043 first.letter_spacing,
2044 dx,
2045 dy
2046 );
2047 tm_x = x_cursor;
2048 tm_y = pdf_y;
2049
2050 let raw_text: String = group.iter().map(|g| g.char_value).collect();
2052 let has_placeholder = raw_text.contains(PAGE_NUMBER_SENTINEL)
2053 || raw_text.contains(TOTAL_PAGES_SENTINEL);
2054
2055 let is_custom = builder.custom_font_data.contains_key(&font_key);
2056
2057 if is_custom {
2058 if let Some(embed_data) = builder.custom_font_data.get(&font_key) {
2059 let mut hex = String::new();
2060 let mut continue_after_show = false;
2062 if has_placeholder {
2063 let pn = PAGE_NUMBER_SENTINEL.to_string();
2065 let tp = TOTAL_PAGES_SENTINEL.to_string();
2066 let text_after = raw_text
2067 .replace(&pn, &page_number.to_string())
2068 .replace(&tp, &total_pages.to_string());
2069 for ch in text_after.chars() {
2070 let gid =
2071 embed_data.char_to_gid.get(&ch).copied().unwrap_or(0);
2072 let _ = write!(hex, "{:04X}", gid);
2073 }
2074 } else {
2075 let gids: Vec<u16> = group
2077 .iter()
2078 .map(|g| {
2079 embed_data
2080 .gid_remap
2081 .get(&g.glyph_id)
2082 .copied()
2083 .unwrap_or_else(|| {
2084 embed_data
2086 .char_to_gid
2087 .get(&g.char_value)
2088 .copied()
2089 .unwrap_or(0)
2090 })
2091 })
2092 .collect();
2093 if let Some(tj) = Self::positioned_tj(
2094 group,
2095 &gids,
2096 embed_data,
2097 first.letter_spacing,
2098 ) {
2099 let _ = writeln!(stream, "{}", tj);
2100 continue_after_show = true;
2101 } else {
2102 for gid in &gids {
2103 let _ = write!(hex, "{:04X}", gid);
2104 }
2105 }
2106 }
2107 if !continue_after_show {
2108 let _ = writeln!(stream, "<{}> Tj", hex);
2109 }
2110 } else {
2111 let _ = writeln!(stream, "<> Tj");
2112 }
2113 } else {
2114 let pn = PAGE_NUMBER_SENTINEL.to_string();
2115 let tp = TOTAL_PAGES_SENTINEL.to_string();
2116 let text_after = raw_text
2117 .replace(&pn, &page_number.to_string())
2118 .replace(&tp, &total_pages.to_string());
2119 let mut text_str = String::new();
2120 for ch in text_after.chars() {
2121 let b = Self::unicode_to_winansi(ch).unwrap_or_else(|| {
2122 builder.missing_glyphs.borrow_mut().insert(ch);
2123 b'?'
2124 });
2125 match b {
2126 b'\\' => text_str.push_str("\\\\"),
2127 b'(' => text_str.push_str("\\("),
2128 b')' => text_str.push_str("\\)"),
2129 0x20..=0x7E => text_str.push(b as char),
2130 _ => {
2131 let _ = write!(text_str, "\\{:03o}", b);
2132 }
2133 }
2134 }
2135 let _ = writeln!(stream, "({}) Tj", text_str);
2136 }
2137
2138 let group_start_x = x_cursor;
2140
2141 if let Some(last) = group.last() {
2148 x_cursor = line.x + last.x_offset + last.x_advance;
2149 }
2150
2151 let group_dec = first.text_decoration;
2153 if !matches!(group_dec, TextDecoration::None) {
2154 group_spans.push((group_start_x, x_cursor, group_dec, glyph_color));
2155 }
2156 }
2157
2158 let _ = writeln!(stream, "ET");
2159 if open_link.is_some() {
2163 let tb = tag_builder.as_mut().expect("tag_links implies tagging");
2164 let mcid = tb.continue_current(page_idx);
2165 let _ = writeln!(
2166 stream,
2167 "EMC\n/{} <</MCID {}>> BDC",
2168 bdc_role.unwrap_or("Span"),
2169 mcid
2170 );
2171 }
2172
2173 for (span_x, span_end_x, dec, dec_color) in &group_spans {
2175 match dec {
2176 TextDecoration::Underline => {
2177 let underline_y = pdf_y - 1.5;
2178 let _ = write!(
2179 stream,
2180 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2181 dec_color.r, dec_color.g, dec_color.b,
2182 span_x, underline_y,
2183 span_end_x, underline_y
2184 );
2185 }
2186 TextDecoration::LineThrough => {
2187 let first_size =
2188 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
2189 let strikethrough_y = pdf_y + first_size * 0.3;
2190 let _ = write!(
2191 stream,
2192 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2193 dec_color.r, dec_color.g, dec_color.b,
2194 span_x, strikethrough_y,
2195 span_end_x, strikethrough_y
2196 );
2197 }
2198 TextDecoration::None => {}
2199 }
2200 }
2201
2202 if group_spans.is_empty() {
2204 if matches!(text_decoration, TextDecoration::Underline) {
2205 let underline_y = pdf_y - 1.5;
2206 let _ = write!(
2207 stream,
2208 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2209 color.r, color.g, color.b,
2210 line.x, underline_y,
2211 line.x + line.width, underline_y
2212 );
2213 }
2214 if matches!(text_decoration, TextDecoration::LineThrough) {
2215 let first_size =
2216 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
2217 let strikethrough_y = pdf_y + first_size * 0.3;
2218 let _ = write!(
2219 stream,
2220 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2221 color.r, color.g, color.b,
2222 line.x, strikethrough_y,
2223 line.x + line.width, strikethrough_y
2224 );
2225 }
2226 }
2227 }
2228
2229 if tw_set {
2233 let _ = writeln!(stream, "0 Tw");
2234 }
2235 if let Some(tb) = tag_builder.as_mut() {
2239 while let Some(idx) = inline_links.pop_front() {
2240 tb.attach_inline_link_without_content(idx);
2241 }
2242 }
2243
2244 if needs_opacity {
2245 let _ = writeln!(stream, "Q");
2246 }
2247 }
2248
2249 DrawCommand::Image { .. } => {
2250 let elem_idx = *element_counter;
2251 *element_counter += 1;
2252 if let Some(&img_idx) = builder.image_index_map.get(&(page_idx, elem_idx)) {
2253 let x = element.x;
2254 let y = page_height - element.y - element.height;
2255 let _ = write!(
2256 stream,
2257 "q\n{:.4} 0 0 {:.4} {:.2} {:.2} cm\n/Im{} Do\nQ\n",
2258 element.width, element.height, x, y, img_idx
2259 );
2260 } else {
2261 let x = element.x;
2263 let y = page_height - element.y - element.height;
2264 let _ = write!(
2265 stream,
2266 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
2267 x, y, element.width, element.height
2268 );
2269 }
2270 if tagged_mcid.is_some() {
2271 let _ = writeln!(stream, "EMC");
2272 if let Some(ref mut tb) = tag_builder {
2273 tb.end_element();
2274 }
2275 } else if is_artifact {
2276 let _ = writeln!(stream, "EMC");
2277 } else if wrap_own_draw_as_artifact {
2278 let _ = writeln!(stream, "EMC");
2282 if let Some(ref mut tb) = tag_builder {
2283 tb.end_element();
2284 }
2285 }
2286 return; }
2288
2289 DrawCommand::ImagePlaceholder => {
2290 *element_counter += 1;
2291 let x = element.x;
2292 let y = page_height - element.y - element.height;
2293 let _ = write!(
2294 stream,
2295 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
2296 x, y, element.width, element.height
2297 );
2298 if tagged_mcid.is_some() {
2299 let _ = writeln!(stream, "EMC");
2300 if let Some(ref mut tb) = tag_builder {
2301 tb.end_element();
2302 }
2303 } else if is_artifact {
2304 let _ = writeln!(stream, "EMC");
2305 } else if wrap_own_draw_as_artifact {
2306 let _ = writeln!(stream, "EMC");
2310 if let Some(ref mut tb) = tag_builder {
2311 tb.end_element();
2312 }
2313 }
2314 return;
2315 }
2316
2317 DrawCommand::Svg {
2318 commands,
2319 width: _svg_w,
2320 height: _svg_h,
2321 viewbox_min_x,
2322 viewbox_min_y,
2323 viewbox_width,
2324 viewbox_height,
2325 clip,
2326 } => {
2327 let x = element.x;
2328 let y = page_height - element.y - element.height;
2329
2330 let _ = writeln!(stream, "q");
2332 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", x, y);
2333
2334 if *viewbox_width > 0.0 && *viewbox_height > 0.0 {
2340 let raw_sx = element.width / *viewbox_width;
2341 let raw_sy = element.height / *viewbox_height;
2342 let s = raw_sx.min(raw_sy);
2343 let tx = (element.width - s * *viewbox_width) / 2.0;
2344 let ty = (element.height - s * *viewbox_height) / 2.0;
2345 let _ = writeln!(stream, "{:.4} 0 0 {:.4} {:.2} {:.2} cm", s, s, tx, ty);
2346 }
2347
2348 let _ = writeln!(stream, "1 0 0 -1 0 {:.2} cm", *viewbox_height);
2351
2352 if *viewbox_min_x != 0.0 || *viewbox_min_y != 0.0 {
2354 let _ = writeln!(
2355 stream,
2356 "1 0 0 1 {:.2} {:.2} cm",
2357 -*viewbox_min_x, -*viewbox_min_y
2358 );
2359 }
2360
2361 if *clip {
2363 let _ = writeln!(
2364 stream,
2365 "{:.2} {:.2} {:.2} {:.2} re W n",
2366 *viewbox_min_x, *viewbox_min_y, *viewbox_width, *viewbox_height
2367 );
2368 }
2369
2370 Self::write_svg_commands(stream, commands, &builder.ext_gstate_map);
2371
2372 let _ = writeln!(stream, "Q");
2373 if tagged_mcid.is_some() {
2374 let _ = writeln!(stream, "EMC");
2375 if let Some(ref mut tb) = tag_builder {
2376 tb.end_element();
2377 }
2378 } else if is_artifact {
2379 let _ = writeln!(stream, "EMC");
2380 } else if wrap_own_draw_as_artifact {
2381 let _ = writeln!(stream, "EMC");
2385 if let Some(ref mut tb) = tag_builder {
2386 tb.end_element();
2387 }
2388 }
2389 return;
2390 }
2391
2392 DrawCommand::Barcode {
2393 bars,
2394 bar_width,
2395 height,
2396 color,
2397 } => {
2398 *element_counter += 1;
2399 let _ = writeln!(stream, "q");
2400 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2401 for (i, &bar) in bars.iter().enumerate() {
2402 if bar == 1 {
2403 let bx = element.x + i as f64 * bar_width;
2404 let by = page_height - element.y - height;
2405 let _ = writeln!(
2406 stream,
2407 "{:.2} {:.2} {:.2} {:.2} re",
2408 bx, by, bar_width, height
2409 );
2410 }
2411 }
2412 let _ = writeln!(stream, "f\nQ");
2413 if tagged_mcid.is_some() {
2414 let _ = writeln!(stream, "EMC");
2415 if let Some(ref mut tb) = tag_builder {
2416 tb.end_element();
2417 }
2418 } else if is_artifact {
2419 let _ = writeln!(stream, "EMC");
2420 } else if wrap_own_draw_as_artifact {
2421 let _ = writeln!(stream, "EMC");
2425 if let Some(ref mut tb) = tag_builder {
2426 tb.end_element();
2427 }
2428 }
2429 return;
2430 }
2431
2432 DrawCommand::QrCode {
2433 modules,
2434 module_size,
2435 color,
2436 } => {
2437 *element_counter += 1;
2438 let _ = writeln!(stream, "q");
2439 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2440 for (row_idx, row) in modules.iter().enumerate() {
2441 for (col_idx, &dark) in row.iter().enumerate() {
2442 if dark {
2443 let mx = element.x + col_idx as f64 * module_size;
2444 let my = page_height - element.y - (row_idx as f64 + 1.0) * module_size;
2445 let _ = writeln!(
2446 stream,
2447 "{:.2} {:.2} {:.2} {:.2} re",
2448 mx, my, module_size, module_size
2449 );
2450 }
2451 }
2452 }
2453 let _ = writeln!(stream, "f\nQ");
2454 if tagged_mcid.is_some() {
2455 let _ = writeln!(stream, "EMC");
2456 if let Some(ref mut tb) = tag_builder {
2457 tb.end_element();
2458 }
2459 } else if is_artifact {
2460 let _ = writeln!(stream, "EMC");
2461 } else if wrap_own_draw_as_artifact {
2462 let _ = writeln!(stream, "EMC");
2466 if let Some(ref mut tb) = tag_builder {
2467 tb.end_element();
2468 }
2469 }
2470 return;
2471 }
2472
2473 DrawCommand::Chart { primitives } => {
2474 *element_counter += 1;
2475 let _ = writeln!(stream, "q");
2476 let _ = writeln!(
2478 stream,
2479 "1 0 0 -1 {:.4} {:.4} cm",
2480 element.x,
2481 page_height - element.y
2482 );
2483
2484 for prim in primitives {
2485 write_chart_primitive(stream, prim, element.height, builder);
2486 }
2487
2488 let _ = writeln!(stream, "Q");
2489 if tagged_mcid.is_some() {
2490 let _ = writeln!(stream, "EMC");
2491 if let Some(ref mut tb) = tag_builder {
2492 tb.end_element();
2493 }
2494 } else if is_artifact {
2495 let _ = writeln!(stream, "EMC");
2496 } else if wrap_own_draw_as_artifact {
2497 let _ = writeln!(stream, "EMC");
2501 if let Some(ref mut tb) = tag_builder {
2502 tb.end_element();
2503 }
2504 }
2505 return;
2506 }
2507
2508 DrawCommand::Watermark {
2509 lines,
2510 color,
2511 opacity,
2512 angle_rad,
2513 font_family: _,
2514 } => {
2515 let _ = writeln!(stream, "q");
2516 if *opacity < 1.0 {
2518 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
2519 let _ = writeln!(stream, "/{} gs", gs_name);
2520 }
2521 }
2522 let pdf_cx = element.x;
2524 let pdf_cy = page_height - element.y;
2525 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", pdf_cx, pdf_cy);
2526 let cos_a = angle_rad.cos();
2528 let sin_a = angle_rad.sin();
2529 let _ = writeln!(
2530 stream,
2531 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
2532 cos_a, sin_a, -sin_a, cos_a
2533 );
2534 let _ = writeln!(stream, "BT");
2536 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2537 if let Some(line) = lines.first() {
2538 let groups = Self::group_glyphs_by_style(&line.glyphs);
2539 let text_width = line.width;
2540 let cap_height = line.height * 0.7;
2541 let _ = writeln!(
2542 stream,
2543 "{:.2} {:.2} Td",
2544 -text_width / 2.0,
2545 -cap_height / 2.0
2546 );
2547 for group in &groups {
2548 let first = &group[0];
2549 let italic =
2550 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
2551 let fk = FontKey {
2552 family: first.font_family.to_string(),
2553 weight: first.font_weight,
2554 italic,
2555 };
2556 let idx = self.font_index(
2557 &first.font_family,
2558 first.font_weight,
2559 first.font_style,
2560 &builder.font_objects,
2561 );
2562 let font_name = format!("F{}", idx);
2563 let _ = writeln!(stream, "/{} {:.1} Tf", font_name, first.font_size);
2564 let is_custom = builder.custom_font_data.contains_key(&fk);
2565 if is_custom {
2566 if let Some(embed_data) = builder.custom_font_data.get(&fk) {
2567 let mut hex = String::new();
2568 for g in group.iter() {
2569 let gid =
2570 embed_data.gid_remap.get(&g.glyph_id).copied().unwrap_or(0);
2571 let _ = write!(hex, "{:04X}", gid);
2572 }
2573 let _ = writeln!(stream, "<{}> Tj", hex);
2574 }
2575 } else {
2576 let hex_str: String = group
2577 .iter()
2578 .map(|g| format!("{:02X}", g.glyph_id as u8))
2579 .collect();
2580 let _ = writeln!(stream, "<{}> Tj", hex_str);
2581 }
2582 }
2583 }
2584 let _ = writeln!(stream, "ET");
2585 let _ = writeln!(stream, "Q");
2586 if tagged_mcid.is_some() {
2587 let _ = writeln!(stream, "EMC");
2588 if let Some(ref mut tb) = tag_builder {
2589 tb.end_element();
2590 }
2591 } else if is_artifact {
2592 let _ = writeln!(stream, "EMC");
2593 } else if wrap_own_draw_as_artifact {
2594 let _ = writeln!(stream, "EMC");
2598 if let Some(ref mut tb) = tag_builder {
2599 tb.end_element();
2600 }
2601 }
2602 return;
2603 }
2604
2605 DrawCommand::FormField { field_type, .. } => {
2606 let pdf_x = element.x;
2610 let pdf_y = page_height - element.y - element.height;
2611 let w = element.width;
2612 let h = element.height;
2613 let _ = writeln!(stream, "q");
2614 match field_type {
2615 FormFieldType::Checkbox { checked, .. } => {
2616 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
2619 let _ =
2620 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
2621 if *checked {
2622 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
2624 let sx = w / 14.0;
2625 let sy = h / 14.0;
2626 let _ = writeln!(
2627 stream,
2628 "{:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l f",
2629 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
2630 pdf_x + 5.5 * sx, pdf_y + 2.0 * sy,
2631 pdf_x + 12.0 * sx, pdf_y + 11.0 * sy,
2632 pdf_x + 11.0 * sx, pdf_y + 12.0 * sy,
2633 pdf_x + 5.5 * sx, pdf_y + 4.5 * sy,
2634 pdf_x + 3.0 * sx, pdf_y + 7.0 * sy,
2635 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
2636 );
2637 }
2638 }
2639 FormFieldType::RadioButton { checked, .. } => {
2640 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
2643 let _ =
2644 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
2645 if *checked {
2646 let cx = pdf_x + w / 2.0;
2648 let cy = pdf_y + h / 2.0;
2649 let r = (w.min(h) / 2.0) * 0.6;
2650 let k = r * 0.5523;
2651 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
2652 let _ = writeln!(
2653 stream,
2654 "{:.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",
2655 cx, cy + r,
2656 cx + k, cy + r, cx + r, cy + k, cx + r, cy,
2657 cx + r, cy - k, cx + k, cy - r, cx, cy - r,
2658 cx - k, cy - r, cx - r, cy - k, cx - r, cy,
2659 cx - r, cy + k, cx - k, cy + r, cx, cy + r,
2660 );
2661 }
2662 }
2663 FormFieldType::TextField {
2664 value,
2665 placeholder,
2666 font_size,
2667 multiline,
2668 password,
2669 ..
2670 } => {
2671 let _ = writeln!(stream, "1 1 1 rg");
2673 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2674 let _ = writeln!(stream, "0.5 w");
2675 let _ =
2676 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2677 if flatten_forms {
2679 let has_value = value.as_ref().is_some_and(|v| !v.is_empty());
2680 if has_value {
2681 let val = value.as_ref().unwrap();
2682 let display_text = if *password {
2683 "\u{2022}".repeat(val.len())
2684 } else {
2685 val.clone()
2686 };
2687 let font_idx = builder
2688 .font_objects
2689 .iter()
2690 .enumerate()
2691 .find(|(_, (key, _))| {
2692 key.family == "Helvetica"
2693 && key.weight == 400
2694 && !key.italic
2695 })
2696 .map(|(i, _)| i)
2697 .unwrap_or(0);
2698 if *multiline {
2699 let metrics = crate::font::StandardFont::Helvetica.metrics();
2701 let max_w = w - 4.0;
2702 let mut lines: Vec<String> = Vec::new();
2703 for paragraph in display_text.split('\n') {
2704 let mut line = String::new();
2705 let mut line_w = 0.0;
2706 for word in paragraph.split_whitespace() {
2707 let word_w =
2708 metrics.measure_string(word, *font_size, 0.0);
2709 let space_w = if line.is_empty() {
2710 0.0
2711 } else {
2712 metrics.measure_string(" ", *font_size, 0.0)
2713 };
2714 if word_w > max_w {
2716 let mut char_line = String::new();
2717 let mut char_w = 0.0;
2718 for ch in word.chars() {
2719 let cw = metrics.char_width(ch, *font_size);
2720 if !char_line.is_empty() && char_w + cw > max_w
2721 {
2722 if !line.is_empty() {
2723 lines.push(line.clone());
2724 line.clear();
2725 line_w = 0.0;
2726 }
2727 lines.push(char_line.clone());
2728 char_line.clear();
2729 char_w = 0.0;
2730 }
2731 char_line.push(ch);
2732 char_w += cw;
2733 }
2734 if !char_line.is_empty() {
2736 if !line.is_empty() {
2737 line.push(' ');
2738 line_w += metrics
2739 .measure_string(" ", *font_size, 0.0);
2740 }
2741 line.push_str(&char_line);
2742 line_w += char_w;
2743 }
2744 continue;
2745 }
2746 if !line.is_empty() && line_w + space_w + word_w > max_w
2747 {
2748 lines.push(line.clone());
2749 line.clear();
2750 line_w = 0.0;
2751 }
2752 if !line.is_empty() {
2753 line.push(' ');
2754 line_w += space_w;
2755 }
2756 line.push_str(word);
2757 line_w += word_w;
2758 }
2759 if !line.is_empty() {
2760 lines.push(line);
2761 }
2762 }
2763 let text_y = pdf_y + h - font_size - 2.0;
2764 for (i, line_text) in lines.iter().enumerate() {
2765 let ly = text_y - (i as f64) * (font_size * 1.2);
2766 if ly < pdf_y {
2767 break;
2768 }
2769 let esc = Self::encode_winansi_text(builder, line_text);
2770 let _ = writeln!(
2771 stream,
2772 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2773 font_idx,
2774 font_size,
2775 pdf_x + 2.0,
2776 ly,
2777 esc
2778 );
2779 }
2780 } else {
2781 let escaped = Self::encode_winansi_text(builder, &display_text);
2782 let text_y = pdf_y + (h - font_size) / 2.0;
2783 let _ = writeln!(
2784 stream,
2785 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2786 font_idx,
2787 font_size,
2788 pdf_x + 2.0,
2789 text_y,
2790 escaped
2791 );
2792 }
2793 } else if let Some(ref ph) = placeholder {
2794 if !ph.is_empty() {
2795 let font_idx = builder
2797 .font_objects
2798 .iter()
2799 .enumerate()
2800 .find(|(_, (key, _))| {
2801 key.family == "Helvetica"
2802 && key.weight == 400
2803 && !key.italic
2804 })
2805 .map(|(i, _)| i)
2806 .unwrap_or(0);
2807 let escaped = Self::encode_winansi_text(builder, ph);
2808 let text_y = pdf_y + (h - font_size) / 2.0;
2809 let _ = writeln!(
2810 stream,
2811 "BT /F{} {:.1} Tf 0.6 g {:.2} {:.2} Td ({}) Tj ET",
2812 font_idx,
2813 font_size,
2814 pdf_x + 2.0,
2815 text_y,
2816 escaped
2817 );
2818 }
2819 }
2820 }
2821 }
2822 FormFieldType::Dropdown {
2823 value, font_size, ..
2824 } => {
2825 let _ = writeln!(stream, "1 1 1 rg");
2827 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2828 let _ = writeln!(stream, "0.5 w");
2829 let _ =
2830 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2831 if flatten_forms {
2833 if let Some(ref val) = value {
2834 if !val.is_empty() {
2835 let font_idx = builder
2836 .font_objects
2837 .iter()
2838 .enumerate()
2839 .find(|(_, (key, _))| {
2840 key.family == "Helvetica"
2841 && key.weight == 400
2842 && !key.italic
2843 })
2844 .map(|(i, _)| i)
2845 .unwrap_or(0);
2846 let escaped = Self::encode_winansi_text(builder, val);
2847 let text_y = pdf_y + (h - font_size) / 2.0;
2848 let _ = writeln!(
2849 stream,
2850 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2851 font_idx,
2852 font_size,
2853 pdf_x + 2.0,
2854 text_y,
2855 escaped
2856 );
2857 }
2858 }
2859 }
2860 }
2861 }
2862 let _ = writeln!(stream, "Q");
2863 }
2864 }
2865
2866 if wrap_own_draw_as_artifact {
2869 let _ = writeln!(stream, "EMC");
2870 }
2871
2872 let clip_overflow = matches!(element.overflow, Overflow::Hidden);
2877 if clip_overflow {
2878 let clip_x = element.x;
2879 let clip_y = page_height - element.y - element.height;
2880 let clip_w = element.width;
2881 let clip_h = element.height;
2882 let radius = if let DrawCommand::Rect { border_radius, .. } = &element.draw {
2886 Some(border_radius)
2887 } else {
2888 None
2889 };
2890 let has_rounded_corners = radius.is_some_and(|r| {
2891 r.top_left > 0.0 || r.top_right > 0.0 || r.bottom_right > 0.0 || r.bottom_left > 0.0
2892 });
2893 let _ = writeln!(stream, "q");
2894 if has_rounded_corners {
2895 self.write_rounded_rect(stream, clip_x, clip_y, clip_w, clip_h, radius.unwrap());
2896 let _ = writeln!(stream, "W n");
2897 } else {
2898 let _ = writeln!(
2899 stream,
2900 "{:.2} {:.2} {:.2} {:.2} re W n",
2901 clip_x, clip_y, clip_w, clip_h
2902 );
2903 }
2904 }
2905
2906 for child in &element.children {
2907 self.write_element(
2908 stream,
2909 child,
2910 page_height,
2911 builder,
2912 page_idx,
2913 element_counter,
2914 gradient_counter,
2915 page_number,
2916 total_pages,
2917 tag_builder.as_deref_mut(),
2918 flatten_forms,
2919 );
2920 }
2921
2922 if clip_overflow {
2923 let _ = writeln!(stream, "Q");
2924 }
2925
2926 if has_transform {
2928 let _ = writeln!(stream, "Q");
2929 }
2930
2931 if needs_element_opacity {
2934 let _ = writeln!(stream, "Q");
2935 }
2936
2937 if tagged_mcid.is_some() {
2939 let _ = writeln!(stream, "EMC");
2940 if let Some(ref mut tb) = tag_builder {
2941 tb.end_element();
2942 }
2943 } else if is_artifact {
2944 let _ = writeln!(stream, "EMC");
2945 } else if artifact_own_draw {
2946 if let Some(ref mut tb) = tag_builder {
2949 tb.end_element();
2950 }
2951 }
2952 }
2953
2954 fn write_rounded_rect(
2955 &self,
2956 stream: &mut String,
2957 x: f64,
2958 y: f64,
2959 w: f64,
2960 h: f64,
2961 r: &crate::style::CornerValues,
2962 ) {
2963 let k = 0.5522847498;
2964
2965 let tl = r.top_left.min(w / 2.0).min(h / 2.0);
2966 let tr = r.top_right.min(w / 2.0).min(h / 2.0);
2967 let br = r.bottom_right.min(w / 2.0).min(h / 2.0);
2968 let bl = r.bottom_left.min(w / 2.0).min(h / 2.0);
2969
2970 let _ = writeln!(stream, "{:.2} {:.2} m", x + bl, y);
2971
2972 let _ = writeln!(stream, "{:.2} {:.2} l", x + w - br, y);
2973 if br > 0.0 {
2974 let _ = writeln!(
2975 stream,
2976 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2977 x + w - br + br * k,
2978 y,
2979 x + w,
2980 y + br - br * k,
2981 x + w,
2982 y + br
2983 );
2984 }
2985
2986 let _ = writeln!(stream, "{:.2} {:.2} l", x + w, y + h - tr);
2987 if tr > 0.0 {
2988 let _ = writeln!(
2989 stream,
2990 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2991 x + w,
2992 y + h - tr + tr * k,
2993 x + w - tr + tr * k,
2994 y + h,
2995 x + w - tr,
2996 y + h
2997 );
2998 }
2999
3000 let _ = writeln!(stream, "{:.2} {:.2} l", x + tl, y + h);
3001 if tl > 0.0 {
3002 let _ = writeln!(
3003 stream,
3004 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3005 x + tl - tl * k,
3006 y + h,
3007 x,
3008 y + h - tl + tl * k,
3009 x,
3010 y + h - tl
3011 );
3012 }
3013
3014 let _ = writeln!(stream, "{:.2} {:.2} l", x, y + bl);
3015 if bl > 0.0 {
3016 let _ = writeln!(
3017 stream,
3018 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3019 x,
3020 y + bl - bl * k,
3021 x + bl - bl * k,
3022 y,
3023 x + bl,
3024 y
3025 );
3026 }
3027
3028 let _ = writeln!(stream, "h");
3029 }
3030
3031 #[allow(clippy::too_many_arguments)]
3032 fn write_border_sides(
3033 &self,
3034 stream: &mut String,
3035 x: f64,
3036 y: f64,
3037 w: f64,
3038 h: f64,
3039 bw: &Edges,
3040 bc: &crate::style::EdgeValues<Color>,
3041 bs: &crate::style::EdgeValues<crate::style::BorderStyle>,
3042 ) {
3043 fn dash_ops(style: crate::style::BorderStyle, width: f64) -> String {
3048 use crate::style::BorderStyle::*;
3049 match style {
3050 Solid => String::new(),
3051 Dashed => format!("[{:.2} {:.2}] 0 d\n", width * 2.0, width),
3052 Dotted => format!("1 J\n[0 {:.2}] 0 d\n", width * 2.0),
3053 }
3054 }
3055 let sides = [
3057 (bc.top, bw.top, bs.top, x, y + h, x + w, y + h),
3058 (bc.bottom, bw.bottom, bs.bottom, x, y, x + w, y),
3059 (bc.left, bw.left, bs.left, x, y, x, y + h),
3060 (bc.right, bw.right, bs.right, x + w, y, x + w, y + h),
3061 ];
3062 for (color, width, style, x0, y0, x1, y1) in sides {
3063 if width <= 0.0 {
3064 continue;
3065 }
3066 let _ = write!(
3067 stream,
3068 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{}{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
3069 color.r,
3070 color.g,
3071 color.b,
3072 width,
3073 dash_ops(style, width),
3074 x0,
3075 y0,
3076 x1,
3077 y1
3078 );
3079 }
3080 }
3081
3082 fn emit_pdfua_embedded_standard(
3094 builder: &mut PdfBuilder,
3095 key: &FontKey,
3096 std_font: &crate::font::StandardFont,
3097 metrics: &crate::font::StandardFontMetrics,
3098 font_context: &FontContext,
3099 ) -> bool {
3100 let lib_family = match std_font.liberation_family() {
3101 Some(f) => f,
3102 None => return false, };
3104 let lib_bytes: &[u8] = match font_context.resolve(lib_family, key.weight, key.italic) {
3107 FontData::Custom { data, .. } => data,
3108 FontData::Standard(_) => return false,
3109 };
3110 let face = match ttf_parser::Face::parse(lib_bytes, 0) {
3111 Ok(f) => f,
3112 Err(_) => return false,
3113 };
3114 let scale = 1000.0 / face.units_per_em() as f64;
3115 let bbox = face.global_bounding_box();
3116 let pdf_name = Self::sanitize_font_name(lib_family, key.weight, key.italic);
3117
3118 let compressed = compress_to_vec_zlib(lib_bytes, 6);
3120 let fontfile2_id = builder.objects.len();
3121 let mut ff2: Vec<u8> = Vec::new();
3122 let _ = write!(
3123 ff2,
3124 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
3125 compressed.len(),
3126 lib_bytes.len()
3127 );
3128 ff2.extend_from_slice(&compressed);
3129 ff2.extend_from_slice(b"\nendstream");
3130 builder.objects.push(PdfObject {
3131 id: fontfile2_id,
3132 data: ff2,
3133 });
3134
3135 let fd_id = builder.objects.len();
3137 let cap_height =
3138 (face.capital_height().unwrap_or_else(|| face.ascender()) as f64 * scale) as i32;
3139 let fd = format!(
3140 "<< /Type /FontDescriptor /FontName /{name} /Flags {flags} \
3141 /FontBBox [{x0} {y0} {x1} {y1}] /ItalicAngle {ia} \
3142 /Ascent {asc} /Descent {desc} /CapHeight {cap} /StemV {stem} \
3143 /FontFile2 {ff2} 0 R >>",
3144 name = pdf_name,
3145 flags = std_font.descriptor_flags(),
3146 x0 = (bbox.x_min as f64 * scale) as i32,
3147 y0 = (bbox.y_min as f64 * scale) as i32,
3148 x1 = (bbox.x_max as f64 * scale) as i32,
3149 y1 = (bbox.y_max as f64 * scale) as i32,
3150 ia = if key.italic { -12 } else { 0 },
3151 asc = (face.ascender() as f64 * scale) as i32,
3152 desc = (face.descender() as f64 * scale) as i32,
3153 cap = cap_height,
3154 stem = if key.weight >= 700 { 120 } else { 80 },
3155 ff2 = fontfile2_id,
3156 );
3157 builder.objects.push(PdfObject {
3158 id: fd_id,
3159 data: fd.into_bytes(),
3160 });
3161
3162 let declared_widths: Vec<u16> = metrics
3176 .widths
3177 .iter()
3178 .enumerate()
3179 .map(|(i, &afm)| {
3180 let code = 32u8.wrapping_add(i as u8); if let Some(ch) = crate::font::winansi_to_char(code) {
3182 if let Some(gid) = face.glyph_index(ch) {
3183 if let Some(adv) = face.glyph_hor_advance(gid) {
3184 let hmtx = (adv as f64 * scale).round() as u16;
3185 if (hmtx as i32 - afm as i32).abs() > 1 {
3186 return hmtx;
3187 }
3188 }
3189 }
3190 }
3191 afm
3192 })
3193 .collect();
3194 let widths_str: String = declared_widths
3195 .iter()
3196 .map(|w| w.to_string())
3197 .collect::<Vec<_>>()
3198 .join(" ");
3199 let obj_id = builder.objects.len();
3200 let font_dict = format!(
3201 "<< /Type /Font /Subtype /TrueType /BaseFont /{name} \
3202 /Encoding /WinAnsiEncoding \
3203 /FirstChar 32 /LastChar 255 /Widths [{w}] \
3204 /FontDescriptor {fd} 0 R >>",
3205 name = pdf_name,
3206 w = widths_str,
3207 fd = fd_id,
3208 );
3209 builder.objects.push(PdfObject {
3210 id: obj_id,
3211 data: font_dict.into_bytes(),
3212 });
3213 builder.font_objects.push((key.clone(), obj_id));
3214 builder.embedded_standard_fonts.insert(key.clone());
3218 true
3219 }
3220
3221 #[allow(clippy::too_many_arguments)]
3222 fn register_fonts(
3223 &self,
3224 builder: &mut PdfBuilder,
3225 pages: &[LayoutPage],
3226 font_context: &FontContext,
3227 pdf_ua: bool,
3228 pdfa: bool,
3229 pdf_version: crate::model::PdfVersion,
3230 ) -> Result<(), FormeError> {
3231 let mut font_usage_map: HashMap<FontKey, FontUsage> = HashMap::new();
3233
3234 for page in pages {
3235 Self::collect_font_usage(&page.elements, &mut font_usage_map);
3236 }
3237
3238 let mut keys: Vec<FontKey> = font_usage_map.keys().cloned().collect();
3239
3240 keys.sort_by(|a, b| {
3242 a.family
3243 .cmp(&b.family)
3244 .then(a.weight.cmp(&b.weight))
3245 .then(a.italic.cmp(&b.italic))
3246 });
3247 keys.dedup();
3248
3249 if keys.is_empty() {
3251 keys.push(FontKey {
3252 family: "Helvetica".to_string(),
3253 weight: 400,
3254 italic: false,
3255 });
3256 }
3257
3258 for key in &keys {
3259 let font_data = font_context.resolve(&key.family, key.weight, key.italic);
3260
3261 match font_data {
3262 FontData::Standard(std_font) => {
3263 let metrics = std_font.metrics();
3264
3265 if pdf_ua || pdfa || pdf_version == crate::model::PdfVersion::V2_0 {
3280 if Self::emit_pdfua_embedded_standard(
3281 builder,
3282 key,
3283 std_font,
3284 &metrics,
3285 font_context,
3286 ) {
3287 continue;
3288 }
3289 if pdf_version == crate::model::PdfVersion::V2_0 {
3295 let remedy = match std_font.liberation_family() {
3296 Some(lib) => format!(
3297 "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"
3298 ),
3299 None => "register an embeddable TrueType font for this text (Symbol/ZapfDingbats have no metric-compatible substitute)"
3300 .to_string(),
3301 };
3302 return Err(FormeError::FontError(format!(
3303 "pdfVersion \"2.0\": font '{}' is not embedded. ISO 32000-2 removes the standard-14 provision, so every font must be embedded — {}.",
3304 std_font.pdf_name(),
3305 remedy,
3306 )));
3307 }
3308 if let Some(lib) = std_font.liberation_family() {
3314 builder.warnings.push(format!(
3315 "pdfUa: font '{}' is not embedded, so the PDF will not conform to \
3316 PDF/UA (all fonts must be embedded). Install \
3317 @formepdf/fonts-standard and register its fonts \
3318 (`for (const f of standardFonts()) Font.register(f)`) — Forme \
3319 will then embed the metric-compatible {} in its place.",
3320 std_font.pdf_name(),
3321 lib,
3322 ));
3323 }
3324 }
3325
3326 let obj_id = builder.objects.len();
3327 let widths_str: String = metrics
3330 .widths
3331 .iter()
3332 .map(|w| w.to_string())
3333 .collect::<Vec<_>>()
3334 .join(" ");
3335 let font_dict = format!(
3336 "<< /Type /Font /Subtype /Type1 /BaseFont /{} \
3337 /Encoding /WinAnsiEncoding \
3338 /FirstChar 32 /LastChar 255 /Widths [{}] >>",
3339 std_font.pdf_name(),
3340 widths_str,
3341 );
3342 builder.objects.push(PdfObject {
3343 id: obj_id,
3344 data: font_dict.into_bytes(),
3345 });
3346 builder.font_objects.push((key.clone(), obj_id));
3347 }
3348 FontData::Custom { data, .. } => {
3349 let usage = font_usage_map.get(key);
3350 let used_glyph_ids = usage.map(|u| &u.glyph_ids);
3351 let used_chars = usage.map(|u| &u.chars);
3352 let glyph_to_text = usage.map(|u| &u.glyph_to_text);
3353 let type0_obj_id = Self::write_custom_font_objects(
3354 builder,
3355 key,
3356 data,
3357 used_glyph_ids.cloned().unwrap_or_default(),
3358 used_chars.cloned().unwrap_or_default(),
3359 glyph_to_text.cloned().unwrap_or_default(),
3360 )?;
3361 builder.font_objects.push((key.clone(), type0_obj_id));
3362 }
3363 }
3364 }
3365
3366 Ok(())
3367 }
3368
3369 fn collect_font_usage(
3371 elements: &[LayoutElement],
3372 font_usage: &mut HashMap<FontKey, FontUsage>,
3373 ) {
3374 for element in elements {
3375 let lines_opt = match &element.draw {
3376 DrawCommand::Text { lines, .. } => Some(lines),
3377 DrawCommand::Watermark { lines, .. } => Some(lines),
3378 _ => None,
3379 };
3380 if let Some(lines) = lines_opt {
3381 for line in lines {
3382 for glyph in &line.glyphs {
3383 let italic =
3384 matches!(glyph.font_style, FontStyle::Italic | FontStyle::Oblique);
3385 let key = FontKey {
3386 family: glyph.font_family.to_string(),
3387 weight: glyph.font_weight,
3388 italic,
3389 };
3390 let usage = font_usage.entry(key).or_insert_with(|| FontUsage {
3391 chars: HashSet::new(),
3392 glyph_ids: HashSet::new(),
3393 glyph_to_text: HashMap::new(),
3394 });
3395 usage.chars.insert(glyph.char_value);
3396 if glyph.char_value == PAGE_NUMBER_SENTINEL
3401 || glyph.char_value == TOTAL_PAGES_SENTINEL
3402 {
3403 usage.chars.extend('0'..='9');
3404 }
3405 usage.glyph_ids.insert(glyph.glyph_id);
3406 record_glyph_text(&mut usage.glyph_to_text, glyph);
3407 }
3408 }
3409 }
3410 Self::collect_font_usage(&element.children, font_usage);
3411 }
3412 }
3413
3414 fn register_shadings(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3420 for (page_idx, page) in pages.iter().enumerate() {
3421 let mut counter = 0usize;
3422 Self::collect_shadings_recursive(&page.elements, page_idx, &mut counter, builder);
3423 }
3424 }
3425
3426 fn collect_shadings_recursive(
3427 elements: &[LayoutElement],
3428 page_idx: usize,
3429 counter: &mut usize,
3430 builder: &mut PdfBuilder,
3431 ) {
3432 for element in elements {
3433 if let DrawCommand::Rect {
3434 background_gradient: Some(gradient),
3435 ..
3436 } = &element.draw
3437 {
3438 let ordinal = *counter;
3439 *counter += 1;
3440 let (obj_id, name) =
3441 Self::write_shading_objects(builder, gradient, element, ordinal);
3442 builder
3443 .shading_map
3444 .insert((page_idx, ordinal), (obj_id, name));
3445 }
3446 Self::collect_shadings_recursive(&element.children, page_idx, counter, builder);
3447 }
3448 }
3449
3450 fn write_shading_objects(
3456 builder: &mut PdfBuilder,
3457 gradient: &crate::style::Background,
3458 element: &LayoutElement,
3459 ordinal: usize,
3460 ) -> (usize, String) {
3461 use crate::style::Background;
3462 use crate::style::GradientStop;
3463
3464 let black = Color {
3468 r: 0.0,
3469 g: 0.0,
3470 b: 0.0,
3471 a: 1.0,
3472 };
3473 let stops: Vec<GradientStop> = match gradient {
3474 Background::Color(c) => vec![
3475 GradientStop {
3476 position: 0.0,
3477 color: *c,
3478 },
3479 GradientStop {
3480 position: 1.0,
3481 color: *c,
3482 },
3483 ],
3484 Background::Linear(g) => normalize_gradient_stops(&g.stops, black),
3485 Background::Radial(g) => normalize_gradient_stops(&g.stops, black),
3486 };
3487
3488 let function_id = if stops.len() <= 2 {
3492 let c0 = stops.first().map(|s| s.color).unwrap_or(black);
3493 let c1 = stops.last().map(|s| s.color).unwrap_or(c0);
3494 let id = builder.objects.len();
3495 let data = format!(
3496 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
3497 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
3498 );
3499 builder.objects.push(PdfObject {
3500 id,
3501 data: data.into_bytes(),
3502 });
3503 id
3504 } else {
3505 let mut sub_ids: Vec<usize> = Vec::with_capacity(stops.len() - 1);
3507 for window in stops.windows(2) {
3508 let c0 = window[0].color;
3509 let c1 = window[1].color;
3510 let id = builder.objects.len();
3511 let data = format!(
3512 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
3513 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
3514 );
3515 builder.objects.push(PdfObject {
3516 id,
3517 data: data.into_bytes(),
3518 });
3519 sub_ids.push(id);
3520 }
3521 let bounds: Vec<String> = stops[1..stops.len() - 1]
3525 .iter()
3526 .map(|s| format!("{:.4}", s.position))
3527 .collect();
3528 let encode: Vec<&str> = (0..sub_ids.len()).map(|_| "0 1").collect();
3529 let functions: Vec<String> = sub_ids.iter().map(|i| format!("{} 0 R", i)).collect();
3530 let id = builder.objects.len();
3531 let data = format!(
3532 "<< /FunctionType 3 /Domain [0 1] /Functions [{}] /Bounds [{}] /Encode [{}] >>",
3533 functions.join(" "),
3534 bounds.join(" "),
3535 encode.join(" "),
3536 );
3537 builder.objects.push(PdfObject {
3538 id,
3539 data: data.into_bytes(),
3540 });
3541 id
3542 };
3543
3544 let _ = element.x;
3548 let _ = element.y;
3549 let w = element.width;
3550 let h = element.height;
3551
3552 let shading_id = builder.objects.len();
3553 let shading_data = match gradient {
3554 Background::Linear(g) => {
3555 let theta = g.angle_deg.to_radians();
3565 let dx = theta.sin();
3566 let dy = theta.cos();
3567 let axis_len = w * dx.abs() + h * dy.abs();
3570 let cx_rel = w / 2.0;
3573 let cy_rel = h / 2.0;
3574 let half = axis_len / 2.0;
3575 let x0 = cx_rel - dx * half;
3576 let y0 = cy_rel - dy * half;
3577 let x1 = cx_rel + dx * half;
3578 let y1 = cy_rel + dy * half;
3579 format!(
3580 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
3581 x0, y0, x1, y1, function_id,
3582 )
3583 }
3584 Background::Radial(_) => {
3585 let cx_rel = w / 2.0;
3588 let cy_rel = h / 2.0;
3589 let r_outer = (w / 2.0).max(h / 2.0);
3590 format!(
3591 "<< /ShadingType 3 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} 0 {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
3592 cx_rel, cy_rel, cx_rel, cy_rel, r_outer, function_id,
3593 )
3594 }
3595 Background::Color(_) => {
3596 format!(
3600 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 0 0] /Function {} 0 R /Extend [true true] >>",
3601 function_id,
3602 )
3603 }
3604 };
3605 builder.objects.push(PdfObject {
3606 id: shading_id,
3607 data: shading_data.into_bytes(),
3608 });
3609 (shading_id, format!("Sh{}", ordinal))
3610 }
3611
3612 fn register_page_background_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3616 for (page_idx, page) in pages.iter().enumerate() {
3617 let Some(src) = &page.config.background_image else {
3618 continue;
3619 };
3620 if let Some(&entry) = builder.page_background_url_cache.get(src) {
3622 builder.page_background_image_map.insert(page_idx, entry);
3623 continue;
3624 }
3625 match crate::image_loader::load_image(src) {
3628 Ok(image_data) => {
3629 let img_idx = builder.image_objects.len();
3630 let dims = (img_idx, image_data.width_px, image_data.height_px);
3631 let xobj_id = Self::write_image_xobject(builder, &image_data);
3632 builder.image_objects.push(xobj_id);
3633 builder.page_background_image_map.insert(page_idx, dims);
3634 builder.page_background_url_cache.insert(src.clone(), dims);
3635 }
3636 Err(e) => {
3637 eprintln!("[forme] page background image failed to load: {}", e);
3638 }
3639 }
3640 }
3641 }
3642
3643 fn write_page_background(
3648 &self,
3649 stream: &mut String,
3650 page: &LayoutPage,
3651 page_bg: (usize, u32, u32),
3652 builder: &PdfBuilder,
3653 ) {
3654 use crate::model::{BackgroundPosition, BackgroundSize};
3655 let (img_idx, iw_px, ih_px) = page_bg;
3656 let page_w = page.width;
3657 let page_h = page.height;
3658 let iw = iw_px as f64;
3659 let ih = ih_px as f64;
3660
3661 let size = page.config.background_size.unwrap_or_default();
3662 let (dest_w, dest_h) = match size {
3663 BackgroundSize::Fill => (page_w, page_h),
3664 BackgroundSize::Cover => {
3665 let s = (page_w / iw).max(page_h / ih);
3666 (iw * s, ih * s)
3667 }
3668 BackgroundSize::Contain => {
3669 let s = (page_w / iw).min(page_h / ih);
3670 (iw * s, ih * s)
3671 }
3672 };
3673
3674 let position = page.config.background_position.unwrap_or_default();
3678 let (dest_x, dest_y) = match position {
3681 BackgroundPosition::TopLeft => (0.0, page_h - dest_h),
3682 BackgroundPosition::TopRight => (page_w - dest_w, page_h - dest_h),
3683 BackgroundPosition::BottomLeft => (0.0, 0.0),
3684 BackgroundPosition::BottomRight => (page_w - dest_w, 0.0),
3685 BackgroundPosition::Center => ((page_w - dest_w) / 2.0, (page_h - dest_h) / 2.0),
3686 };
3687
3688 let opacity = page.config.background_opacity.unwrap_or(1.0);
3690 let needs_opacity = opacity < 1.0;
3691 if needs_opacity {
3692 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
3693 let _ = writeln!(stream, "q\n/{} gs", gs_name);
3694 } else {
3695 let _ = writeln!(stream, "q");
3696 }
3697 } else {
3698 let _ = writeln!(stream, "q");
3699 }
3700 let _ = writeln!(
3703 stream,
3704 "{:.2} 0 0 {:.2} {:.2} {:.2} cm\n/Im{} Do\nQ",
3705 dest_w, dest_h, dest_x, dest_y, img_idx,
3706 );
3707 }
3708
3709 fn register_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3711 for (page_idx, page) in pages.iter().enumerate() {
3712 let mut element_counter = 0usize;
3713 Self::collect_images_recursive(&page.elements, page_idx, &mut element_counter, builder);
3714 }
3715 }
3716
3717 fn collect_images_recursive(
3718 elements: &[LayoutElement],
3719 page_idx: usize,
3720 element_counter: &mut usize,
3721 builder: &mut PdfBuilder,
3722 ) {
3723 for element in elements {
3724 match &element.draw {
3725 DrawCommand::Image { image_data } => {
3726 let elem_idx = *element_counter;
3727 *element_counter += 1;
3728
3729 let img_idx = builder.image_objects.len();
3730 let xobj_id = Self::write_image_xobject(builder, image_data);
3731 builder.image_objects.push(xobj_id);
3732 builder
3733 .image_index_map
3734 .insert((page_idx, elem_idx), img_idx);
3735 }
3736 DrawCommand::ImagePlaceholder => {
3737 *element_counter += 1;
3738 }
3739 _ => {
3740 Self::collect_images_recursive(
3741 &element.children,
3742 page_idx,
3743 element_counter,
3744 builder,
3745 );
3746 }
3747 }
3748 }
3749 }
3750
3751 fn register_ext_gstates(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3753 let mut unique_opacities: Vec<f64> = Vec::new();
3754 for page in pages {
3755 Self::collect_opacities_recursive(&page.elements, &mut unique_opacities);
3756 if let Some(o) = page.config.background_opacity {
3758 if o < 1.0 {
3759 unique_opacities.push(o);
3760 }
3761 }
3762 }
3763 unique_opacities.sort_by(|a, b| a.partial_cmp(b).unwrap());
3764 unique_opacities.dedup();
3765
3766 for (idx, &opacity) in unique_opacities.iter().enumerate() {
3767 let obj_id = builder.objects.len();
3768 let gs_name = format!("GS{}", idx);
3769 let obj_data = format!(
3770 "<< /Type /ExtGState /ca {:.4} /CA {:.4} >>",
3771 opacity, opacity
3772 );
3773 builder.objects.push(PdfObject {
3774 id: obj_id,
3775 data: obj_data.into_bytes(),
3776 });
3777 let key = opacity.to_bits();
3778 builder.ext_gstate_map.insert(key, (obj_id, gs_name));
3779 }
3780 }
3781
3782 fn collect_opacities_recursive(elements: &[LayoutElement], opacities: &mut Vec<f64>) {
3783 for element in elements {
3784 if element.opacity < 1.0 {
3791 opacities.push(element.opacity);
3792 }
3793 if let DrawCommand::Rect {
3797 box_shadow: Some(shadow),
3798 ..
3799 } = &element.draw
3800 {
3801 if shadow.color.a < 1.0 {
3802 opacities.push(shadow.color.a);
3803 }
3804 }
3805 match &element.draw {
3806 DrawCommand::Rect { opacity, .. }
3807 | DrawCommand::Text { opacity, .. }
3808 | DrawCommand::Watermark { opacity, .. }
3809 if *opacity < 1.0 =>
3810 {
3811 opacities.push(*opacity);
3812 }
3813 DrawCommand::Chart { primitives } => {
3814 for prim in primitives {
3815 if let crate::chart::ChartPrimitive::FilledPath { opacity, .. } = prim {
3816 if *opacity < 1.0 {
3817 opacities.push(*opacity);
3818 }
3819 }
3820 }
3821 }
3822 DrawCommand::Svg { commands, .. } => {
3823 for cmd in commands {
3824 if let crate::svg::SvgCommand::SetOpacity(opacity) = cmd {
3825 if *opacity < 1.0 {
3826 opacities.push(*opacity);
3827 }
3828 }
3829 }
3830 }
3831 _ => {}
3832 }
3833 Self::collect_opacities_recursive(&element.children, opacities);
3834 }
3835 }
3836
3837 fn build_ext_gstate_resource_dict(&self, builder: &PdfBuilder) -> String {
3839 if builder.ext_gstate_map.is_empty() {
3840 return String::new();
3841 }
3842 let mut entries: Vec<(&String, usize)> = builder
3843 .ext_gstate_map
3844 .values()
3845 .map(|(obj_id, name)| (name, *obj_id))
3846 .collect();
3847 entries.sort_by_key(|(name, _)| (*name).clone());
3848 entries
3849 .iter()
3850 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3851 .collect::<Vec<_>>()
3852 .join(" ")
3853 }
3854
3855 fn write_image_xobject(
3858 builder: &mut PdfBuilder,
3859 image: &crate::image_loader::LoadedImage,
3860 ) -> usize {
3861 use crate::image_loader::{ImagePixelData, JpegColorSpace};
3862
3863 match &image.pixel_data {
3864 ImagePixelData::Jpeg { data, color_space } => {
3865 let color_space_str = match color_space {
3866 JpegColorSpace::DeviceRGB => "/DeviceRGB",
3867 JpegColorSpace::DeviceGray => "/DeviceGray",
3868 };
3869
3870 let obj_id = builder.objects.len();
3871 let mut obj_data: Vec<u8> = Vec::new();
3872 let _ = write!(
3873 obj_data,
3874 "<< /Type /XObject /Subtype /Image \
3875 /Width {} /Height {} \
3876 /ColorSpace {} \
3877 /BitsPerComponent 8 \
3878 /Filter /DCTDecode \
3879 /Length {} >>\nstream\n",
3880 image.width_px,
3881 image.height_px,
3882 color_space_str,
3883 data.len()
3884 );
3885 obj_data.extend_from_slice(data);
3886 obj_data.extend_from_slice(b"\nendstream");
3887 builder.objects.push(PdfObject {
3888 id: obj_id,
3889 data: obj_data,
3890 });
3891 obj_id
3892 }
3893
3894 ImagePixelData::Decoded { rgb, alpha } => {
3895 let smask_id = alpha.as_ref().map(|alpha_data| {
3897 let compressed_alpha = compress_to_vec_zlib(alpha_data, 6);
3898 let smask_obj_id = builder.objects.len();
3899 let mut smask_data: Vec<u8> = Vec::new();
3900 let _ = write!(
3901 smask_data,
3902 "<< /Type /XObject /Subtype /Image \
3903 /Width {} /Height {} \
3904 /ColorSpace /DeviceGray \
3905 /BitsPerComponent 8 \
3906 /Filter /FlateDecode \
3907 /Length {} >>\nstream\n",
3908 image.width_px,
3909 image.height_px,
3910 compressed_alpha.len()
3911 );
3912 smask_data.extend_from_slice(&compressed_alpha);
3913 smask_data.extend_from_slice(b"\nendstream");
3914 builder.objects.push(PdfObject {
3915 id: smask_obj_id,
3916 data: smask_data,
3917 });
3918 smask_obj_id
3919 });
3920
3921 let compressed_rgb = compress_to_vec_zlib(rgb, 6);
3923 let obj_id = builder.objects.len();
3924 let mut obj_data: Vec<u8> = Vec::new();
3925
3926 let smask_ref = smask_id
3927 .map(|id| format!(" /SMask {} 0 R", id))
3928 .unwrap_or_default();
3929
3930 let _ = write!(
3931 obj_data,
3932 "<< /Type /XObject /Subtype /Image \
3933 /Width {} /Height {} \
3934 /ColorSpace /DeviceRGB \
3935 /BitsPerComponent 8 \
3936 /Filter /FlateDecode \
3937 /Length {}{} >>\nstream\n",
3938 image.width_px,
3939 image.height_px,
3940 compressed_rgb.len(),
3941 smask_ref
3942 );
3943 obj_data.extend_from_slice(&compressed_rgb);
3944 obj_data.extend_from_slice(b"\nendstream");
3945 builder.objects.push(PdfObject {
3946 id: obj_id,
3947 data: obj_data,
3948 });
3949 obj_id
3950 }
3951 }
3952 }
3953
3954 fn build_shading_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3958 let mut entries: Vec<(String, usize)> = builder
3959 .shading_map
3960 .iter()
3961 .filter(|(&(p, _), _)| p == page_idx)
3962 .map(|(_, (obj_id, name))| (name.clone(), *obj_id))
3963 .collect();
3964 if entries.is_empty() {
3965 return String::new();
3966 }
3967 entries.sort_by(|a, b| a.0.cmp(&b.0));
3968 entries
3969 .iter()
3970 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3971 .collect::<Vec<_>>()
3972 .join(" ")
3973 }
3974
3975 fn build_xobject_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3976 let mut entries: Vec<(usize, usize)> = Vec::new();
3977 for (&(pidx, _), &img_idx) in &builder.image_index_map {
3978 if pidx == page_idx {
3979 let obj_id = builder.image_objects[img_idx];
3980 entries.push((img_idx, obj_id));
3981 }
3982 }
3983 if let Some(&(img_idx, _, _)) = builder.page_background_image_map.get(&page_idx) {
3986 let obj_id = builder.image_objects[img_idx];
3987 entries.push((img_idx, obj_id));
3988 }
3989 if entries.is_empty() {
3990 return String::new();
3991 }
3992 entries.sort_by_key(|(idx, _)| *idx);
3993 entries.dedup();
3994 entries
3995 .iter()
3996 .map(|(idx, obj_id)| format!("/Im{} {} 0 R", idx, obj_id))
3997 .collect::<Vec<_>>()
3998 .join(" ")
3999 }
4000
4001 fn write_custom_font_objects(
4008 builder: &mut PdfBuilder,
4009 key: &FontKey,
4010 ttf_data: &[u8],
4011 used_glyph_ids: HashSet<u16>,
4012 used_chars: HashSet<char>,
4013 glyph_to_text_map: HashMap<u16, String>,
4014 ) -> Result<usize, FormeError> {
4015 let face = ttf_parser::Face::parse(ttf_data, 0).map_err(|e| {
4016 FormeError::FontError(format!(
4017 "Failed to parse TTF data for font '{}': {}",
4018 key.family, e
4019 ))
4020 })?;
4021
4022 let units_per_em = face.units_per_em();
4023 let ascender = face.ascender();
4024 let descender = face.descender();
4025
4026 let mut char_to_orig_gid: HashMap<char, u16> = HashMap::new();
4028 for &ch in &used_chars {
4029 if let Some(gid) = face.glyph_index(ch) {
4030 char_to_orig_gid.insert(ch, gid.0);
4031 }
4032 }
4033
4034 let mut all_orig_gids: HashSet<u16> = used_glyph_ids.clone();
4038 for &gid in char_to_orig_gid.values() {
4039 all_orig_gids.insert(gid);
4040 }
4041
4042 let (embed_ttf, gid_remap) = match subset_ttf(ttf_data, &all_orig_gids) {
4044 Ok(subset_result) => (subset_result.ttf_data, subset_result.gid_remap),
4045 Err(_) => {
4046 let identity: HashMap<u16, u16> =
4048 all_orig_gids.iter().map(|&gid| (gid, gid)).collect();
4049 (ttf_data.to_vec(), identity)
4050 }
4051 };
4052
4053 let char_to_gid: HashMap<char, u16> = char_to_orig_gid
4055 .iter()
4056 .filter_map(|(&ch, &orig_gid)| gid_remap.get(&orig_gid).map(|&new_gid| (ch, new_gid)))
4057 .collect();
4058
4059 let gid_remap_for_embed = gid_remap.clone();
4061
4062 let mut new_gid_to_text: HashMap<u16, String> = HashMap::new();
4064 for (orig_gid, text) in &glyph_to_text_map {
4066 if let Some(&new_gid) = gid_remap.get(orig_gid) {
4067 new_gid_to_text
4068 .entry(new_gid)
4069 .or_insert_with(|| text.clone());
4070 }
4071 }
4072 for (&ch, &new_gid) in &char_to_gid {
4074 new_gid_to_text
4075 .entry(new_gid)
4076 .or_insert_with(|| ch.to_string());
4077 }
4078
4079 let pdf_font_name = Self::sanitize_font_name(&key.family, key.weight, key.italic);
4080
4081 let compressed_ttf = compress_to_vec_zlib(&embed_ttf, 6);
4083 let fontfile2_id = builder.objects.len();
4084 let mut fontfile2_data: Vec<u8> = Vec::new();
4085 let _ = write!(
4086 fontfile2_data,
4087 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
4088 compressed_ttf.len(),
4089 embed_ttf.len()
4090 );
4091 fontfile2_data.extend_from_slice(&compressed_ttf);
4092 fontfile2_data.extend_from_slice(b"\nendstream");
4093 builder.objects.push(PdfObject {
4094 id: fontfile2_id,
4095 data: fontfile2_data,
4096 });
4097
4098 let subset_face = ttf_parser::Face::parse(&embed_ttf, 0).unwrap_or_else(|_| face.clone());
4100 let subset_upem = subset_face.units_per_em();
4101
4102 let font_descriptor_id = builder.objects.len();
4104 let bbox = face.global_bounding_box();
4105 let scale = 1000.0 / units_per_em as f64;
4106 let bbox_str = format!(
4107 "[{} {} {} {}]",
4108 (bbox.x_min as f64 * scale) as i32,
4109 (bbox.y_min as f64 * scale) as i32,
4110 (bbox.x_max as f64 * scale) as i32,
4111 (bbox.y_max as f64 * scale) as i32,
4112 );
4113
4114 let flags = 4u32;
4115 let cap_height = face.capital_height().unwrap_or(ascender) as f64 * scale;
4116 let stem_v = if key.weight >= 700 { 120 } else { 80 };
4117
4118 let font_descriptor_dict = format!(
4119 "<< /Type /FontDescriptor /FontName /{} /Flags {} \
4120 /FontBBox {} /ItalicAngle {} \
4121 /Ascent {} /Descent {} /CapHeight {} /StemV {} \
4122 /FontFile2 {} 0 R >>",
4123 pdf_font_name,
4124 flags,
4125 bbox_str,
4126 if key.italic { -12 } else { 0 },
4127 (ascender as f64 * scale) as i32,
4128 (descender as f64 * scale) as i32,
4129 cap_height as i32,
4130 stem_v,
4131 fontfile2_id,
4132 );
4133 builder.objects.push(PdfObject {
4134 id: font_descriptor_id,
4135 data: font_descriptor_dict.into_bytes(),
4136 });
4137
4138 let cidfont_id = builder.objects.len();
4140 let (w_array, pdf_widths) =
4142 Self::build_w_array_from_gids(&gid_remap, &subset_face, subset_upem);
4143 let default_width = subset_face
4144 .glyph_hor_advance(ttf_parser::GlyphId(0))
4145 .map(|adv| (adv as f64 * 1000.0 / subset_upem as f64) as u32)
4146 .unwrap_or(1000);
4147 let cidfont_dict = format!(
4148 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{} \
4149 /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> \
4150 /FontDescriptor {} 0 R /DW {} /W {} \
4151 /CIDToGIDMap /Identity >>",
4152 pdf_font_name, font_descriptor_id, default_width, w_array,
4153 );
4154 builder.objects.push(PdfObject {
4155 id: cidfont_id,
4156 data: cidfont_dict.into_bytes(),
4157 });
4158
4159 let tounicode_id = builder.objects.len();
4161 let cmap_content = Self::build_tounicode_cmap_from_gids(&new_gid_to_text, &pdf_font_name);
4162 let compressed_cmap = compress_to_vec_zlib(cmap_content.as_bytes(), 6);
4163 let mut tounicode_data: Vec<u8> = Vec::new();
4164 let _ = write!(
4165 tounicode_data,
4166 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
4167 compressed_cmap.len()
4168 );
4169 tounicode_data.extend_from_slice(&compressed_cmap);
4170 tounicode_data.extend_from_slice(b"\nendstream");
4171 builder.objects.push(PdfObject {
4172 id: tounicode_id,
4173 data: tounicode_data,
4174 });
4175
4176 let type0_id = builder.objects.len();
4178 let type0_dict = format!(
4179 "<< /Type /Font /Subtype /Type0 /BaseFont /{} \
4180 /Encoding /Identity-H \
4181 /DescendantFonts [{} 0 R] \
4182 /ToUnicode {} 0 R >>",
4183 pdf_font_name, cidfont_id, tounicode_id,
4184 );
4185 builder.objects.push(PdfObject {
4186 id: type0_id,
4187 data: type0_dict.into_bytes(),
4188 });
4189
4190 builder.custom_font_data.insert(
4192 key.clone(),
4193 CustomFontEmbedData {
4194 ttf_data: embed_ttf,
4195 gid_remap: gid_remap_for_embed,
4196 glyph_to_text: glyph_to_text_map,
4197 char_to_gid,
4198 pdf_widths,
4199 default_width,
4200 units_per_em,
4201 ascender,
4202 descender,
4203 },
4204 );
4205
4206 Ok(type0_id)
4207 }
4208
4209 fn build_w_array_from_gids(
4211 gid_remap: &HashMap<u16, u16>,
4212 face: &ttf_parser::Face,
4213 units_per_em: u16,
4214 ) -> (String, HashMap<u16, f64>) {
4215 let scale = 1000.0 / units_per_em as f64;
4216
4217 let mut entries: Vec<(u16, f64)> = Vec::new();
4218 let mut seen_gids: HashSet<u16> = HashSet::new();
4219
4220 for &new_gid in gid_remap.values() {
4221 if seen_gids.contains(&new_gid) {
4222 continue;
4223 }
4224 seen_gids.insert(new_gid);
4225 let advance = face
4226 .glyph_hor_advance(ttf_parser::GlyphId(new_gid))
4227 .unwrap_or(0);
4228 let width = advance as f64 * scale;
4232 entries.push((new_gid, width));
4233 }
4234
4235 entries.sort_by_key(|(gid, _)| *gid);
4236
4237 let mut result = String::from("[");
4239 for (gid, width) in &entries {
4240 let _ = write!(result, " {} [{}]", gid, pdf_number(*width));
4241 }
4242 result.push_str(" ]");
4243 (result, entries.into_iter().collect())
4244 }
4245
4246 fn build_tounicode_cmap_from_gids(
4253 gid_to_text: &HashMap<u16, String>,
4254 font_name: &str,
4255 ) -> String {
4256 let mut gid_to_unicode: Vec<(u16, String)> = gid_to_text
4257 .iter()
4258 .filter(|(_, text)| !text.is_empty())
4259 .map(|(&gid, text)| {
4260 let hex: String = text
4261 .encode_utf16()
4262 .map(|unit| format!("{:04X}", unit))
4263 .collect();
4264 (gid, hex)
4265 })
4266 .collect();
4267 gid_to_unicode.sort_by_key(|(gid, _)| *gid);
4268
4269 let mut cmap = String::new();
4270 let _ = writeln!(cmap, "/CIDInit /ProcSet findresource begin");
4271 let _ = writeln!(cmap, "12 dict begin");
4272 let _ = writeln!(cmap, "begincmap");
4273 let _ = writeln!(cmap, "/CIDSystemInfo");
4274 let _ = writeln!(
4275 cmap,
4276 "<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def"
4277 );
4278 let _ = writeln!(cmap, "/CMapName /{}-UTF16 def", font_name);
4279 let _ = writeln!(cmap, "/CMapType 2 def");
4280 let _ = writeln!(cmap, "1 begincodespacerange");
4281 let _ = writeln!(cmap, "<0000> <FFFF>");
4282 let _ = writeln!(cmap, "endcodespacerange");
4283
4284 for chunk in gid_to_unicode.chunks(100) {
4286 let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
4287 for (gid, unicode) in chunk {
4288 let _ = writeln!(cmap, "<{:04X}> <{}>", gid, unicode);
4289 }
4290 let _ = writeln!(cmap, "endbfchar");
4291 }
4292
4293 let _ = writeln!(cmap, "endcmap");
4294 let _ = writeln!(cmap, "CMapName currentdict /CMap defineresource pop");
4295 let _ = writeln!(cmap, "end");
4296 let _ = writeln!(cmap, "end");
4297
4298 cmap
4299 }
4300
4301 fn sanitize_font_name(family: &str, weight: u32, italic: bool) -> String {
4304 let mut name: String = family
4305 .chars()
4306 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
4307 .collect();
4308
4309 if weight >= 700 {
4310 name.push_str("-Bold");
4311 }
4312 if italic {
4313 name.push_str("-Italic");
4314 }
4315
4316 if name.is_empty() {
4318 name = "CustomFont".to_string();
4319 }
4320
4321 name
4322 }
4323
4324 fn build_font_resource_dict(&self, font_objects: &[(FontKey, usize)]) -> String {
4325 font_objects
4326 .iter()
4327 .enumerate()
4328 .map(|(i, (_, obj_id))| format!("/F{} {} 0 R", i, obj_id))
4329 .collect::<Vec<_>>()
4330 .join(" ")
4331 }
4332
4333 fn font_index(
4335 &self,
4336 family: &str,
4337 weight: u32,
4338 font_style: FontStyle,
4339 font_objects: &[(FontKey, usize)],
4340 ) -> usize {
4341 let italic = matches!(font_style, FontStyle::Italic | FontStyle::Oblique);
4342
4343 for (i, (key, _)) in font_objects.iter().enumerate() {
4345 if key.family == family && key.weight == weight && key.italic == italic {
4346 return i;
4347 }
4348 }
4349
4350 let snapped = if weight >= 600 { 700 } else { 400 };
4352 for (i, (key, _)) in font_objects.iter().enumerate() {
4353 if key.family == family && key.weight == snapped && key.italic == italic {
4354 return i;
4355 }
4356 }
4357
4358 for (i, (key, _)) in font_objects.iter().enumerate() {
4360 if key.family == "Helvetica" && key.weight == snapped && key.italic == italic {
4361 return i;
4362 }
4363 }
4364
4365 0
4367 }
4368
4369 fn positioned_tj(
4384 group: &[&PositionedGlyph],
4385 gids: &[u16],
4386 embed: &CustomFontEmbedData,
4387 char_spacing: f64,
4388 ) -> Option<String> {
4389 let size = group.first()?.font_size;
4390 if size <= 0.0 {
4391 return None;
4392 }
4393 let mut adjustments = vec![0.0_f64; group.len()];
4394 let mut any = false;
4395 for i in 0..group.len().saturating_sub(1) {
4396 let width = embed
4397 .pdf_widths
4398 .get(&gids[i])
4399 .copied()
4400 .unwrap_or(embed.default_width as f64);
4401 let drawn = width * size / 1000.0 + char_spacing;
4402 let wanted = group[i + 1].x_offset - group[i].x_offset;
4403 let delta = drawn - wanted;
4404 if delta.abs() > 0.001 {
4405 adjustments[i] = delta * 1000.0 / size;
4406 any = true;
4407 }
4408 }
4409 let rises: Vec<f64> = group
4410 .iter()
4411 .map(|g| {
4412 if g.y_offset.abs() > 0.001 {
4413 g.y_offset
4414 } else {
4415 0.0
4416 }
4417 })
4418 .collect();
4419 if !any && rises.iter().all(|r| *r == 0.0) {
4420 return None;
4421 }
4422 let mut out = String::new();
4427 let mut rise = 0.0_f64;
4428 let mut open = false;
4429 for (i, gid) in gids.iter().enumerate() {
4430 if rises[i] != rise {
4431 if open {
4432 out.push_str("] TJ\n");
4433 open = false;
4434 }
4435 let _ = writeln!(out, "{} Ts", pdf_number(rises[i]));
4436 rise = rises[i];
4437 }
4438 if !open {
4439 out.push('[');
4440 open = true;
4441 }
4442 let _ = write!(out, "<{:04X}>", gid);
4443 if adjustments[i] != 0.0 && i + 1 < gids.len() {
4444 let _ = write!(out, " {:.2} ", adjustments[i]);
4445 }
4446 }
4447 out.push_str("] TJ");
4448 if rise != 0.0 {
4449 out.push_str("\n0 Ts");
4450 }
4451 Some(out)
4452 }
4453
4454 fn group_glyphs_by_style(glyphs: &[PositionedGlyph]) -> Vec<Vec<&PositionedGlyph>> {
4455 Self::group_glyphs(glyphs, false)
4456 }
4457
4458 fn group_glyphs(glyphs: &[PositionedGlyph], split_on_href: bool) -> Vec<Vec<&PositionedGlyph>> {
4461 if glyphs.is_empty() {
4462 return vec![];
4463 }
4464
4465 let mut groups: Vec<Vec<&PositionedGlyph>> = Vec::new();
4466 let mut current_group: Vec<&PositionedGlyph> = vec![&glyphs[0]];
4467
4468 for glyph in &glyphs[1..] {
4469 let prev = current_group.last().unwrap();
4470 let same_style = glyph.font_family == prev.font_family
4471 && glyph.font_weight == prev.font_weight
4472 && std::mem::discriminant(&glyph.font_style)
4473 == std::mem::discriminant(&prev.font_style)
4474 && (glyph.font_size - prev.font_size).abs() < 0.01
4475 && Self::colors_equal(&glyph.color, &prev.color)
4476 && std::mem::discriminant(&glyph.text_decoration)
4477 == std::mem::discriminant(&prev.text_decoration)
4478 && (!split_on_href || glyph.href == prev.href);
4479
4480 if same_style {
4481 current_group.push(glyph);
4482 } else {
4483 groups.push(current_group);
4484 current_group = vec![glyph];
4485 }
4486 }
4487 groups.push(current_group);
4488 groups
4489 }
4490
4491 fn group_link_runs<'a>(groups: &[Vec<&'a PositionedGlyph>]) -> Vec<Option<&'a str>> {
4496 let hrefs: Vec<Option<&'a str>> = groups
4497 .iter()
4498 .map(|g| g[0].href.as_deref().filter(|h| !h.is_empty()))
4499 .collect();
4500 let mut out = vec![None; groups.len()];
4501 let mut i = 0;
4502 while i < groups.len() {
4503 let mut j = i + 1;
4504 while j < groups.len() && hrefs[j] == hrefs[i] {
4505 j += 1;
4506 }
4507 let inked = groups[i..j]
4508 .iter()
4509 .any(|g| g.iter().any(|gl| !gl.char_value.is_whitespace()));
4510 if hrefs[i].is_some() && inked {
4511 for slot in &mut out[i..j] {
4512 *slot = hrefs[i];
4513 }
4514 }
4515 i = j;
4516 }
4517 out
4518 }
4519
4520 fn colors_equal(a: &Option<Color>, b: &Option<Color>) -> bool {
4521 match (a, b) {
4522 (None, None) => true,
4523 (Some(ca), Some(cb)) => {
4524 (ca.r - cb.r).abs() < 0.001
4525 && (ca.g - cb.g).abs() < 0.001
4526 && (ca.b - cb.b).abs() < 0.001
4527 && (ca.a - cb.a).abs() < 0.001
4528 }
4529 _ => false,
4530 }
4531 }
4532
4533 fn warn_nested_links(
4542 elements: &[LayoutElement],
4543 outer: Option<&str>,
4544 warnings: &mut Vec<String>,
4545 ) {
4546 fn report(warnings: &mut Vec<String>, inner: &str, outer: &str) {
4547 let msg = format!(
4548 "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"
4549 );
4550 if !warnings.contains(&msg) {
4551 warnings.push(msg);
4552 }
4553 }
4554 for el in elements {
4555 let own = el.href.as_deref().filter(|h| !h.is_empty());
4556 if let (Some(o), Some(h)) = (outer, own) {
4557 if h != o {
4558 report(warnings, h, o);
4559 }
4560 }
4561 let enclosing = outer.or(own);
4562 if let Some(o) = enclosing {
4563 if let DrawCommand::Text { ref lines, .. } = el.draw {
4564 for g in lines.iter().flat_map(|l| l.glyphs.iter()) {
4565 if let Some(h) = g.href.as_deref().filter(|h| !h.is_empty()) {
4566 if h != o {
4567 report(warnings, h, o);
4568 }
4569 }
4570 }
4571 }
4572 }
4573 Self::warn_nested_links(&el.children, enclosing, warnings);
4574 }
4575 }
4576
4577 fn collect_link_annotations(
4578 elements: &[LayoutElement],
4579 page_height: f64,
4580 annotations: &mut Vec<LinkAnnotation>,
4581 ) {
4582 for element in elements {
4583 if let Some(ref href) = element.href {
4584 if !href.is_empty() {
4585 let pdf_y = page_height - element.y - element.height;
4586 annotations.push(LinkAnnotation {
4587 x: element.x,
4588 y: pdf_y,
4589 width: element.width,
4590 height: element.height,
4591 href: href.clone(),
4592 });
4593 continue;
4595 }
4596 }
4597 if let DrawCommand::Text { ref lines, .. } = element.draw {
4602 for line in lines {
4603 let spans = Self::inline_link_spans(line);
4604 if spans.is_empty() {
4605 continue;
4606 }
4607 let (top, height) = if lines.len() == 1 {
4611 (element.y, element.height)
4612 } else {
4613 let fs = line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
4614 (line.y - fs * 0.8 - (line.height - fs) / 2.0, line.height)
4615 };
4616 for span in spans {
4617 annotations.push(LinkAnnotation {
4618 x: span.x0,
4619 y: page_height - top - height,
4620 width: span.x1 - span.x0,
4621 height,
4622 href: span.href,
4623 });
4624 }
4625 }
4626 }
4627 Self::collect_link_annotations(&element.children, page_height, annotations);
4628 }
4629 }
4630
4631 fn inline_link_spans(line: &TextLine) -> Vec<InlineLinkSpan> {
4638 let mut spans: Vec<InlineLinkSpan> = Vec::new();
4639 if !line.glyphs.iter().any(|g| g.href.is_some()) {
4640 return spans;
4641 }
4642 let mut prev_href: Option<&str> = None;
4643 for g in &line.glyphs {
4644 let x0 = line.x + g.x_offset;
4645 let x1 = x0 + g.x_advance;
4646 let href = g.href.as_deref().filter(|h| !h.is_empty());
4647 if let Some(h) = href {
4648 if prev_href != Some(h) {
4649 spans.push(InlineLinkSpan {
4650 href: h.to_string(),
4651 x0: f64::INFINITY,
4652 x1: f64::NEG_INFINITY,
4653 });
4654 }
4655 if !g.char_value.is_whitespace() {
4659 if let Some(last) = spans.last_mut() {
4660 last.x0 = last.x0.min(x0);
4661 last.x1 = last.x1.max(x1);
4662 }
4663 }
4664 }
4665 prev_href = href;
4666 }
4667 spans.retain(|s| s.x1 > s.x0);
4669 spans
4670 }
4671
4672 fn collect_form_fields(
4674 elements: &[LayoutElement],
4675 page_height: f64,
4676 page_idx: usize,
4677 fields: &mut Vec<FormFieldData>,
4678 ) {
4679 for element in elements {
4680 if let DrawCommand::FormField {
4681 ref field_type,
4682 ref name,
4683 } = element.draw
4684 {
4685 let pdf_y = page_height - element.y - element.height;
4686 fields.push(FormFieldData {
4687 field_type: field_type.clone(),
4688 name: name.clone(),
4689 x: element.x,
4690 y: pdf_y,
4691 width: element.width,
4692 height: element.height,
4693 page_idx,
4694 });
4695 }
4696 Self::collect_form_fields(&element.children, page_height, page_idx, fields);
4697 }
4698 }
4699
4700 fn collect_bookmarks(
4702 elements: &[LayoutElement],
4703 page_height: f64,
4704 page_obj_id: usize,
4705 bookmarks: &mut Vec<PdfBookmark>,
4706 ) {
4707 for element in elements {
4708 if let Some(ref title) = element.bookmark {
4709 let y_pdf = page_height - element.y;
4710 bookmarks.push(PdfBookmark {
4711 title: title.clone(),
4712 page_obj_id,
4713 y_pdf,
4714 });
4715 }
4716 Self::collect_bookmarks(&element.children, page_height, page_obj_id, bookmarks);
4717 }
4718 }
4719
4720 fn write_outline_tree(
4723 &self,
4724 builder: &mut PdfBuilder,
4725 bookmarks: &[PdfBookmark],
4726 mut tag_builder: Option<&mut tagged::TagBuilder>,
4727 ) -> usize {
4728 let outlines_id = builder.objects.len();
4730 builder.objects.push(PdfObject {
4731 id: outlines_id,
4732 data: vec![],
4733 });
4734
4735 let mut item_ids: Vec<usize> = Vec::new();
4737 for _bm in bookmarks {
4738 let item_id = builder.objects.len();
4739 builder.objects.push(PdfObject {
4740 id: item_id,
4741 data: vec![],
4742 });
4743 item_ids.push(item_id);
4744 }
4745
4746 for (i, (bm, &item_id)) in bookmarks.iter().zip(item_ids.iter()).enumerate() {
4748 let wants_sd = tag_builder
4755 .as_mut()
4756 .map(|tb| tb.request_struct_destination(&bm.title, item_id))
4757 .unwrap_or(false);
4758 let dest = if wants_sd {
4759 format!("/SD [999999999 0 R /XYZ 0 {:.2} null]", bm.y_pdf)
4760 } else {
4761 format!("/Dest [{} 0 R /XYZ 0 {:.2} null]", bm.page_obj_id, bm.y_pdf)
4762 };
4763 let mut dict = format!(
4764 "<< /Title {} /Parent {} 0 R {}",
4765 Self::encode_text_string(&bm.title),
4766 outlines_id,
4767 dest,
4768 );
4769 if i > 0 {
4770 let _ = write!(dict, " /Prev {} 0 R", item_ids[i - 1]);
4771 }
4772 if i + 1 < item_ids.len() {
4773 let _ = write!(dict, " /Next {} 0 R", item_ids[i + 1]);
4774 }
4775 dict.push_str(" >>");
4776 builder.objects[item_id].data = dict.into_bytes();
4777 }
4778
4779 let first_id = item_ids.first().copied().unwrap_or(0);
4781 let last_id = item_ids.last().copied().unwrap_or(0);
4782 let outlines_dict = format!(
4783 "<< /Type /Outlines /First {} 0 R /Last {} 0 R /Count {} >>",
4784 first_id,
4785 last_id,
4786 bookmarks.len()
4787 );
4788 builder.objects[outlines_id].data = outlines_dict.into_bytes();
4789
4790 outlines_id
4791 }
4792
4793 fn write_svg_commands(
4795 stream: &mut String,
4796 commands: &[SvgCommand],
4797 ext_gstate_map: &HashMap<u64, (usize, String)>,
4798 ) {
4799 for cmd in commands {
4800 match cmd {
4801 SvgCommand::MoveTo(x, y) => {
4802 let _ = writeln!(stream, "{:.2} {:.2} m", x, y);
4803 }
4804 SvgCommand::LineTo(x, y) => {
4805 let _ = writeln!(stream, "{:.2} {:.2} l", x, y);
4806 }
4807 SvgCommand::CurveTo(x1, y1, x2, y2, x3, y3) => {
4808 let _ = writeln!(
4809 stream,
4810 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4811 x1, y1, x2, y2, x3, y3
4812 );
4813 }
4814 SvgCommand::ClosePath => {
4815 let _ = writeln!(stream, "h");
4816 }
4817 SvgCommand::SetFill(r, g, b) => {
4818 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", r, g, b);
4819 }
4820 SvgCommand::SetFillNone => {
4821 }
4823 SvgCommand::SetStroke(r, g, b) => {
4824 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", r, g, b);
4825 }
4826 SvgCommand::SetStrokeNone => {
4827 }
4829 SvgCommand::SetStrokeWidth(w) => {
4830 let _ = writeln!(stream, "{:.2} w", w);
4831 }
4832 SvgCommand::Fill => {
4833 let _ = writeln!(stream, "f");
4834 }
4835 SvgCommand::Stroke => {
4836 let _ = writeln!(stream, "S");
4837 }
4838 SvgCommand::FillAndStroke => {
4839 let _ = writeln!(stream, "B");
4840 }
4841 SvgCommand::SetLineCap(cap) => {
4842 let _ = writeln!(stream, "{} J", cap);
4843 }
4844 SvgCommand::SetLineJoin(join) => {
4845 let _ = writeln!(stream, "{} j", join);
4846 }
4847 SvgCommand::SaveState => {
4848 let _ = writeln!(stream, "q");
4849 }
4850 SvgCommand::RestoreState => {
4851 let _ = writeln!(stream, "Q");
4852 }
4853 SvgCommand::SetOpacity(opacity) => {
4854 if let Some((_, gs_name)) = ext_gstate_map.get(&opacity.to_bits()) {
4855 let _ = writeln!(stream, "/{} gs", gs_name);
4856 }
4857 }
4858 }
4859 }
4860 }
4861
4862 pub(crate) fn escape_pdf_string(s: &str) -> String {
4864 s.replace('\\', "\\\\")
4865 .replace('(', "\\(")
4866 .replace(')', "\\)")
4867 }
4868
4869 pub(crate) fn encode_text_string(s: &str) -> String {
4882 if s.bytes().all(|b| (0x20..=0x7E).contains(&b)) {
4883 return format!("({})", Self::escape_pdf_string(s));
4884 }
4885 let mut out = String::with_capacity(6 + s.len() * 4);
4886 out.push_str("<FEFF");
4887 for unit in s.encode_utf16() {
4888 let _ = write!(out, "{unit:04X}");
4889 }
4890 out.push('>');
4891 out
4892 }
4893
4894 fn decode_attachment_src(src: &str) -> Result<Vec<u8>, FormeError> {
4897 use base64::Engine as _;
4898 let b64 = src.rsplit_once(";base64,").map(|(_, d)| d).unwrap_or(src);
4899 base64::engine::general_purpose::STANDARD
4900 .decode(b64.trim())
4901 .map_err(|e| {
4902 FormeError::RenderError(format!(
4903 "attachment src is not valid base64 (expected base64 bytes or a data: URI): {e}"
4904 ))
4905 })
4906 }
4907
4908 fn mime_to_pdf_name(mime: &str) -> String {
4911 let mut out = String::with_capacity(mime.len() + 2);
4912 for b in mime.bytes() {
4913 let regular =
4914 b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'+' | b'\'' | b'"');
4915 if regular {
4916 out.push(b as char);
4917 } else {
4918 let _ = write!(out, "#{:02X}", b);
4919 }
4920 }
4921 out
4922 }
4923
4924 fn encode_winansi_text(builder: &PdfBuilder, s: &str) -> String {
4928 let mut result = String::with_capacity(s.len());
4929 for ch in s.chars() {
4930 let b = Self::unicode_to_winansi(ch).unwrap_or_else(|| {
4931 builder.missing_glyphs.borrow_mut().insert(ch);
4932 b'?'
4933 });
4934 match b {
4935 b'\\' => result.push_str("\\\\"),
4936 b'(' => result.push_str("\\("),
4937 b')' => result.push_str("\\)"),
4938 0x20..=0x7E => result.push(b as char),
4939 _ => {
4940 let _ = write!(result, "\\{:03o}", b);
4941 }
4942 }
4943 }
4944 result
4945 }
4946
4947 fn unicode_to_winansi(ch: char) -> Option<u8> {
4949 crate::font::unicode_to_winansi(ch)
4950 }
4951
4952 fn serialize(&self, builder: &PdfBuilder, info_obj_id: Option<usize>) -> Vec<u8> {
4954 let mut output: Vec<u8> = Vec::new();
4955 let mut offsets: Vec<usize> = vec![0; builder.objects.len()];
4956
4957 output.extend_from_slice(match builder.pdf_version {
4959 crate::model::PdfVersion::V1_7 => b"%PDF-1.7\n".as_slice(),
4960 crate::model::PdfVersion::V2_0 => b"%PDF-2.0\n".as_slice(),
4961 });
4962 output.extend_from_slice(b"%\xe2\xe3\xcf\xd3\n");
4963
4964 for (i, obj) in builder.objects.iter().enumerate().skip(1) {
4965 offsets[i] = output.len();
4966 let header = format!("{} 0 obj\n", i);
4967 output.extend_from_slice(header.as_bytes());
4968 output.extend_from_slice(&obj.data);
4969 output.extend_from_slice(b"\nendobj\n\n");
4970 }
4971
4972 let xref_offset = output.len();
4973 let _ = writeln!(output, "xref\n0 {}", builder.objects.len());
4974 let _ = writeln!(output, "0000000000 65535 f ");
4975 for offset in offsets.iter().skip(1) {
4976 let _ = writeln!(output, "{:010} 00000 n ", offset);
4977 }
4978
4979 let _ = write!(
4980 output,
4981 "trailer\n<< /Size {} /Root 1 0 R",
4982 builder.objects.len()
4983 );
4984 if let Some(info_id) = info_obj_id {
4985 let _ = write!(output, " /Info {} 0 R", info_id);
4986 }
4987 {
4993 use sha2::Digest as _;
4994 let digest = sha2::Sha256::digest(&output);
4995 let mut id_hex = String::with_capacity(32);
4996 for b in &digest[..16] {
4997 let _ = write!(id_hex, "{:02X}", b);
4998 }
4999 let _ = write!(output, " /ID [<{id_hex}> <{id_hex}>]");
5000 }
5001 let _ = writeln!(output, " >>\nstartxref\n{}\n%%EOF", xref_offset);
5002
5003 output
5004 }
5005}
5006
5007fn write_chart_primitive(
5012 stream: &mut String,
5013 prim: &crate::chart::ChartPrimitive,
5014 _chart_height: f64,
5015 builder: &PdfBuilder,
5016) {
5017 use crate::chart::{ChartPrimitive, TextAnchor};
5018 use crate::font::metrics::unicode_to_winansi;
5019
5020 match prim {
5021 ChartPrimitive::Rect { x, y, w, h, fill } => {
5022 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
5023 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re f", x, y, w, h);
5024 }
5025
5026 ChartPrimitive::Line {
5027 x1,
5028 y1,
5029 x2,
5030 y2,
5031 stroke,
5032 width,
5033 } => {
5034 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
5035 let _ = writeln!(stream, "{:.2} w", width);
5036 let _ = writeln!(stream, "{:.2} {:.2} m {:.2} {:.2} l S", x1, y1, x2, y2);
5037 }
5038
5039 ChartPrimitive::Polyline {
5040 points,
5041 stroke,
5042 width,
5043 } => {
5044 if points.len() < 2 {
5045 return;
5046 }
5047 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
5048 let _ = writeln!(stream, "{:.2} w", width);
5049 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
5050 for &(px, py) in &points[1..] {
5051 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
5052 }
5053 let _ = writeln!(stream, "S");
5054 }
5055
5056 ChartPrimitive::FilledPath {
5057 points,
5058 fill,
5059 opacity,
5060 } => {
5061 if points.len() < 3 {
5062 return;
5063 }
5064 let _ = writeln!(stream, "q");
5065 if *opacity < 1.0 {
5067 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
5068 let _ = writeln!(stream, "/{} gs", gs_name);
5069 }
5070 }
5071 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
5072 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
5073 for &(px, py) in &points[1..] {
5074 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
5075 }
5076 let _ = writeln!(stream, "h f");
5077 let _ = writeln!(stream, "Q");
5078 }
5079
5080 ChartPrimitive::Circle { cx, cy, r, fill } => {
5081 let kappa: f64 = 0.5523;
5083 let kr = kappa * r;
5084 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
5085 let _ = writeln!(stream, "{:.2} {:.2} m", cx + r, cy);
5086 let _ = writeln!(
5087 stream,
5088 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5089 cx + r,
5090 cy + kr,
5091 cx + kr,
5092 cy + r,
5093 cx,
5094 cy + r
5095 );
5096 let _ = writeln!(
5097 stream,
5098 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5099 cx - kr,
5100 cy + r,
5101 cx - r,
5102 cy + kr,
5103 cx - r,
5104 cy
5105 );
5106 let _ = writeln!(
5107 stream,
5108 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5109 cx - r,
5110 cy - kr,
5111 cx - kr,
5112 cy - r,
5113 cx,
5114 cy - r
5115 );
5116 let _ = writeln!(
5117 stream,
5118 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
5119 cx + kr,
5120 cy - r,
5121 cx + r,
5122 cy - kr,
5123 cx + r,
5124 cy
5125 );
5126 let _ = writeln!(stream, "f");
5127 }
5128
5129 ChartPrimitive::ArcSector {
5130 cx,
5131 cy,
5132 r,
5133 start_angle,
5134 end_angle,
5135 fill,
5136 } => {
5137 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
5138 let _ = writeln!(stream, "{:.2} {:.2} m", cx, cy);
5140 let sx = cx + r * start_angle.cos();
5142 let sy = cy + r * start_angle.sin();
5143 let _ = writeln!(stream, "{:.2} {:.2} l", sx, sy);
5144
5145 let mut angle = *start_angle;
5147 let total = end_angle - start_angle;
5148 let segments = ((total.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
5149 let step = total / segments as f64;
5150
5151 for _ in 0..segments {
5152 let a1 = angle;
5153 let a2 = angle + step;
5154 let alpha = 4.0 / 3.0 * ((a2 - a1) / 4.0).tan();
5155
5156 let p1x = cx + r * a1.cos();
5157 let p1y = cy + r * a1.sin();
5158 let p2x = cx + r * a2.cos();
5159 let p2y = cy + r * a2.sin();
5160
5161 let cp1x = p1x - alpha * r * a1.sin();
5162 let cp1y = p1y + alpha * r * a1.cos();
5163 let cp2x = p2x + alpha * r * a2.sin();
5164 let cp2y = p2y - alpha * r * a2.cos();
5165
5166 let _ = writeln!(
5167 stream,
5168 "{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c",
5169 cp1x, cp1y, cp2x, cp2y, p2x, p2y
5170 );
5171 angle = a2;
5172 }
5173
5174 let _ = writeln!(stream, "h f");
5176 }
5177
5178 ChartPrimitive::Label {
5179 text,
5180 x,
5181 y,
5182 font_size,
5183 color,
5184 anchor,
5185 } => {
5186 let metrics = crate::font::StandardFont::Helvetica.metrics();
5188 let text_width = metrics.measure_string(text, *font_size, 0.0);
5189 let x_offset = match anchor {
5190 TextAnchor::Left => 0.0,
5191 TextAnchor::Center => -text_width / 2.0,
5192 TextAnchor::Right => -text_width,
5193 };
5194
5195 let font_idx = builder
5197 .font_objects
5198 .iter()
5199 .enumerate()
5200 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
5201 .map(|(i, _)| i)
5202 .unwrap_or(0);
5203
5204 let encoded: String = text
5206 .chars()
5207 .map(|ch| {
5208 if let Some(code) = unicode_to_winansi(ch) {
5209 code as char
5210 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
5211 ch
5212 } else {
5213 builder.missing_glyphs.borrow_mut().insert(ch);
5214 '?'
5215 }
5216 })
5217 .collect();
5218 let escaped = pdf_escape_string(&encoded);
5219
5220 let _ = writeln!(stream, "q");
5222 let _ = writeln!(stream, "1 0 0 -1 {:.4} {:.4} cm", x + x_offset, *y);
5223 let _ = writeln!(
5224 stream,
5225 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
5226 font_idx, font_size, color.r, color.g, color.b, escaped
5227 );
5228 let _ = writeln!(stream, "Q");
5229 }
5230 ChartPrimitive::VerticalLabel {
5231 text,
5232 x,
5233 y,
5234 font_size,
5235 color,
5236 } => {
5237 let metrics = crate::font::StandardFont::Helvetica.metrics();
5238 let text_width = metrics.measure_string(text, *font_size, 0.0);
5239 let font_idx = builder
5240 .font_objects
5241 .iter()
5242 .enumerate()
5243 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
5244 .map(|(i, _)| i)
5245 .unwrap_or(0);
5246 let encoded: String = text
5247 .chars()
5248 .map(|ch| {
5249 if let Some(code) = unicode_to_winansi(ch) {
5250 code as char
5251 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
5252 ch
5253 } else {
5254 builder.missing_glyphs.borrow_mut().insert(ch);
5255 '?'
5256 }
5257 })
5258 .collect();
5259 let escaped = pdf_escape_string(&encoded);
5260
5261 let _ = writeln!(stream, "q");
5266 let _ = writeln!(
5267 stream,
5268 "0 -1 -1 0 {:.4} {:.4} cm",
5269 x + font_size * 0.35,
5270 y + text_width / 2.0
5271 );
5272 let _ = writeln!(
5273 stream,
5274 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
5275 font_idx, font_size, color.r, color.g, color.b, escaped
5276 );
5277 let _ = writeln!(stream, "Q");
5278 }
5279 }
5280}
5281
5282fn normalize_gradient_stops(
5289 stops: &[crate::style::GradientStop],
5290 fallback: Color,
5291) -> Vec<crate::style::GradientStop> {
5292 use crate::style::GradientStop;
5293 if stops.is_empty() {
5294 return vec![
5295 GradientStop {
5296 position: 0.0,
5297 color: fallback,
5298 },
5299 GradientStop {
5300 position: 1.0,
5301 color: fallback,
5302 },
5303 ];
5304 }
5305 let mut sorted: Vec<GradientStop> = stops
5306 .iter()
5307 .map(|s| GradientStop {
5308 position: s.position.clamp(0.0, 1.0),
5309 color: s.color,
5310 })
5311 .collect();
5312 sorted.sort_by(|a, b| {
5313 a.position
5314 .partial_cmp(&b.position)
5315 .unwrap_or(std::cmp::Ordering::Equal)
5316 });
5317 if sorted[0].position > 0.0 {
5318 sorted.insert(
5319 0,
5320 GradientStop {
5321 position: 0.0,
5322 color: sorted[0].color,
5323 },
5324 );
5325 }
5326 if sorted[sorted.len() - 1].position < 1.0 {
5327 let last = sorted[sorted.len() - 1].color;
5328 sorted.push(GradientStop {
5329 position: 1.0,
5330 color: last,
5331 });
5332 }
5333 sorted
5334}
5335
5336fn pdf_escape_string(s: &str) -> String {
5337 let mut out = String::with_capacity(s.len());
5338 for ch in s.chars() {
5339 match ch {
5340 '(' => out.push_str("\\("),
5341 ')' => out.push_str("\\)"),
5342 '\\' => out.push_str("\\\\"),
5343 _ => out.push(ch),
5344 }
5345 }
5346 out
5347}
5348
5349fn has_placeholder_group(group: &[&PositionedGlyph]) -> bool {
5352 group
5353 .iter()
5354 .any(|g| g.char_value == PAGE_NUMBER_SENTINEL || g.char_value == TOTAL_PAGES_SENTINEL)
5355}
5356
5357fn pdf_number(v: f64) -> String {
5360 let s = format!("{:.3}", v);
5361 let s = s.trim_end_matches('0').trim_end_matches('.');
5362 if s.is_empty() || s == "-" {
5363 "0".to_string()
5364 } else {
5365 s.to_string()
5366 }
5367}
5368
5369#[cfg(test)]
5370mod tests {
5371 use super::*;
5372 use crate::font::FontContext;
5373
5374 #[test]
5381 fn test_embedded_srgb_is_a_valid_icc_profile() {
5382 let icc: &[u8] = include_bytes!("sRGB.icc");
5383 assert!(
5384 icc.len() >= 128,
5385 "ICC shorter than its 128-byte header: {}",
5386 icc.len()
5387 );
5388 assert_ne!(
5390 icc[0], b'<',
5391 "embedded ICC starts with '<' — looks like HTML, not a profile"
5392 );
5393 assert_eq!(&icc[36..40], b"acsp", "missing ICC 'acsp' signature");
5395 let device_class = &icc[12..16];
5397 assert!(
5398 device_class == b"mntr" || device_class == b"prtr",
5399 "ICC device class {:?} is not mntr/prtr (PDF/A 6.2.3)",
5400 String::from_utf8_lossy(device_class),
5401 );
5402 assert_eq!(&icc[16..20], b"RGB ", "ICC data colour space is not RGB");
5404 }
5405
5406 #[test]
5407 fn test_escape_pdf_string() {
5408 assert_eq!(
5409 PdfWriter::escape_pdf_string("Hello (World)"),
5410 "Hello \\(World\\)"
5411 );
5412 assert_eq!(PdfWriter::escape_pdf_string("back\\slash"), "back\\\\slash");
5413 }
5414
5415 #[test]
5416 fn test_encode_text_string() {
5417 assert_eq!(PdfWriter::encode_text_string("A (b)"), "(A \\(b\\))");
5419 assert_eq!(PdfWriter::encode_text_string(""), "()");
5420 assert_eq!(PdfWriter::encode_text_string("Üb"), "<FEFF00DC0062>");
5422 assert_eq!(PdfWriter::encode_text_string("𝄞"), "<FEFFD834DD1E>");
5424 assert_eq!(PdfWriter::encode_text_string("a\nb"), "<FEFF0061000A0062>");
5426 }
5427
5428 #[test]
5429 fn test_empty_document_produces_valid_pdf() {
5430 let writer = PdfWriter::new();
5431 let font_context = FontContext::new();
5432 let pages = vec![LayoutPage {
5433 width: 595.28,
5434 height: 841.89,
5435 elements: vec![],
5436 fixed_header: vec![],
5437 fixed_footer: vec![],
5438 watermarks: vec![],
5439 config: PageConfig::default(),
5440 page_name: None,
5441 }];
5442 let metadata = Metadata::default();
5443 let (bytes, _warnings) = writer
5444 .write(
5445 &pages,
5446 &metadata,
5447 &font_context,
5448 false,
5449 None,
5450 false,
5451 None,
5452 &[],
5453 None,
5454 false,
5455 crate::model::PdfVersion::V1_7,
5456 false,
5457 )
5458 .unwrap();
5459
5460 assert!(bytes.starts_with(b"%PDF-1.7"));
5461 assert!(bytes.windows(5).any(|w| w == b"%%EOF"));
5462 assert!(bytes.windows(4).any(|w| w == b"xref"));
5463 assert!(bytes.windows(7).any(|w| w == b"trailer"));
5464 }
5465
5466 #[test]
5467 fn test_metadata_in_pdf() {
5468 let writer = PdfWriter::new();
5469 let font_context = FontContext::new();
5470 let pages = vec![LayoutPage {
5471 width: 595.28,
5472 height: 841.89,
5473 elements: vec![],
5474 fixed_header: vec![],
5475 fixed_footer: vec![],
5476 watermarks: vec![],
5477 config: PageConfig::default(),
5478 page_name: None,
5479 }];
5480 let metadata = Metadata {
5481 title: Some("Test Document".to_string()),
5482 author: Some("Forme".to_string()),
5483 subject: None,
5484 creator: None,
5485 lang: None,
5486 };
5487 let (bytes, _warnings) = writer
5488 .write(
5489 &pages,
5490 &metadata,
5491 &font_context,
5492 false,
5493 None,
5494 false,
5495 None,
5496 &[],
5497 None,
5498 false,
5499 crate::model::PdfVersion::V1_7,
5500 false,
5501 )
5502 .unwrap();
5503 let text = String::from_utf8_lossy(&bytes);
5504
5505 assert!(text.contains("/Title (Test Document)"));
5506 assert!(text.contains("/Author (Forme)"));
5507 }
5508
5509 #[test]
5510 fn test_bold_font_registered_separately() {
5511 let writer = PdfWriter::new();
5512 let font_context = FontContext::new();
5513
5514 let pages = vec![LayoutPage {
5516 width: 595.28,
5517 height: 841.89,
5518 elements: vec![
5519 LayoutElement {
5520 x: 54.0,
5521 y: 54.0,
5522 width: 100.0,
5523 height: 16.8,
5524 draw: DrawCommand::Text {
5525 lines: vec![TextLine {
5526 x: 54.0,
5527 y: 66.0,
5528 width: 50.0,
5529 height: 16.8,
5530 glyphs: vec![PositionedGlyph {
5531 glyph_id: 65,
5532 x_offset: 0.0,
5533 y_offset: 0.0,
5534 x_advance: 8.0,
5535 font_size: 12.0,
5536 font_family: "Helvetica".into(),
5537 font_weight: 400,
5538 font_style: FontStyle::Normal,
5539 char_value: 'A',
5540 color: None,
5541 href: None,
5542 text_decoration: TextDecoration::None,
5543 letter_spacing: 0.0,
5544 cluster_text: None,
5545 ligature: false,
5546 }],
5547 word_spacing: 0.0,
5548 }],
5549 color: Color::BLACK,
5550 text_decoration: TextDecoration::None,
5551 opacity: 1.0,
5552 },
5553 children: vec![],
5554 node_type: None,
5555 resolved_style: None,
5556 source_location: None,
5557 href: None,
5558 bookmark: None,
5559 alt: None,
5560 is_header_row: false,
5561 actual_text: None,
5562 list_numbering: None,
5563 col_span: 1,
5564 overflow: Overflow::default(),
5565 opacity: 1.0,
5566 },
5567 LayoutElement {
5568 x: 54.0,
5569 y: 74.0,
5570 width: 100.0,
5571 height: 16.8,
5572 draw: DrawCommand::Text {
5573 lines: vec![TextLine {
5574 x: 54.0,
5575 y: 86.0,
5576 width: 50.0,
5577 height: 16.8,
5578 glyphs: vec![PositionedGlyph {
5579 glyph_id: 65,
5580 x_offset: 0.0,
5581 y_offset: 0.0,
5582 x_advance: 8.0,
5583 font_size: 12.0,
5584 font_family: "Helvetica".into(),
5585 font_weight: 700,
5586 font_style: FontStyle::Normal,
5587 char_value: 'A',
5588 color: None,
5589 href: None,
5590 text_decoration: TextDecoration::None,
5591 letter_spacing: 0.0,
5592 cluster_text: None,
5593 ligature: false,
5594 }],
5595 word_spacing: 0.0,
5596 }],
5597 color: Color::BLACK,
5598 text_decoration: TextDecoration::None,
5599 opacity: 1.0,
5600 },
5601 children: vec![],
5602 node_type: None,
5603 resolved_style: None,
5604 source_location: None,
5605 href: None,
5606 bookmark: None,
5607 alt: None,
5608 is_header_row: false,
5609 actual_text: None,
5610 list_numbering: None,
5611 col_span: 1,
5612 overflow: Overflow::default(),
5613 opacity: 1.0,
5614 },
5615 ],
5616 fixed_header: vec![],
5617 fixed_footer: vec![],
5618 watermarks: vec![],
5619 config: PageConfig::default(),
5620 page_name: None,
5621 }];
5622
5623 let metadata = Metadata::default();
5624 let (bytes, _warnings) = writer
5625 .write(
5626 &pages,
5627 &metadata,
5628 &font_context,
5629 false,
5630 None,
5631 false,
5632 None,
5633 &[],
5634 None,
5635 false,
5636 crate::model::PdfVersion::V1_7,
5637 false,
5638 )
5639 .unwrap();
5640 let text = String::from_utf8_lossy(&bytes);
5641
5642 assert!(
5644 text.contains("Helvetica"),
5645 "Should contain regular Helvetica"
5646 );
5647 assert!(
5648 text.contains("Helvetica-Bold"),
5649 "Should contain Helvetica-Bold"
5650 );
5651 }
5652
5653 #[test]
5654 fn test_sanitize_font_name() {
5655 assert_eq!(PdfWriter::sanitize_font_name("Inter", 400, false), "Inter");
5656 assert_eq!(
5657 PdfWriter::sanitize_font_name("Inter", 700, false),
5658 "Inter-Bold"
5659 );
5660 assert_eq!(
5661 PdfWriter::sanitize_font_name("Inter", 400, true),
5662 "Inter-Italic"
5663 );
5664 assert_eq!(
5665 PdfWriter::sanitize_font_name("Inter", 700, true),
5666 "Inter-Bold-Italic"
5667 );
5668 assert_eq!(
5669 PdfWriter::sanitize_font_name("Noto Sans", 400, false),
5670 "NotoSans"
5671 );
5672 assert_eq!(
5673 PdfWriter::sanitize_font_name("Font (Display)", 400, false),
5674 "FontDisplay"
5675 );
5676 }
5677
5678 #[test]
5679 fn test_tounicode_cmap_format() {
5680 let mut glyph_to_char = HashMap::new();
5682 glyph_to_char.insert(36u16, "A".to_string());
5683 glyph_to_char.insert(37u16, "B".to_string());
5684
5685 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&glyph_to_char, "TestFont");
5686
5687 assert!(cmap.contains("begincmap"), "CMap should contain begincmap");
5688 assert!(cmap.contains("endcmap"), "CMap should contain endcmap");
5689 assert!(
5690 cmap.contains("beginbfchar"),
5691 "CMap should contain beginbfchar"
5692 );
5693 assert!(cmap.contains("endbfchar"), "CMap should contain endbfchar");
5694 assert!(
5695 cmap.contains("<0024> <0041>"),
5696 "Should map gid 0x0024 to Unicode 'A' 0x0041"
5697 );
5698 assert!(
5699 cmap.contains("<0025> <0042>"),
5700 "Should map gid 0x0025 to Unicode 'B' 0x0042"
5701 );
5702 assert!(
5703 cmap.contains("begincodespacerange"),
5704 "Should define codespace range"
5705 );
5706 assert!(
5707 cmap.contains("<0000> <FFFF>"),
5708 "Codespace should be 0000-FFFF"
5709 );
5710 }
5711
5712 #[test]
5717 fn test_tounicode_cmap_multi_char_and_non_bmp_destinations() {
5718 let mut gid_to_text = HashMap::new();
5719 gid_to_text.insert(5u16, "ffi".to_string());
5720 gid_to_text.insert(6u16, "Th".to_string());
5721 gid_to_text.insert(7u16, "\u{1F600}".to_string());
5722
5723 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&gid_to_text, "TestFont");
5724
5725 assert!(cmap.contains("<0005> <006600660069>"), "{cmap}");
5726 assert!(cmap.contains("<0006> <00540068>"), "{cmap}");
5727 assert!(cmap.contains("<0007> <D83DDE00>"), "{cmap}");
5728 }
5729
5730 fn text_glyph(
5731 glyph_id: u16,
5732 ch: char,
5733 cluster: Option<&str>,
5734 ligature: bool,
5735 ) -> PositionedGlyph {
5736 PositionedGlyph {
5737 glyph_id,
5738 x_offset: 0.0,
5739 y_offset: 0.0,
5740 x_advance: 5.0,
5741 font_size: 12.0,
5742 font_family: "Lig".into(),
5743 font_weight: 400,
5744 font_style: FontStyle::Normal,
5745 char_value: ch,
5746 color: None,
5747 href: None,
5748 text_decoration: TextDecoration::None,
5749 letter_spacing: 0.0,
5750 cluster_text: cluster.map(str::to_string),
5751 ligature,
5752 }
5753 }
5754
5755 #[test]
5756 fn test_record_glyph_text_ligature_maps_whole_cluster() {
5757 let mut map = HashMap::new();
5758 record_glyph_text(&mut map, &text_glyph(9, 'f', Some("ffi"), true));
5759 assert_eq!(map[&9], "ffi");
5760 }
5761
5762 #[test]
5766 fn test_record_glyph_text_shared_cluster_glyph_keeps_its_char() {
5767 let mut map = HashMap::new();
5768 record_glyph_text(&mut map, &text_glyph(9, 'k', Some("ki"), false));
5769 assert_eq!(map[&9], "k");
5770 }
5771
5772 #[test]
5776 fn test_record_glyph_text_single_char_beats_multi_char() {
5777 let mut first_multi = HashMap::new();
5778 record_glyph_text(
5779 &mut first_multi,
5780 &text_glyph(9, 'e', Some("e\u{FE0F}"), true),
5781 );
5782 record_glyph_text(&mut first_multi, &text_glyph(9, 'e', None, false));
5783 assert_eq!(first_multi[&9], "e");
5784
5785 let mut first_single = HashMap::new();
5786 record_glyph_text(&mut first_single, &text_glyph(9, 'e', None, false));
5787 record_glyph_text(
5788 &mut first_single,
5789 &text_glyph(9, 'e', Some("e\u{FE0F}"), true),
5790 );
5791 assert_eq!(first_single[&9], "e");
5792
5793 let mut two_multi = HashMap::new();
5795 record_glyph_text(&mut two_multi, &text_glyph(9, 'f', Some("fi"), true));
5796 record_glyph_text(&mut two_multi, &text_glyph(9, 'f', Some("ffi"), true));
5797 assert_eq!(two_multi[&9], "fi");
5798 }
5799
5800 #[test]
5801 fn test_w_array_format() {
5802 let mut char_to_gid = HashMap::new();
5803 char_to_gid.insert('A', 36u16);
5804
5805 let w_array_str = "[ 36 [600] ]";
5808 assert!(w_array_str.starts_with('['));
5809 assert!(w_array_str.ends_with(']'));
5810 }
5811
5812 #[test]
5813 fn test_hex_glyph_encoding() {
5814 let gid: u16 = 0x0041;
5816 let hex = format!("{:04X}", gid);
5817 assert_eq!(hex, "0041");
5818
5819 let gids = [0x0041u16, 0x0042, 0x0043];
5820 let hex_str: String = gids.iter().map(|g| format!("{:04X}", g)).collect();
5821 assert_eq!(hex_str, "004100420043");
5822 }
5823
5824 #[test]
5825 fn test_standard_font_still_uses_text_string() {
5826 let writer = PdfWriter::new();
5827 let font_context = FontContext::new();
5828
5829 let pages = vec![LayoutPage {
5830 width: 595.28,
5831 height: 841.89,
5832 elements: vec![LayoutElement {
5833 x: 54.0,
5834 y: 54.0,
5835 width: 100.0,
5836 height: 16.8,
5837 draw: DrawCommand::Text {
5838 lines: vec![TextLine {
5839 x: 54.0,
5840 y: 66.0,
5841 width: 50.0,
5842 height: 16.8,
5843 glyphs: vec![PositionedGlyph {
5844 glyph_id: 65,
5845 x_offset: 0.0,
5846 y_offset: 0.0,
5847 x_advance: 8.0,
5848 font_size: 12.0,
5849 font_family: "Helvetica".into(),
5850 font_weight: 400,
5851 font_style: FontStyle::Normal,
5852 char_value: 'H',
5853 color: None,
5854 href: None,
5855 text_decoration: TextDecoration::None,
5856 letter_spacing: 0.0,
5857 cluster_text: None,
5858 ligature: false,
5859 }],
5860 word_spacing: 0.0,
5861 }],
5862 color: Color::BLACK,
5863 text_decoration: TextDecoration::None,
5864 opacity: 1.0,
5865 },
5866 children: vec![],
5867 node_type: None,
5868 resolved_style: None,
5869 source_location: None,
5870 href: None,
5871 bookmark: None,
5872 alt: None,
5873 is_header_row: false,
5874 actual_text: None,
5875 list_numbering: None,
5876 col_span: 1,
5877 overflow: Overflow::default(),
5878 opacity: 1.0,
5879 }],
5880 fixed_header: vec![],
5881 fixed_footer: vec![],
5882 watermarks: vec![],
5883 config: PageConfig::default(),
5884 page_name: None,
5885 }];
5886
5887 let metadata = Metadata::default();
5888 let (bytes, _warnings) = writer
5889 .write(
5890 &pages,
5891 &metadata,
5892 &font_context,
5893 false,
5894 None,
5895 false,
5896 None,
5897 &[],
5898 None,
5899 false,
5900 crate::model::PdfVersion::V1_7,
5901 false,
5902 )
5903 .unwrap();
5904 let text = String::from_utf8_lossy(&bytes);
5905
5906 assert!(
5908 text.contains("/Type1"),
5909 "Standard font should use Type1 subtype"
5910 );
5911 assert!(
5912 !text.contains("CIDFontType2"),
5913 "Standard font should not use CIDFontType2"
5914 );
5915 }
5916}