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
53struct LinkAnnotation {
55 x: f64,
56 y: f64,
57 width: f64,
58 height: f64,
59 href: String,
60}
61
62struct PdfBookmark {
64 title: String,
65 page_obj_id: usize,
66 y_pdf: f64,
67}
68
69struct FormFieldData {
71 field_type: FormFieldType,
72 name: String,
73 x: f64,
74 y: f64,
75 width: f64,
76 height: f64,
77 page_idx: usize,
78}
79
80pub struct PdfWriter;
81
82#[allow(dead_code)]
84struct CustomFontEmbedData {
85 ttf_data: Vec<u8>,
86 gid_remap: HashMap<u16, u16>,
88 glyph_to_char: HashMap<u16, char>,
90 char_to_gid: HashMap<char, u16>,
92 units_per_em: u16,
93 ascender: i16,
94 descender: i16,
95}
96
97struct FontUsage {
99 chars: HashSet<char>,
101 glyph_ids: HashSet<u16>,
103 glyph_to_char: HashMap<u16, char>,
105}
106
107struct PdfBuilder {
109 objects: Vec<PdfObject>,
110 font_objects: Vec<(FontKey, usize)>,
112 custom_font_data: HashMap<FontKey, CustomFontEmbedData>,
114 embedded_standard_fonts: std::collections::HashSet<FontKey>,
119 image_objects: Vec<usize>,
122 image_index_map: HashMap<(usize, usize), usize>,
125 page_background_image_map: HashMap<usize, (usize, u32, u32)>,
130 page_background_url_cache: HashMap<String, (usize, u32, u32)>,
134 ext_gstate_map: HashMap<u64, (usize, String)>,
137 shading_map: HashMap<(usize, usize), (usize, String)>,
141 warnings: Vec<String>,
145 missing_glyphs: std::cell::RefCell<std::collections::BTreeSet<char>>,
153 pdf_version: crate::model::PdfVersion,
156}
157
158pub(crate) struct PdfObject {
159 #[allow(dead_code)]
160 pub(crate) id: usize,
161 pub(crate) data: Vec<u8>,
162}
163
164impl Default for PdfWriter {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170impl PdfWriter {
171 pub fn new() -> Self {
172 Self
173 }
174
175 #[allow(clippy::too_many_arguments)]
197 pub fn write(
198 &self,
199 pages: &[LayoutPage],
200 metadata: &Metadata,
201 font_context: &FontContext,
202 tagged: bool,
203 pdfa: Option<&PdfAConformance>,
204 pdf_ua: bool,
205 embedded_data: Option<&str>,
206 attachments: &[Attachment],
207 zugferd: Option<&ZugferdMeta>,
208 flatten_forms: bool,
209 pdf_version: crate::model::PdfVersion,
210 pdf_ua2: bool,
211 ) -> Result<(Vec<u8>, Vec<String>), FormeError> {
212 if let Some(level) = pdfa {
220 if !level.allows_attachments() && (embedded_data.is_some() || !attachments.is_empty()) {
221 use crate::model::PdfAConformance as L;
222 let (family, clause, remedy) = match level {
223 L::A4 => ("PDF/A-4", "ISO 19005-4", "pdfa: \"4f\""),
224 _ => (
225 "PDF/A-2",
226 "ISO 19005-2, 6.8",
227 "a PDF/A-3 level — e.g. pdfa: \"3b\"",
228 ),
229 };
230 return Err(FormeError::RenderError(format!(
231 "{family} forbids embedded files that are not themselves PDF/A \
232 ({clause}), which the engine cannot verify. Use {remedy}, which \
233 permits arbitrary attachments, or remove the attachment / embedData."
234 )));
235 }
236 if matches!(level, crate::model::PdfAConformance::A4f)
241 && embedded_data.is_none()
242 && attachments.is_empty()
243 {
244 return Err(FormeError::RenderError(
245 "pdfa: \"4f\" requires at least one embedded file (ISO 19005-4, \
246 Annex A; veraPDF 6.9-t5 — the EmbeddedFiles name tree must exist). \
247 Add an attachment or embedData, or claim pdfa: \"4\" instead."
248 .to_string(),
249 ));
250 }
251 }
252 let zugferd_filename: Option<String> = if let Some(z) = zugferd {
256 const LEVELS: [&str; 6] = [
257 "MINIMUM",
258 "BASIC WL",
259 "BASIC",
260 "EN 16931",
261 "EXTENDED",
262 "XRECHNUNG",
263 ];
264 if !LEVELS.contains(&z.conformance_level.as_str()) {
265 return Err(FormeError::RenderError(format!(
266 "zugferd.conformanceLevel {:?} is not a Factur-X profile — expected one of \
267 MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, XRECHNUNG (exact spelling, \
268 spaces included).",
269 z.conformance_level
270 )));
271 }
272 if !pdfa.is_some_and(|l| l.allows_attachments()) {
273 return Err(FormeError::RenderError(
274 "Factur-X/ZUGFeRD (zugferd) requires a PDF/A-3 conformance level — set \
275 pdfa: \"3b\" (or \"3a\"/\"3u\"). The e-invoice XML is an embedded file, \
276 which only PDF/A-3 permits."
277 .to_string(),
278 ));
279 }
280 let filename = z.document_file_name.clone().unwrap_or_else(|| {
281 if z.conformance_level == "XRECHNUNG" {
282 "xrechnung.xml".to_string()
283 } else {
284 "factur-x.xml".to_string()
285 }
286 });
287 if !attachments.iter().any(|a| a.name == filename) {
288 return Err(FormeError::RenderError(format!(
289 "zugferd is set but no attachment is named {filename:?} — the XMP would \
290 point at a file that isn't embedded. Attach the invoice XML with name: \
291 {filename:?}, or set zugferd.documentFileName to the attachment's name."
292 )));
293 }
294 Some(filename)
295 } else {
296 None
297 };
298 let mut builder = PdfBuilder {
299 objects: Vec::new(),
300 font_objects: Vec::new(),
301 custom_font_data: HashMap::new(),
302 embedded_standard_fonts: std::collections::HashSet::new(),
303 image_objects: Vec::new(),
304 image_index_map: HashMap::new(),
305 page_background_image_map: HashMap::new(),
306 page_background_url_cache: HashMap::new(),
307 ext_gstate_map: HashMap::new(),
308 shading_map: HashMap::new(),
309 warnings: Vec::new(),
310 missing_glyphs: Default::default(),
311 pdf_version: Default::default(),
312 };
313
314 builder.objects.push(PdfObject {
320 id: 0,
321 data: vec![],
322 });
323 builder.objects.push(PdfObject {
324 id: 1,
325 data: vec![],
326 });
327 builder.objects.push(PdfObject {
328 id: 2,
329 data: vec![],
330 });
331
332 builder.pdf_version = pdf_version;
334 self.register_fonts(
335 &mut builder,
336 pages,
337 font_context,
338 pdf_ua,
339 pdfa.is_some(),
340 pdf_version,
341 )?;
342
343 if pdfa.is_some() {
349 for (key, _) in &builder.font_objects {
350 if !builder.custom_font_data.contains_key(key)
351 && !builder.embedded_standard_fonts.contains(key)
352 {
353 return Err(FormeError::RenderError(format!(
354 "PDF/A requires all fonts to be embedded, but '{}' is not. Register a \
355 metric-compatible font — install @formepdf/fonts-standard and register \
356 its fonts (`for (const f of standardFonts()) Font.register(f)`), or supply \
357 your own via Font.register().",
358 key.family
359 )));
360 }
361 }
362 }
363
364 self.register_images(&mut builder, pages);
366
367 self.register_page_background_images(&mut builder, pages);
371
372 self.register_ext_gstates(&mut builder, pages);
374
375 self.register_shadings(&mut builder, pages);
377
378 let mut tag_builder = if tagged {
381 Some(tagged::TagBuilder::new(pages.len(), pdf_ua2))
382 } else {
383 None
384 };
385
386 let mut page_obj_ids: Vec<usize> = Vec::new();
390 let mut all_bookmarks: Vec<PdfBookmark> = Vec::new();
391 let mut per_page_content_obj_ids: Vec<usize> = Vec::new();
392 let mut per_page_annotations: Vec<Vec<LinkAnnotation>> = Vec::new();
393 let mut per_page_resources: Vec<String> = Vec::new();
394 let mut all_form_fields: Vec<FormFieldData> = Vec::new();
395
396 for (page_idx, page) in pages.iter().enumerate() {
398 let content = self.build_content_stream_for_page(
399 page,
400 page_idx,
401 &builder,
402 page_idx + 1,
403 pages.len(),
404 tag_builder.as_mut(),
405 flatten_forms,
406 );
407 let compressed = compress_to_vec_zlib(content.as_bytes(), 6);
408
409 let content_obj_id = builder.objects.len();
410 let mut content_data: Vec<u8> = Vec::new();
411 let _ = write!(
412 content_data,
413 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
414 compressed.len()
415 );
416 content_data.extend_from_slice(&compressed);
417 content_data.extend_from_slice(b"\nendstream");
418 builder.objects.push(PdfObject {
419 id: content_obj_id,
420 data: content_data,
421 });
422 per_page_content_obj_ids.push(content_obj_id);
423
424 let mut annotations: Vec<LinkAnnotation> = Vec::new();
426 Self::collect_link_annotations(&page.elements, page.height, &mut annotations);
427 per_page_annotations.push(annotations);
428
429 Self::collect_form_fields(&page.elements, page.height, page_idx, &mut all_form_fields);
431
432 let page_obj_id = builder.objects.len();
434 builder.objects.push(PdfObject {
435 id: page_obj_id,
436 data: vec![],
437 });
438
439 let font_resources = self.build_font_resource_dict(&builder.font_objects);
441 let xobject_resources = self.build_xobject_resource_dict(page_idx, &builder);
442 let ext_gstate_resources = self.build_ext_gstate_resource_dict(&builder);
443 let shading_resources = self.build_shading_resource_dict(page_idx, &builder);
444 let mut resources = format!("/Font << {} >>", font_resources);
445 if !xobject_resources.is_empty() {
446 let _ = write!(resources, " /XObject << {} >>", xobject_resources);
447 }
448 if !ext_gstate_resources.is_empty() {
449 let _ = write!(resources, " /ExtGState << {} >>", ext_gstate_resources);
450 }
451 if !shading_resources.is_empty() {
452 let _ = write!(resources, " /Shading << {} >>", shading_resources);
453 }
454 per_page_resources.push(resources);
455
456 Self::collect_bookmarks(&page.elements, page.height, page_obj_id, &mut all_bookmarks);
458
459 page_obj_ids.push(page_obj_id);
460 }
461
462 for (page_idx, annotations) in per_page_annotations.iter().enumerate() {
464 let mut annot_obj_ids: Vec<usize> = Vec::new();
465 for annot in annotations {
466 let rect = format!(
467 "[{:.2} {:.2} {:.2} {:.2}]",
468 annot.x,
469 annot.y,
470 annot.x + annot.width,
471 annot.y + annot.height
472 );
473
474 if let Some(anchor) = annot.href.strip_prefix('#') {
475 if let Some(bm) = all_bookmarks.iter().find(|b| b.title == anchor) {
477 let annot_obj_id = builder.objects.len();
478 let sp_str = tag_builder
482 .as_mut()
483 .and_then(|tb| {
484 tb.connect_link_annotation(page_idx, &annot.href, annot_obj_id)
485 })
486 .map(|sp| format!(" /StructParent {}", sp))
487 .unwrap_or_default();
488 let contents = Self::escape_pdf_string(&format!("Link to {anchor}"));
491 let wants_sd = tag_builder
498 .as_mut()
499 .map(|tb| tb.request_struct_destination(anchor, annot_obj_id))
500 .unwrap_or(false);
501 let sd_str = if wants_sd {
502 format!(" /SD [999999999 0 R /XYZ 0 {:.2} null]", bm.y_pdf)
503 } else {
504 String::new()
505 };
506 let annot_dict = format!(
507 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
508 /F 4 /Contents ({}){} \
509 /A << /S /GoTo /D [{} 0 R /XYZ 0 {:.2} null]{} >> >>",
510 rect, contents, sp_str, bm.page_obj_id, bm.y_pdf, sd_str
511 );
512 builder.objects.push(PdfObject {
513 id: annot_obj_id,
514 data: annot_dict.into_bytes(),
515 });
516 annot_obj_ids.push(annot_obj_id);
517 }
518 } else {
520 let annot_obj_id = builder.objects.len();
522 let sp_str = tag_builder
523 .as_mut()
524 .and_then(|tb| {
525 tb.connect_link_annotation(page_idx, &annot.href, annot_obj_id)
526 })
527 .map(|sp| format!(" /StructParent {}", sp))
528 .unwrap_or_default();
529 let href_esc = Self::escape_pdf_string(&annot.href);
530 let annot_dict = format!(
531 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
532 /F 4 /Contents ({}){} \
533 /A << /Type /Action /S /URI /URI ({}) >> >>",
534 rect, href_esc, sp_str, href_esc
535 );
536 builder.objects.push(PdfObject {
537 id: annot_obj_id,
538 data: annot_dict.into_bytes(),
539 });
540 annot_obj_ids.push(annot_obj_id);
541 }
542 }
543
544 let annots_str = if annot_obj_ids.is_empty() {
545 String::new()
546 } else {
547 let refs: String = annot_obj_ids
548 .iter()
549 .map(|id| format!("{} 0 R", id))
550 .collect::<Vec<_>>()
551 .join(" ");
552 format!(" /Annots [{}]", refs)
553 };
554
555 let page_obj_id = page_obj_ids[page_idx];
556 let content_obj_id = per_page_content_obj_ids[page_idx];
557 let struct_parents_str = if tagged {
558 format!(" /StructParents {} /Tabs /S", page_idx)
559 } else {
560 String::new()
561 };
562 let page_dict = format!(
563 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {:.2} {:.2}] \
564 /Contents {} 0 R /Resources << {} >>{}{} >>",
565 pages[page_idx].width,
566 pages[page_idx].height,
567 content_obj_id,
568 per_page_resources[page_idx],
569 annots_str,
570 struct_parents_str
571 );
572 builder.objects[page_obj_id].data = page_dict.into_bytes();
573 }
574
575 let outlines_obj_id = if !all_bookmarks.is_empty() {
577 Some(self.write_outline_tree(&mut builder, &all_bookmarks, tag_builder.as_mut()))
578 } else {
579 None
580 };
581
582 let struct_tree_root_id = if let Some(ref tb) = tag_builder {
584 let (root_id, _parent_tree_id, sd_patches) = tb.write_objects(
585 &mut builder.objects,
586 &page_obj_ids,
587 metadata.lang.as_deref(),
588 pdf_version == crate::model::PdfVersion::V2_0,
589 );
590 for (annot_obj_id, elem_obj_id) in sd_patches {
594 let data = std::mem::take(&mut builder.objects[annot_obj_id].data);
595 let patched = String::from_utf8(data)
596 .expect("annotation dicts are ASCII")
597 .replacen("999999999 0 R", &format!("{} 0 R", elem_obj_id), 1);
598 builder.objects[annot_obj_id].data = patched.into_bytes();
599 }
600 Some(root_id)
601 } else {
602 None
603 };
604
605 let xmp_metadata_id =
610 if pdfa.is_some() || pdf_ua || pdf_version == crate::model::PdfVersion::V2_0 {
611 let xmp_xml = xmp::generate_xmp(metadata, pdfa, pdf_ua, pdf_ua2, zugferd);
612 let xmp_bytes = xmp_xml.as_bytes();
613 let xmp_obj_id = builder.objects.len();
614 let xmp_data = format!(
616 "<< /Type /Metadata /Subtype /XML /Length {} >>\nstream\n",
617 xmp_bytes.len()
618 );
619 let mut xmp_obj_data: Vec<u8> = xmp_data.into_bytes();
620 xmp_obj_data.extend_from_slice(xmp_bytes);
621 xmp_obj_data.extend_from_slice(b"\nendstream");
622 builder.objects.push(PdfObject {
623 id: xmp_obj_id,
624 data: xmp_obj_data,
625 });
626 Some(xmp_obj_id)
627 } else {
628 None
629 };
630
631 let output_intent_id = if pdfa.is_some() {
632 static SRGB_ICC: &[u8] = include_bytes!("sRGB.icc");
634 let compressed_icc = compress_to_vec_zlib(SRGB_ICC, 6);
635
636 let icc_obj_id = builder.objects.len();
637 let mut icc_data: Vec<u8> = Vec::new();
638 let _ = write!(
639 icc_data,
640 "<< /N 3 /Length {} /Filter /FlateDecode >>\nstream\n",
641 compressed_icc.len()
642 );
643 icc_data.extend_from_slice(&compressed_icc);
644 icc_data.extend_from_slice(b"\nendstream");
645 builder.objects.push(PdfObject {
646 id: icc_obj_id,
647 data: icc_data,
648 });
649
650 let oi_obj_id = builder.objects.len();
652 let oi_data = format!(
653 "<< /Type /OutputIntent /S /GTS_PDFA1 \
654 /OutputConditionIdentifier (sRGB IEC61966-2.1) \
655 /RegistryName (http://www.color.org) \
656 /DestOutputProfile {} 0 R >>",
657 icc_obj_id
658 );
659 builder.objects.push(PdfObject {
660 id: oi_obj_id,
661 data: oi_data.into_bytes(),
662 });
663 Some(oi_obj_id)
664 } else {
665 None
666 };
667
668 let mut name_tree_entries: Vec<(String, usize)> = Vec::new();
675 let mut af_filespec_ids: Vec<usize> = Vec::new();
676 if let Some(data) = embedded_data {
677 let compressed = compress_to_vec_zlib(data.as_bytes(), 6);
678
679 let ef_obj_id = builder.objects.len();
681 let ef_data = format!(
682 "<< /Type /EmbeddedFile /Subtype /application#2Fjson /Length {} /Filter /FlateDecode >>\nstream\n",
683 compressed.len()
684 );
685 let mut ef_bytes = ef_data.into_bytes();
686 ef_bytes.extend_from_slice(&compressed);
687 ef_bytes.extend_from_slice(b"\nendstream");
688 builder.objects.push(PdfObject {
689 id: ef_obj_id,
690 data: ef_bytes,
691 });
692
693 let fs_obj_id = builder.objects.len();
695 let desc = if builder.pdf_version == crate::model::PdfVersion::V2_0 {
696 " /Desc (forme-data.json)"
698 } else {
699 ""
700 };
701 let fs_data = format!(
702 "<< /Type /Filespec /F (forme-data.json) /UF (forme-data.json) /EF << /F {} 0 R >> /AFRelationship /Data{} >>",
703 ef_obj_id, desc
704 );
705 builder.objects.push(PdfObject {
706 id: fs_obj_id,
707 data: fs_data.into_bytes(),
708 });
709 name_tree_entries.push(("forme-data.json".to_string(), fs_obj_id));
710 if pdfa.is_some_and(|l| l.allows_attachments()) {
713 af_filespec_ids.push(fs_obj_id);
714 }
715 }
716 for att in attachments {
717 let bytes = Self::decode_attachment_src(&att.src)?;
718 let compressed = compress_to_vec_zlib(&bytes, 6);
719 let mime = att
720 .mime_type
721 .as_deref()
722 .unwrap_or("application/octet-stream");
723 let mod_date = att
724 .mod_date
725 .as_deref()
726 .unwrap_or(DEFAULT_ATTACHMENT_MOD_DATE);
727
728 let ef_obj_id = builder.objects.len();
729 let ef_head = format!(
730 "<< /Type /EmbeddedFile /Subtype /{} /Length {} /Filter /FlateDecode \
731 /Params << /Size {} /ModDate ({}) >> >>\nstream\n",
732 Self::mime_to_pdf_name(mime),
733 compressed.len(),
734 bytes.len(),
735 Self::escape_pdf_string(mod_date),
736 );
737 let mut ef_bytes = ef_head.into_bytes();
738 ef_bytes.extend_from_slice(&compressed);
739 ef_bytes.extend_from_slice(b"\nendstream");
740 builder.objects.push(PdfObject {
741 id: ef_obj_id,
742 data: ef_bytes,
743 });
744
745 let relationship = att.relationship.unwrap_or_else(|| {
750 if zugferd_filename.as_deref() == Some(att.name.as_str()) {
751 match zugferd.map(|z| z.conformance_level.as_str()) {
752 Some("MINIMUM") | Some("BASIC WL") => AfRelationship::Data,
753 _ => AfRelationship::Alternative,
754 }
755 } else {
756 AfRelationship::Unspecified
757 }
758 });
759
760 let fs_obj_id = builder.objects.len();
761 let mut fs_data = format!(
762 "<< /Type /Filespec /F ({name}) /UF ({name}) /EF << /F {ef} 0 R >> /AFRelationship /{rel}",
763 name = Self::escape_pdf_string(&att.name),
764 ef = ef_obj_id,
765 rel = relationship.pdf_name(),
766 );
767 if let Some(desc) = &att.description {
768 let _ = write!(fs_data, " /Desc ({})", Self::escape_pdf_string(desc));
769 } else if builder.pdf_version == crate::model::PdfVersion::V2_0 {
770 let _ = write!(fs_data, " /Desc ({})", Self::escape_pdf_string(&att.name));
775 }
776 fs_data.push_str(" >>");
777 builder.objects.push(PdfObject {
778 id: fs_obj_id,
779 data: fs_data.into_bytes(),
780 });
781 name_tree_entries.push((att.name.clone(), fs_obj_id));
782 af_filespec_ids.push(fs_obj_id);
783 }
784 let embedded_names_id = if name_tree_entries.is_empty() {
785 None
786 } else {
787 name_tree_entries.sort_by(|a, b| a.0.cmp(&b.0));
789 let names_obj_id = builder.objects.len();
790 let pairs = name_tree_entries
791 .iter()
792 .map(|(name, id)| format!("({}) {} 0 R", Self::escape_pdf_string(name), id))
793 .collect::<Vec<_>>()
794 .join(" ");
795 let names_data = format!("<< /Names [{}] >>", pairs);
796 builder.objects.push(PdfObject {
797 id: names_obj_id,
798 data: names_data.into_bytes(),
799 });
800 Some(names_obj_id)
801 };
802
803 let acroform_obj_id = if !all_form_fields.is_empty() && !flatten_forms {
805 let helv_obj_id = builder
807 .font_objects
808 .iter()
809 .find(|(key, _)| key.family == "Helvetica" && key.weight == 400 && !key.italic)
810 .map(|(_, id)| *id);
811
812 let mut radio_groups: HashMap<String, Vec<usize>> = HashMap::new(); let mut non_radio_indices: Vec<usize> = Vec::new();
815 for (i, field) in all_form_fields.iter().enumerate() {
816 if matches!(field.field_type, FormFieldType::RadioButton { .. }) {
817 radio_groups.entry(field.name.clone()).or_default().push(i);
818 } else {
819 non_radio_indices.push(i);
820 }
821 }
822
823 let mut radio_parent_ids: HashMap<String, usize> = HashMap::new();
825 for group_name in radio_groups.keys() {
826 let parent_id = builder.objects.len();
827 builder.objects.push(PdfObject {
828 id: parent_id,
829 data: vec![], });
831 radio_parent_ids.insert(group_name.clone(), parent_id);
832 }
833
834 let checkbox_yes_stream_id = builder.objects.len();
837 {
838 let stream_content =
839 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";
840 let mut data: Vec<u8> = Vec::new();
841 let _ = write!(
842 data,
843 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
844 stream_content.len()
845 );
846 data.extend_from_slice(stream_content);
847 data.extend_from_slice(b"\nendstream");
848 builder.objects.push(PdfObject {
849 id: checkbox_yes_stream_id,
850 data,
851 });
852 }
853 let checkbox_off_stream_id = builder.objects.len();
855 {
856 let stream_content = b"";
857 let mut data: Vec<u8> = Vec::new();
858 let _ = write!(
859 data,
860 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
861 stream_content.len()
862 );
863 data.extend_from_slice(stream_content);
864 data.extend_from_slice(b"\nendstream");
865 builder.objects.push(PdfObject {
866 id: checkbox_off_stream_id,
867 data,
868 });
869 }
870 let radio_on_stream_id = builder.objects.len();
872 {
873 let k = 2.761; let stream_content = format!(
876 "0.2 0.2 0.2 rg\n\
877 7 12 m {:.2} 12 12 {:.2} 12 7 c\n\
878 12 {:.2} {:.2} 2 7 2 c\n\
879 {:.2} 2 2 {:.2} 2 7 c\n\
880 2 {:.2} {:.2} 12 7 12 c f\n",
881 7.0 + k,
882 7.0 + k, 7.0 - k,
884 7.0 - k, 7.0 - k,
886 7.0 - k, 7.0 + k,
888 7.0 + k, );
890 let stream_bytes = stream_content.as_bytes();
891 let mut data: Vec<u8> = Vec::new();
892 let _ = write!(
893 data,
894 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
895 stream_bytes.len()
896 );
897 data.extend_from_slice(stream_bytes);
898 data.extend_from_slice(b"\nendstream");
899 builder.objects.push(PdfObject {
900 id: radio_on_stream_id,
901 data,
902 });
903 }
904 let radio_off_stream_id = builder.objects.len();
906 {
907 let stream_content = b"";
908 let mut data: Vec<u8> = Vec::new();
909 let _ = write!(
910 data,
911 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
912 stream_content.len()
913 );
914 data.extend_from_slice(stream_content);
915 data.extend_from_slice(b"\nendstream");
916 builder.objects.push(PdfObject {
917 id: radio_off_stream_id,
918 data,
919 });
920 }
921
922 let mut acroform_field_ids: Vec<usize> = Vec::new();
924 let mut per_page_widget_ids: Vec<Vec<usize>> = vec![Vec::new(); pages.len()];
925 let mut radio_kid_ids: HashMap<String, Vec<usize>> = HashMap::new();
926
927 for field in all_form_fields.iter() {
928 let rect = format!(
929 "[{:.2} {:.2} {:.2} {:.2}]",
930 field.x,
931 field.y,
932 field.x + field.width,
933 field.y + field.height
934 );
935 let page_ref = format!("{} 0 R", page_obj_ids[field.page_idx]);
936
937 match &field.field_type {
938 FormFieldType::TextField {
939 value,
940 multiline,
941 password,
942 read_only,
943 max_length,
944 font_size,
945 ..
946 } => {
947 let mut flags: u32 = 0;
948 if *multiline {
949 flags |= 1 << 12; }
951 if *password {
952 flags |= 1 << 13; }
954 if *read_only {
955 flags |= 1; }
957 let da = if let Some(helv_id) = helv_obj_id {
958 let _ = helv_id; format!("/Helv {} Tf 0 g", font_size)
960 } else {
961 format!("/Helv {} Tf 0 g", font_size)
962 };
963 let v_str = if let Some(ref v) = value {
964 format!(
965 " /V ({}) /DV ({})",
966 Self::escape_pdf_string(v),
967 Self::escape_pdf_string(v)
968 )
969 } else {
970 String::new()
971 };
972 let max_len_str = if let Some(ml) = max_length {
973 format!(" /MaxLen {}", ml)
974 } else {
975 String::new()
976 };
977 let ap_w = field.width;
979 let ap_h = field.height;
980 let text_y = if *multiline {
981 ap_h - *font_size - 2.0
982 } else {
983 (ap_h - *font_size) / 2.0
984 };
985 let ap_content = if let Some(ref v) = value {
986 format!(
987 "1 1 1 rg 0 0 {} {} re f \
988 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S \
989 BT /Helv {} Tf 0 g 2 {} Td ({}) Tj ET",
990 ap_w,
991 ap_h,
992 ap_w,
993 ap_h,
994 font_size,
995 text_y,
996 Self::escape_pdf_string(v)
997 )
998 } else {
999 format!(
1000 "1 1 1 rg 0 0 {} {} re f \
1001 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S",
1002 ap_w, ap_h, ap_w, ap_h
1003 )
1004 };
1005 let ap_stream_id = builder.objects.len();
1006 let ap_stream = format!(
1007 "<< /Type /XObject /Subtype /Form /BBox [0 0 {} {}] \
1008 /Resources << /Font << /Helv {} 0 R >> >> /Length {} >>\nstream\n{}\nendstream",
1009 ap_w, ap_h,
1010 helv_obj_id.unwrap_or(0),
1011 ap_content.len(),
1012 ap_content
1013 );
1014 builder.objects.push(PdfObject {
1015 id: ap_stream_id,
1016 data: ap_stream.into_bytes(),
1017 });
1018
1019 let widget_obj_id = builder.objects.len();
1020 let widget_dict = format!(
1021 "<< /Type /Annot /Subtype /Widget /FT /Tx \
1022 /T ({}) /Rect {} /P {}\
1023 {} /DA ({}) /Ff {}{} \
1024 /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> \
1025 /AP << /N {} 0 R >> >>",
1026 Self::escape_pdf_string(&field.name),
1027 rect,
1028 page_ref,
1029 v_str,
1030 da,
1031 flags,
1032 max_len_str,
1033 ap_stream_id
1034 );
1035 builder.objects.push(PdfObject {
1036 id: widget_obj_id,
1037 data: widget_dict.into_bytes(),
1038 });
1039 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1040 acroform_field_ids.push(widget_obj_id);
1041 }
1042
1043 FormFieldType::Checkbox {
1044 checked, read_only, ..
1045 } => {
1046 let state = if *checked { "Yes" } else { "Off" };
1047 let mut flags: u32 = 0;
1048 if *read_only {
1049 flags |= 1;
1050 }
1051 let ff_str = if flags > 0 {
1052 format!(" /Ff {}", flags)
1053 } else {
1054 String::new()
1055 };
1056 let widget_obj_id = builder.objects.len();
1057 let widget_dict = format!(
1058 "<< /Type /Annot /Subtype /Widget /FT /Btn \
1059 /T ({}) /Rect {} /P {} \
1060 /V /{} /AS /{}{} \
1061 /MK << /BC [0.6 0.6 0.6] /CA (4) >> \
1062 /AP << /N << /Yes {} 0 R /Off {} 0 R >> >> >>",
1063 Self::escape_pdf_string(&field.name),
1064 rect,
1065 page_ref,
1066 state,
1067 state,
1068 ff_str,
1069 checkbox_yes_stream_id,
1070 checkbox_off_stream_id,
1071 );
1072 builder.objects.push(PdfObject {
1073 id: widget_obj_id,
1074 data: widget_dict.into_bytes(),
1075 });
1076 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1077 acroform_field_ids.push(widget_obj_id);
1078 }
1079
1080 FormFieldType::Dropdown {
1081 options,
1082 value,
1083 read_only,
1084 font_size,
1085 ..
1086 } => {
1087 let mut flags: u32 = 1 << 17; if *read_only {
1089 flags |= 1;
1090 }
1091 let opts_str: String = options
1092 .iter()
1093 .map(|o| format!("({})", Self::escape_pdf_string(o)))
1094 .collect::<Vec<_>>()
1095 .join(" ");
1096 let v_str = if let Some(ref v) = value {
1097 format!(" /V ({})", Self::escape_pdf_string(v))
1098 } else {
1099 String::new()
1100 };
1101 let ap_w = field.width;
1103 let ap_h = field.height;
1104 let text_y = (ap_h - *font_size) / 2.0;
1105 let ap_content = if let Some(ref v) = value {
1106 format!(
1107 "1 1 1 rg 0 0 {} {} re f \
1108 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S \
1109 BT /Helv {} Tf 0 g 2 {} Td ({}) Tj ET",
1110 ap_w,
1111 ap_h,
1112 ap_w,
1113 ap_h,
1114 font_size,
1115 text_y,
1116 Self::escape_pdf_string(v)
1117 )
1118 } else {
1119 format!(
1120 "1 1 1 rg 0 0 {} {} re f \
1121 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S",
1122 ap_w, ap_h, ap_w, ap_h
1123 )
1124 };
1125 let ap_stream_id = builder.objects.len();
1126 let ap_stream = format!(
1127 "<< /Type /XObject /Subtype /Form /BBox [0 0 {} {}] \
1128 /Resources << /Font << /Helv {} 0 R >> >> /Length {} >>\nstream\n{}\nendstream",
1129 ap_w, ap_h,
1130 helv_obj_id.unwrap_or(0),
1131 ap_content.len(),
1132 ap_content
1133 );
1134 builder.objects.push(PdfObject {
1135 id: ap_stream_id,
1136 data: ap_stream.into_bytes(),
1137 });
1138
1139 let widget_obj_id = builder.objects.len();
1140 let widget_dict = format!(
1141 "<< /Type /Annot /Subtype /Widget /FT /Ch \
1142 /T ({}) /Rect {} /P {} \
1143 /Opt [{}]{} \
1144 /DA (/Helv {} Tf 0 g) /Ff {} \
1145 /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> \
1146 /AP << /N {} 0 R >> >>",
1147 Self::escape_pdf_string(&field.name),
1148 rect,
1149 page_ref,
1150 opts_str,
1151 v_str,
1152 font_size,
1153 flags,
1154 ap_stream_id
1155 );
1156 builder.objects.push(PdfObject {
1157 id: widget_obj_id,
1158 data: widget_dict.into_bytes(),
1159 });
1160 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1161 acroform_field_ids.push(widget_obj_id);
1162 }
1163
1164 FormFieldType::RadioButton {
1165 value,
1166 checked,
1167 read_only: _,
1168 } => {
1169 let parent_id = radio_parent_ids[&field.name];
1171 let as_value = if *checked { value.as_str() } else { "Off" };
1172 let widget_obj_id = builder.objects.len();
1173 let widget_dict = format!(
1174 "<< /Type /Annot /Subtype /Widget \
1175 /Parent {} 0 R \
1176 /Rect {} /P {} \
1177 /AS /{} \
1178 /AP << /N << /{} {} 0 R /Off {} 0 R >> >> \
1179 /MK << /BC [0.6 0.6 0.6] >> >>",
1180 parent_id,
1181 rect,
1182 page_ref,
1183 Self::escape_pdf_string(as_value),
1184 Self::escape_pdf_string(value),
1185 radio_on_stream_id,
1186 radio_off_stream_id,
1187 );
1188 builder.objects.push(PdfObject {
1189 id: widget_obj_id,
1190 data: widget_dict.into_bytes(),
1191 });
1192 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1193 radio_kid_ids
1195 .entry(field.name.clone())
1196 .or_default()
1197 .push(widget_obj_id);
1198 }
1199 }
1200 }
1201
1202 for (group_name, kid_indices) in &radio_kid_ids {
1204 let parent_id = radio_parent_ids[group_name];
1205 let checked_value = all_form_fields
1207 .iter()
1208 .filter(|f| f.name == *group_name)
1209 .find_map(|f| {
1210 if let FormFieldType::RadioButton {
1211 ref value, checked, ..
1212 } = f.field_type
1213 {
1214 if checked {
1215 Some(value.clone())
1216 } else {
1217 None
1218 }
1219 } else {
1220 None
1221 }
1222 })
1223 .unwrap_or_else(|| "Off".to_string());
1224
1225 let kids_refs: String = kid_indices
1226 .iter()
1227 .map(|id| format!("{} 0 R", id))
1228 .collect::<Vec<_>>()
1229 .join(" ");
1230
1231 let mut flags: u32 = (1 << 14) | (1 << 15); let is_read_only = all_form_fields
1234 .iter()
1235 .filter(|f| f.name == *group_name)
1236 .any(|f| {
1237 matches!(
1238 f.field_type,
1239 FormFieldType::RadioButton {
1240 read_only: true,
1241 ..
1242 }
1243 )
1244 });
1245 if is_read_only {
1246 flags |= 1;
1247 }
1248
1249 let parent_dict = format!(
1250 "<< /FT /Btn /T ({}) /Ff {} /Kids [{}] /V /{} >>",
1251 Self::escape_pdf_string(group_name),
1252 flags,
1253 kids_refs,
1254 Self::escape_pdf_string(&checked_value),
1255 );
1256 builder.objects[parent_id].data = parent_dict.into_bytes();
1257 acroform_field_ids.push(parent_id);
1258 }
1259
1260 for (page_idx, widget_ids) in per_page_widget_ids.iter().enumerate() {
1264 if widget_ids.is_empty() {
1265 continue;
1266 }
1267 let page_obj_id = page_obj_ids[page_idx];
1268 let existing_page_data =
1269 String::from_utf8_lossy(&builder.objects[page_obj_id].data).to_string();
1270
1271 let new_refs: String = widget_ids
1273 .iter()
1274 .map(|id| format!("{} 0 R", id))
1275 .collect::<Vec<_>>()
1276 .join(" ");
1277
1278 let updated = if let Some(pos) = existing_page_data.find("/Annots [") {
1279 let bracket_end = existing_page_data[pos..].find(']').unwrap() + pos;
1281 format!(
1282 "{} {}{}",
1283 &existing_page_data[..bracket_end],
1284 new_refs,
1285 &existing_page_data[bracket_end..]
1286 )
1287 } else {
1288 let end = existing_page_data.rfind(">>").unwrap();
1290 format!(
1291 "{} /Annots [{}]{}",
1292 &existing_page_data[..end],
1293 new_refs,
1294 &existing_page_data[end..]
1295 )
1296 };
1297 builder.objects[page_obj_id].data = updated.into_bytes();
1298 }
1299
1300 let acroform_id = builder.objects.len();
1302 let fields_refs: String = acroform_field_ids
1303 .iter()
1304 .map(|id| format!("{} 0 R", id))
1305 .collect::<Vec<_>>()
1306 .join(" ");
1307 let dr_str = if let Some(helv_id) = helv_obj_id {
1308 format!(" /DR << /Font << /Helv {} 0 R >> >>", helv_id)
1309 } else {
1310 String::new()
1311 };
1312 let acroform_dict = format!(
1319 "<< /Fields [{}]{} /DA (/Helv 0 Tf 0 g) >>",
1320 fields_refs, dr_str
1321 );
1322 builder.objects.push(PdfObject {
1323 id: acroform_id,
1324 data: acroform_dict.into_bytes(),
1325 });
1326 Some(acroform_id)
1327 } else {
1328 None
1329 };
1330
1331 let mut catalog = String::from("<< /Type /Catalog /Pages 2 0 R");
1333 if let Some(acroform_id) = acroform_obj_id {
1334 write!(catalog, " /AcroForm {} 0 R", acroform_id).unwrap();
1335 }
1336 if let Some(outlines_id) = outlines_obj_id {
1337 write!(
1338 catalog,
1339 " /Outlines {} 0 R /PageMode /UseOutlines",
1340 outlines_id
1341 )
1342 .unwrap();
1343 }
1344 if let Some(ref lang) = metadata.lang {
1345 write!(catalog, " /Lang ({})", Self::escape_pdf_string(lang)).unwrap();
1346 }
1347 if let Some(struct_root_id) = struct_tree_root_id {
1348 write!(
1349 catalog,
1350 " /MarkInfo << /Marked true >> /StructTreeRoot {} 0 R",
1351 struct_root_id
1352 )
1353 .unwrap();
1354 }
1355 if let Some(xmp_id) = xmp_metadata_id {
1356 write!(catalog, " /Metadata {} 0 R", xmp_id).unwrap();
1357 }
1358 if let Some(oi_id) = output_intent_id {
1359 write!(catalog, " /OutputIntents [{} 0 R]", oi_id).unwrap();
1360 }
1361 if let Some(names_id) = embedded_names_id {
1362 write!(catalog, " /Names << /EmbeddedFiles {} 0 R >>", names_id).unwrap();
1363 }
1364 if !af_filespec_ids.is_empty() {
1365 let refs = af_filespec_ids
1368 .iter()
1369 .map(|id| format!("{} 0 R", id))
1370 .collect::<Vec<_>>()
1371 .join(" ");
1372 write!(catalog, " /AF [{}]", refs).unwrap();
1373 }
1374 if pdf_ua || pdf_ua2 {
1375 catalog.push_str(" /ViewerPreferences << /DisplayDocTitle true >>");
1376 }
1377 catalog.push_str(" >>");
1378 builder.objects[1].data = catalog.into_bytes();
1379
1380 let kids: String = page_obj_ids
1382 .iter()
1383 .map(|id| format!("{} 0 R", id))
1384 .collect::<Vec<_>>()
1385 .join(" ");
1386 builder.objects[2].data = format!(
1387 "<< /Type /Pages /Kids [{}] /Count {} >>",
1388 kids,
1389 page_obj_ids.len()
1390 )
1391 .into_bytes();
1392
1393 let info_obj_id = if pdf_version == crate::model::PdfVersion::V1_7
1401 && (metadata.title.is_some() || metadata.author.is_some())
1402 {
1403 let id = builder.objects.len();
1404 let mut info = String::from("<< ");
1405 if let Some(ref title) = metadata.title {
1406 let _ = write!(info, "/Title ({}) ", Self::escape_pdf_string(title));
1407 }
1408 if let Some(ref author) = metadata.author {
1409 let _ = write!(info, "/Author ({}) ", Self::escape_pdf_string(author));
1410 }
1411 if let Some(ref subject) = metadata.subject {
1412 let _ = write!(info, "/Subject ({}) ", Self::escape_pdf_string(subject));
1413 }
1414 let _ = write!(info, "/Producer (Forme 0.6) /Creator (Forme) >>");
1415 builder.objects.push(PdfObject {
1416 id,
1417 data: info.into_bytes(),
1418 });
1419 Some(id)
1420 } else {
1421 None
1422 };
1423
1424 let pdf = self.serialize(&builder, info_obj_id);
1425 let mut warnings = builder.warnings;
1430 for ch in builder.missing_glyphs.into_inner() {
1431 warnings.push(format!(
1432 "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)",
1433 ch as u32
1434 ));
1435 }
1436 Ok((pdf, warnings))
1437 }
1438
1439 #[allow(clippy::too_many_arguments)]
1441 fn build_content_stream_for_page(
1442 &self,
1443 page: &LayoutPage,
1444 page_idx: usize,
1445 builder: &PdfBuilder,
1446 page_number: usize,
1447 total_pages: usize,
1448 mut tag_builder: Option<&mut tagged::TagBuilder>,
1449 flatten_forms: bool,
1450 ) -> String {
1451 let mut stream = String::new();
1452 let page_height = page.height;
1453 let mut element_counter = 0usize;
1454 let mut gradient_counter = 0usize;
1455
1456 if let Some(&img_idx) = builder.page_background_image_map.get(&page_idx) {
1462 self.write_page_background(&mut stream, page, img_idx, builder);
1463 }
1464
1465 let clip_x = page.config.clip_content_x;
1469 if clip_x {
1470 let x = page.config.margin.left;
1471 let w = page.width - page.config.margin.left - page.config.margin.right;
1472 stream.push_str(&format!(
1473 "q\n{:.2} 0 {:.2} {:.2} re W n\n",
1474 x, w, page.height
1475 ));
1476 }
1477
1478 for element in &page.elements {
1479 self.write_element(
1480 &mut stream,
1481 element,
1482 page_height,
1483 builder,
1484 page_idx,
1485 &mut element_counter,
1486 &mut gradient_counter,
1487 page_number,
1488 total_pages,
1489 tag_builder.as_deref_mut(),
1490 flatten_forms,
1491 );
1492 }
1493
1494 if clip_x {
1495 stream.push_str("Q\n");
1496 }
1497
1498 stream
1499 }
1500
1501 #[allow(clippy::too_many_arguments)]
1503 #[allow(clippy::too_many_arguments)]
1504 fn write_element(
1505 &self,
1506 stream: &mut String,
1507 element: &LayoutElement,
1508 page_height: f64,
1509 builder: &PdfBuilder,
1510 page_idx: usize,
1511 element_counter: &mut usize,
1512 gradient_counter: &mut usize,
1513 page_number: usize,
1514 total_pages: usize,
1515 mut tag_builder: Option<&mut tagged::TagBuilder>,
1516 flatten_forms: bool,
1517 ) {
1518 let mut is_artifact = false;
1521 let mut artifact_own_draw = false;
1526 let tagged_mcid = if let Some(ref mut tb) = tag_builder {
1527 if let Some(ref nt) = element.node_type {
1528 if nt == "Watermark" {
1529 let _ = writeln!(stream, "/Artifact BMC");
1531 is_artifact = true;
1532 None
1533 } else {
1534 let is_header = element.is_header_row;
1535 let href = element.href.as_deref();
1536 let mcid = tb.begin_element(
1537 nt,
1538 is_header,
1539 element.alt.as_deref(),
1540 page_idx,
1541 href,
1542 element.col_span,
1543 element.list_numbering,
1544 element.actual_text.as_deref(),
1545 );
1546 if let Some(ref bm) = element.bookmark {
1550 tb.note_bookmark(bm);
1551 }
1552 match mcid {
1553 Some(mcid) => {
1554 let role = if href.is_some() {
1557 "Link"
1558 } else {
1559 tb.map_role_public(nt, is_header)
1560 };
1561 let _ = writeln!(stream, "/{} <</MCID {}>> BDC", role, mcid);
1562 Some(mcid)
1563 }
1564 None => {
1565 artifact_own_draw = true;
1566 None
1567 }
1568 }
1569 }
1570 } else if !matches!(element.draw, DrawCommand::None) {
1571 let _ = writeln!(stream, "/Artifact BMC");
1573 is_artifact = true;
1574 None
1575 } else {
1576 None
1577 }
1578 } else {
1579 None
1580 };
1581
1582 let needs_element_opacity = element.opacity < 1.0;
1590 if needs_element_opacity {
1591 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&element.opacity.to_bits()) {
1592 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1593 }
1594 }
1595
1596 let transform_ops: &[TransformOp] = element
1602 .resolved_style
1603 .as_ref()
1604 .map(|s| s.transform.as_slice())
1605 .unwrap_or(&[]);
1606 let has_transform = !transform_ops.is_empty();
1607 if has_transform {
1608 let rs = element.resolved_style.as_ref().unwrap();
1609 let pdf_x = element.x;
1610 let pdf_y_bottom = page_height - element.y - element.height;
1611 let (ox_frac, oy_frac) = rs.transform_origin;
1612 let origin_x = pdf_x + element.width * ox_frac;
1613 let origin_y = pdf_y_bottom + (1.0 - oy_frac) * element.height;
1616
1617 let _ = writeln!(stream, "q");
1618 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", -origin_x, -origin_y);
1620 for op in transform_ops.iter().rev() {
1628 match op {
1629 TransformOp::Rotate { deg } => {
1630 let theta = (-deg).to_radians();
1635 let c = theta.cos();
1636 let s = theta.sin();
1637 let _ = writeln!(stream, "{:.6} {:.6} {:.6} {:.6} 0 0 cm", c, s, -s, c);
1638 }
1639 TransformOp::Scale { x, y } => {
1640 let _ = writeln!(stream, "{:.6} 0 0 {:.6} 0 0 cm", x, y);
1641 }
1642 TransformOp::Translate { x, y } => {
1643 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", x, -y);
1645 }
1646 }
1647 }
1648 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", origin_x, origin_y);
1650 }
1651
1652 let wrap_own_draw_as_artifact =
1659 artifact_own_draw && !matches!(element.draw, DrawCommand::None);
1660 if wrap_own_draw_as_artifact {
1661 let _ = writeln!(stream, "/Artifact BMC");
1662 }
1663
1664 match &element.draw {
1665 DrawCommand::None => {}
1666
1667 DrawCommand::Rect {
1668 background,
1669 border_width,
1670 border_color,
1671 border_style,
1672 border_radius,
1673 opacity,
1674 box_shadow,
1675 background_gradient,
1676 } => {
1677 let x = element.x;
1678 let y = page_height - element.y - element.height;
1679 let w = element.width;
1680 let h = element.height;
1681
1682 let needs_opacity = *opacity < 1.0;
1684 if needs_opacity {
1685 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1686 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1687 }
1688 }
1689
1690 if let Some(shadow) = box_shadow {
1696 if shadow.color.a > 0.0 {
1697 let sx = x + shadow.offset_x;
1702 let sy = y - shadow.offset_y;
1703 let needs_shadow_alpha = shadow.color.a < 1.0;
1704 if needs_shadow_alpha {
1705 if let Some((_, gs_name)) =
1706 builder.ext_gstate_map.get(&shadow.color.a.to_bits())
1707 {
1708 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1709 } else {
1710 let _ = writeln!(stream, "q");
1711 }
1712 } else {
1713 let _ = writeln!(stream, "q");
1714 }
1715 let _ = writeln!(
1716 stream,
1717 "{:.3} {:.3} {:.3} rg",
1718 shadow.color.r, shadow.color.g, shadow.color.b
1719 );
1720 if border_radius.top_left > 0.0
1721 || border_radius.top_right > 0.0
1722 || border_radius.bottom_right > 0.0
1723 || border_radius.bottom_left > 0.0
1724 {
1725 self.write_rounded_rect(stream, sx, sy, w, h, border_radius);
1726 } else {
1727 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", sx, sy, w, h);
1728 }
1729 let _ = writeln!(stream, "f\nQ");
1730 }
1731 }
1732
1733 if background_gradient.is_some() {
1739 let key = (page_idx, *gradient_counter);
1740 *gradient_counter += 1;
1741 if let Some((_, sh_name)) = builder.shading_map.get(&key) {
1742 let _ = writeln!(stream, "q");
1743 if border_radius.top_left > 0.0
1745 || border_radius.top_right > 0.0
1746 || border_radius.bottom_right > 0.0
1747 || border_radius.bottom_left > 0.0
1748 {
1749 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1750 let _ = writeln!(stream, "W n");
1751 } else {
1752 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re W n", x, y, w, h);
1753 }
1754 let _ =
1757 writeln!(stream, "1 0 0 1 {:.3} {:.3} cm\n/{} sh\nQ", x, y, sh_name);
1758 }
1759 } else if let Some(bg) = background {
1760 if bg.a > 0.0 {
1761 let _ = writeln!(stream, "q\n{:.3} {:.3} {:.3} rg", bg.r, bg.g, bg.b);
1762
1763 if border_radius.top_left > 0.0 {
1764 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1765 } else {
1766 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1767 }
1768
1769 let _ = writeln!(stream, "f\nQ");
1770 }
1771 }
1772
1773 let bw = border_width;
1774 if bw.top > 0.0 || bw.right > 0.0 || bw.bottom > 0.0 || bw.left > 0.0 {
1775 use crate::style::BorderStyle::Solid;
1776 let all_solid = border_style.top == Solid
1777 && border_style.right == Solid
1778 && border_style.bottom == Solid
1779 && border_style.left == Solid;
1780 if all_solid
1786 && (bw.top - bw.right).abs() < 0.001
1787 && (bw.right - bw.bottom).abs() < 0.001
1788 && (bw.bottom - bw.left).abs() < 0.001
1789 {
1790 let bc = &border_color.top;
1791 let _ = writeln!(
1792 stream,
1793 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w",
1794 bc.r, bc.g, bc.b, bw.top
1795 );
1796
1797 if border_radius.top_left > 0.0 {
1798 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1799 } else {
1800 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1801 }
1802
1803 let _ = writeln!(stream, "S\nQ");
1804 } else {
1805 self.write_border_sides(stream, x, y, w, h, bw, border_color, border_style);
1806 }
1807 }
1808
1809 if needs_opacity {
1810 let _ = writeln!(stream, "Q");
1811 }
1812 }
1813
1814 DrawCommand::Text {
1815 lines,
1816 color,
1817 text_decoration,
1818 opacity,
1819 } => {
1820 let needs_opacity = *opacity < 1.0;
1822 if needs_opacity {
1823 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1824 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1825 }
1826 }
1827
1828 for line in lines {
1829 if line.glyphs.is_empty() {
1830 continue;
1831 }
1832
1833 let groups = Self::group_glyphs_by_style(&line.glyphs);
1836 let pdf_y = page_height - line.y;
1837
1838 let _ = writeln!(stream, "BT");
1839
1840 if line.word_spacing.abs() > 0.001 {
1842 let _ = writeln!(stream, "{:.4} Tw", line.word_spacing);
1843 }
1844
1845 let mut tm_x = 0.0_f64;
1847 let mut tm_y = 0.0_f64;
1848 let mut x_cursor = line.x;
1849
1850 let mut group_spans: Vec<(f64, f64, TextDecoration, Color)> = Vec::new();
1852
1853 for group in &groups {
1854 let first = &group[0];
1855 let glyph_color = first.color.unwrap_or(*color);
1856
1857 let idx = self.font_index(
1858 &first.font_family,
1859 first.font_weight,
1860 first.font_style,
1861 &builder.font_objects,
1862 );
1863 let italic =
1864 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
1865 let font_key = FontKey {
1866 family: first.font_family.to_string(),
1867 weight: first.font_weight,
1868 italic,
1869 };
1870 let font_name = format!("F{}", idx);
1871
1872 let dx = x_cursor - tm_x;
1874 let dy = pdf_y - tm_y;
1875 let _ = writeln!(
1876 stream,
1877 "{:.3} {:.3} {:.3} rg\n/{} {:.1} Tf\n{:.2} Tc\n{:.2} {:.2} Td",
1878 glyph_color.r,
1879 glyph_color.g,
1880 glyph_color.b,
1881 font_name,
1882 first.font_size,
1883 first.letter_spacing,
1884 dx,
1885 dy
1886 );
1887 tm_x = x_cursor;
1888 tm_y = pdf_y;
1889
1890 let raw_text: String = group.iter().map(|g| g.char_value).collect();
1892 let has_placeholder = raw_text.contains(PAGE_NUMBER_SENTINEL)
1893 || raw_text.contains(TOTAL_PAGES_SENTINEL);
1894
1895 let is_custom = builder.custom_font_data.contains_key(&font_key);
1896
1897 if is_custom {
1898 if let Some(embed_data) = builder.custom_font_data.get(&font_key) {
1899 let mut hex = String::new();
1900 if has_placeholder {
1901 let pn = PAGE_NUMBER_SENTINEL.to_string();
1903 let tp = TOTAL_PAGES_SENTINEL.to_string();
1904 let text_after = raw_text
1905 .replace(&pn, &page_number.to_string())
1906 .replace(&tp, &total_pages.to_string());
1907 for ch in text_after.chars() {
1908 let gid =
1909 embed_data.char_to_gid.get(&ch).copied().unwrap_or(0);
1910 let _ = write!(hex, "{:04X}", gid);
1911 }
1912 } else {
1913 for g in group.iter() {
1915 let new_gid = embed_data
1916 .gid_remap
1917 .get(&g.glyph_id)
1918 .copied()
1919 .unwrap_or_else(|| {
1920 embed_data
1922 .char_to_gid
1923 .get(&g.char_value)
1924 .copied()
1925 .unwrap_or(0)
1926 });
1927 let _ = write!(hex, "{:04X}", new_gid);
1928 }
1929 }
1930 let _ = writeln!(stream, "<{}> Tj", hex);
1931 } else {
1932 let _ = writeln!(stream, "<> Tj");
1933 }
1934 } else {
1935 let pn = PAGE_NUMBER_SENTINEL.to_string();
1936 let tp = TOTAL_PAGES_SENTINEL.to_string();
1937 let text_after = raw_text
1938 .replace(&pn, &page_number.to_string())
1939 .replace(&tp, &total_pages.to_string());
1940 let mut text_str = String::new();
1941 for ch in text_after.chars() {
1942 let b = Self::unicode_to_winansi(ch).unwrap_or_else(|| {
1943 builder.missing_glyphs.borrow_mut().insert(ch);
1944 b'?'
1945 });
1946 match b {
1947 b'\\' => text_str.push_str("\\\\"),
1948 b'(' => text_str.push_str("\\("),
1949 b')' => text_str.push_str("\\)"),
1950 0x20..=0x7E => text_str.push(b as char),
1951 _ => {
1952 let _ = write!(text_str, "\\{:03o}", b);
1953 }
1954 }
1955 }
1956 let _ = writeln!(stream, "({}) Tj", text_str);
1957 }
1958
1959 let group_start_x = x_cursor;
1961
1962 if let Some(last) = group.last() {
1965 let space_count_in_group =
1966 group.iter().filter(|g| g.char_value == ' ').count();
1967 x_cursor = line.x
1968 + last.x_offset
1969 + last.x_advance
1970 + space_count_in_group as f64 * line.word_spacing;
1971 }
1972
1973 let group_dec = first.text_decoration;
1975 if !matches!(group_dec, TextDecoration::None) {
1976 group_spans.push((group_start_x, x_cursor, group_dec, glyph_color));
1977 }
1978 }
1979
1980 let _ = writeln!(stream, "ET");
1981
1982 for (span_x, span_end_x, dec, dec_color) in &group_spans {
1984 match dec {
1985 TextDecoration::Underline => {
1986 let underline_y = pdf_y - 1.5;
1987 let _ = write!(
1988 stream,
1989 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1990 dec_color.r, dec_color.g, dec_color.b,
1991 span_x, underline_y,
1992 span_end_x, underline_y
1993 );
1994 }
1995 TextDecoration::LineThrough => {
1996 let first_size =
1997 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
1998 let strikethrough_y = pdf_y + first_size * 0.3;
1999 let _ = write!(
2000 stream,
2001 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2002 dec_color.r, dec_color.g, dec_color.b,
2003 span_x, strikethrough_y,
2004 span_end_x, strikethrough_y
2005 );
2006 }
2007 TextDecoration::None => {}
2008 }
2009 }
2010
2011 if group_spans.is_empty() {
2013 if matches!(text_decoration, TextDecoration::Underline) {
2014 let underline_y = pdf_y - 1.5;
2015 let _ = write!(
2016 stream,
2017 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2018 color.r, color.g, color.b,
2019 line.x, underline_y,
2020 line.x + line.width, underline_y
2021 );
2022 }
2023 if matches!(text_decoration, TextDecoration::LineThrough) {
2024 let first_size =
2025 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
2026 let strikethrough_y = pdf_y + first_size * 0.3;
2027 let _ = write!(
2028 stream,
2029 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2030 color.r, color.g, color.b,
2031 line.x, strikethrough_y,
2032 line.x + line.width, strikethrough_y
2033 );
2034 }
2035 }
2036 }
2037
2038 if needs_opacity {
2039 let _ = writeln!(stream, "Q");
2040 }
2041 }
2042
2043 DrawCommand::Image { .. } => {
2044 let elem_idx = *element_counter;
2045 *element_counter += 1;
2046 if let Some(&img_idx) = builder.image_index_map.get(&(page_idx, elem_idx)) {
2047 let x = element.x;
2048 let y = page_height - element.y - element.height;
2049 let _ = write!(
2050 stream,
2051 "q\n{:.4} 0 0 {:.4} {:.2} {:.2} cm\n/Im{} Do\nQ\n",
2052 element.width, element.height, x, y, img_idx
2053 );
2054 } else {
2055 let x = element.x;
2057 let y = page_height - element.y - element.height;
2058 let _ = write!(
2059 stream,
2060 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
2061 x, y, element.width, element.height
2062 );
2063 }
2064 if tagged_mcid.is_some() {
2065 let _ = writeln!(stream, "EMC");
2066 if let Some(ref mut tb) = tag_builder {
2067 tb.end_element();
2068 }
2069 } else if is_artifact {
2070 let _ = writeln!(stream, "EMC");
2071 } else if wrap_own_draw_as_artifact {
2072 let _ = writeln!(stream, "EMC");
2076 if let Some(ref mut tb) = tag_builder {
2077 tb.end_element();
2078 }
2079 }
2080 return; }
2082
2083 DrawCommand::ImagePlaceholder => {
2084 *element_counter += 1;
2085 let x = element.x;
2086 let y = page_height - element.y - element.height;
2087 let _ = write!(
2088 stream,
2089 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
2090 x, y, element.width, element.height
2091 );
2092 if tagged_mcid.is_some() {
2093 let _ = writeln!(stream, "EMC");
2094 if let Some(ref mut tb) = tag_builder {
2095 tb.end_element();
2096 }
2097 } else if is_artifact {
2098 let _ = writeln!(stream, "EMC");
2099 } else if wrap_own_draw_as_artifact {
2100 let _ = writeln!(stream, "EMC");
2104 if let Some(ref mut tb) = tag_builder {
2105 tb.end_element();
2106 }
2107 }
2108 return;
2109 }
2110
2111 DrawCommand::Svg {
2112 commands,
2113 width: _svg_w,
2114 height: _svg_h,
2115 viewbox_min_x,
2116 viewbox_min_y,
2117 viewbox_width,
2118 viewbox_height,
2119 clip,
2120 } => {
2121 let x = element.x;
2122 let y = page_height - element.y - element.height;
2123
2124 let _ = writeln!(stream, "q");
2126 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", x, y);
2127
2128 if *viewbox_width > 0.0 && *viewbox_height > 0.0 {
2134 let raw_sx = element.width / *viewbox_width;
2135 let raw_sy = element.height / *viewbox_height;
2136 let s = raw_sx.min(raw_sy);
2137 let tx = (element.width - s * *viewbox_width) / 2.0;
2138 let ty = (element.height - s * *viewbox_height) / 2.0;
2139 let _ = writeln!(stream, "{:.4} 0 0 {:.4} {:.2} {:.2} cm", s, s, tx, ty);
2140 }
2141
2142 let _ = writeln!(stream, "1 0 0 -1 0 {:.2} cm", *viewbox_height);
2145
2146 if *viewbox_min_x != 0.0 || *viewbox_min_y != 0.0 {
2148 let _ = writeln!(
2149 stream,
2150 "1 0 0 1 {:.2} {:.2} cm",
2151 -*viewbox_min_x, -*viewbox_min_y
2152 );
2153 }
2154
2155 if *clip {
2157 let _ = writeln!(
2158 stream,
2159 "{:.2} {:.2} {:.2} {:.2} re W n",
2160 *viewbox_min_x, *viewbox_min_y, *viewbox_width, *viewbox_height
2161 );
2162 }
2163
2164 Self::write_svg_commands(stream, commands, &builder.ext_gstate_map);
2165
2166 let _ = writeln!(stream, "Q");
2167 if tagged_mcid.is_some() {
2168 let _ = writeln!(stream, "EMC");
2169 if let Some(ref mut tb) = tag_builder {
2170 tb.end_element();
2171 }
2172 } else if is_artifact {
2173 let _ = writeln!(stream, "EMC");
2174 } else if wrap_own_draw_as_artifact {
2175 let _ = writeln!(stream, "EMC");
2179 if let Some(ref mut tb) = tag_builder {
2180 tb.end_element();
2181 }
2182 }
2183 return;
2184 }
2185
2186 DrawCommand::Barcode {
2187 bars,
2188 bar_width,
2189 height,
2190 color,
2191 } => {
2192 *element_counter += 1;
2193 let _ = writeln!(stream, "q");
2194 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2195 for (i, &bar) in bars.iter().enumerate() {
2196 if bar == 1 {
2197 let bx = element.x + i as f64 * bar_width;
2198 let by = page_height - element.y - height;
2199 let _ = writeln!(
2200 stream,
2201 "{:.2} {:.2} {:.2} {:.2} re",
2202 bx, by, bar_width, height
2203 );
2204 }
2205 }
2206 let _ = writeln!(stream, "f\nQ");
2207 if tagged_mcid.is_some() {
2208 let _ = writeln!(stream, "EMC");
2209 if let Some(ref mut tb) = tag_builder {
2210 tb.end_element();
2211 }
2212 } else if is_artifact {
2213 let _ = writeln!(stream, "EMC");
2214 } else if wrap_own_draw_as_artifact {
2215 let _ = writeln!(stream, "EMC");
2219 if let Some(ref mut tb) = tag_builder {
2220 tb.end_element();
2221 }
2222 }
2223 return;
2224 }
2225
2226 DrawCommand::QrCode {
2227 modules,
2228 module_size,
2229 color,
2230 } => {
2231 *element_counter += 1;
2232 let _ = writeln!(stream, "q");
2233 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2234 for (row_idx, row) in modules.iter().enumerate() {
2235 for (col_idx, &dark) in row.iter().enumerate() {
2236 if dark {
2237 let mx = element.x + col_idx as f64 * module_size;
2238 let my = page_height - element.y - (row_idx as f64 + 1.0) * module_size;
2239 let _ = writeln!(
2240 stream,
2241 "{:.2} {:.2} {:.2} {:.2} re",
2242 mx, my, module_size, module_size
2243 );
2244 }
2245 }
2246 }
2247 let _ = writeln!(stream, "f\nQ");
2248 if tagged_mcid.is_some() {
2249 let _ = writeln!(stream, "EMC");
2250 if let Some(ref mut tb) = tag_builder {
2251 tb.end_element();
2252 }
2253 } else if is_artifact {
2254 let _ = writeln!(stream, "EMC");
2255 } else if wrap_own_draw_as_artifact {
2256 let _ = writeln!(stream, "EMC");
2260 if let Some(ref mut tb) = tag_builder {
2261 tb.end_element();
2262 }
2263 }
2264 return;
2265 }
2266
2267 DrawCommand::Chart { primitives } => {
2268 *element_counter += 1;
2269 let _ = writeln!(stream, "q");
2270 let _ = writeln!(
2272 stream,
2273 "1 0 0 -1 {:.4} {:.4} cm",
2274 element.x,
2275 page_height - element.y
2276 );
2277
2278 for prim in primitives {
2279 write_chart_primitive(stream, prim, element.height, builder);
2280 }
2281
2282 let _ = writeln!(stream, "Q");
2283 if tagged_mcid.is_some() {
2284 let _ = writeln!(stream, "EMC");
2285 if let Some(ref mut tb) = tag_builder {
2286 tb.end_element();
2287 }
2288 } else if is_artifact {
2289 let _ = writeln!(stream, "EMC");
2290 } else if wrap_own_draw_as_artifact {
2291 let _ = writeln!(stream, "EMC");
2295 if let Some(ref mut tb) = tag_builder {
2296 tb.end_element();
2297 }
2298 }
2299 return;
2300 }
2301
2302 DrawCommand::Watermark {
2303 lines,
2304 color,
2305 opacity,
2306 angle_rad,
2307 font_family: _,
2308 } => {
2309 let _ = writeln!(stream, "q");
2310 if *opacity < 1.0 {
2312 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
2313 let _ = writeln!(stream, "/{} gs", gs_name);
2314 }
2315 }
2316 let pdf_cx = element.x;
2318 let pdf_cy = page_height - element.y;
2319 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", pdf_cx, pdf_cy);
2320 let cos_a = angle_rad.cos();
2322 let sin_a = angle_rad.sin();
2323 let _ = writeln!(
2324 stream,
2325 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
2326 cos_a, sin_a, -sin_a, cos_a
2327 );
2328 let _ = writeln!(stream, "BT");
2330 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2331 if let Some(line) = lines.first() {
2332 let groups = Self::group_glyphs_by_style(&line.glyphs);
2333 let text_width = line.width;
2334 let cap_height = line.height * 0.7;
2335 let _ = writeln!(
2336 stream,
2337 "{:.2} {:.2} Td",
2338 -text_width / 2.0,
2339 -cap_height / 2.0
2340 );
2341 for group in &groups {
2342 let first = &group[0];
2343 let italic =
2344 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
2345 let fk = FontKey {
2346 family: first.font_family.to_string(),
2347 weight: first.font_weight,
2348 italic,
2349 };
2350 let idx = self.font_index(
2351 &first.font_family,
2352 first.font_weight,
2353 first.font_style,
2354 &builder.font_objects,
2355 );
2356 let font_name = format!("F{}", idx);
2357 let _ = writeln!(stream, "/{} {:.1} Tf", font_name, first.font_size);
2358 let is_custom = builder.custom_font_data.contains_key(&fk);
2359 if is_custom {
2360 if let Some(embed_data) = builder.custom_font_data.get(&fk) {
2361 let mut hex = String::new();
2362 for g in group.iter() {
2363 let gid =
2364 embed_data.gid_remap.get(&g.glyph_id).copied().unwrap_or(0);
2365 let _ = write!(hex, "{:04X}", gid);
2366 }
2367 let _ = writeln!(stream, "<{}> Tj", hex);
2368 }
2369 } else {
2370 let hex_str: String = group
2371 .iter()
2372 .map(|g| format!("{:02X}", g.glyph_id as u8))
2373 .collect();
2374 let _ = writeln!(stream, "<{}> Tj", hex_str);
2375 }
2376 }
2377 }
2378 let _ = writeln!(stream, "ET");
2379 let _ = writeln!(stream, "Q");
2380 if tagged_mcid.is_some() {
2381 let _ = writeln!(stream, "EMC");
2382 if let Some(ref mut tb) = tag_builder {
2383 tb.end_element();
2384 }
2385 } else if is_artifact {
2386 let _ = writeln!(stream, "EMC");
2387 } else if wrap_own_draw_as_artifact {
2388 let _ = writeln!(stream, "EMC");
2392 if let Some(ref mut tb) = tag_builder {
2393 tb.end_element();
2394 }
2395 }
2396 return;
2397 }
2398
2399 DrawCommand::FormField { field_type, .. } => {
2400 let pdf_x = element.x;
2404 let pdf_y = page_height - element.y - element.height;
2405 let w = element.width;
2406 let h = element.height;
2407 let _ = writeln!(stream, "q");
2408 match field_type {
2409 FormFieldType::Checkbox { checked, .. } => {
2410 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
2413 let _ =
2414 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
2415 if *checked {
2416 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
2418 let sx = w / 14.0;
2419 let sy = h / 14.0;
2420 let _ = writeln!(
2421 stream,
2422 "{:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l f",
2423 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
2424 pdf_x + 5.5 * sx, pdf_y + 2.0 * sy,
2425 pdf_x + 12.0 * sx, pdf_y + 11.0 * sy,
2426 pdf_x + 11.0 * sx, pdf_y + 12.0 * sy,
2427 pdf_x + 5.5 * sx, pdf_y + 4.5 * sy,
2428 pdf_x + 3.0 * sx, pdf_y + 7.0 * sy,
2429 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
2430 );
2431 }
2432 }
2433 FormFieldType::RadioButton { checked, .. } => {
2434 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
2437 let _ =
2438 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
2439 if *checked {
2440 let cx = pdf_x + w / 2.0;
2442 let cy = pdf_y + h / 2.0;
2443 let r = (w.min(h) / 2.0) * 0.6;
2444 let k = r * 0.5523;
2445 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
2446 let _ = writeln!(
2447 stream,
2448 "{:.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",
2449 cx, cy + r,
2450 cx + k, cy + r, cx + r, cy + k, cx + r, cy,
2451 cx + r, cy - k, cx + k, cy - r, cx, cy - r,
2452 cx - k, cy - r, cx - r, cy - k, cx - r, cy,
2453 cx - r, cy + k, cx - k, cy + r, cx, cy + r,
2454 );
2455 }
2456 }
2457 FormFieldType::TextField {
2458 value,
2459 placeholder,
2460 font_size,
2461 multiline,
2462 password,
2463 ..
2464 } => {
2465 let _ = writeln!(stream, "1 1 1 rg");
2467 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2468 let _ = writeln!(stream, "0.5 w");
2469 let _ =
2470 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2471 if flatten_forms {
2473 let has_value = value.as_ref().is_some_and(|v| !v.is_empty());
2474 if has_value {
2475 let val = value.as_ref().unwrap();
2476 let display_text = if *password {
2477 "\u{2022}".repeat(val.len())
2478 } else {
2479 val.clone()
2480 };
2481 let font_idx = builder
2482 .font_objects
2483 .iter()
2484 .enumerate()
2485 .find(|(_, (key, _))| {
2486 key.family == "Helvetica"
2487 && key.weight == 400
2488 && !key.italic
2489 })
2490 .map(|(i, _)| i)
2491 .unwrap_or(0);
2492 if *multiline {
2493 let metrics = crate::font::StandardFont::Helvetica.metrics();
2495 let max_w = w - 4.0;
2496 let mut lines: Vec<String> = Vec::new();
2497 for paragraph in display_text.split('\n') {
2498 let mut line = String::new();
2499 let mut line_w = 0.0;
2500 for word in paragraph.split_whitespace() {
2501 let word_w =
2502 metrics.measure_string(word, *font_size, 0.0);
2503 let space_w = if line.is_empty() {
2504 0.0
2505 } else {
2506 metrics.measure_string(" ", *font_size, 0.0)
2507 };
2508 if word_w > max_w {
2510 let mut char_line = String::new();
2511 let mut char_w = 0.0;
2512 for ch in word.chars() {
2513 let cw = metrics.char_width(ch, *font_size);
2514 if !char_line.is_empty() && char_w + cw > max_w
2515 {
2516 if !line.is_empty() {
2517 lines.push(line.clone());
2518 line.clear();
2519 line_w = 0.0;
2520 }
2521 lines.push(char_line.clone());
2522 char_line.clear();
2523 char_w = 0.0;
2524 }
2525 char_line.push(ch);
2526 char_w += cw;
2527 }
2528 if !char_line.is_empty() {
2530 if !line.is_empty() {
2531 line.push(' ');
2532 line_w += metrics
2533 .measure_string(" ", *font_size, 0.0);
2534 }
2535 line.push_str(&char_line);
2536 line_w += char_w;
2537 }
2538 continue;
2539 }
2540 if !line.is_empty() && line_w + space_w + word_w > max_w
2541 {
2542 lines.push(line.clone());
2543 line.clear();
2544 line_w = 0.0;
2545 }
2546 if !line.is_empty() {
2547 line.push(' ');
2548 line_w += space_w;
2549 }
2550 line.push_str(word);
2551 line_w += word_w;
2552 }
2553 if !line.is_empty() {
2554 lines.push(line);
2555 }
2556 }
2557 let text_y = pdf_y + h - font_size - 2.0;
2558 for (i, line_text) in lines.iter().enumerate() {
2559 let ly = text_y - (i as f64) * (font_size * 1.2);
2560 if ly < pdf_y {
2561 break;
2562 }
2563 let esc = Self::encode_winansi_text(builder, line_text);
2564 let _ = writeln!(
2565 stream,
2566 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2567 font_idx,
2568 font_size,
2569 pdf_x + 2.0,
2570 ly,
2571 esc
2572 );
2573 }
2574 } else {
2575 let escaped = Self::encode_winansi_text(builder, &display_text);
2576 let text_y = pdf_y + (h - font_size) / 2.0;
2577 let _ = writeln!(
2578 stream,
2579 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2580 font_idx,
2581 font_size,
2582 pdf_x + 2.0,
2583 text_y,
2584 escaped
2585 );
2586 }
2587 } else if let Some(ref ph) = placeholder {
2588 if !ph.is_empty() {
2589 let font_idx = builder
2591 .font_objects
2592 .iter()
2593 .enumerate()
2594 .find(|(_, (key, _))| {
2595 key.family == "Helvetica"
2596 && key.weight == 400
2597 && !key.italic
2598 })
2599 .map(|(i, _)| i)
2600 .unwrap_or(0);
2601 let escaped = Self::encode_winansi_text(builder, ph);
2602 let text_y = pdf_y + (h - font_size) / 2.0;
2603 let _ = writeln!(
2604 stream,
2605 "BT /F{} {:.1} Tf 0.6 g {:.2} {:.2} Td ({}) Tj ET",
2606 font_idx,
2607 font_size,
2608 pdf_x + 2.0,
2609 text_y,
2610 escaped
2611 );
2612 }
2613 }
2614 }
2615 }
2616 FormFieldType::Dropdown {
2617 value, font_size, ..
2618 } => {
2619 let _ = writeln!(stream, "1 1 1 rg");
2621 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2622 let _ = writeln!(stream, "0.5 w");
2623 let _ =
2624 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2625 if flatten_forms {
2627 if let Some(ref val) = value {
2628 if !val.is_empty() {
2629 let font_idx = builder
2630 .font_objects
2631 .iter()
2632 .enumerate()
2633 .find(|(_, (key, _))| {
2634 key.family == "Helvetica"
2635 && key.weight == 400
2636 && !key.italic
2637 })
2638 .map(|(i, _)| i)
2639 .unwrap_or(0);
2640 let escaped = Self::encode_winansi_text(builder, val);
2641 let text_y = pdf_y + (h - font_size) / 2.0;
2642 let _ = writeln!(
2643 stream,
2644 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2645 font_idx,
2646 font_size,
2647 pdf_x + 2.0,
2648 text_y,
2649 escaped
2650 );
2651 }
2652 }
2653 }
2654 }
2655 }
2656 let _ = writeln!(stream, "Q");
2657 }
2658 }
2659
2660 if wrap_own_draw_as_artifact {
2663 let _ = writeln!(stream, "EMC");
2664 }
2665
2666 let clip_overflow = matches!(element.overflow, Overflow::Hidden);
2671 if clip_overflow {
2672 let clip_x = element.x;
2673 let clip_y = page_height - element.y - element.height;
2674 let clip_w = element.width;
2675 let clip_h = element.height;
2676 let radius = if let DrawCommand::Rect { border_radius, .. } = &element.draw {
2680 Some(border_radius)
2681 } else {
2682 None
2683 };
2684 let has_rounded_corners = radius.is_some_and(|r| {
2685 r.top_left > 0.0 || r.top_right > 0.0 || r.bottom_right > 0.0 || r.bottom_left > 0.0
2686 });
2687 let _ = writeln!(stream, "q");
2688 if has_rounded_corners {
2689 self.write_rounded_rect(stream, clip_x, clip_y, clip_w, clip_h, radius.unwrap());
2690 let _ = writeln!(stream, "W n");
2691 } else {
2692 let _ = writeln!(
2693 stream,
2694 "{:.2} {:.2} {:.2} {:.2} re W n",
2695 clip_x, clip_y, clip_w, clip_h
2696 );
2697 }
2698 }
2699
2700 for child in &element.children {
2701 self.write_element(
2702 stream,
2703 child,
2704 page_height,
2705 builder,
2706 page_idx,
2707 element_counter,
2708 gradient_counter,
2709 page_number,
2710 total_pages,
2711 tag_builder.as_deref_mut(),
2712 flatten_forms,
2713 );
2714 }
2715
2716 if clip_overflow {
2717 let _ = writeln!(stream, "Q");
2718 }
2719
2720 if has_transform {
2722 let _ = writeln!(stream, "Q");
2723 }
2724
2725 if needs_element_opacity {
2728 let _ = writeln!(stream, "Q");
2729 }
2730
2731 if tagged_mcid.is_some() {
2733 let _ = writeln!(stream, "EMC");
2734 if let Some(ref mut tb) = tag_builder {
2735 tb.end_element();
2736 }
2737 } else if is_artifact {
2738 let _ = writeln!(stream, "EMC");
2739 } else if artifact_own_draw {
2740 if let Some(ref mut tb) = tag_builder {
2743 tb.end_element();
2744 }
2745 }
2746 }
2747
2748 fn write_rounded_rect(
2749 &self,
2750 stream: &mut String,
2751 x: f64,
2752 y: f64,
2753 w: f64,
2754 h: f64,
2755 r: &crate::style::CornerValues,
2756 ) {
2757 let k = 0.5522847498;
2758
2759 let tl = r.top_left.min(w / 2.0).min(h / 2.0);
2760 let tr = r.top_right.min(w / 2.0).min(h / 2.0);
2761 let br = r.bottom_right.min(w / 2.0).min(h / 2.0);
2762 let bl = r.bottom_left.min(w / 2.0).min(h / 2.0);
2763
2764 let _ = writeln!(stream, "{:.2} {:.2} m", x + bl, y);
2765
2766 let _ = writeln!(stream, "{:.2} {:.2} l", x + w - br, y);
2767 if br > 0.0 {
2768 let _ = writeln!(
2769 stream,
2770 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2771 x + w - br + br * k,
2772 y,
2773 x + w,
2774 y + br - br * k,
2775 x + w,
2776 y + br
2777 );
2778 }
2779
2780 let _ = writeln!(stream, "{:.2} {:.2} l", x + w, y + h - tr);
2781 if tr > 0.0 {
2782 let _ = writeln!(
2783 stream,
2784 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2785 x + w,
2786 y + h - tr + tr * k,
2787 x + w - tr + tr * k,
2788 y + h,
2789 x + w - tr,
2790 y + h
2791 );
2792 }
2793
2794 let _ = writeln!(stream, "{:.2} {:.2} l", x + tl, y + h);
2795 if tl > 0.0 {
2796 let _ = writeln!(
2797 stream,
2798 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2799 x + tl - tl * k,
2800 y + h,
2801 x,
2802 y + h - tl + tl * k,
2803 x,
2804 y + h - tl
2805 );
2806 }
2807
2808 let _ = writeln!(stream, "{:.2} {:.2} l", x, y + bl);
2809 if bl > 0.0 {
2810 let _ = writeln!(
2811 stream,
2812 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2813 x,
2814 y + bl - bl * k,
2815 x + bl - bl * k,
2816 y,
2817 x + bl,
2818 y
2819 );
2820 }
2821
2822 let _ = writeln!(stream, "h");
2823 }
2824
2825 #[allow(clippy::too_many_arguments)]
2826 fn write_border_sides(
2827 &self,
2828 stream: &mut String,
2829 x: f64,
2830 y: f64,
2831 w: f64,
2832 h: f64,
2833 bw: &Edges,
2834 bc: &crate::style::EdgeValues<Color>,
2835 bs: &crate::style::EdgeValues<crate::style::BorderStyle>,
2836 ) {
2837 fn dash_ops(style: crate::style::BorderStyle, width: f64) -> String {
2842 use crate::style::BorderStyle::*;
2843 match style {
2844 Solid => String::new(),
2845 Dashed => format!("[{:.2} {:.2}] 0 d\n", width * 2.0, width),
2846 Dotted => format!("1 J\n[0 {:.2}] 0 d\n", width * 2.0),
2847 }
2848 }
2849 let sides = [
2851 (bc.top, bw.top, bs.top, x, y + h, x + w, y + h),
2852 (bc.bottom, bw.bottom, bs.bottom, x, y, x + w, y),
2853 (bc.left, bw.left, bs.left, x, y, x, y + h),
2854 (bc.right, bw.right, bs.right, x + w, y, x + w, y + h),
2855 ];
2856 for (color, width, style, x0, y0, x1, y1) in sides {
2857 if width <= 0.0 {
2858 continue;
2859 }
2860 let _ = write!(
2861 stream,
2862 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{}{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2863 color.r,
2864 color.g,
2865 color.b,
2866 width,
2867 dash_ops(style, width),
2868 x0,
2869 y0,
2870 x1,
2871 y1
2872 );
2873 }
2874 }
2875
2876 fn emit_pdfua_embedded_standard(
2888 builder: &mut PdfBuilder,
2889 key: &FontKey,
2890 std_font: &crate::font::StandardFont,
2891 metrics: &crate::font::StandardFontMetrics,
2892 font_context: &FontContext,
2893 ) -> bool {
2894 let lib_family = match std_font.liberation_family() {
2895 Some(f) => f,
2896 None => return false, };
2898 let lib_bytes: &[u8] = match font_context.resolve(lib_family, key.weight, key.italic) {
2901 FontData::Custom { data, .. } => data,
2902 FontData::Standard(_) => return false,
2903 };
2904 let face = match ttf_parser::Face::parse(lib_bytes, 0) {
2905 Ok(f) => f,
2906 Err(_) => return false,
2907 };
2908 let scale = 1000.0 / face.units_per_em() as f64;
2909 let bbox = face.global_bounding_box();
2910 let pdf_name = Self::sanitize_font_name(lib_family, key.weight, key.italic);
2911
2912 let compressed = compress_to_vec_zlib(lib_bytes, 6);
2914 let fontfile2_id = builder.objects.len();
2915 let mut ff2: Vec<u8> = Vec::new();
2916 let _ = write!(
2917 ff2,
2918 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
2919 compressed.len(),
2920 lib_bytes.len()
2921 );
2922 ff2.extend_from_slice(&compressed);
2923 ff2.extend_from_slice(b"\nendstream");
2924 builder.objects.push(PdfObject {
2925 id: fontfile2_id,
2926 data: ff2,
2927 });
2928
2929 let fd_id = builder.objects.len();
2931 let cap_height =
2932 (face.capital_height().unwrap_or_else(|| face.ascender()) as f64 * scale) as i32;
2933 let fd = format!(
2934 "<< /Type /FontDescriptor /FontName /{name} /Flags {flags} \
2935 /FontBBox [{x0} {y0} {x1} {y1}] /ItalicAngle {ia} \
2936 /Ascent {asc} /Descent {desc} /CapHeight {cap} /StemV {stem} \
2937 /FontFile2 {ff2} 0 R >>",
2938 name = pdf_name,
2939 flags = std_font.descriptor_flags(),
2940 x0 = (bbox.x_min as f64 * scale) as i32,
2941 y0 = (bbox.y_min as f64 * scale) as i32,
2942 x1 = (bbox.x_max as f64 * scale) as i32,
2943 y1 = (bbox.y_max as f64 * scale) as i32,
2944 ia = if key.italic { -12 } else { 0 },
2945 asc = (face.ascender() as f64 * scale) as i32,
2946 desc = (face.descender() as f64 * scale) as i32,
2947 cap = cap_height,
2948 stem = if key.weight >= 700 { 120 } else { 80 },
2949 ff2 = fontfile2_id,
2950 );
2951 builder.objects.push(PdfObject {
2952 id: fd_id,
2953 data: fd.into_bytes(),
2954 });
2955
2956 let declared_widths: Vec<u16> = metrics
2970 .widths
2971 .iter()
2972 .enumerate()
2973 .map(|(i, &afm)| {
2974 let code = 32u8.wrapping_add(i as u8); if let Some(ch) = crate::font::winansi_to_char(code) {
2976 if let Some(gid) = face.glyph_index(ch) {
2977 if let Some(adv) = face.glyph_hor_advance(gid) {
2978 let hmtx = (adv as f64 * scale).round() as u16;
2979 if (hmtx as i32 - afm as i32).abs() > 1 {
2980 return hmtx;
2981 }
2982 }
2983 }
2984 }
2985 afm
2986 })
2987 .collect();
2988 let widths_str: String = declared_widths
2989 .iter()
2990 .map(|w| w.to_string())
2991 .collect::<Vec<_>>()
2992 .join(" ");
2993 let obj_id = builder.objects.len();
2994 let font_dict = format!(
2995 "<< /Type /Font /Subtype /TrueType /BaseFont /{name} \
2996 /Encoding /WinAnsiEncoding \
2997 /FirstChar 32 /LastChar 255 /Widths [{w}] \
2998 /FontDescriptor {fd} 0 R >>",
2999 name = pdf_name,
3000 w = widths_str,
3001 fd = fd_id,
3002 );
3003 builder.objects.push(PdfObject {
3004 id: obj_id,
3005 data: font_dict.into_bytes(),
3006 });
3007 builder.font_objects.push((key.clone(), obj_id));
3008 builder.embedded_standard_fonts.insert(key.clone());
3012 true
3013 }
3014
3015 #[allow(clippy::too_many_arguments)]
3016 fn register_fonts(
3017 &self,
3018 builder: &mut PdfBuilder,
3019 pages: &[LayoutPage],
3020 font_context: &FontContext,
3021 pdf_ua: bool,
3022 pdfa: bool,
3023 pdf_version: crate::model::PdfVersion,
3024 ) -> Result<(), FormeError> {
3025 let mut font_usage_map: HashMap<FontKey, FontUsage> = HashMap::new();
3027
3028 for page in pages {
3029 Self::collect_font_usage(&page.elements, &mut font_usage_map);
3030 }
3031
3032 let mut keys: Vec<FontKey> = font_usage_map.keys().cloned().collect();
3033
3034 keys.sort_by(|a, b| {
3036 a.family
3037 .cmp(&b.family)
3038 .then(a.weight.cmp(&b.weight))
3039 .then(a.italic.cmp(&b.italic))
3040 });
3041 keys.dedup();
3042
3043 if keys.is_empty() {
3045 keys.push(FontKey {
3046 family: "Helvetica".to_string(),
3047 weight: 400,
3048 italic: false,
3049 });
3050 }
3051
3052 for key in &keys {
3053 let font_data = font_context.resolve(&key.family, key.weight, key.italic);
3054
3055 match font_data {
3056 FontData::Standard(std_font) => {
3057 let metrics = std_font.metrics();
3058
3059 if pdf_ua || pdfa || pdf_version == crate::model::PdfVersion::V2_0 {
3074 if Self::emit_pdfua_embedded_standard(
3075 builder,
3076 key,
3077 std_font,
3078 &metrics,
3079 font_context,
3080 ) {
3081 continue;
3082 }
3083 if pdf_version == crate::model::PdfVersion::V2_0 {
3089 let remedy = match std_font.liberation_family() {
3090 Some(lib) => format!(
3091 "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"
3092 ),
3093 None => "register an embeddable TrueType font for this text (Symbol/ZapfDingbats have no metric-compatible substitute)"
3094 .to_string(),
3095 };
3096 return Err(FormeError::FontError(format!(
3097 "pdfVersion \"2.0\": font '{}' is not embedded. ISO 32000-2 removes the standard-14 provision, so every font must be embedded — {}.",
3098 std_font.pdf_name(),
3099 remedy,
3100 )));
3101 }
3102 if let Some(lib) = std_font.liberation_family() {
3108 builder.warnings.push(format!(
3109 "pdfUa: font '{}' is not embedded, so the PDF will not conform to \
3110 PDF/UA (all fonts must be embedded). Install \
3111 @formepdf/fonts-standard and register its fonts \
3112 (`for (const f of standardFonts()) Font.register(f)`) — Forme \
3113 will then embed the metric-compatible {} in its place.",
3114 std_font.pdf_name(),
3115 lib,
3116 ));
3117 }
3118 }
3119
3120 let obj_id = builder.objects.len();
3121 let widths_str: String = metrics
3124 .widths
3125 .iter()
3126 .map(|w| w.to_string())
3127 .collect::<Vec<_>>()
3128 .join(" ");
3129 let font_dict = format!(
3130 "<< /Type /Font /Subtype /Type1 /BaseFont /{} \
3131 /Encoding /WinAnsiEncoding \
3132 /FirstChar 32 /LastChar 255 /Widths [{}] >>",
3133 std_font.pdf_name(),
3134 widths_str,
3135 );
3136 builder.objects.push(PdfObject {
3137 id: obj_id,
3138 data: font_dict.into_bytes(),
3139 });
3140 builder.font_objects.push((key.clone(), obj_id));
3141 }
3142 FontData::Custom { data, .. } => {
3143 let usage = font_usage_map.get(key);
3144 let used_glyph_ids = usage.map(|u| &u.glyph_ids);
3145 let used_chars = usage.map(|u| &u.chars);
3146 let glyph_to_char = usage.map(|u| &u.glyph_to_char);
3147 let type0_obj_id = Self::write_custom_font_objects(
3148 builder,
3149 key,
3150 data,
3151 used_glyph_ids.cloned().unwrap_or_default(),
3152 used_chars.cloned().unwrap_or_default(),
3153 glyph_to_char.cloned().unwrap_or_default(),
3154 )?;
3155 builder.font_objects.push((key.clone(), type0_obj_id));
3156 }
3157 }
3158 }
3159
3160 Ok(())
3161 }
3162
3163 fn collect_font_usage(
3165 elements: &[LayoutElement],
3166 font_usage: &mut HashMap<FontKey, FontUsage>,
3167 ) {
3168 for element in elements {
3169 let lines_opt = match &element.draw {
3170 DrawCommand::Text { lines, .. } => Some(lines),
3171 DrawCommand::Watermark { lines, .. } => Some(lines),
3172 _ => None,
3173 };
3174 if let Some(lines) = lines_opt {
3175 for line in lines {
3176 for glyph in &line.glyphs {
3177 let italic =
3178 matches!(glyph.font_style, FontStyle::Italic | FontStyle::Oblique);
3179 let key = FontKey {
3180 family: glyph.font_family.to_string(),
3181 weight: glyph.font_weight,
3182 italic,
3183 };
3184 let usage = font_usage.entry(key).or_insert_with(|| FontUsage {
3185 chars: HashSet::new(),
3186 glyph_ids: HashSet::new(),
3187 glyph_to_char: HashMap::new(),
3188 });
3189 usage.chars.insert(glyph.char_value);
3190 if glyph.char_value == PAGE_NUMBER_SENTINEL
3195 || glyph.char_value == TOTAL_PAGES_SENTINEL
3196 {
3197 usage.chars.extend('0'..='9');
3198 }
3199 usage.glyph_ids.insert(glyph.glyph_id);
3200 usage
3202 .glyph_to_char
3203 .entry(glyph.glyph_id)
3204 .or_insert(glyph.char_value);
3205 if let Some(ref ct) = glyph.cluster_text {
3207 if let Some(first_char) = ct.chars().next() {
3209 usage
3210 .glyph_to_char
3211 .entry(glyph.glyph_id)
3212 .or_insert(first_char);
3213 }
3214 }
3215 }
3216 }
3217 }
3218 Self::collect_font_usage(&element.children, font_usage);
3219 }
3220 }
3221
3222 fn register_shadings(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3228 for (page_idx, page) in pages.iter().enumerate() {
3229 let mut counter = 0usize;
3230 Self::collect_shadings_recursive(&page.elements, page_idx, &mut counter, builder);
3231 }
3232 }
3233
3234 fn collect_shadings_recursive(
3235 elements: &[LayoutElement],
3236 page_idx: usize,
3237 counter: &mut usize,
3238 builder: &mut PdfBuilder,
3239 ) {
3240 for element in elements {
3241 if let DrawCommand::Rect {
3242 background_gradient: Some(gradient),
3243 ..
3244 } = &element.draw
3245 {
3246 let ordinal = *counter;
3247 *counter += 1;
3248 let (obj_id, name) =
3249 Self::write_shading_objects(builder, gradient, element, ordinal);
3250 builder
3251 .shading_map
3252 .insert((page_idx, ordinal), (obj_id, name));
3253 }
3254 Self::collect_shadings_recursive(&element.children, page_idx, counter, builder);
3255 }
3256 }
3257
3258 fn write_shading_objects(
3264 builder: &mut PdfBuilder,
3265 gradient: &crate::style::Background,
3266 element: &LayoutElement,
3267 ordinal: usize,
3268 ) -> (usize, String) {
3269 use crate::style::Background;
3270 use crate::style::GradientStop;
3271
3272 let black = Color {
3276 r: 0.0,
3277 g: 0.0,
3278 b: 0.0,
3279 a: 1.0,
3280 };
3281 let stops: Vec<GradientStop> = match gradient {
3282 Background::Color(c) => vec![
3283 GradientStop {
3284 position: 0.0,
3285 color: *c,
3286 },
3287 GradientStop {
3288 position: 1.0,
3289 color: *c,
3290 },
3291 ],
3292 Background::Linear(g) => normalize_gradient_stops(&g.stops, black),
3293 Background::Radial(g) => normalize_gradient_stops(&g.stops, black),
3294 };
3295
3296 let function_id = if stops.len() <= 2 {
3300 let c0 = stops.first().map(|s| s.color).unwrap_or(black);
3301 let c1 = stops.last().map(|s| s.color).unwrap_or(c0);
3302 let id = builder.objects.len();
3303 let data = format!(
3304 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
3305 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
3306 );
3307 builder.objects.push(PdfObject {
3308 id,
3309 data: data.into_bytes(),
3310 });
3311 id
3312 } else {
3313 let mut sub_ids: Vec<usize> = Vec::with_capacity(stops.len() - 1);
3315 for window in stops.windows(2) {
3316 let c0 = window[0].color;
3317 let c1 = window[1].color;
3318 let id = builder.objects.len();
3319 let data = format!(
3320 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
3321 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
3322 );
3323 builder.objects.push(PdfObject {
3324 id,
3325 data: data.into_bytes(),
3326 });
3327 sub_ids.push(id);
3328 }
3329 let bounds: Vec<String> = stops[1..stops.len() - 1]
3333 .iter()
3334 .map(|s| format!("{:.4}", s.position))
3335 .collect();
3336 let encode: Vec<&str> = (0..sub_ids.len()).map(|_| "0 1").collect();
3337 let functions: Vec<String> = sub_ids.iter().map(|i| format!("{} 0 R", i)).collect();
3338 let id = builder.objects.len();
3339 let data = format!(
3340 "<< /FunctionType 3 /Domain [0 1] /Functions [{}] /Bounds [{}] /Encode [{}] >>",
3341 functions.join(" "),
3342 bounds.join(" "),
3343 encode.join(" "),
3344 );
3345 builder.objects.push(PdfObject {
3346 id,
3347 data: data.into_bytes(),
3348 });
3349 id
3350 };
3351
3352 let _ = element.x;
3356 let _ = element.y;
3357 let w = element.width;
3358 let h = element.height;
3359
3360 let shading_id = builder.objects.len();
3361 let shading_data = match gradient {
3362 Background::Linear(g) => {
3363 let theta = g.angle_deg.to_radians();
3373 let dx = theta.sin();
3374 let dy = theta.cos();
3375 let axis_len = w * dx.abs() + h * dy.abs();
3378 let cx_rel = w / 2.0;
3381 let cy_rel = h / 2.0;
3382 let half = axis_len / 2.0;
3383 let x0 = cx_rel - dx * half;
3384 let y0 = cy_rel - dy * half;
3385 let x1 = cx_rel + dx * half;
3386 let y1 = cy_rel + dy * half;
3387 format!(
3388 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
3389 x0, y0, x1, y1, function_id,
3390 )
3391 }
3392 Background::Radial(_) => {
3393 let cx_rel = w / 2.0;
3396 let cy_rel = h / 2.0;
3397 let r_outer = (w / 2.0).max(h / 2.0);
3398 format!(
3399 "<< /ShadingType 3 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} 0 {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
3400 cx_rel, cy_rel, cx_rel, cy_rel, r_outer, function_id,
3401 )
3402 }
3403 Background::Color(_) => {
3404 format!(
3408 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 0 0] /Function {} 0 R /Extend [true true] >>",
3409 function_id,
3410 )
3411 }
3412 };
3413 builder.objects.push(PdfObject {
3414 id: shading_id,
3415 data: shading_data.into_bytes(),
3416 });
3417 (shading_id, format!("Sh{}", ordinal))
3418 }
3419
3420 fn register_page_background_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3424 for (page_idx, page) in pages.iter().enumerate() {
3425 let Some(src) = &page.config.background_image else {
3426 continue;
3427 };
3428 if let Some(&entry) = builder.page_background_url_cache.get(src) {
3430 builder.page_background_image_map.insert(page_idx, entry);
3431 continue;
3432 }
3433 match crate::image_loader::load_image(src) {
3436 Ok(image_data) => {
3437 let img_idx = builder.image_objects.len();
3438 let dims = (img_idx, image_data.width_px, image_data.height_px);
3439 let xobj_id = Self::write_image_xobject(builder, &image_data);
3440 builder.image_objects.push(xobj_id);
3441 builder.page_background_image_map.insert(page_idx, dims);
3442 builder.page_background_url_cache.insert(src.clone(), dims);
3443 }
3444 Err(e) => {
3445 eprintln!("[forme] page background image failed to load: {}", e);
3446 }
3447 }
3448 }
3449 }
3450
3451 fn write_page_background(
3456 &self,
3457 stream: &mut String,
3458 page: &LayoutPage,
3459 page_bg: (usize, u32, u32),
3460 builder: &PdfBuilder,
3461 ) {
3462 use crate::model::{BackgroundPosition, BackgroundSize};
3463 let (img_idx, iw_px, ih_px) = page_bg;
3464 let page_w = page.width;
3465 let page_h = page.height;
3466 let iw = iw_px as f64;
3467 let ih = ih_px as f64;
3468
3469 let size = page.config.background_size.unwrap_or_default();
3470 let (dest_w, dest_h) = match size {
3471 BackgroundSize::Fill => (page_w, page_h),
3472 BackgroundSize::Cover => {
3473 let s = (page_w / iw).max(page_h / ih);
3474 (iw * s, ih * s)
3475 }
3476 BackgroundSize::Contain => {
3477 let s = (page_w / iw).min(page_h / ih);
3478 (iw * s, ih * s)
3479 }
3480 };
3481
3482 let position = page.config.background_position.unwrap_or_default();
3486 let (dest_x, dest_y) = match position {
3489 BackgroundPosition::TopLeft => (0.0, page_h - dest_h),
3490 BackgroundPosition::TopRight => (page_w - dest_w, page_h - dest_h),
3491 BackgroundPosition::BottomLeft => (0.0, 0.0),
3492 BackgroundPosition::BottomRight => (page_w - dest_w, 0.0),
3493 BackgroundPosition::Center => ((page_w - dest_w) / 2.0, (page_h - dest_h) / 2.0),
3494 };
3495
3496 let opacity = page.config.background_opacity.unwrap_or(1.0);
3498 let needs_opacity = opacity < 1.0;
3499 if needs_opacity {
3500 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
3501 let _ = writeln!(stream, "q\n/{} gs", gs_name);
3502 } else {
3503 let _ = writeln!(stream, "q");
3504 }
3505 } else {
3506 let _ = writeln!(stream, "q");
3507 }
3508 let _ = writeln!(
3511 stream,
3512 "{:.2} 0 0 {:.2} {:.2} {:.2} cm\n/Im{} Do\nQ",
3513 dest_w, dest_h, dest_x, dest_y, img_idx,
3514 );
3515 }
3516
3517 fn register_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3519 for (page_idx, page) in pages.iter().enumerate() {
3520 let mut element_counter = 0usize;
3521 Self::collect_images_recursive(&page.elements, page_idx, &mut element_counter, builder);
3522 }
3523 }
3524
3525 fn collect_images_recursive(
3526 elements: &[LayoutElement],
3527 page_idx: usize,
3528 element_counter: &mut usize,
3529 builder: &mut PdfBuilder,
3530 ) {
3531 for element in elements {
3532 match &element.draw {
3533 DrawCommand::Image { image_data } => {
3534 let elem_idx = *element_counter;
3535 *element_counter += 1;
3536
3537 let img_idx = builder.image_objects.len();
3538 let xobj_id = Self::write_image_xobject(builder, image_data);
3539 builder.image_objects.push(xobj_id);
3540 builder
3541 .image_index_map
3542 .insert((page_idx, elem_idx), img_idx);
3543 }
3544 DrawCommand::ImagePlaceholder => {
3545 *element_counter += 1;
3546 }
3547 _ => {
3548 Self::collect_images_recursive(
3549 &element.children,
3550 page_idx,
3551 element_counter,
3552 builder,
3553 );
3554 }
3555 }
3556 }
3557 }
3558
3559 fn register_ext_gstates(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3561 let mut unique_opacities: Vec<f64> = Vec::new();
3562 for page in pages {
3563 Self::collect_opacities_recursive(&page.elements, &mut unique_opacities);
3564 if let Some(o) = page.config.background_opacity {
3566 if o < 1.0 {
3567 unique_opacities.push(o);
3568 }
3569 }
3570 }
3571 unique_opacities.sort_by(|a, b| a.partial_cmp(b).unwrap());
3572 unique_opacities.dedup();
3573
3574 for (idx, &opacity) in unique_opacities.iter().enumerate() {
3575 let obj_id = builder.objects.len();
3576 let gs_name = format!("GS{}", idx);
3577 let obj_data = format!(
3578 "<< /Type /ExtGState /ca {:.4} /CA {:.4} >>",
3579 opacity, opacity
3580 );
3581 builder.objects.push(PdfObject {
3582 id: obj_id,
3583 data: obj_data.into_bytes(),
3584 });
3585 let key = opacity.to_bits();
3586 builder.ext_gstate_map.insert(key, (obj_id, gs_name));
3587 }
3588 }
3589
3590 fn collect_opacities_recursive(elements: &[LayoutElement], opacities: &mut Vec<f64>) {
3591 for element in elements {
3592 if element.opacity < 1.0 {
3599 opacities.push(element.opacity);
3600 }
3601 if let DrawCommand::Rect {
3605 box_shadow: Some(shadow),
3606 ..
3607 } = &element.draw
3608 {
3609 if shadow.color.a < 1.0 {
3610 opacities.push(shadow.color.a);
3611 }
3612 }
3613 match &element.draw {
3614 DrawCommand::Rect { opacity, .. }
3615 | DrawCommand::Text { opacity, .. }
3616 | DrawCommand::Watermark { opacity, .. }
3617 if *opacity < 1.0 =>
3618 {
3619 opacities.push(*opacity);
3620 }
3621 DrawCommand::Chart { primitives } => {
3622 for prim in primitives {
3623 if let crate::chart::ChartPrimitive::FilledPath { opacity, .. } = prim {
3624 if *opacity < 1.0 {
3625 opacities.push(*opacity);
3626 }
3627 }
3628 }
3629 }
3630 DrawCommand::Svg { commands, .. } => {
3631 for cmd in commands {
3632 if let crate::svg::SvgCommand::SetOpacity(opacity) = cmd {
3633 if *opacity < 1.0 {
3634 opacities.push(*opacity);
3635 }
3636 }
3637 }
3638 }
3639 _ => {}
3640 }
3641 Self::collect_opacities_recursive(&element.children, opacities);
3642 }
3643 }
3644
3645 fn build_ext_gstate_resource_dict(&self, builder: &PdfBuilder) -> String {
3647 if builder.ext_gstate_map.is_empty() {
3648 return String::new();
3649 }
3650 let mut entries: Vec<(&String, usize)> = builder
3651 .ext_gstate_map
3652 .values()
3653 .map(|(obj_id, name)| (name, *obj_id))
3654 .collect();
3655 entries.sort_by_key(|(name, _)| (*name).clone());
3656 entries
3657 .iter()
3658 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3659 .collect::<Vec<_>>()
3660 .join(" ")
3661 }
3662
3663 fn write_image_xobject(
3666 builder: &mut PdfBuilder,
3667 image: &crate::image_loader::LoadedImage,
3668 ) -> usize {
3669 use crate::image_loader::{ImagePixelData, JpegColorSpace};
3670
3671 match &image.pixel_data {
3672 ImagePixelData::Jpeg { data, color_space } => {
3673 let color_space_str = match color_space {
3674 JpegColorSpace::DeviceRGB => "/DeviceRGB",
3675 JpegColorSpace::DeviceGray => "/DeviceGray",
3676 };
3677
3678 let obj_id = builder.objects.len();
3679 let mut obj_data: Vec<u8> = Vec::new();
3680 let _ = write!(
3681 obj_data,
3682 "<< /Type /XObject /Subtype /Image \
3683 /Width {} /Height {} \
3684 /ColorSpace {} \
3685 /BitsPerComponent 8 \
3686 /Filter /DCTDecode \
3687 /Length {} >>\nstream\n",
3688 image.width_px,
3689 image.height_px,
3690 color_space_str,
3691 data.len()
3692 );
3693 obj_data.extend_from_slice(data);
3694 obj_data.extend_from_slice(b"\nendstream");
3695 builder.objects.push(PdfObject {
3696 id: obj_id,
3697 data: obj_data,
3698 });
3699 obj_id
3700 }
3701
3702 ImagePixelData::Decoded { rgb, alpha } => {
3703 let smask_id = alpha.as_ref().map(|alpha_data| {
3705 let compressed_alpha = compress_to_vec_zlib(alpha_data, 6);
3706 let smask_obj_id = builder.objects.len();
3707 let mut smask_data: Vec<u8> = Vec::new();
3708 let _ = write!(
3709 smask_data,
3710 "<< /Type /XObject /Subtype /Image \
3711 /Width {} /Height {} \
3712 /ColorSpace /DeviceGray \
3713 /BitsPerComponent 8 \
3714 /Filter /FlateDecode \
3715 /Length {} >>\nstream\n",
3716 image.width_px,
3717 image.height_px,
3718 compressed_alpha.len()
3719 );
3720 smask_data.extend_from_slice(&compressed_alpha);
3721 smask_data.extend_from_slice(b"\nendstream");
3722 builder.objects.push(PdfObject {
3723 id: smask_obj_id,
3724 data: smask_data,
3725 });
3726 smask_obj_id
3727 });
3728
3729 let compressed_rgb = compress_to_vec_zlib(rgb, 6);
3731 let obj_id = builder.objects.len();
3732 let mut obj_data: Vec<u8> = Vec::new();
3733
3734 let smask_ref = smask_id
3735 .map(|id| format!(" /SMask {} 0 R", id))
3736 .unwrap_or_default();
3737
3738 let _ = write!(
3739 obj_data,
3740 "<< /Type /XObject /Subtype /Image \
3741 /Width {} /Height {} \
3742 /ColorSpace /DeviceRGB \
3743 /BitsPerComponent 8 \
3744 /Filter /FlateDecode \
3745 /Length {}{} >>\nstream\n",
3746 image.width_px,
3747 image.height_px,
3748 compressed_rgb.len(),
3749 smask_ref
3750 );
3751 obj_data.extend_from_slice(&compressed_rgb);
3752 obj_data.extend_from_slice(b"\nendstream");
3753 builder.objects.push(PdfObject {
3754 id: obj_id,
3755 data: obj_data,
3756 });
3757 obj_id
3758 }
3759 }
3760 }
3761
3762 fn build_shading_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3766 let mut entries: Vec<(String, usize)> = builder
3767 .shading_map
3768 .iter()
3769 .filter(|(&(p, _), _)| p == page_idx)
3770 .map(|(_, (obj_id, name))| (name.clone(), *obj_id))
3771 .collect();
3772 if entries.is_empty() {
3773 return String::new();
3774 }
3775 entries.sort_by(|a, b| a.0.cmp(&b.0));
3776 entries
3777 .iter()
3778 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3779 .collect::<Vec<_>>()
3780 .join(" ")
3781 }
3782
3783 fn build_xobject_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3784 let mut entries: Vec<(usize, usize)> = Vec::new();
3785 for (&(pidx, _), &img_idx) in &builder.image_index_map {
3786 if pidx == page_idx {
3787 let obj_id = builder.image_objects[img_idx];
3788 entries.push((img_idx, obj_id));
3789 }
3790 }
3791 if let Some(&(img_idx, _, _)) = builder.page_background_image_map.get(&page_idx) {
3794 let obj_id = builder.image_objects[img_idx];
3795 entries.push((img_idx, obj_id));
3796 }
3797 if entries.is_empty() {
3798 return String::new();
3799 }
3800 entries.sort_by_key(|(idx, _)| *idx);
3801 entries.dedup();
3802 entries
3803 .iter()
3804 .map(|(idx, obj_id)| format!("/Im{} {} 0 R", idx, obj_id))
3805 .collect::<Vec<_>>()
3806 .join(" ")
3807 }
3808
3809 fn write_custom_font_objects(
3816 builder: &mut PdfBuilder,
3817 key: &FontKey,
3818 ttf_data: &[u8],
3819 used_glyph_ids: HashSet<u16>,
3820 used_chars: HashSet<char>,
3821 glyph_to_char_map: HashMap<u16, char>,
3822 ) -> Result<usize, FormeError> {
3823 let face = ttf_parser::Face::parse(ttf_data, 0).map_err(|e| {
3824 FormeError::FontError(format!(
3825 "Failed to parse TTF data for font '{}': {}",
3826 key.family, e
3827 ))
3828 })?;
3829
3830 let units_per_em = face.units_per_em();
3831 let ascender = face.ascender();
3832 let descender = face.descender();
3833
3834 let mut char_to_orig_gid: HashMap<char, u16> = HashMap::new();
3836 for &ch in &used_chars {
3837 if let Some(gid) = face.glyph_index(ch) {
3838 char_to_orig_gid.insert(ch, gid.0);
3839 }
3840 }
3841
3842 let mut all_orig_gids: HashSet<u16> = used_glyph_ids.clone();
3846 for &gid in char_to_orig_gid.values() {
3847 all_orig_gids.insert(gid);
3848 }
3849
3850 let (embed_ttf, gid_remap) = match subset_ttf(ttf_data, &all_orig_gids) {
3852 Ok(subset_result) => (subset_result.ttf_data, subset_result.gid_remap),
3853 Err(_) => {
3854 let identity: HashMap<u16, u16> =
3856 all_orig_gids.iter().map(|&gid| (gid, gid)).collect();
3857 (ttf_data.to_vec(), identity)
3858 }
3859 };
3860
3861 let char_to_gid: HashMap<char, u16> = char_to_orig_gid
3863 .iter()
3864 .filter_map(|(&ch, &orig_gid)| gid_remap.get(&orig_gid).map(|&new_gid| (ch, new_gid)))
3865 .collect();
3866
3867 let gid_remap_for_embed = gid_remap.clone();
3869
3870 let mut new_gid_to_char: HashMap<u16, char> = HashMap::new();
3872 for (&orig_gid, &ch) in &glyph_to_char_map {
3874 if let Some(&new_gid) = gid_remap.get(&orig_gid) {
3875 new_gid_to_char.entry(new_gid).or_insert(ch);
3876 }
3877 }
3878 for (&ch, &new_gid) in &char_to_gid {
3880 new_gid_to_char.entry(new_gid).or_insert(ch);
3881 }
3882
3883 let pdf_font_name = Self::sanitize_font_name(&key.family, key.weight, key.italic);
3884
3885 let compressed_ttf = compress_to_vec_zlib(&embed_ttf, 6);
3887 let fontfile2_id = builder.objects.len();
3888 let mut fontfile2_data: Vec<u8> = Vec::new();
3889 let _ = write!(
3890 fontfile2_data,
3891 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
3892 compressed_ttf.len(),
3893 embed_ttf.len()
3894 );
3895 fontfile2_data.extend_from_slice(&compressed_ttf);
3896 fontfile2_data.extend_from_slice(b"\nendstream");
3897 builder.objects.push(PdfObject {
3898 id: fontfile2_id,
3899 data: fontfile2_data,
3900 });
3901
3902 let subset_face = ttf_parser::Face::parse(&embed_ttf, 0).unwrap_or_else(|_| face.clone());
3904 let subset_upem = subset_face.units_per_em();
3905
3906 let font_descriptor_id = builder.objects.len();
3908 let bbox = face.global_bounding_box();
3909 let scale = 1000.0 / units_per_em as f64;
3910 let bbox_str = format!(
3911 "[{} {} {} {}]",
3912 (bbox.x_min as f64 * scale) as i32,
3913 (bbox.y_min as f64 * scale) as i32,
3914 (bbox.x_max as f64 * scale) as i32,
3915 (bbox.y_max as f64 * scale) as i32,
3916 );
3917
3918 let flags = 4u32;
3919 let cap_height = face.capital_height().unwrap_or(ascender) as f64 * scale;
3920 let stem_v = if key.weight >= 700 { 120 } else { 80 };
3921
3922 let font_descriptor_dict = format!(
3923 "<< /Type /FontDescriptor /FontName /{} /Flags {} \
3924 /FontBBox {} /ItalicAngle {} \
3925 /Ascent {} /Descent {} /CapHeight {} /StemV {} \
3926 /FontFile2 {} 0 R >>",
3927 pdf_font_name,
3928 flags,
3929 bbox_str,
3930 if key.italic { -12 } else { 0 },
3931 (ascender as f64 * scale) as i32,
3932 (descender as f64 * scale) as i32,
3933 cap_height as i32,
3934 stem_v,
3935 fontfile2_id,
3936 );
3937 builder.objects.push(PdfObject {
3938 id: font_descriptor_id,
3939 data: font_descriptor_dict.into_bytes(),
3940 });
3941
3942 let cidfont_id = builder.objects.len();
3944 let w_array = Self::build_w_array_from_gids(&gid_remap, &subset_face, subset_upem);
3946 let default_width = subset_face
3947 .glyph_hor_advance(ttf_parser::GlyphId(0))
3948 .map(|adv| (adv as f64 * 1000.0 / subset_upem as f64) as u32)
3949 .unwrap_or(1000);
3950 let cidfont_dict = format!(
3951 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{} \
3952 /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> \
3953 /FontDescriptor {} 0 R /DW {} /W {} \
3954 /CIDToGIDMap /Identity >>",
3955 pdf_font_name, font_descriptor_id, default_width, w_array,
3956 );
3957 builder.objects.push(PdfObject {
3958 id: cidfont_id,
3959 data: cidfont_dict.into_bytes(),
3960 });
3961
3962 let tounicode_id = builder.objects.len();
3964 let cmap_content = Self::build_tounicode_cmap_from_gids(&new_gid_to_char, &pdf_font_name);
3965 let compressed_cmap = compress_to_vec_zlib(cmap_content.as_bytes(), 6);
3966 let mut tounicode_data: Vec<u8> = Vec::new();
3967 let _ = write!(
3968 tounicode_data,
3969 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
3970 compressed_cmap.len()
3971 );
3972 tounicode_data.extend_from_slice(&compressed_cmap);
3973 tounicode_data.extend_from_slice(b"\nendstream");
3974 builder.objects.push(PdfObject {
3975 id: tounicode_id,
3976 data: tounicode_data,
3977 });
3978
3979 let type0_id = builder.objects.len();
3981 let type0_dict = format!(
3982 "<< /Type /Font /Subtype /Type0 /BaseFont /{} \
3983 /Encoding /Identity-H \
3984 /DescendantFonts [{} 0 R] \
3985 /ToUnicode {} 0 R >>",
3986 pdf_font_name, cidfont_id, tounicode_id,
3987 );
3988 builder.objects.push(PdfObject {
3989 id: type0_id,
3990 data: type0_dict.into_bytes(),
3991 });
3992
3993 builder.custom_font_data.insert(
3995 key.clone(),
3996 CustomFontEmbedData {
3997 ttf_data: embed_ttf,
3998 gid_remap: gid_remap_for_embed,
3999 glyph_to_char: glyph_to_char_map,
4000 char_to_gid,
4001 units_per_em,
4002 ascender,
4003 descender,
4004 },
4005 );
4006
4007 Ok(type0_id)
4008 }
4009
4010 fn build_w_array_from_gids(
4012 gid_remap: &HashMap<u16, u16>,
4013 face: &ttf_parser::Face,
4014 units_per_em: u16,
4015 ) -> String {
4016 let scale = 1000.0 / units_per_em as f64;
4017
4018 let mut entries: Vec<(u16, u32)> = Vec::new();
4019 let mut seen_gids: HashSet<u16> = HashSet::new();
4020
4021 for &new_gid in gid_remap.values() {
4022 if seen_gids.contains(&new_gid) {
4023 continue;
4024 }
4025 seen_gids.insert(new_gid);
4026 let advance = face
4027 .glyph_hor_advance(ttf_parser::GlyphId(new_gid))
4028 .unwrap_or(0);
4029 let width = (advance as f64 * scale) as u32;
4030 entries.push((new_gid, width));
4031 }
4032
4033 entries.sort_by_key(|(gid, _)| *gid);
4034
4035 let mut result = String::from("[");
4037 for (gid, width) in &entries {
4038 let _ = write!(result, " {} [{}]", gid, width);
4039 }
4040 result.push_str(" ]");
4041 result
4042 }
4043
4044 fn build_tounicode_cmap_from_gids(gid_to_char: &HashMap<u16, char>, font_name: &str) -> String {
4046 let mut gid_to_unicode: Vec<(u16, u32)> = gid_to_char
4047 .iter()
4048 .map(|(&gid, &ch)| (gid, ch as u32))
4049 .collect();
4050 gid_to_unicode.sort_by_key(|(gid, _)| *gid);
4051
4052 let mut cmap = String::new();
4053 let _ = writeln!(cmap, "/CIDInit /ProcSet findresource begin");
4054 let _ = writeln!(cmap, "12 dict begin");
4055 let _ = writeln!(cmap, "begincmap");
4056 let _ = writeln!(cmap, "/CIDSystemInfo");
4057 let _ = writeln!(
4058 cmap,
4059 "<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def"
4060 );
4061 let _ = writeln!(cmap, "/CMapName /{}-UTF16 def", font_name);
4062 let _ = writeln!(cmap, "/CMapType 2 def");
4063 let _ = writeln!(cmap, "1 begincodespacerange");
4064 let _ = writeln!(cmap, "<0000> <FFFF>");
4065 let _ = writeln!(cmap, "endcodespacerange");
4066
4067 for chunk in gid_to_unicode.chunks(100) {
4069 let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
4070 for &(gid, unicode) in chunk {
4071 let _ = writeln!(cmap, "<{:04X}> <{:04X}>", gid, unicode);
4072 }
4073 let _ = writeln!(cmap, "endbfchar");
4074 }
4075
4076 let _ = writeln!(cmap, "endcmap");
4077 let _ = writeln!(cmap, "CMapName currentdict /CMap defineresource pop");
4078 let _ = writeln!(cmap, "end");
4079 let _ = writeln!(cmap, "end");
4080
4081 cmap
4082 }
4083
4084 fn sanitize_font_name(family: &str, weight: u32, italic: bool) -> String {
4087 let mut name: String = family
4088 .chars()
4089 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
4090 .collect();
4091
4092 if weight >= 700 {
4093 name.push_str("-Bold");
4094 }
4095 if italic {
4096 name.push_str("-Italic");
4097 }
4098
4099 if name.is_empty() {
4101 name = "CustomFont".to_string();
4102 }
4103
4104 name
4105 }
4106
4107 fn build_font_resource_dict(&self, font_objects: &[(FontKey, usize)]) -> String {
4108 font_objects
4109 .iter()
4110 .enumerate()
4111 .map(|(i, (_, obj_id))| format!("/F{} {} 0 R", i, obj_id))
4112 .collect::<Vec<_>>()
4113 .join(" ")
4114 }
4115
4116 fn font_index(
4118 &self,
4119 family: &str,
4120 weight: u32,
4121 font_style: FontStyle,
4122 font_objects: &[(FontKey, usize)],
4123 ) -> usize {
4124 let italic = matches!(font_style, FontStyle::Italic | FontStyle::Oblique);
4125
4126 for (i, (key, _)) in font_objects.iter().enumerate() {
4128 if key.family == family && key.weight == weight && key.italic == italic {
4129 return i;
4130 }
4131 }
4132
4133 let snapped = if weight >= 600 { 700 } else { 400 };
4135 for (i, (key, _)) in font_objects.iter().enumerate() {
4136 if key.family == family && key.weight == snapped && key.italic == italic {
4137 return i;
4138 }
4139 }
4140
4141 for (i, (key, _)) in font_objects.iter().enumerate() {
4143 if key.family == "Helvetica" && key.weight == snapped && key.italic == italic {
4144 return i;
4145 }
4146 }
4147
4148 0
4150 }
4151
4152 fn group_glyphs_by_style(glyphs: &[PositionedGlyph]) -> Vec<Vec<&PositionedGlyph>> {
4155 if glyphs.is_empty() {
4156 return vec![];
4157 }
4158
4159 let mut groups: Vec<Vec<&PositionedGlyph>> = Vec::new();
4160 let mut current_group: Vec<&PositionedGlyph> = vec![&glyphs[0]];
4161
4162 for glyph in &glyphs[1..] {
4163 let prev = current_group.last().unwrap();
4164 let same_style = glyph.font_family == prev.font_family
4165 && glyph.font_weight == prev.font_weight
4166 && std::mem::discriminant(&glyph.font_style)
4167 == std::mem::discriminant(&prev.font_style)
4168 && (glyph.font_size - prev.font_size).abs() < 0.01
4169 && Self::colors_equal(&glyph.color, &prev.color)
4170 && std::mem::discriminant(&glyph.text_decoration)
4171 == std::mem::discriminant(&prev.text_decoration);
4172
4173 if same_style {
4174 current_group.push(glyph);
4175 } else {
4176 groups.push(current_group);
4177 current_group = vec![glyph];
4178 }
4179 }
4180 groups.push(current_group);
4181 groups
4182 }
4183
4184 fn colors_equal(a: &Option<Color>, b: &Option<Color>) -> bool {
4185 match (a, b) {
4186 (None, None) => true,
4187 (Some(ca), Some(cb)) => {
4188 (ca.r - cb.r).abs() < 0.001
4189 && (ca.g - cb.g).abs() < 0.001
4190 && (ca.b - cb.b).abs() < 0.001
4191 && (ca.a - cb.a).abs() < 0.001
4192 }
4193 _ => false,
4194 }
4195 }
4196
4197 fn collect_link_annotations(
4201 elements: &[LayoutElement],
4202 page_height: f64,
4203 annotations: &mut Vec<LinkAnnotation>,
4204 ) {
4205 for element in elements {
4206 if let Some(ref href) = element.href {
4207 if !href.is_empty() {
4208 let pdf_y = page_height - element.y - element.height;
4209 annotations.push(LinkAnnotation {
4210 x: element.x,
4211 y: pdf_y,
4212 width: element.width,
4213 height: element.height,
4214 href: href.clone(),
4215 });
4216 continue;
4218 }
4219 }
4220 Self::collect_link_annotations(&element.children, page_height, annotations);
4221 }
4222 }
4223
4224 fn collect_form_fields(
4226 elements: &[LayoutElement],
4227 page_height: f64,
4228 page_idx: usize,
4229 fields: &mut Vec<FormFieldData>,
4230 ) {
4231 for element in elements {
4232 if let DrawCommand::FormField {
4233 ref field_type,
4234 ref name,
4235 } = element.draw
4236 {
4237 let pdf_y = page_height - element.y - element.height;
4238 fields.push(FormFieldData {
4239 field_type: field_type.clone(),
4240 name: name.clone(),
4241 x: element.x,
4242 y: pdf_y,
4243 width: element.width,
4244 height: element.height,
4245 page_idx,
4246 });
4247 }
4248 Self::collect_form_fields(&element.children, page_height, page_idx, fields);
4249 }
4250 }
4251
4252 fn collect_bookmarks(
4254 elements: &[LayoutElement],
4255 page_height: f64,
4256 page_obj_id: usize,
4257 bookmarks: &mut Vec<PdfBookmark>,
4258 ) {
4259 for element in elements {
4260 if let Some(ref title) = element.bookmark {
4261 let y_pdf = page_height - element.y;
4262 bookmarks.push(PdfBookmark {
4263 title: title.clone(),
4264 page_obj_id,
4265 y_pdf,
4266 });
4267 }
4268 Self::collect_bookmarks(&element.children, page_height, page_obj_id, bookmarks);
4269 }
4270 }
4271
4272 fn write_outline_tree(
4275 &self,
4276 builder: &mut PdfBuilder,
4277 bookmarks: &[PdfBookmark],
4278 mut tag_builder: Option<&mut tagged::TagBuilder>,
4279 ) -> usize {
4280 let outlines_id = builder.objects.len();
4282 builder.objects.push(PdfObject {
4283 id: outlines_id,
4284 data: vec![],
4285 });
4286
4287 let mut item_ids: Vec<usize> = Vec::new();
4289 for _bm in bookmarks {
4290 let item_id = builder.objects.len();
4291 builder.objects.push(PdfObject {
4292 id: item_id,
4293 data: vec![],
4294 });
4295 item_ids.push(item_id);
4296 }
4297
4298 for (i, (bm, &item_id)) in bookmarks.iter().zip(item_ids.iter()).enumerate() {
4300 let wants_sd = tag_builder
4307 .as_mut()
4308 .map(|tb| tb.request_struct_destination(&bm.title, item_id))
4309 .unwrap_or(false);
4310 let dest = if wants_sd {
4311 format!("/SD [999999999 0 R /XYZ 0 {:.2} null]", bm.y_pdf)
4312 } else {
4313 format!("/Dest [{} 0 R /XYZ 0 {:.2} null]", bm.page_obj_id, bm.y_pdf)
4314 };
4315 let mut dict = format!(
4316 "<< /Title ({}) /Parent {} 0 R {}",
4317 Self::escape_pdf_string(&bm.title),
4318 outlines_id,
4319 dest,
4320 );
4321 if i > 0 {
4322 let _ = write!(dict, " /Prev {} 0 R", item_ids[i - 1]);
4323 }
4324 if i + 1 < item_ids.len() {
4325 let _ = write!(dict, " /Next {} 0 R", item_ids[i + 1]);
4326 }
4327 dict.push_str(" >>");
4328 builder.objects[item_id].data = dict.into_bytes();
4329 }
4330
4331 let first_id = item_ids.first().copied().unwrap_or(0);
4333 let last_id = item_ids.last().copied().unwrap_or(0);
4334 let outlines_dict = format!(
4335 "<< /Type /Outlines /First {} 0 R /Last {} 0 R /Count {} >>",
4336 first_id,
4337 last_id,
4338 bookmarks.len()
4339 );
4340 builder.objects[outlines_id].data = outlines_dict.into_bytes();
4341
4342 outlines_id
4343 }
4344
4345 fn write_svg_commands(
4347 stream: &mut String,
4348 commands: &[SvgCommand],
4349 ext_gstate_map: &HashMap<u64, (usize, String)>,
4350 ) {
4351 for cmd in commands {
4352 match cmd {
4353 SvgCommand::MoveTo(x, y) => {
4354 let _ = writeln!(stream, "{:.2} {:.2} m", x, y);
4355 }
4356 SvgCommand::LineTo(x, y) => {
4357 let _ = writeln!(stream, "{:.2} {:.2} l", x, y);
4358 }
4359 SvgCommand::CurveTo(x1, y1, x2, y2, x3, y3) => {
4360 let _ = writeln!(
4361 stream,
4362 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4363 x1, y1, x2, y2, x3, y3
4364 );
4365 }
4366 SvgCommand::ClosePath => {
4367 let _ = writeln!(stream, "h");
4368 }
4369 SvgCommand::SetFill(r, g, b) => {
4370 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", r, g, b);
4371 }
4372 SvgCommand::SetFillNone => {
4373 }
4375 SvgCommand::SetStroke(r, g, b) => {
4376 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", r, g, b);
4377 }
4378 SvgCommand::SetStrokeNone => {
4379 }
4381 SvgCommand::SetStrokeWidth(w) => {
4382 let _ = writeln!(stream, "{:.2} w", w);
4383 }
4384 SvgCommand::Fill => {
4385 let _ = writeln!(stream, "f");
4386 }
4387 SvgCommand::Stroke => {
4388 let _ = writeln!(stream, "S");
4389 }
4390 SvgCommand::FillAndStroke => {
4391 let _ = writeln!(stream, "B");
4392 }
4393 SvgCommand::SetLineCap(cap) => {
4394 let _ = writeln!(stream, "{} J", cap);
4395 }
4396 SvgCommand::SetLineJoin(join) => {
4397 let _ = writeln!(stream, "{} j", join);
4398 }
4399 SvgCommand::SaveState => {
4400 let _ = writeln!(stream, "q");
4401 }
4402 SvgCommand::RestoreState => {
4403 let _ = writeln!(stream, "Q");
4404 }
4405 SvgCommand::SetOpacity(opacity) => {
4406 if let Some((_, gs_name)) = ext_gstate_map.get(&opacity.to_bits()) {
4407 let _ = writeln!(stream, "/{} gs", gs_name);
4408 }
4409 }
4410 }
4411 }
4412 }
4413
4414 pub(crate) fn escape_pdf_string(s: &str) -> String {
4416 s.replace('\\', "\\\\")
4417 .replace('(', "\\(")
4418 .replace(')', "\\)")
4419 }
4420
4421 fn decode_attachment_src(src: &str) -> Result<Vec<u8>, FormeError> {
4424 use base64::Engine as _;
4425 let b64 = src.rsplit_once(";base64,").map(|(_, d)| d).unwrap_or(src);
4426 base64::engine::general_purpose::STANDARD
4427 .decode(b64.trim())
4428 .map_err(|e| {
4429 FormeError::RenderError(format!(
4430 "attachment src is not valid base64 (expected base64 bytes or a data: URI): {e}"
4431 ))
4432 })
4433 }
4434
4435 fn mime_to_pdf_name(mime: &str) -> String {
4438 let mut out = String::with_capacity(mime.len() + 2);
4439 for b in mime.bytes() {
4440 let regular =
4441 b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'+' | b'\'' | b'"');
4442 if regular {
4443 out.push(b as char);
4444 } else {
4445 let _ = write!(out, "#{:02X}", b);
4446 }
4447 }
4448 out
4449 }
4450
4451 fn encode_winansi_text(builder: &PdfBuilder, s: &str) -> String {
4455 let mut result = String::with_capacity(s.len());
4456 for ch in s.chars() {
4457 let b = Self::unicode_to_winansi(ch).unwrap_or_else(|| {
4458 builder.missing_glyphs.borrow_mut().insert(ch);
4459 b'?'
4460 });
4461 match b {
4462 b'\\' => result.push_str("\\\\"),
4463 b'(' => result.push_str("\\("),
4464 b')' => result.push_str("\\)"),
4465 0x20..=0x7E => result.push(b as char),
4466 _ => {
4467 let _ = write!(result, "\\{:03o}", b);
4468 }
4469 }
4470 }
4471 result
4472 }
4473
4474 fn unicode_to_winansi(ch: char) -> Option<u8> {
4476 crate::font::unicode_to_winansi(ch)
4477 }
4478
4479 fn serialize(&self, builder: &PdfBuilder, info_obj_id: Option<usize>) -> Vec<u8> {
4481 let mut output: Vec<u8> = Vec::new();
4482 let mut offsets: Vec<usize> = vec![0; builder.objects.len()];
4483
4484 output.extend_from_slice(match builder.pdf_version {
4486 crate::model::PdfVersion::V1_7 => b"%PDF-1.7\n".as_slice(),
4487 crate::model::PdfVersion::V2_0 => b"%PDF-2.0\n".as_slice(),
4488 });
4489 output.extend_from_slice(b"%\xe2\xe3\xcf\xd3\n");
4490
4491 for (i, obj) in builder.objects.iter().enumerate().skip(1) {
4492 offsets[i] = output.len();
4493 let header = format!("{} 0 obj\n", i);
4494 output.extend_from_slice(header.as_bytes());
4495 output.extend_from_slice(&obj.data);
4496 output.extend_from_slice(b"\nendobj\n\n");
4497 }
4498
4499 let xref_offset = output.len();
4500 let _ = writeln!(output, "xref\n0 {}", builder.objects.len());
4501 let _ = writeln!(output, "0000000000 65535 f ");
4502 for offset in offsets.iter().skip(1) {
4503 let _ = writeln!(output, "{:010} 00000 n ", offset);
4504 }
4505
4506 let _ = write!(
4507 output,
4508 "trailer\n<< /Size {} /Root 1 0 R",
4509 builder.objects.len()
4510 );
4511 if let Some(info_id) = info_obj_id {
4512 let _ = write!(output, " /Info {} 0 R", info_id);
4513 }
4514 {
4520 use sha2::Digest as _;
4521 let digest = sha2::Sha256::digest(&output);
4522 let mut id_hex = String::with_capacity(32);
4523 for b in &digest[..16] {
4524 let _ = write!(id_hex, "{:02X}", b);
4525 }
4526 let _ = write!(output, " /ID [<{id_hex}> <{id_hex}>]");
4527 }
4528 let _ = writeln!(output, " >>\nstartxref\n{}\n%%EOF", xref_offset);
4529
4530 output
4531 }
4532}
4533
4534fn write_chart_primitive(
4539 stream: &mut String,
4540 prim: &crate::chart::ChartPrimitive,
4541 _chart_height: f64,
4542 builder: &PdfBuilder,
4543) {
4544 use crate::chart::{ChartPrimitive, TextAnchor};
4545 use crate::font::metrics::unicode_to_winansi;
4546
4547 match prim {
4548 ChartPrimitive::Rect { x, y, w, h, fill } => {
4549 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
4550 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re f", x, y, w, h);
4551 }
4552
4553 ChartPrimitive::Line {
4554 x1,
4555 y1,
4556 x2,
4557 y2,
4558 stroke,
4559 width,
4560 } => {
4561 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
4562 let _ = writeln!(stream, "{:.2} w", width);
4563 let _ = writeln!(stream, "{:.2} {:.2} m {:.2} {:.2} l S", x1, y1, x2, y2);
4564 }
4565
4566 ChartPrimitive::Polyline {
4567 points,
4568 stroke,
4569 width,
4570 } => {
4571 if points.len() < 2 {
4572 return;
4573 }
4574 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
4575 let _ = writeln!(stream, "{:.2} w", width);
4576 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
4577 for &(px, py) in &points[1..] {
4578 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
4579 }
4580 let _ = writeln!(stream, "S");
4581 }
4582
4583 ChartPrimitive::FilledPath {
4584 points,
4585 fill,
4586 opacity,
4587 } => {
4588 if points.len() < 3 {
4589 return;
4590 }
4591 let _ = writeln!(stream, "q");
4592 if *opacity < 1.0 {
4594 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
4595 let _ = writeln!(stream, "/{} gs", gs_name);
4596 }
4597 }
4598 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
4599 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
4600 for &(px, py) in &points[1..] {
4601 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
4602 }
4603 let _ = writeln!(stream, "h f");
4604 let _ = writeln!(stream, "Q");
4605 }
4606
4607 ChartPrimitive::Circle { cx, cy, r, fill } => {
4608 let kappa: f64 = 0.5523;
4610 let kr = kappa * r;
4611 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
4612 let _ = writeln!(stream, "{:.2} {:.2} m", cx + r, cy);
4613 let _ = writeln!(
4614 stream,
4615 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4616 cx + r,
4617 cy + kr,
4618 cx + kr,
4619 cy + r,
4620 cx,
4621 cy + r
4622 );
4623 let _ = writeln!(
4624 stream,
4625 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4626 cx - kr,
4627 cy + r,
4628 cx - r,
4629 cy + kr,
4630 cx - r,
4631 cy
4632 );
4633 let _ = writeln!(
4634 stream,
4635 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4636 cx - r,
4637 cy - kr,
4638 cx - kr,
4639 cy - r,
4640 cx,
4641 cy - r
4642 );
4643 let _ = writeln!(
4644 stream,
4645 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4646 cx + kr,
4647 cy - r,
4648 cx + r,
4649 cy - kr,
4650 cx + r,
4651 cy
4652 );
4653 let _ = writeln!(stream, "f");
4654 }
4655
4656 ChartPrimitive::ArcSector {
4657 cx,
4658 cy,
4659 r,
4660 start_angle,
4661 end_angle,
4662 fill,
4663 } => {
4664 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
4665 let _ = writeln!(stream, "{:.2} {:.2} m", cx, cy);
4667 let sx = cx + r * start_angle.cos();
4669 let sy = cy + r * start_angle.sin();
4670 let _ = writeln!(stream, "{:.2} {:.2} l", sx, sy);
4671
4672 let mut angle = *start_angle;
4674 let total = end_angle - start_angle;
4675 let segments = ((total.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
4676 let step = total / segments as f64;
4677
4678 for _ in 0..segments {
4679 let a1 = angle;
4680 let a2 = angle + step;
4681 let alpha = 4.0 / 3.0 * ((a2 - a1) / 4.0).tan();
4682
4683 let p1x = cx + r * a1.cos();
4684 let p1y = cy + r * a1.sin();
4685 let p2x = cx + r * a2.cos();
4686 let p2y = cy + r * a2.sin();
4687
4688 let cp1x = p1x - alpha * r * a1.sin();
4689 let cp1y = p1y + alpha * r * a1.cos();
4690 let cp2x = p2x + alpha * r * a2.sin();
4691 let cp2y = p2y - alpha * r * a2.cos();
4692
4693 let _ = writeln!(
4694 stream,
4695 "{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c",
4696 cp1x, cp1y, cp2x, cp2y, p2x, p2y
4697 );
4698 angle = a2;
4699 }
4700
4701 let _ = writeln!(stream, "h f");
4703 }
4704
4705 ChartPrimitive::Label {
4706 text,
4707 x,
4708 y,
4709 font_size,
4710 color,
4711 anchor,
4712 } => {
4713 let metrics = crate::font::StandardFont::Helvetica.metrics();
4715 let text_width = metrics.measure_string(text, *font_size, 0.0);
4716 let x_offset = match anchor {
4717 TextAnchor::Left => 0.0,
4718 TextAnchor::Center => -text_width / 2.0,
4719 TextAnchor::Right => -text_width,
4720 };
4721
4722 let font_idx = builder
4724 .font_objects
4725 .iter()
4726 .enumerate()
4727 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
4728 .map(|(i, _)| i)
4729 .unwrap_or(0);
4730
4731 let encoded: String = text
4733 .chars()
4734 .map(|ch| {
4735 if let Some(code) = unicode_to_winansi(ch) {
4736 code as char
4737 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
4738 ch
4739 } else {
4740 builder.missing_glyphs.borrow_mut().insert(ch);
4741 '?'
4742 }
4743 })
4744 .collect();
4745 let escaped = pdf_escape_string(&encoded);
4746
4747 let _ = writeln!(stream, "q");
4749 let _ = writeln!(stream, "1 0 0 -1 {:.4} {:.4} cm", x + x_offset, *y);
4750 let _ = writeln!(
4751 stream,
4752 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
4753 font_idx, font_size, color.r, color.g, color.b, escaped
4754 );
4755 let _ = writeln!(stream, "Q");
4756 }
4757 }
4758}
4759
4760fn normalize_gradient_stops(
4767 stops: &[crate::style::GradientStop],
4768 fallback: Color,
4769) -> Vec<crate::style::GradientStop> {
4770 use crate::style::GradientStop;
4771 if stops.is_empty() {
4772 return vec![
4773 GradientStop {
4774 position: 0.0,
4775 color: fallback,
4776 },
4777 GradientStop {
4778 position: 1.0,
4779 color: fallback,
4780 },
4781 ];
4782 }
4783 let mut sorted: Vec<GradientStop> = stops
4784 .iter()
4785 .map(|s| GradientStop {
4786 position: s.position.clamp(0.0, 1.0),
4787 color: s.color,
4788 })
4789 .collect();
4790 sorted.sort_by(|a, b| {
4791 a.position
4792 .partial_cmp(&b.position)
4793 .unwrap_or(std::cmp::Ordering::Equal)
4794 });
4795 if sorted[0].position > 0.0 {
4796 sorted.insert(
4797 0,
4798 GradientStop {
4799 position: 0.0,
4800 color: sorted[0].color,
4801 },
4802 );
4803 }
4804 if sorted[sorted.len() - 1].position < 1.0 {
4805 let last = sorted[sorted.len() - 1].color;
4806 sorted.push(GradientStop {
4807 position: 1.0,
4808 color: last,
4809 });
4810 }
4811 sorted
4812}
4813
4814fn pdf_escape_string(s: &str) -> String {
4815 let mut out = String::with_capacity(s.len());
4816 for ch in s.chars() {
4817 match ch {
4818 '(' => out.push_str("\\("),
4819 ')' => out.push_str("\\)"),
4820 '\\' => out.push_str("\\\\"),
4821 _ => out.push(ch),
4822 }
4823 }
4824 out
4825}
4826
4827#[cfg(test)]
4828mod tests {
4829 use super::*;
4830 use crate::font::FontContext;
4831
4832 #[test]
4839 fn test_embedded_srgb_is_a_valid_icc_profile() {
4840 let icc: &[u8] = include_bytes!("sRGB.icc");
4841 assert!(
4842 icc.len() >= 128,
4843 "ICC shorter than its 128-byte header: {}",
4844 icc.len()
4845 );
4846 assert_ne!(
4848 icc[0], b'<',
4849 "embedded ICC starts with '<' — looks like HTML, not a profile"
4850 );
4851 assert_eq!(&icc[36..40], b"acsp", "missing ICC 'acsp' signature");
4853 let device_class = &icc[12..16];
4855 assert!(
4856 device_class == b"mntr" || device_class == b"prtr",
4857 "ICC device class {:?} is not mntr/prtr (PDF/A 6.2.3)",
4858 String::from_utf8_lossy(device_class),
4859 );
4860 assert_eq!(&icc[16..20], b"RGB ", "ICC data colour space is not RGB");
4862 }
4863
4864 #[test]
4865 fn test_escape_pdf_string() {
4866 assert_eq!(
4867 PdfWriter::escape_pdf_string("Hello (World)"),
4868 "Hello \\(World\\)"
4869 );
4870 assert_eq!(PdfWriter::escape_pdf_string("back\\slash"), "back\\\\slash");
4871 }
4872
4873 #[test]
4874 fn test_empty_document_produces_valid_pdf() {
4875 let writer = PdfWriter::new();
4876 let font_context = FontContext::new();
4877 let pages = vec![LayoutPage {
4878 width: 595.28,
4879 height: 841.89,
4880 elements: vec![],
4881 fixed_header: vec![],
4882 fixed_footer: vec![],
4883 watermarks: vec![],
4884 config: PageConfig::default(),
4885 page_name: None,
4886 }];
4887 let metadata = Metadata::default();
4888 let (bytes, _warnings) = writer
4889 .write(
4890 &pages,
4891 &metadata,
4892 &font_context,
4893 false,
4894 None,
4895 false,
4896 None,
4897 &[],
4898 None,
4899 false,
4900 crate::model::PdfVersion::V1_7,
4901 false,
4902 )
4903 .unwrap();
4904
4905 assert!(bytes.starts_with(b"%PDF-1.7"));
4906 assert!(bytes.windows(5).any(|w| w == b"%%EOF"));
4907 assert!(bytes.windows(4).any(|w| w == b"xref"));
4908 assert!(bytes.windows(7).any(|w| w == b"trailer"));
4909 }
4910
4911 #[test]
4912 fn test_metadata_in_pdf() {
4913 let writer = PdfWriter::new();
4914 let font_context = FontContext::new();
4915 let pages = vec![LayoutPage {
4916 width: 595.28,
4917 height: 841.89,
4918 elements: vec![],
4919 fixed_header: vec![],
4920 fixed_footer: vec![],
4921 watermarks: vec![],
4922 config: PageConfig::default(),
4923 page_name: None,
4924 }];
4925 let metadata = Metadata {
4926 title: Some("Test Document".to_string()),
4927 author: Some("Forme".to_string()),
4928 subject: None,
4929 creator: None,
4930 lang: None,
4931 };
4932 let (bytes, _warnings) = writer
4933 .write(
4934 &pages,
4935 &metadata,
4936 &font_context,
4937 false,
4938 None,
4939 false,
4940 None,
4941 &[],
4942 None,
4943 false,
4944 crate::model::PdfVersion::V1_7,
4945 false,
4946 )
4947 .unwrap();
4948 let text = String::from_utf8_lossy(&bytes);
4949
4950 assert!(text.contains("/Title (Test Document)"));
4951 assert!(text.contains("/Author (Forme)"));
4952 }
4953
4954 #[test]
4955 fn test_bold_font_registered_separately() {
4956 let writer = PdfWriter::new();
4957 let font_context = FontContext::new();
4958
4959 let pages = vec![LayoutPage {
4961 width: 595.28,
4962 height: 841.89,
4963 elements: vec![
4964 LayoutElement {
4965 x: 54.0,
4966 y: 54.0,
4967 width: 100.0,
4968 height: 16.8,
4969 draw: DrawCommand::Text {
4970 lines: vec![TextLine {
4971 x: 54.0,
4972 y: 66.0,
4973 width: 50.0,
4974 height: 16.8,
4975 glyphs: vec![PositionedGlyph {
4976 glyph_id: 65,
4977 x_offset: 0.0,
4978 y_offset: 0.0,
4979 x_advance: 8.0,
4980 font_size: 12.0,
4981 font_family: "Helvetica".into(),
4982 font_weight: 400,
4983 font_style: FontStyle::Normal,
4984 char_value: 'A',
4985 color: None,
4986 href: None,
4987 text_decoration: TextDecoration::None,
4988 letter_spacing: 0.0,
4989 cluster_text: None,
4990 }],
4991 word_spacing: 0.0,
4992 }],
4993 color: Color::BLACK,
4994 text_decoration: TextDecoration::None,
4995 opacity: 1.0,
4996 },
4997 children: vec![],
4998 node_type: None,
4999 resolved_style: None,
5000 source_location: None,
5001 href: None,
5002 bookmark: None,
5003 alt: None,
5004 is_header_row: false,
5005 actual_text: None,
5006 list_numbering: None,
5007 col_span: 1,
5008 overflow: Overflow::default(),
5009 opacity: 1.0,
5010 },
5011 LayoutElement {
5012 x: 54.0,
5013 y: 74.0,
5014 width: 100.0,
5015 height: 16.8,
5016 draw: DrawCommand::Text {
5017 lines: vec![TextLine {
5018 x: 54.0,
5019 y: 86.0,
5020 width: 50.0,
5021 height: 16.8,
5022 glyphs: vec![PositionedGlyph {
5023 glyph_id: 65,
5024 x_offset: 0.0,
5025 y_offset: 0.0,
5026 x_advance: 8.0,
5027 font_size: 12.0,
5028 font_family: "Helvetica".into(),
5029 font_weight: 700,
5030 font_style: FontStyle::Normal,
5031 char_value: 'A',
5032 color: None,
5033 href: None,
5034 text_decoration: TextDecoration::None,
5035 letter_spacing: 0.0,
5036 cluster_text: None,
5037 }],
5038 word_spacing: 0.0,
5039 }],
5040 color: Color::BLACK,
5041 text_decoration: TextDecoration::None,
5042 opacity: 1.0,
5043 },
5044 children: vec![],
5045 node_type: None,
5046 resolved_style: None,
5047 source_location: None,
5048 href: None,
5049 bookmark: None,
5050 alt: None,
5051 is_header_row: false,
5052 actual_text: None,
5053 list_numbering: None,
5054 col_span: 1,
5055 overflow: Overflow::default(),
5056 opacity: 1.0,
5057 },
5058 ],
5059 fixed_header: vec![],
5060 fixed_footer: vec![],
5061 watermarks: vec![],
5062 config: PageConfig::default(),
5063 page_name: None,
5064 }];
5065
5066 let metadata = Metadata::default();
5067 let (bytes, _warnings) = writer
5068 .write(
5069 &pages,
5070 &metadata,
5071 &font_context,
5072 false,
5073 None,
5074 false,
5075 None,
5076 &[],
5077 None,
5078 false,
5079 crate::model::PdfVersion::V1_7,
5080 false,
5081 )
5082 .unwrap();
5083 let text = String::from_utf8_lossy(&bytes);
5084
5085 assert!(
5087 text.contains("Helvetica"),
5088 "Should contain regular Helvetica"
5089 );
5090 assert!(
5091 text.contains("Helvetica-Bold"),
5092 "Should contain Helvetica-Bold"
5093 );
5094 }
5095
5096 #[test]
5097 fn test_sanitize_font_name() {
5098 assert_eq!(PdfWriter::sanitize_font_name("Inter", 400, false), "Inter");
5099 assert_eq!(
5100 PdfWriter::sanitize_font_name("Inter", 700, false),
5101 "Inter-Bold"
5102 );
5103 assert_eq!(
5104 PdfWriter::sanitize_font_name("Inter", 400, true),
5105 "Inter-Italic"
5106 );
5107 assert_eq!(
5108 PdfWriter::sanitize_font_name("Inter", 700, true),
5109 "Inter-Bold-Italic"
5110 );
5111 assert_eq!(
5112 PdfWriter::sanitize_font_name("Noto Sans", 400, false),
5113 "NotoSans"
5114 );
5115 assert_eq!(
5116 PdfWriter::sanitize_font_name("Font (Display)", 400, false),
5117 "FontDisplay"
5118 );
5119 }
5120
5121 #[test]
5122 fn test_tounicode_cmap_format() {
5123 let mut glyph_to_char = HashMap::new();
5125 glyph_to_char.insert(36u16, 'A');
5126 glyph_to_char.insert(37u16, 'B');
5127
5128 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&glyph_to_char, "TestFont");
5129
5130 assert!(cmap.contains("begincmap"), "CMap should contain begincmap");
5131 assert!(cmap.contains("endcmap"), "CMap should contain endcmap");
5132 assert!(
5133 cmap.contains("beginbfchar"),
5134 "CMap should contain beginbfchar"
5135 );
5136 assert!(cmap.contains("endbfchar"), "CMap should contain endbfchar");
5137 assert!(
5138 cmap.contains("<0024> <0041>"),
5139 "Should map gid 0x0024 to Unicode 'A' 0x0041"
5140 );
5141 assert!(
5142 cmap.contains("<0025> <0042>"),
5143 "Should map gid 0x0025 to Unicode 'B' 0x0042"
5144 );
5145 assert!(
5146 cmap.contains("begincodespacerange"),
5147 "Should define codespace range"
5148 );
5149 assert!(
5150 cmap.contains("<0000> <FFFF>"),
5151 "Codespace should be 0000-FFFF"
5152 );
5153 }
5154
5155 #[test]
5156 fn test_w_array_format() {
5157 let mut char_to_gid = HashMap::new();
5158 char_to_gid.insert('A', 36u16);
5159
5160 let w_array_str = "[ 36 [600] ]";
5163 assert!(w_array_str.starts_with('['));
5164 assert!(w_array_str.ends_with(']'));
5165 }
5166
5167 #[test]
5168 fn test_hex_glyph_encoding() {
5169 let gid: u16 = 0x0041;
5171 let hex = format!("{:04X}", gid);
5172 assert_eq!(hex, "0041");
5173
5174 let gids = [0x0041u16, 0x0042, 0x0043];
5175 let hex_str: String = gids.iter().map(|g| format!("{:04X}", g)).collect();
5176 assert_eq!(hex_str, "004100420043");
5177 }
5178
5179 #[test]
5180 fn test_standard_font_still_uses_text_string() {
5181 let writer = PdfWriter::new();
5182 let font_context = FontContext::new();
5183
5184 let pages = vec![LayoutPage {
5185 width: 595.28,
5186 height: 841.89,
5187 elements: vec![LayoutElement {
5188 x: 54.0,
5189 y: 54.0,
5190 width: 100.0,
5191 height: 16.8,
5192 draw: DrawCommand::Text {
5193 lines: vec![TextLine {
5194 x: 54.0,
5195 y: 66.0,
5196 width: 50.0,
5197 height: 16.8,
5198 glyphs: vec![PositionedGlyph {
5199 glyph_id: 65,
5200 x_offset: 0.0,
5201 y_offset: 0.0,
5202 x_advance: 8.0,
5203 font_size: 12.0,
5204 font_family: "Helvetica".into(),
5205 font_weight: 400,
5206 font_style: FontStyle::Normal,
5207 char_value: 'H',
5208 color: None,
5209 href: None,
5210 text_decoration: TextDecoration::None,
5211 letter_spacing: 0.0,
5212 cluster_text: None,
5213 }],
5214 word_spacing: 0.0,
5215 }],
5216 color: Color::BLACK,
5217 text_decoration: TextDecoration::None,
5218 opacity: 1.0,
5219 },
5220 children: vec![],
5221 node_type: None,
5222 resolved_style: None,
5223 source_location: None,
5224 href: None,
5225 bookmark: None,
5226 alt: None,
5227 is_header_row: false,
5228 actual_text: None,
5229 list_numbering: None,
5230 col_span: 1,
5231 overflow: Overflow::default(),
5232 opacity: 1.0,
5233 }],
5234 fixed_header: vec![],
5235 fixed_footer: vec![],
5236 watermarks: vec![],
5237 config: PageConfig::default(),
5238 page_name: None,
5239 }];
5240
5241 let metadata = Metadata::default();
5242 let (bytes, _warnings) = writer
5243 .write(
5244 &pages,
5245 &metadata,
5246 &font_context,
5247 false,
5248 None,
5249 false,
5250 None,
5251 &[],
5252 None,
5253 false,
5254 crate::model::PdfVersion::V1_7,
5255 false,
5256 )
5257 .unwrap();
5258 let text = String::from_utf8_lossy(&bytes);
5259
5260 assert!(
5262 text.contains("/Type1"),
5263 "Standard font should use Type1 subtype"
5264 );
5265 assert!(
5266 !text.contains("CIDFontType2"),
5267 "Standard font should not use CIDFontType2"
5268 );
5269 }
5270}