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