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}
154
155pub(crate) struct PdfObject {
156 #[allow(dead_code)]
157 pub(crate) id: usize,
158 pub(crate) data: Vec<u8>,
159}
160
161impl Default for PdfWriter {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl PdfWriter {
168 pub fn new() -> Self {
169 Self
170 }
171
172 #[allow(clippy::too_many_arguments)]
194 pub fn write(
195 &self,
196 pages: &[LayoutPage],
197 metadata: &Metadata,
198 font_context: &FontContext,
199 tagged: bool,
200 pdfa: Option<&PdfAConformance>,
201 pdf_ua: bool,
202 embedded_data: Option<&str>,
203 attachments: &[Attachment],
204 zugferd: Option<&ZugferdMeta>,
205 flatten_forms: bool,
206 ) -> Result<(Vec<u8>, Vec<String>), FormeError> {
207 if let Some(level) = pdfa {
215 if !level.allows_attachments() && (embedded_data.is_some() || !attachments.is_empty()) {
216 return Err(FormeError::RenderError(
217 "PDF/A-2 forbids embedded files that are not themselves PDF/A \
218 (ISO 19005-2, 6.8). Use a PDF/A-3 level — e.g. pdfa: \"3b\" — \
219 which permits arbitrary attachments, or remove the attachment / \
220 embedData."
221 .to_string(),
222 ));
223 }
224 }
225 let zugferd_filename: Option<String> = if let Some(z) = zugferd {
229 const LEVELS: [&str; 6] = [
230 "MINIMUM",
231 "BASIC WL",
232 "BASIC",
233 "EN 16931",
234 "EXTENDED",
235 "XRECHNUNG",
236 ];
237 if !LEVELS.contains(&z.conformance_level.as_str()) {
238 return Err(FormeError::RenderError(format!(
239 "zugferd.conformanceLevel {:?} is not a Factur-X profile — expected one of \
240 MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, XRECHNUNG (exact spelling, \
241 spaces included).",
242 z.conformance_level
243 )));
244 }
245 if !pdfa.is_some_and(|l| l.allows_attachments()) {
246 return Err(FormeError::RenderError(
247 "Factur-X/ZUGFeRD (zugferd) requires a PDF/A-3 conformance level — set \
248 pdfa: \"3b\" (or \"3a\"/\"3u\"). The e-invoice XML is an embedded file, \
249 which only PDF/A-3 permits."
250 .to_string(),
251 ));
252 }
253 let filename = z.document_file_name.clone().unwrap_or_else(|| {
254 if z.conformance_level == "XRECHNUNG" {
255 "xrechnung.xml".to_string()
256 } else {
257 "factur-x.xml".to_string()
258 }
259 });
260 if !attachments.iter().any(|a| a.name == filename) {
261 return Err(FormeError::RenderError(format!(
262 "zugferd is set but no attachment is named {filename:?} — the XMP would \
263 point at a file that isn't embedded. Attach the invoice XML with name: \
264 {filename:?}, or set zugferd.documentFileName to the attachment's name."
265 )));
266 }
267 Some(filename)
268 } else {
269 None
270 };
271 let mut builder = PdfBuilder {
272 objects: Vec::new(),
273 font_objects: Vec::new(),
274 custom_font_data: HashMap::new(),
275 embedded_standard_fonts: std::collections::HashSet::new(),
276 image_objects: Vec::new(),
277 image_index_map: HashMap::new(),
278 page_background_image_map: HashMap::new(),
279 page_background_url_cache: HashMap::new(),
280 ext_gstate_map: HashMap::new(),
281 shading_map: HashMap::new(),
282 warnings: Vec::new(),
283 missing_glyphs: Default::default(),
284 };
285
286 builder.objects.push(PdfObject {
292 id: 0,
293 data: vec![],
294 });
295 builder.objects.push(PdfObject {
296 id: 1,
297 data: vec![],
298 });
299 builder.objects.push(PdfObject {
300 id: 2,
301 data: vec![],
302 });
303
304 self.register_fonts(&mut builder, pages, font_context, pdf_ua)?;
306
307 if pdfa.is_some() {
313 for (key, _) in &builder.font_objects {
314 if !builder.custom_font_data.contains_key(key)
315 && !builder.embedded_standard_fonts.contains(key)
316 {
317 return Err(FormeError::RenderError(format!(
318 "PDF/A requires all fonts to be embedded, but '{}' is not. Register a \
319 metric-compatible font — install @formepdf/fonts-standard and register \
320 its fonts (`for (const f of standardFonts()) Font.register(f)`), or supply \
321 your own via Font.register().",
322 key.family
323 )));
324 }
325 }
326 }
327
328 self.register_images(&mut builder, pages);
330
331 self.register_page_background_images(&mut builder, pages);
335
336 self.register_ext_gstates(&mut builder, pages);
338
339 self.register_shadings(&mut builder, pages);
341
342 let mut tag_builder = if tagged {
344 Some(tagged::TagBuilder::new(pages.len()))
345 } else {
346 None
347 };
348
349 let mut page_obj_ids: Vec<usize> = Vec::new();
353 let mut all_bookmarks: Vec<PdfBookmark> = Vec::new();
354 let mut per_page_content_obj_ids: Vec<usize> = Vec::new();
355 let mut per_page_annotations: Vec<Vec<LinkAnnotation>> = Vec::new();
356 let mut per_page_resources: Vec<String> = Vec::new();
357 let mut all_form_fields: Vec<FormFieldData> = Vec::new();
358
359 for (page_idx, page) in pages.iter().enumerate() {
361 let content = self.build_content_stream_for_page(
362 page,
363 page_idx,
364 &builder,
365 page_idx + 1,
366 pages.len(),
367 tag_builder.as_mut(),
368 flatten_forms,
369 );
370 let compressed = compress_to_vec_zlib(content.as_bytes(), 6);
371
372 let content_obj_id = builder.objects.len();
373 let mut content_data: Vec<u8> = Vec::new();
374 let _ = write!(
375 content_data,
376 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
377 compressed.len()
378 );
379 content_data.extend_from_slice(&compressed);
380 content_data.extend_from_slice(b"\nendstream");
381 builder.objects.push(PdfObject {
382 id: content_obj_id,
383 data: content_data,
384 });
385 per_page_content_obj_ids.push(content_obj_id);
386
387 let mut annotations: Vec<LinkAnnotation> = Vec::new();
389 Self::collect_link_annotations(&page.elements, page.height, &mut annotations);
390 per_page_annotations.push(annotations);
391
392 Self::collect_form_fields(&page.elements, page.height, page_idx, &mut all_form_fields);
394
395 let page_obj_id = builder.objects.len();
397 builder.objects.push(PdfObject {
398 id: page_obj_id,
399 data: vec![],
400 });
401
402 let font_resources = self.build_font_resource_dict(&builder.font_objects);
404 let xobject_resources = self.build_xobject_resource_dict(page_idx, &builder);
405 let ext_gstate_resources = self.build_ext_gstate_resource_dict(&builder);
406 let shading_resources = self.build_shading_resource_dict(page_idx, &builder);
407 let mut resources = format!("/Font << {} >>", font_resources);
408 if !xobject_resources.is_empty() {
409 let _ = write!(resources, " /XObject << {} >>", xobject_resources);
410 }
411 if !ext_gstate_resources.is_empty() {
412 let _ = write!(resources, " /ExtGState << {} >>", ext_gstate_resources);
413 }
414 if !shading_resources.is_empty() {
415 let _ = write!(resources, " /Shading << {} >>", shading_resources);
416 }
417 per_page_resources.push(resources);
418
419 Self::collect_bookmarks(&page.elements, page.height, page_obj_id, &mut all_bookmarks);
421
422 page_obj_ids.push(page_obj_id);
423 }
424
425 for (page_idx, annotations) in per_page_annotations.iter().enumerate() {
427 let mut annot_obj_ids: Vec<usize> = Vec::new();
428 for annot in annotations {
429 let rect = format!(
430 "[{:.2} {:.2} {:.2} {:.2}]",
431 annot.x,
432 annot.y,
433 annot.x + annot.width,
434 annot.y + annot.height
435 );
436
437 if let Some(anchor) = annot.href.strip_prefix('#') {
438 if let Some(bm) = all_bookmarks.iter().find(|b| b.title == anchor) {
440 let annot_obj_id = builder.objects.len();
441 let sp_str = tag_builder
445 .as_mut()
446 .and_then(|tb| {
447 tb.connect_link_annotation(page_idx, &annot.href, annot_obj_id)
448 })
449 .map(|sp| format!(" /StructParent {}", sp))
450 .unwrap_or_default();
451 let contents = Self::escape_pdf_string(&format!("Link to {anchor}"));
454 let annot_dict = format!(
455 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
456 /F 4 /Contents ({}){} \
457 /A << /S /GoTo /D [{} 0 R /XYZ 0 {:.2} null] >> >>",
458 rect, contents, sp_str, bm.page_obj_id, bm.y_pdf
459 );
460 builder.objects.push(PdfObject {
461 id: annot_obj_id,
462 data: annot_dict.into_bytes(),
463 });
464 annot_obj_ids.push(annot_obj_id);
465 }
466 } else {
468 let annot_obj_id = builder.objects.len();
470 let sp_str = tag_builder
471 .as_mut()
472 .and_then(|tb| {
473 tb.connect_link_annotation(page_idx, &annot.href, annot_obj_id)
474 })
475 .map(|sp| format!(" /StructParent {}", sp))
476 .unwrap_or_default();
477 let href_esc = Self::escape_pdf_string(&annot.href);
478 let annot_dict = format!(
479 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
480 /F 4 /Contents ({}){} \
481 /A << /Type /Action /S /URI /URI ({}) >> >>",
482 rect, href_esc, sp_str, href_esc
483 );
484 builder.objects.push(PdfObject {
485 id: annot_obj_id,
486 data: annot_dict.into_bytes(),
487 });
488 annot_obj_ids.push(annot_obj_id);
489 }
490 }
491
492 let annots_str = if annot_obj_ids.is_empty() {
493 String::new()
494 } else {
495 let refs: String = annot_obj_ids
496 .iter()
497 .map(|id| format!("{} 0 R", id))
498 .collect::<Vec<_>>()
499 .join(" ");
500 format!(" /Annots [{}]", refs)
501 };
502
503 let page_obj_id = page_obj_ids[page_idx];
504 let content_obj_id = per_page_content_obj_ids[page_idx];
505 let struct_parents_str = if tagged {
506 format!(" /StructParents {} /Tabs /S", page_idx)
507 } else {
508 String::new()
509 };
510 let page_dict = format!(
511 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {:.2} {:.2}] \
512 /Contents {} 0 R /Resources << {} >>{}{} >>",
513 pages[page_idx].width,
514 pages[page_idx].height,
515 content_obj_id,
516 per_page_resources[page_idx],
517 annots_str,
518 struct_parents_str
519 );
520 builder.objects[page_obj_id].data = page_dict.into_bytes();
521 }
522
523 let outlines_obj_id = if !all_bookmarks.is_empty() {
525 Some(self.write_outline_tree(&mut builder, &all_bookmarks))
526 } else {
527 None
528 };
529
530 let struct_tree_root_id = if let Some(ref tb) = tag_builder {
532 let (root_id, _parent_tree_id) = tb.write_objects(
533 &mut builder.objects,
534 &page_obj_ids,
535 metadata.lang.as_deref(),
536 );
537 Some(root_id)
538 } else {
539 None
540 };
541
542 let xmp_metadata_id = if pdfa.is_some() || pdf_ua {
544 let xmp_xml = xmp::generate_xmp(metadata, pdfa, pdf_ua, zugferd);
545 let xmp_bytes = xmp_xml.as_bytes();
546 let xmp_obj_id = builder.objects.len();
547 let xmp_data = format!(
549 "<< /Type /Metadata /Subtype /XML /Length {} >>\nstream\n",
550 xmp_bytes.len()
551 );
552 let mut xmp_obj_data: Vec<u8> = xmp_data.into_bytes();
553 xmp_obj_data.extend_from_slice(xmp_bytes);
554 xmp_obj_data.extend_from_slice(b"\nendstream");
555 builder.objects.push(PdfObject {
556 id: xmp_obj_id,
557 data: xmp_obj_data,
558 });
559 Some(xmp_obj_id)
560 } else {
561 None
562 };
563
564 let output_intent_id = if pdfa.is_some() {
565 static SRGB_ICC: &[u8] = include_bytes!("sRGB.icc");
567 let compressed_icc = compress_to_vec_zlib(SRGB_ICC, 6);
568
569 let icc_obj_id = builder.objects.len();
570 let mut icc_data: Vec<u8> = Vec::new();
571 let _ = write!(
572 icc_data,
573 "<< /N 3 /Length {} /Filter /FlateDecode >>\nstream\n",
574 compressed_icc.len()
575 );
576 icc_data.extend_from_slice(&compressed_icc);
577 icc_data.extend_from_slice(b"\nendstream");
578 builder.objects.push(PdfObject {
579 id: icc_obj_id,
580 data: icc_data,
581 });
582
583 let oi_obj_id = builder.objects.len();
585 let oi_data = format!(
586 "<< /Type /OutputIntent /S /GTS_PDFA1 \
587 /OutputConditionIdentifier (sRGB IEC61966-2.1) \
588 /RegistryName (http://www.color.org) \
589 /DestOutputProfile {} 0 R >>",
590 icc_obj_id
591 );
592 builder.objects.push(PdfObject {
593 id: oi_obj_id,
594 data: oi_data.into_bytes(),
595 });
596 Some(oi_obj_id)
597 } else {
598 None
599 };
600
601 let mut name_tree_entries: Vec<(String, usize)> = Vec::new();
608 let mut af_filespec_ids: Vec<usize> = Vec::new();
609 if let Some(data) = embedded_data {
610 let compressed = compress_to_vec_zlib(data.as_bytes(), 6);
611
612 let ef_obj_id = builder.objects.len();
614 let ef_data = format!(
615 "<< /Type /EmbeddedFile /Subtype /application#2Fjson /Length {} /Filter /FlateDecode >>\nstream\n",
616 compressed.len()
617 );
618 let mut ef_bytes = ef_data.into_bytes();
619 ef_bytes.extend_from_slice(&compressed);
620 ef_bytes.extend_from_slice(b"\nendstream");
621 builder.objects.push(PdfObject {
622 id: ef_obj_id,
623 data: ef_bytes,
624 });
625
626 let fs_obj_id = builder.objects.len();
628 let fs_data = format!(
629 "<< /Type /Filespec /F (forme-data.json) /UF (forme-data.json) /EF << /F {} 0 R >> /AFRelationship /Data >>",
630 ef_obj_id
631 );
632 builder.objects.push(PdfObject {
633 id: fs_obj_id,
634 data: fs_data.into_bytes(),
635 });
636 name_tree_entries.push(("forme-data.json".to_string(), fs_obj_id));
637 if pdfa.is_some_and(|l| l.allows_attachments()) {
640 af_filespec_ids.push(fs_obj_id);
641 }
642 }
643 for att in attachments {
644 let bytes = Self::decode_attachment_src(&att.src)?;
645 let compressed = compress_to_vec_zlib(&bytes, 6);
646 let mime = att
647 .mime_type
648 .as_deref()
649 .unwrap_or("application/octet-stream");
650 let mod_date = att
651 .mod_date
652 .as_deref()
653 .unwrap_or(DEFAULT_ATTACHMENT_MOD_DATE);
654
655 let ef_obj_id = builder.objects.len();
656 let ef_head = format!(
657 "<< /Type /EmbeddedFile /Subtype /{} /Length {} /Filter /FlateDecode \
658 /Params << /Size {} /ModDate ({}) >> >>\nstream\n",
659 Self::mime_to_pdf_name(mime),
660 compressed.len(),
661 bytes.len(),
662 Self::escape_pdf_string(mod_date),
663 );
664 let mut ef_bytes = ef_head.into_bytes();
665 ef_bytes.extend_from_slice(&compressed);
666 ef_bytes.extend_from_slice(b"\nendstream");
667 builder.objects.push(PdfObject {
668 id: ef_obj_id,
669 data: ef_bytes,
670 });
671
672 let relationship = att.relationship.unwrap_or_else(|| {
677 if zugferd_filename.as_deref() == Some(att.name.as_str()) {
678 match zugferd.map(|z| z.conformance_level.as_str()) {
679 Some("MINIMUM") | Some("BASIC WL") => AfRelationship::Data,
680 _ => AfRelationship::Alternative,
681 }
682 } else {
683 AfRelationship::Unspecified
684 }
685 });
686
687 let fs_obj_id = builder.objects.len();
688 let mut fs_data = format!(
689 "<< /Type /Filespec /F ({name}) /UF ({name}) /EF << /F {ef} 0 R >> /AFRelationship /{rel}",
690 name = Self::escape_pdf_string(&att.name),
691 ef = ef_obj_id,
692 rel = relationship.pdf_name(),
693 );
694 if let Some(desc) = &att.description {
695 let _ = write!(fs_data, " /Desc ({})", Self::escape_pdf_string(desc));
696 }
697 fs_data.push_str(" >>");
698 builder.objects.push(PdfObject {
699 id: fs_obj_id,
700 data: fs_data.into_bytes(),
701 });
702 name_tree_entries.push((att.name.clone(), fs_obj_id));
703 af_filespec_ids.push(fs_obj_id);
704 }
705 let embedded_names_id = if name_tree_entries.is_empty() {
706 None
707 } else {
708 name_tree_entries.sort_by(|a, b| a.0.cmp(&b.0));
710 let names_obj_id = builder.objects.len();
711 let pairs = name_tree_entries
712 .iter()
713 .map(|(name, id)| format!("({}) {} 0 R", Self::escape_pdf_string(name), id))
714 .collect::<Vec<_>>()
715 .join(" ");
716 let names_data = format!("<< /Names [{}] >>", pairs);
717 builder.objects.push(PdfObject {
718 id: names_obj_id,
719 data: names_data.into_bytes(),
720 });
721 Some(names_obj_id)
722 };
723
724 let acroform_obj_id = if !all_form_fields.is_empty() && !flatten_forms {
726 let helv_obj_id = builder
728 .font_objects
729 .iter()
730 .find(|(key, _)| key.family == "Helvetica" && key.weight == 400 && !key.italic)
731 .map(|(_, id)| *id);
732
733 let mut radio_groups: HashMap<String, Vec<usize>> = HashMap::new(); let mut non_radio_indices: Vec<usize> = Vec::new();
736 for (i, field) in all_form_fields.iter().enumerate() {
737 if matches!(field.field_type, FormFieldType::RadioButton { .. }) {
738 radio_groups.entry(field.name.clone()).or_default().push(i);
739 } else {
740 non_radio_indices.push(i);
741 }
742 }
743
744 let mut radio_parent_ids: HashMap<String, usize> = HashMap::new();
746 for group_name in radio_groups.keys() {
747 let parent_id = builder.objects.len();
748 builder.objects.push(PdfObject {
749 id: parent_id,
750 data: vec![], });
752 radio_parent_ids.insert(group_name.clone(), parent_id);
753 }
754
755 let checkbox_yes_stream_id = builder.objects.len();
758 {
759 let stream_content =
760 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";
761 let mut data: Vec<u8> = Vec::new();
762 let _ = write!(
763 data,
764 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
765 stream_content.len()
766 );
767 data.extend_from_slice(stream_content);
768 data.extend_from_slice(b"\nendstream");
769 builder.objects.push(PdfObject {
770 id: checkbox_yes_stream_id,
771 data,
772 });
773 }
774 let checkbox_off_stream_id = builder.objects.len();
776 {
777 let stream_content = b"";
778 let mut data: Vec<u8> = Vec::new();
779 let _ = write!(
780 data,
781 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
782 stream_content.len()
783 );
784 data.extend_from_slice(stream_content);
785 data.extend_from_slice(b"\nendstream");
786 builder.objects.push(PdfObject {
787 id: checkbox_off_stream_id,
788 data,
789 });
790 }
791 let radio_on_stream_id = builder.objects.len();
793 {
794 let k = 2.761; let stream_content = format!(
797 "0.2 0.2 0.2 rg\n\
798 7 12 m {:.2} 12 12 {:.2} 12 7 c\n\
799 12 {:.2} {:.2} 2 7 2 c\n\
800 {:.2} 2 2 {:.2} 2 7 c\n\
801 2 {:.2} {:.2} 12 7 12 c f\n",
802 7.0 + k,
803 7.0 + k, 7.0 - k,
805 7.0 - k, 7.0 - k,
807 7.0 - k, 7.0 + k,
809 7.0 + k, );
811 let stream_bytes = stream_content.as_bytes();
812 let mut data: Vec<u8> = Vec::new();
813 let _ = write!(
814 data,
815 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
816 stream_bytes.len()
817 );
818 data.extend_from_slice(stream_bytes);
819 data.extend_from_slice(b"\nendstream");
820 builder.objects.push(PdfObject {
821 id: radio_on_stream_id,
822 data,
823 });
824 }
825 let radio_off_stream_id = builder.objects.len();
827 {
828 let stream_content = b"";
829 let mut data: Vec<u8> = Vec::new();
830 let _ = write!(
831 data,
832 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
833 stream_content.len()
834 );
835 data.extend_from_slice(stream_content);
836 data.extend_from_slice(b"\nendstream");
837 builder.objects.push(PdfObject {
838 id: radio_off_stream_id,
839 data,
840 });
841 }
842
843 let mut acroform_field_ids: Vec<usize> = Vec::new();
845 let mut per_page_widget_ids: Vec<Vec<usize>> = vec![Vec::new(); pages.len()];
846 let mut radio_kid_ids: HashMap<String, Vec<usize>> = HashMap::new();
847
848 for field in all_form_fields.iter() {
849 let rect = format!(
850 "[{:.2} {:.2} {:.2} {:.2}]",
851 field.x,
852 field.y,
853 field.x + field.width,
854 field.y + field.height
855 );
856 let page_ref = format!("{} 0 R", page_obj_ids[field.page_idx]);
857
858 match &field.field_type {
859 FormFieldType::TextField {
860 value,
861 multiline,
862 password,
863 read_only,
864 max_length,
865 font_size,
866 ..
867 } => {
868 let mut flags: u32 = 0;
869 if *multiline {
870 flags |= 1 << 12; }
872 if *password {
873 flags |= 1 << 13; }
875 if *read_only {
876 flags |= 1; }
878 let da = if let Some(helv_id) = helv_obj_id {
879 let _ = helv_id; format!("/Helv {} Tf 0 g", font_size)
881 } else {
882 format!("/Helv {} Tf 0 g", font_size)
883 };
884 let v_str = if let Some(ref v) = value {
885 format!(
886 " /V ({}) /DV ({})",
887 Self::escape_pdf_string(v),
888 Self::escape_pdf_string(v)
889 )
890 } else {
891 String::new()
892 };
893 let max_len_str = if let Some(ml) = max_length {
894 format!(" /MaxLen {}", ml)
895 } else {
896 String::new()
897 };
898 let ap_w = field.width;
900 let ap_h = field.height;
901 let text_y = if *multiline {
902 ap_h - *font_size - 2.0
903 } else {
904 (ap_h - *font_size) / 2.0
905 };
906 let ap_content = if let Some(ref v) = value {
907 format!(
908 "1 1 1 rg 0 0 {} {} re f \
909 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S \
910 BT /Helv {} Tf 0 g 2 {} Td ({}) Tj ET",
911 ap_w,
912 ap_h,
913 ap_w,
914 ap_h,
915 font_size,
916 text_y,
917 Self::escape_pdf_string(v)
918 )
919 } else {
920 format!(
921 "1 1 1 rg 0 0 {} {} re f \
922 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S",
923 ap_w, ap_h, ap_w, ap_h
924 )
925 };
926 let ap_stream_id = builder.objects.len();
927 let ap_stream = format!(
928 "<< /Type /XObject /Subtype /Form /BBox [0 0 {} {}] \
929 /Resources << /Font << /Helv {} 0 R >> >> /Length {} >>\nstream\n{}\nendstream",
930 ap_w, ap_h,
931 helv_obj_id.unwrap_or(0),
932 ap_content.len(),
933 ap_content
934 );
935 builder.objects.push(PdfObject {
936 id: ap_stream_id,
937 data: ap_stream.into_bytes(),
938 });
939
940 let widget_obj_id = builder.objects.len();
941 let widget_dict = format!(
942 "<< /Type /Annot /Subtype /Widget /FT /Tx \
943 /T ({}) /Rect {} /P {}\
944 {} /DA ({}) /Ff {}{} \
945 /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> \
946 /AP << /N {} 0 R >> >>",
947 Self::escape_pdf_string(&field.name),
948 rect,
949 page_ref,
950 v_str,
951 da,
952 flags,
953 max_len_str,
954 ap_stream_id
955 );
956 builder.objects.push(PdfObject {
957 id: widget_obj_id,
958 data: widget_dict.into_bytes(),
959 });
960 per_page_widget_ids[field.page_idx].push(widget_obj_id);
961 acroform_field_ids.push(widget_obj_id);
962 }
963
964 FormFieldType::Checkbox {
965 checked, read_only, ..
966 } => {
967 let state = if *checked { "Yes" } else { "Off" };
968 let mut flags: u32 = 0;
969 if *read_only {
970 flags |= 1;
971 }
972 let ff_str = if flags > 0 {
973 format!(" /Ff {}", flags)
974 } else {
975 String::new()
976 };
977 let widget_obj_id = builder.objects.len();
978 let widget_dict = format!(
979 "<< /Type /Annot /Subtype /Widget /FT /Btn \
980 /T ({}) /Rect {} /P {} \
981 /V /{} /AS /{}{} \
982 /MK << /BC [0.6 0.6 0.6] /CA (4) >> \
983 /AP << /N << /Yes {} 0 R /Off {} 0 R >> >> >>",
984 Self::escape_pdf_string(&field.name),
985 rect,
986 page_ref,
987 state,
988 state,
989 ff_str,
990 checkbox_yes_stream_id,
991 checkbox_off_stream_id,
992 );
993 builder.objects.push(PdfObject {
994 id: widget_obj_id,
995 data: widget_dict.into_bytes(),
996 });
997 per_page_widget_ids[field.page_idx].push(widget_obj_id);
998 acroform_field_ids.push(widget_obj_id);
999 }
1000
1001 FormFieldType::Dropdown {
1002 options,
1003 value,
1004 read_only,
1005 font_size,
1006 ..
1007 } => {
1008 let mut flags: u32 = 1 << 17; if *read_only {
1010 flags |= 1;
1011 }
1012 let opts_str: String = options
1013 .iter()
1014 .map(|o| format!("({})", Self::escape_pdf_string(o)))
1015 .collect::<Vec<_>>()
1016 .join(" ");
1017 let v_str = if let Some(ref v) = value {
1018 format!(" /V ({})", Self::escape_pdf_string(v))
1019 } else {
1020 String::new()
1021 };
1022 let ap_w = field.width;
1024 let ap_h = field.height;
1025 let text_y = (ap_h - *font_size) / 2.0;
1026 let ap_content = if let Some(ref v) = value {
1027 format!(
1028 "1 1 1 rg 0 0 {} {} re f \
1029 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S \
1030 BT /Helv {} Tf 0 g 2 {} Td ({}) Tj ET",
1031 ap_w,
1032 ap_h,
1033 ap_w,
1034 ap_h,
1035 font_size,
1036 text_y,
1037 Self::escape_pdf_string(v)
1038 )
1039 } else {
1040 format!(
1041 "1 1 1 rg 0 0 {} {} re f \
1042 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S",
1043 ap_w, ap_h, ap_w, ap_h
1044 )
1045 };
1046 let ap_stream_id = builder.objects.len();
1047 let ap_stream = format!(
1048 "<< /Type /XObject /Subtype /Form /BBox [0 0 {} {}] \
1049 /Resources << /Font << /Helv {} 0 R >> >> /Length {} >>\nstream\n{}\nendstream",
1050 ap_w, ap_h,
1051 helv_obj_id.unwrap_or(0),
1052 ap_content.len(),
1053 ap_content
1054 );
1055 builder.objects.push(PdfObject {
1056 id: ap_stream_id,
1057 data: ap_stream.into_bytes(),
1058 });
1059
1060 let widget_obj_id = builder.objects.len();
1061 let widget_dict = format!(
1062 "<< /Type /Annot /Subtype /Widget /FT /Ch \
1063 /T ({}) /Rect {} /P {} \
1064 /Opt [{}]{} \
1065 /DA (/Helv {} Tf 0 g) /Ff {} \
1066 /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> \
1067 /AP << /N {} 0 R >> >>",
1068 Self::escape_pdf_string(&field.name),
1069 rect,
1070 page_ref,
1071 opts_str,
1072 v_str,
1073 font_size,
1074 flags,
1075 ap_stream_id
1076 );
1077 builder.objects.push(PdfObject {
1078 id: widget_obj_id,
1079 data: widget_dict.into_bytes(),
1080 });
1081 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1082 acroform_field_ids.push(widget_obj_id);
1083 }
1084
1085 FormFieldType::RadioButton {
1086 value,
1087 checked,
1088 read_only: _,
1089 } => {
1090 let parent_id = radio_parent_ids[&field.name];
1092 let as_value = if *checked { value.as_str() } else { "Off" };
1093 let widget_obj_id = builder.objects.len();
1094 let widget_dict = format!(
1095 "<< /Type /Annot /Subtype /Widget \
1096 /Parent {} 0 R \
1097 /Rect {} /P {} \
1098 /AS /{} \
1099 /AP << /N << /{} {} 0 R /Off {} 0 R >> >> \
1100 /MK << /BC [0.6 0.6 0.6] >> >>",
1101 parent_id,
1102 rect,
1103 page_ref,
1104 Self::escape_pdf_string(as_value),
1105 Self::escape_pdf_string(value),
1106 radio_on_stream_id,
1107 radio_off_stream_id,
1108 );
1109 builder.objects.push(PdfObject {
1110 id: widget_obj_id,
1111 data: widget_dict.into_bytes(),
1112 });
1113 per_page_widget_ids[field.page_idx].push(widget_obj_id);
1114 radio_kid_ids
1116 .entry(field.name.clone())
1117 .or_default()
1118 .push(widget_obj_id);
1119 }
1120 }
1121 }
1122
1123 for (group_name, kid_indices) in &radio_kid_ids {
1125 let parent_id = radio_parent_ids[group_name];
1126 let checked_value = all_form_fields
1128 .iter()
1129 .filter(|f| f.name == *group_name)
1130 .find_map(|f| {
1131 if let FormFieldType::RadioButton {
1132 ref value, checked, ..
1133 } = f.field_type
1134 {
1135 if checked {
1136 Some(value.clone())
1137 } else {
1138 None
1139 }
1140 } else {
1141 None
1142 }
1143 })
1144 .unwrap_or_else(|| "Off".to_string());
1145
1146 let kids_refs: String = kid_indices
1147 .iter()
1148 .map(|id| format!("{} 0 R", id))
1149 .collect::<Vec<_>>()
1150 .join(" ");
1151
1152 let mut flags: u32 = (1 << 14) | (1 << 15); let is_read_only = all_form_fields
1155 .iter()
1156 .filter(|f| f.name == *group_name)
1157 .any(|f| {
1158 matches!(
1159 f.field_type,
1160 FormFieldType::RadioButton {
1161 read_only: true,
1162 ..
1163 }
1164 )
1165 });
1166 if is_read_only {
1167 flags |= 1;
1168 }
1169
1170 let parent_dict = format!(
1171 "<< /FT /Btn /T ({}) /Ff {} /Kids [{}] /V /{} >>",
1172 Self::escape_pdf_string(group_name),
1173 flags,
1174 kids_refs,
1175 Self::escape_pdf_string(&checked_value),
1176 );
1177 builder.objects[parent_id].data = parent_dict.into_bytes();
1178 acroform_field_ids.push(parent_id);
1179 }
1180
1181 for (page_idx, widget_ids) in per_page_widget_ids.iter().enumerate() {
1185 if widget_ids.is_empty() {
1186 continue;
1187 }
1188 let page_obj_id = page_obj_ids[page_idx];
1189 let existing_page_data =
1190 String::from_utf8_lossy(&builder.objects[page_obj_id].data).to_string();
1191
1192 let new_refs: String = widget_ids
1194 .iter()
1195 .map(|id| format!("{} 0 R", id))
1196 .collect::<Vec<_>>()
1197 .join(" ");
1198
1199 let updated = if let Some(pos) = existing_page_data.find("/Annots [") {
1200 let bracket_end = existing_page_data[pos..].find(']').unwrap() + pos;
1202 format!(
1203 "{} {}{}",
1204 &existing_page_data[..bracket_end],
1205 new_refs,
1206 &existing_page_data[bracket_end..]
1207 )
1208 } else {
1209 let end = existing_page_data.rfind(">>").unwrap();
1211 format!(
1212 "{} /Annots [{}]{}",
1213 &existing_page_data[..end],
1214 new_refs,
1215 &existing_page_data[end..]
1216 )
1217 };
1218 builder.objects[page_obj_id].data = updated.into_bytes();
1219 }
1220
1221 let acroform_id = builder.objects.len();
1223 let fields_refs: String = acroform_field_ids
1224 .iter()
1225 .map(|id| format!("{} 0 R", id))
1226 .collect::<Vec<_>>()
1227 .join(" ");
1228 let dr_str = if let Some(helv_id) = helv_obj_id {
1229 format!(" /DR << /Font << /Helv {} 0 R >> >>", helv_id)
1230 } else {
1231 String::new()
1232 };
1233 let acroform_dict = format!(
1234 "<< /Fields [{}] /NeedAppearances true{} /DA (/Helv 0 Tf 0 g) >>",
1235 fields_refs, dr_str
1236 );
1237 builder.objects.push(PdfObject {
1238 id: acroform_id,
1239 data: acroform_dict.into_bytes(),
1240 });
1241 Some(acroform_id)
1242 } else {
1243 None
1244 };
1245
1246 let mut catalog = String::from("<< /Type /Catalog /Pages 2 0 R");
1248 if let Some(acroform_id) = acroform_obj_id {
1249 write!(catalog, " /AcroForm {} 0 R", acroform_id).unwrap();
1250 }
1251 if let Some(outlines_id) = outlines_obj_id {
1252 write!(
1253 catalog,
1254 " /Outlines {} 0 R /PageMode /UseOutlines",
1255 outlines_id
1256 )
1257 .unwrap();
1258 }
1259 if let Some(ref lang) = metadata.lang {
1260 write!(catalog, " /Lang ({})", Self::escape_pdf_string(lang)).unwrap();
1261 }
1262 if let Some(struct_root_id) = struct_tree_root_id {
1263 write!(
1264 catalog,
1265 " /MarkInfo << /Marked true >> /StructTreeRoot {} 0 R",
1266 struct_root_id
1267 )
1268 .unwrap();
1269 }
1270 if let Some(xmp_id) = xmp_metadata_id {
1271 write!(catalog, " /Metadata {} 0 R", xmp_id).unwrap();
1272 }
1273 if let Some(oi_id) = output_intent_id {
1274 write!(catalog, " /OutputIntents [{} 0 R]", oi_id).unwrap();
1275 }
1276 if let Some(names_id) = embedded_names_id {
1277 write!(catalog, " /Names << /EmbeddedFiles {} 0 R >>", names_id).unwrap();
1278 }
1279 if !af_filespec_ids.is_empty() {
1280 let refs = af_filespec_ids
1283 .iter()
1284 .map(|id| format!("{} 0 R", id))
1285 .collect::<Vec<_>>()
1286 .join(" ");
1287 write!(catalog, " /AF [{}]", refs).unwrap();
1288 }
1289 if pdf_ua {
1290 catalog.push_str(" /ViewerPreferences << /DisplayDocTitle true >>");
1291 }
1292 catalog.push_str(" >>");
1293 builder.objects[1].data = catalog.into_bytes();
1294
1295 let kids: String = page_obj_ids
1297 .iter()
1298 .map(|id| format!("{} 0 R", id))
1299 .collect::<Vec<_>>()
1300 .join(" ");
1301 builder.objects[2].data = format!(
1302 "<< /Type /Pages /Kids [{}] /Count {} >>",
1303 kids,
1304 page_obj_ids.len()
1305 )
1306 .into_bytes();
1307
1308 let info_obj_id = if metadata.title.is_some() || metadata.author.is_some() {
1310 let id = builder.objects.len();
1311 let mut info = String::from("<< ");
1312 if let Some(ref title) = metadata.title {
1313 let _ = write!(info, "/Title ({}) ", Self::escape_pdf_string(title));
1314 }
1315 if let Some(ref author) = metadata.author {
1316 let _ = write!(info, "/Author ({}) ", Self::escape_pdf_string(author));
1317 }
1318 if let Some(ref subject) = metadata.subject {
1319 let _ = write!(info, "/Subject ({}) ", Self::escape_pdf_string(subject));
1320 }
1321 let _ = write!(info, "/Producer (Forme 0.6) /Creator (Forme) >>");
1322 builder.objects.push(PdfObject {
1323 id,
1324 data: info.into_bytes(),
1325 });
1326 Some(id)
1327 } else {
1328 None
1329 };
1330
1331 let pdf = self.serialize(&builder, info_obj_id);
1332 let mut warnings = builder.warnings;
1337 for ch in builder.missing_glyphs.into_inner() {
1338 warnings.push(format!(
1339 "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)",
1340 ch as u32
1341 ));
1342 }
1343 Ok((pdf, warnings))
1344 }
1345
1346 #[allow(clippy::too_many_arguments)]
1348 fn build_content_stream_for_page(
1349 &self,
1350 page: &LayoutPage,
1351 page_idx: usize,
1352 builder: &PdfBuilder,
1353 page_number: usize,
1354 total_pages: usize,
1355 mut tag_builder: Option<&mut tagged::TagBuilder>,
1356 flatten_forms: bool,
1357 ) -> String {
1358 let mut stream = String::new();
1359 let page_height = page.height;
1360 let mut element_counter = 0usize;
1361 let mut gradient_counter = 0usize;
1362
1363 if let Some(&img_idx) = builder.page_background_image_map.get(&page_idx) {
1369 self.write_page_background(&mut stream, page, img_idx, builder);
1370 }
1371
1372 let clip_x = page.config.clip_content_x;
1376 if clip_x {
1377 let x = page.config.margin.left;
1378 let w = page.width - page.config.margin.left - page.config.margin.right;
1379 stream.push_str(&format!(
1380 "q\n{:.2} 0 {:.2} {:.2} re W n\n",
1381 x, w, page.height
1382 ));
1383 }
1384
1385 for element in &page.elements {
1386 self.write_element(
1387 &mut stream,
1388 element,
1389 page_height,
1390 builder,
1391 page_idx,
1392 &mut element_counter,
1393 &mut gradient_counter,
1394 page_number,
1395 total_pages,
1396 tag_builder.as_deref_mut(),
1397 flatten_forms,
1398 );
1399 }
1400
1401 if clip_x {
1402 stream.push_str("Q\n");
1403 }
1404
1405 stream
1406 }
1407
1408 #[allow(clippy::too_many_arguments)]
1410 #[allow(clippy::too_many_arguments)]
1411 fn write_element(
1412 &self,
1413 stream: &mut String,
1414 element: &LayoutElement,
1415 page_height: f64,
1416 builder: &PdfBuilder,
1417 page_idx: usize,
1418 element_counter: &mut usize,
1419 gradient_counter: &mut usize,
1420 page_number: usize,
1421 total_pages: usize,
1422 mut tag_builder: Option<&mut tagged::TagBuilder>,
1423 flatten_forms: bool,
1424 ) {
1425 let mut is_artifact = false;
1428 let tagged_mcid = if let Some(ref mut tb) = tag_builder {
1429 if let Some(ref nt) = element.node_type {
1430 if nt == "Watermark" {
1431 let _ = writeln!(stream, "/Artifact BMC");
1433 is_artifact = true;
1434 None
1435 } else {
1436 let is_header = element.is_header_row;
1437 let href = element.href.as_deref();
1438 let mcid = tb.begin_element(
1439 nt,
1440 is_header,
1441 element.alt.as_deref(),
1442 page_idx,
1443 href,
1444 element.col_span,
1445 );
1446 let role = if href.is_some() {
1449 "Link"
1450 } else {
1451 tb.map_role_public(nt, is_header)
1452 };
1453 let _ = writeln!(stream, "/{} <</MCID {}>> BDC", role, mcid);
1454 Some(mcid)
1455 }
1456 } else if !matches!(element.draw, DrawCommand::None) {
1457 let _ = writeln!(stream, "/Artifact BMC");
1459 is_artifact = true;
1460 None
1461 } else {
1462 None
1463 }
1464 } else {
1465 None
1466 };
1467
1468 let needs_element_opacity = element.opacity < 1.0;
1476 if needs_element_opacity {
1477 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&element.opacity.to_bits()) {
1478 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1479 }
1480 }
1481
1482 let transform_ops: &[TransformOp] = element
1488 .resolved_style
1489 .as_ref()
1490 .map(|s| s.transform.as_slice())
1491 .unwrap_or(&[]);
1492 let has_transform = !transform_ops.is_empty();
1493 if has_transform {
1494 let rs = element.resolved_style.as_ref().unwrap();
1495 let pdf_x = element.x;
1496 let pdf_y_bottom = page_height - element.y - element.height;
1497 let (ox_frac, oy_frac) = rs.transform_origin;
1498 let origin_x = pdf_x + element.width * ox_frac;
1499 let origin_y = pdf_y_bottom + (1.0 - oy_frac) * element.height;
1502
1503 let _ = writeln!(stream, "q");
1504 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", -origin_x, -origin_y);
1506 for op in transform_ops.iter().rev() {
1514 match op {
1515 TransformOp::Rotate { deg } => {
1516 let theta = (-deg).to_radians();
1521 let c = theta.cos();
1522 let s = theta.sin();
1523 let _ = writeln!(stream, "{:.6} {:.6} {:.6} {:.6} 0 0 cm", c, s, -s, c);
1524 }
1525 TransformOp::Scale { x, y } => {
1526 let _ = writeln!(stream, "{:.6} 0 0 {:.6} 0 0 cm", x, y);
1527 }
1528 TransformOp::Translate { x, y } => {
1529 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", x, -y);
1531 }
1532 }
1533 }
1534 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", origin_x, origin_y);
1536 }
1537
1538 match &element.draw {
1539 DrawCommand::None => {}
1540
1541 DrawCommand::Rect {
1542 background,
1543 border_width,
1544 border_color,
1545 border_style,
1546 border_radius,
1547 opacity,
1548 box_shadow,
1549 background_gradient,
1550 } => {
1551 let x = element.x;
1552 let y = page_height - element.y - element.height;
1553 let w = element.width;
1554 let h = element.height;
1555
1556 let needs_opacity = *opacity < 1.0;
1558 if needs_opacity {
1559 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1560 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1561 }
1562 }
1563
1564 if let Some(shadow) = box_shadow {
1570 if shadow.color.a > 0.0 {
1571 let sx = x + shadow.offset_x;
1576 let sy = y - shadow.offset_y;
1577 let needs_shadow_alpha = shadow.color.a < 1.0;
1578 if needs_shadow_alpha {
1579 if let Some((_, gs_name)) =
1580 builder.ext_gstate_map.get(&shadow.color.a.to_bits())
1581 {
1582 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1583 } else {
1584 let _ = writeln!(stream, "q");
1585 }
1586 } else {
1587 let _ = writeln!(stream, "q");
1588 }
1589 let _ = writeln!(
1590 stream,
1591 "{:.3} {:.3} {:.3} rg",
1592 shadow.color.r, shadow.color.g, shadow.color.b
1593 );
1594 if border_radius.top_left > 0.0
1595 || border_radius.top_right > 0.0
1596 || border_radius.bottom_right > 0.0
1597 || border_radius.bottom_left > 0.0
1598 {
1599 self.write_rounded_rect(stream, sx, sy, w, h, border_radius);
1600 } else {
1601 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", sx, sy, w, h);
1602 }
1603 let _ = writeln!(stream, "f\nQ");
1604 }
1605 }
1606
1607 if background_gradient.is_some() {
1613 let key = (page_idx, *gradient_counter);
1614 *gradient_counter += 1;
1615 if let Some((_, sh_name)) = builder.shading_map.get(&key) {
1616 let _ = writeln!(stream, "q");
1617 if border_radius.top_left > 0.0
1619 || border_radius.top_right > 0.0
1620 || border_radius.bottom_right > 0.0
1621 || border_radius.bottom_left > 0.0
1622 {
1623 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1624 let _ = writeln!(stream, "W n");
1625 } else {
1626 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re W n", x, y, w, h);
1627 }
1628 let _ =
1631 writeln!(stream, "1 0 0 1 {:.3} {:.3} cm\n/{} sh\nQ", x, y, sh_name);
1632 }
1633 } else if let Some(bg) = background {
1634 if bg.a > 0.0 {
1635 let _ = writeln!(stream, "q\n{:.3} {:.3} {:.3} rg", bg.r, bg.g, bg.b);
1636
1637 if border_radius.top_left > 0.0 {
1638 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1639 } else {
1640 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1641 }
1642
1643 let _ = writeln!(stream, "f\nQ");
1644 }
1645 }
1646
1647 let bw = border_width;
1648 if bw.top > 0.0 || bw.right > 0.0 || bw.bottom > 0.0 || bw.left > 0.0 {
1649 use crate::style::BorderStyle::Solid;
1650 let all_solid = border_style.top == Solid
1651 && border_style.right == Solid
1652 && border_style.bottom == Solid
1653 && border_style.left == Solid;
1654 if all_solid
1660 && (bw.top - bw.right).abs() < 0.001
1661 && (bw.right - bw.bottom).abs() < 0.001
1662 && (bw.bottom - bw.left).abs() < 0.001
1663 {
1664 let bc = &border_color.top;
1665 let _ = writeln!(
1666 stream,
1667 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w",
1668 bc.r, bc.g, bc.b, bw.top
1669 );
1670
1671 if border_radius.top_left > 0.0 {
1672 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1673 } else {
1674 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1675 }
1676
1677 let _ = writeln!(stream, "S\nQ");
1678 } else {
1679 self.write_border_sides(stream, x, y, w, h, bw, border_color, border_style);
1680 }
1681 }
1682
1683 if needs_opacity {
1684 let _ = writeln!(stream, "Q");
1685 }
1686 }
1687
1688 DrawCommand::Text {
1689 lines,
1690 color,
1691 text_decoration,
1692 opacity,
1693 } => {
1694 let needs_opacity = *opacity < 1.0;
1696 if needs_opacity {
1697 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1698 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1699 }
1700 }
1701
1702 for line in lines {
1703 if line.glyphs.is_empty() {
1704 continue;
1705 }
1706
1707 let groups = Self::group_glyphs_by_style(&line.glyphs);
1710 let pdf_y = page_height - line.y;
1711
1712 let _ = writeln!(stream, "BT");
1713
1714 if line.word_spacing.abs() > 0.001 {
1716 let _ = writeln!(stream, "{:.4} Tw", line.word_spacing);
1717 }
1718
1719 let mut tm_x = 0.0_f64;
1721 let mut tm_y = 0.0_f64;
1722 let mut x_cursor = line.x;
1723
1724 let mut group_spans: Vec<(f64, f64, TextDecoration, Color)> = Vec::new();
1726
1727 for group in &groups {
1728 let first = &group[0];
1729 let glyph_color = first.color.unwrap_or(*color);
1730
1731 let idx = self.font_index(
1732 &first.font_family,
1733 first.font_weight,
1734 first.font_style,
1735 &builder.font_objects,
1736 );
1737 let italic =
1738 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
1739 let font_key = FontKey {
1740 family: first.font_family.to_string(),
1741 weight: first.font_weight,
1742 italic,
1743 };
1744 let font_name = format!("F{}", idx);
1745
1746 let dx = x_cursor - tm_x;
1748 let dy = pdf_y - tm_y;
1749 let _ = writeln!(
1750 stream,
1751 "{:.3} {:.3} {:.3} rg\n/{} {:.1} Tf\n{:.2} Tc\n{:.2} {:.2} Td",
1752 glyph_color.r,
1753 glyph_color.g,
1754 glyph_color.b,
1755 font_name,
1756 first.font_size,
1757 first.letter_spacing,
1758 dx,
1759 dy
1760 );
1761 tm_x = x_cursor;
1762 tm_y = pdf_y;
1763
1764 let raw_text: String = group.iter().map(|g| g.char_value).collect();
1766 let has_placeholder = raw_text.contains(PAGE_NUMBER_SENTINEL)
1767 || raw_text.contains(TOTAL_PAGES_SENTINEL);
1768
1769 let is_custom = builder.custom_font_data.contains_key(&font_key);
1770
1771 if is_custom {
1772 if let Some(embed_data) = builder.custom_font_data.get(&font_key) {
1773 let mut hex = String::new();
1774 if has_placeholder {
1775 let pn = PAGE_NUMBER_SENTINEL.to_string();
1777 let tp = TOTAL_PAGES_SENTINEL.to_string();
1778 let text_after = raw_text
1779 .replace(&pn, &page_number.to_string())
1780 .replace(&tp, &total_pages.to_string());
1781 for ch in text_after.chars() {
1782 let gid =
1783 embed_data.char_to_gid.get(&ch).copied().unwrap_or(0);
1784 let _ = write!(hex, "{:04X}", gid);
1785 }
1786 } else {
1787 for g in group.iter() {
1789 let new_gid = embed_data
1790 .gid_remap
1791 .get(&g.glyph_id)
1792 .copied()
1793 .unwrap_or_else(|| {
1794 embed_data
1796 .char_to_gid
1797 .get(&g.char_value)
1798 .copied()
1799 .unwrap_or(0)
1800 });
1801 let _ = write!(hex, "{:04X}", new_gid);
1802 }
1803 }
1804 let _ = writeln!(stream, "<{}> Tj", hex);
1805 } else {
1806 let _ = writeln!(stream, "<> Tj");
1807 }
1808 } else {
1809 let pn = PAGE_NUMBER_SENTINEL.to_string();
1810 let tp = TOTAL_PAGES_SENTINEL.to_string();
1811 let text_after = raw_text
1812 .replace(&pn, &page_number.to_string())
1813 .replace(&tp, &total_pages.to_string());
1814 let mut text_str = String::new();
1815 for ch in text_after.chars() {
1816 let b = Self::unicode_to_winansi(ch).unwrap_or_else(|| {
1817 builder.missing_glyphs.borrow_mut().insert(ch);
1818 b'?'
1819 });
1820 match b {
1821 b'\\' => text_str.push_str("\\\\"),
1822 b'(' => text_str.push_str("\\("),
1823 b')' => text_str.push_str("\\)"),
1824 0x20..=0x7E => text_str.push(b as char),
1825 _ => {
1826 let _ = write!(text_str, "\\{:03o}", b);
1827 }
1828 }
1829 }
1830 let _ = writeln!(stream, "({}) Tj", text_str);
1831 }
1832
1833 let group_start_x = x_cursor;
1835
1836 if let Some(last) = group.last() {
1839 let space_count_in_group =
1840 group.iter().filter(|g| g.char_value == ' ').count();
1841 x_cursor = line.x
1842 + last.x_offset
1843 + last.x_advance
1844 + space_count_in_group as f64 * line.word_spacing;
1845 }
1846
1847 let group_dec = first.text_decoration;
1849 if !matches!(group_dec, TextDecoration::None) {
1850 group_spans.push((group_start_x, x_cursor, group_dec, glyph_color));
1851 }
1852 }
1853
1854 let _ = writeln!(stream, "ET");
1855
1856 for (span_x, span_end_x, dec, dec_color) in &group_spans {
1858 match dec {
1859 TextDecoration::Underline => {
1860 let underline_y = pdf_y - 1.5;
1861 let _ = write!(
1862 stream,
1863 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1864 dec_color.r, dec_color.g, dec_color.b,
1865 span_x, underline_y,
1866 span_end_x, underline_y
1867 );
1868 }
1869 TextDecoration::LineThrough => {
1870 let first_size =
1871 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
1872 let strikethrough_y = pdf_y + first_size * 0.3;
1873 let _ = write!(
1874 stream,
1875 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1876 dec_color.r, dec_color.g, dec_color.b,
1877 span_x, strikethrough_y,
1878 span_end_x, strikethrough_y
1879 );
1880 }
1881 TextDecoration::None => {}
1882 }
1883 }
1884
1885 if group_spans.is_empty() {
1887 if matches!(text_decoration, TextDecoration::Underline) {
1888 let underline_y = pdf_y - 1.5;
1889 let _ = write!(
1890 stream,
1891 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1892 color.r, color.g, color.b,
1893 line.x, underline_y,
1894 line.x + line.width, underline_y
1895 );
1896 }
1897 if matches!(text_decoration, TextDecoration::LineThrough) {
1898 let first_size =
1899 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
1900 let strikethrough_y = pdf_y + first_size * 0.3;
1901 let _ = write!(
1902 stream,
1903 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1904 color.r, color.g, color.b,
1905 line.x, strikethrough_y,
1906 line.x + line.width, strikethrough_y
1907 );
1908 }
1909 }
1910 }
1911
1912 if needs_opacity {
1913 let _ = writeln!(stream, "Q");
1914 }
1915 }
1916
1917 DrawCommand::Image { .. } => {
1918 let elem_idx = *element_counter;
1919 *element_counter += 1;
1920 if let Some(&img_idx) = builder.image_index_map.get(&(page_idx, elem_idx)) {
1921 let x = element.x;
1922 let y = page_height - element.y - element.height;
1923 let _ = write!(
1924 stream,
1925 "q\n{:.4} 0 0 {:.4} {:.2} {:.2} cm\n/Im{} Do\nQ\n",
1926 element.width, element.height, x, y, img_idx
1927 );
1928 } else {
1929 let x = element.x;
1931 let y = page_height - element.y - element.height;
1932 let _ = write!(
1933 stream,
1934 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
1935 x, y, element.width, element.height
1936 );
1937 }
1938 if tagged_mcid.is_some() {
1939 let _ = writeln!(stream, "EMC");
1940 if let Some(ref mut tb) = tag_builder {
1941 tb.end_element();
1942 }
1943 } else if is_artifact {
1944 let _ = writeln!(stream, "EMC");
1945 }
1946 return; }
1948
1949 DrawCommand::ImagePlaceholder => {
1950 *element_counter += 1;
1951 let x = element.x;
1952 let y = page_height - element.y - element.height;
1953 let _ = write!(
1954 stream,
1955 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
1956 x, y, element.width, element.height
1957 );
1958 if tagged_mcid.is_some() {
1959 let _ = writeln!(stream, "EMC");
1960 if let Some(ref mut tb) = tag_builder {
1961 tb.end_element();
1962 }
1963 } else if is_artifact {
1964 let _ = writeln!(stream, "EMC");
1965 }
1966 return;
1967 }
1968
1969 DrawCommand::Svg {
1970 commands,
1971 width: _svg_w,
1972 height: _svg_h,
1973 viewbox_min_x,
1974 viewbox_min_y,
1975 viewbox_width,
1976 viewbox_height,
1977 clip,
1978 } => {
1979 let x = element.x;
1980 let y = page_height - element.y - element.height;
1981
1982 let _ = writeln!(stream, "q");
1984 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", x, y);
1985
1986 if *viewbox_width > 0.0 && *viewbox_height > 0.0 {
1992 let raw_sx = element.width / *viewbox_width;
1993 let raw_sy = element.height / *viewbox_height;
1994 let s = raw_sx.min(raw_sy);
1995 let tx = (element.width - s * *viewbox_width) / 2.0;
1996 let ty = (element.height - s * *viewbox_height) / 2.0;
1997 let _ = writeln!(stream, "{:.4} 0 0 {:.4} {:.2} {:.2} cm", s, s, tx, ty);
1998 }
1999
2000 let _ = writeln!(stream, "1 0 0 -1 0 {:.2} cm", *viewbox_height);
2003
2004 if *viewbox_min_x != 0.0 || *viewbox_min_y != 0.0 {
2006 let _ = writeln!(
2007 stream,
2008 "1 0 0 1 {:.2} {:.2} cm",
2009 -*viewbox_min_x, -*viewbox_min_y
2010 );
2011 }
2012
2013 if *clip {
2015 let _ = writeln!(
2016 stream,
2017 "{:.2} {:.2} {:.2} {:.2} re W n",
2018 *viewbox_min_x, *viewbox_min_y, *viewbox_width, *viewbox_height
2019 );
2020 }
2021
2022 Self::write_svg_commands(stream, commands, &builder.ext_gstate_map);
2023
2024 let _ = writeln!(stream, "Q");
2025 if tagged_mcid.is_some() {
2026 let _ = writeln!(stream, "EMC");
2027 if let Some(ref mut tb) = tag_builder {
2028 tb.end_element();
2029 }
2030 } else if is_artifact {
2031 let _ = writeln!(stream, "EMC");
2032 }
2033 return;
2034 }
2035
2036 DrawCommand::Barcode {
2037 bars,
2038 bar_width,
2039 height,
2040 color,
2041 } => {
2042 *element_counter += 1;
2043 let _ = writeln!(stream, "q");
2044 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2045 for (i, &bar) in bars.iter().enumerate() {
2046 if bar == 1 {
2047 let bx = element.x + i as f64 * bar_width;
2048 let by = page_height - element.y - height;
2049 let _ = writeln!(
2050 stream,
2051 "{:.2} {:.2} {:.2} {:.2} re",
2052 bx, by, bar_width, height
2053 );
2054 }
2055 }
2056 let _ = writeln!(stream, "f\nQ");
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 }
2065 return;
2066 }
2067
2068 DrawCommand::QrCode {
2069 modules,
2070 module_size,
2071 color,
2072 } => {
2073 *element_counter += 1;
2074 let _ = writeln!(stream, "q");
2075 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2076 for (row_idx, row) in modules.iter().enumerate() {
2077 for (col_idx, &dark) in row.iter().enumerate() {
2078 if dark {
2079 let mx = element.x + col_idx as f64 * module_size;
2080 let my = page_height - element.y - (row_idx as f64 + 1.0) * module_size;
2081 let _ = writeln!(
2082 stream,
2083 "{:.2} {:.2} {:.2} {:.2} re",
2084 mx, my, module_size, module_size
2085 );
2086 }
2087 }
2088 }
2089 let _ = writeln!(stream, "f\nQ");
2090 if tagged_mcid.is_some() {
2091 let _ = writeln!(stream, "EMC");
2092 if let Some(ref mut tb) = tag_builder {
2093 tb.end_element();
2094 }
2095 } else if is_artifact {
2096 let _ = writeln!(stream, "EMC");
2097 }
2098 return;
2099 }
2100
2101 DrawCommand::Chart { primitives } => {
2102 *element_counter += 1;
2103 let _ = writeln!(stream, "q");
2104 let _ = writeln!(
2106 stream,
2107 "1 0 0 -1 {:.4} {:.4} cm",
2108 element.x,
2109 page_height - element.y
2110 );
2111
2112 for prim in primitives {
2113 write_chart_primitive(stream, prim, element.height, builder);
2114 }
2115
2116 let _ = writeln!(stream, "Q");
2117 if tagged_mcid.is_some() {
2118 let _ = writeln!(stream, "EMC");
2119 if let Some(ref mut tb) = tag_builder {
2120 tb.end_element();
2121 }
2122 } else if is_artifact {
2123 let _ = writeln!(stream, "EMC");
2124 }
2125 return;
2126 }
2127
2128 DrawCommand::Watermark {
2129 lines,
2130 color,
2131 opacity,
2132 angle_rad,
2133 font_family: _,
2134 } => {
2135 let _ = writeln!(stream, "q");
2136 if *opacity < 1.0 {
2138 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
2139 let _ = writeln!(stream, "/{} gs", gs_name);
2140 }
2141 }
2142 let pdf_cx = element.x;
2144 let pdf_cy = page_height - element.y;
2145 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", pdf_cx, pdf_cy);
2146 let cos_a = angle_rad.cos();
2148 let sin_a = angle_rad.sin();
2149 let _ = writeln!(
2150 stream,
2151 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
2152 cos_a, sin_a, -sin_a, cos_a
2153 );
2154 let _ = writeln!(stream, "BT");
2156 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2157 if let Some(line) = lines.first() {
2158 let groups = Self::group_glyphs_by_style(&line.glyphs);
2159 let text_width = line.width;
2160 let cap_height = line.height * 0.7;
2161 let _ = writeln!(
2162 stream,
2163 "{:.2} {:.2} Td",
2164 -text_width / 2.0,
2165 -cap_height / 2.0
2166 );
2167 for group in &groups {
2168 let first = &group[0];
2169 let italic =
2170 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
2171 let fk = FontKey {
2172 family: first.font_family.to_string(),
2173 weight: first.font_weight,
2174 italic,
2175 };
2176 let idx = self.font_index(
2177 &first.font_family,
2178 first.font_weight,
2179 first.font_style,
2180 &builder.font_objects,
2181 );
2182 let font_name = format!("F{}", idx);
2183 let _ = writeln!(stream, "/{} {:.1} Tf", font_name, first.font_size);
2184 let is_custom = builder.custom_font_data.contains_key(&fk);
2185 if is_custom {
2186 if let Some(embed_data) = builder.custom_font_data.get(&fk) {
2187 let mut hex = String::new();
2188 for g in group.iter() {
2189 let gid =
2190 embed_data.gid_remap.get(&g.glyph_id).copied().unwrap_or(0);
2191 let _ = write!(hex, "{:04X}", gid);
2192 }
2193 let _ = writeln!(stream, "<{}> Tj", hex);
2194 }
2195 } else {
2196 let hex_str: String = group
2197 .iter()
2198 .map(|g| format!("{:02X}", g.glyph_id as u8))
2199 .collect();
2200 let _ = writeln!(stream, "<{}> Tj", hex_str);
2201 }
2202 }
2203 }
2204 let _ = writeln!(stream, "ET");
2205 let _ = writeln!(stream, "Q");
2206 if tagged_mcid.is_some() {
2207 let _ = writeln!(stream, "EMC");
2208 if let Some(ref mut tb) = tag_builder {
2209 tb.end_element();
2210 }
2211 } else if is_artifact {
2212 let _ = writeln!(stream, "EMC");
2213 }
2214 return;
2215 }
2216
2217 DrawCommand::FormField { field_type, .. } => {
2218 let pdf_x = element.x;
2222 let pdf_y = page_height - element.y - element.height;
2223 let w = element.width;
2224 let h = element.height;
2225 let _ = writeln!(stream, "q");
2226 match field_type {
2227 FormFieldType::Checkbox { checked, .. } => {
2228 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
2231 let _ =
2232 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
2233 if *checked {
2234 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
2236 let sx = w / 14.0;
2237 let sy = h / 14.0;
2238 let _ = writeln!(
2239 stream,
2240 "{:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l f",
2241 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
2242 pdf_x + 5.5 * sx, pdf_y + 2.0 * sy,
2243 pdf_x + 12.0 * sx, pdf_y + 11.0 * sy,
2244 pdf_x + 11.0 * sx, pdf_y + 12.0 * sy,
2245 pdf_x + 5.5 * sx, pdf_y + 4.5 * sy,
2246 pdf_x + 3.0 * sx, pdf_y + 7.0 * sy,
2247 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
2248 );
2249 }
2250 }
2251 FormFieldType::RadioButton { checked, .. } => {
2252 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
2255 let _ =
2256 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
2257 if *checked {
2258 let cx = pdf_x + w / 2.0;
2260 let cy = pdf_y + h / 2.0;
2261 let r = (w.min(h) / 2.0) * 0.6;
2262 let k = r * 0.5523;
2263 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
2264 let _ = writeln!(
2265 stream,
2266 "{:.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",
2267 cx, cy + r,
2268 cx + k, cy + r, cx + r, cy + k, cx + r, cy,
2269 cx + r, cy - k, cx + k, cy - r, cx, cy - r,
2270 cx - k, cy - r, cx - r, cy - k, cx - r, cy,
2271 cx - r, cy + k, cx - k, cy + r, cx, cy + r,
2272 );
2273 }
2274 }
2275 FormFieldType::TextField {
2276 value,
2277 placeholder,
2278 font_size,
2279 multiline,
2280 password,
2281 ..
2282 } => {
2283 let _ = writeln!(stream, "1 1 1 rg");
2285 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2286 let _ = writeln!(stream, "0.5 w");
2287 let _ =
2288 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2289 if flatten_forms {
2291 let has_value = value.as_ref().is_some_and(|v| !v.is_empty());
2292 if has_value {
2293 let val = value.as_ref().unwrap();
2294 let display_text = if *password {
2295 "\u{2022}".repeat(val.len())
2296 } else {
2297 val.clone()
2298 };
2299 let font_idx = builder
2300 .font_objects
2301 .iter()
2302 .enumerate()
2303 .find(|(_, (key, _))| {
2304 key.family == "Helvetica"
2305 && key.weight == 400
2306 && !key.italic
2307 })
2308 .map(|(i, _)| i)
2309 .unwrap_or(0);
2310 if *multiline {
2311 let metrics = crate::font::StandardFont::Helvetica.metrics();
2313 let max_w = w - 4.0;
2314 let mut lines: Vec<String> = Vec::new();
2315 for paragraph in display_text.split('\n') {
2316 let mut line = String::new();
2317 let mut line_w = 0.0;
2318 for word in paragraph.split_whitespace() {
2319 let word_w =
2320 metrics.measure_string(word, *font_size, 0.0);
2321 let space_w = if line.is_empty() {
2322 0.0
2323 } else {
2324 metrics.measure_string(" ", *font_size, 0.0)
2325 };
2326 if word_w > max_w {
2328 let mut char_line = String::new();
2329 let mut char_w = 0.0;
2330 for ch in word.chars() {
2331 let cw = metrics.char_width(ch, *font_size);
2332 if !char_line.is_empty() && char_w + cw > max_w
2333 {
2334 if !line.is_empty() {
2335 lines.push(line.clone());
2336 line.clear();
2337 line_w = 0.0;
2338 }
2339 lines.push(char_line.clone());
2340 char_line.clear();
2341 char_w = 0.0;
2342 }
2343 char_line.push(ch);
2344 char_w += cw;
2345 }
2346 if !char_line.is_empty() {
2348 if !line.is_empty() {
2349 line.push(' ');
2350 line_w += metrics
2351 .measure_string(" ", *font_size, 0.0);
2352 }
2353 line.push_str(&char_line);
2354 line_w += char_w;
2355 }
2356 continue;
2357 }
2358 if !line.is_empty() && line_w + space_w + word_w > max_w
2359 {
2360 lines.push(line.clone());
2361 line.clear();
2362 line_w = 0.0;
2363 }
2364 if !line.is_empty() {
2365 line.push(' ');
2366 line_w += space_w;
2367 }
2368 line.push_str(word);
2369 line_w += word_w;
2370 }
2371 if !line.is_empty() {
2372 lines.push(line);
2373 }
2374 }
2375 let text_y = pdf_y + h - font_size - 2.0;
2376 for (i, line_text) in lines.iter().enumerate() {
2377 let ly = text_y - (i as f64) * (font_size * 1.2);
2378 if ly < pdf_y {
2379 break;
2380 }
2381 let esc = Self::encode_winansi_text(builder, line_text);
2382 let _ = writeln!(
2383 stream,
2384 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2385 font_idx,
2386 font_size,
2387 pdf_x + 2.0,
2388 ly,
2389 esc
2390 );
2391 }
2392 } else {
2393 let escaped = Self::encode_winansi_text(builder, &display_text);
2394 let text_y = pdf_y + (h - font_size) / 2.0;
2395 let _ = writeln!(
2396 stream,
2397 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2398 font_idx,
2399 font_size,
2400 pdf_x + 2.0,
2401 text_y,
2402 escaped
2403 );
2404 }
2405 } else if let Some(ref ph) = placeholder {
2406 if !ph.is_empty() {
2407 let font_idx = builder
2409 .font_objects
2410 .iter()
2411 .enumerate()
2412 .find(|(_, (key, _))| {
2413 key.family == "Helvetica"
2414 && key.weight == 400
2415 && !key.italic
2416 })
2417 .map(|(i, _)| i)
2418 .unwrap_or(0);
2419 let escaped = Self::encode_winansi_text(builder, ph);
2420 let text_y = pdf_y + (h - font_size) / 2.0;
2421 let _ = writeln!(
2422 stream,
2423 "BT /F{} {:.1} Tf 0.6 g {:.2} {:.2} Td ({}) Tj ET",
2424 font_idx,
2425 font_size,
2426 pdf_x + 2.0,
2427 text_y,
2428 escaped
2429 );
2430 }
2431 }
2432 }
2433 }
2434 FormFieldType::Dropdown {
2435 value, font_size, ..
2436 } => {
2437 let _ = writeln!(stream, "1 1 1 rg");
2439 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2440 let _ = writeln!(stream, "0.5 w");
2441 let _ =
2442 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2443 if flatten_forms {
2445 if let Some(ref val) = value {
2446 if !val.is_empty() {
2447 let font_idx = builder
2448 .font_objects
2449 .iter()
2450 .enumerate()
2451 .find(|(_, (key, _))| {
2452 key.family == "Helvetica"
2453 && key.weight == 400
2454 && !key.italic
2455 })
2456 .map(|(i, _)| i)
2457 .unwrap_or(0);
2458 let escaped = Self::encode_winansi_text(builder, val);
2459 let text_y = pdf_y + (h - font_size) / 2.0;
2460 let _ = writeln!(
2461 stream,
2462 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2463 font_idx,
2464 font_size,
2465 pdf_x + 2.0,
2466 text_y,
2467 escaped
2468 );
2469 }
2470 }
2471 }
2472 }
2473 }
2474 let _ = writeln!(stream, "Q");
2475 }
2476 }
2477
2478 let clip_overflow = matches!(element.overflow, Overflow::Hidden);
2483 if clip_overflow {
2484 let clip_x = element.x;
2485 let clip_y = page_height - element.y - element.height;
2486 let clip_w = element.width;
2487 let clip_h = element.height;
2488 let radius = if let DrawCommand::Rect { border_radius, .. } = &element.draw {
2492 Some(border_radius)
2493 } else {
2494 None
2495 };
2496 let has_rounded_corners = radius.is_some_and(|r| {
2497 r.top_left > 0.0 || r.top_right > 0.0 || r.bottom_right > 0.0 || r.bottom_left > 0.0
2498 });
2499 let _ = writeln!(stream, "q");
2500 if has_rounded_corners {
2501 self.write_rounded_rect(stream, clip_x, clip_y, clip_w, clip_h, radius.unwrap());
2502 let _ = writeln!(stream, "W n");
2503 } else {
2504 let _ = writeln!(
2505 stream,
2506 "{:.2} {:.2} {:.2} {:.2} re W n",
2507 clip_x, clip_y, clip_w, clip_h
2508 );
2509 }
2510 }
2511
2512 for child in &element.children {
2513 self.write_element(
2514 stream,
2515 child,
2516 page_height,
2517 builder,
2518 page_idx,
2519 element_counter,
2520 gradient_counter,
2521 page_number,
2522 total_pages,
2523 tag_builder.as_deref_mut(),
2524 flatten_forms,
2525 );
2526 }
2527
2528 if clip_overflow {
2529 let _ = writeln!(stream, "Q");
2530 }
2531
2532 if has_transform {
2534 let _ = writeln!(stream, "Q");
2535 }
2536
2537 if needs_element_opacity {
2540 let _ = writeln!(stream, "Q");
2541 }
2542
2543 if tagged_mcid.is_some() {
2545 let _ = writeln!(stream, "EMC");
2546 if let Some(ref mut tb) = tag_builder {
2547 tb.end_element();
2548 }
2549 } else if is_artifact {
2550 let _ = writeln!(stream, "EMC");
2551 }
2552 }
2553
2554 fn write_rounded_rect(
2555 &self,
2556 stream: &mut String,
2557 x: f64,
2558 y: f64,
2559 w: f64,
2560 h: f64,
2561 r: &crate::style::CornerValues,
2562 ) {
2563 let k = 0.5522847498;
2564
2565 let tl = r.top_left.min(w / 2.0).min(h / 2.0);
2566 let tr = r.top_right.min(w / 2.0).min(h / 2.0);
2567 let br = r.bottom_right.min(w / 2.0).min(h / 2.0);
2568 let bl = r.bottom_left.min(w / 2.0).min(h / 2.0);
2569
2570 let _ = writeln!(stream, "{:.2} {:.2} m", x + bl, y);
2571
2572 let _ = writeln!(stream, "{:.2} {:.2} l", x + w - br, y);
2573 if br > 0.0 {
2574 let _ = writeln!(
2575 stream,
2576 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2577 x + w - br + br * k,
2578 y,
2579 x + w,
2580 y + br - br * k,
2581 x + w,
2582 y + br
2583 );
2584 }
2585
2586 let _ = writeln!(stream, "{:.2} {:.2} l", x + w, y + h - tr);
2587 if tr > 0.0 {
2588 let _ = writeln!(
2589 stream,
2590 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2591 x + w,
2592 y + h - tr + tr * k,
2593 x + w - tr + tr * k,
2594 y + h,
2595 x + w - tr,
2596 y + h
2597 );
2598 }
2599
2600 let _ = writeln!(stream, "{:.2} {:.2} l", x + tl, y + h);
2601 if tl > 0.0 {
2602 let _ = writeln!(
2603 stream,
2604 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2605 x + tl - tl * k,
2606 y + h,
2607 x,
2608 y + h - tl + tl * k,
2609 x,
2610 y + h - tl
2611 );
2612 }
2613
2614 let _ = writeln!(stream, "{:.2} {:.2} l", x, y + bl);
2615 if bl > 0.0 {
2616 let _ = writeln!(
2617 stream,
2618 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2619 x,
2620 y + bl - bl * k,
2621 x + bl - bl * k,
2622 y,
2623 x + bl,
2624 y
2625 );
2626 }
2627
2628 let _ = writeln!(stream, "h");
2629 }
2630
2631 #[allow(clippy::too_many_arguments)]
2632 fn write_border_sides(
2633 &self,
2634 stream: &mut String,
2635 x: f64,
2636 y: f64,
2637 w: f64,
2638 h: f64,
2639 bw: &Edges,
2640 bc: &crate::style::EdgeValues<Color>,
2641 bs: &crate::style::EdgeValues<crate::style::BorderStyle>,
2642 ) {
2643 fn dash_ops(style: crate::style::BorderStyle, width: f64) -> String {
2648 use crate::style::BorderStyle::*;
2649 match style {
2650 Solid => String::new(),
2651 Dashed => format!("[{:.2} {:.2}] 0 d\n", width * 2.0, width),
2652 Dotted => format!("1 J\n[0 {:.2}] 0 d\n", width * 2.0),
2653 }
2654 }
2655 let sides = [
2657 (bc.top, bw.top, bs.top, x, y + h, x + w, y + h),
2658 (bc.bottom, bw.bottom, bs.bottom, x, y, x + w, y),
2659 (bc.left, bw.left, bs.left, x, y, x, y + h),
2660 (bc.right, bw.right, bs.right, x + w, y, x + w, y + h),
2661 ];
2662 for (color, width, style, x0, y0, x1, y1) in sides {
2663 if width <= 0.0 {
2664 continue;
2665 }
2666 let _ = write!(
2667 stream,
2668 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{}{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2669 color.r,
2670 color.g,
2671 color.b,
2672 width,
2673 dash_ops(style, width),
2674 x0,
2675 y0,
2676 x1,
2677 y1
2678 );
2679 }
2680 }
2681
2682 fn emit_pdfua_embedded_standard(
2694 builder: &mut PdfBuilder,
2695 key: &FontKey,
2696 std_font: &crate::font::StandardFont,
2697 metrics: &crate::font::StandardFontMetrics,
2698 font_context: &FontContext,
2699 ) -> bool {
2700 let lib_family = match std_font.liberation_family() {
2701 Some(f) => f,
2702 None => return false, };
2704 let lib_bytes: &[u8] = match font_context.resolve(lib_family, key.weight, key.italic) {
2707 FontData::Custom { data, .. } => data,
2708 FontData::Standard(_) => return false,
2709 };
2710 let face = match ttf_parser::Face::parse(lib_bytes, 0) {
2711 Ok(f) => f,
2712 Err(_) => return false,
2713 };
2714 let scale = 1000.0 / face.units_per_em() as f64;
2715 let bbox = face.global_bounding_box();
2716 let pdf_name = Self::sanitize_font_name(lib_family, key.weight, key.italic);
2717
2718 let compressed = compress_to_vec_zlib(lib_bytes, 6);
2720 let fontfile2_id = builder.objects.len();
2721 let mut ff2: Vec<u8> = Vec::new();
2722 let _ = write!(
2723 ff2,
2724 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
2725 compressed.len(),
2726 lib_bytes.len()
2727 );
2728 ff2.extend_from_slice(&compressed);
2729 ff2.extend_from_slice(b"\nendstream");
2730 builder.objects.push(PdfObject {
2731 id: fontfile2_id,
2732 data: ff2,
2733 });
2734
2735 let fd_id = builder.objects.len();
2737 let cap_height =
2738 (face.capital_height().unwrap_or_else(|| face.ascender()) as f64 * scale) as i32;
2739 let fd = format!(
2740 "<< /Type /FontDescriptor /FontName /{name} /Flags {flags} \
2741 /FontBBox [{x0} {y0} {x1} {y1}] /ItalicAngle {ia} \
2742 /Ascent {asc} /Descent {desc} /CapHeight {cap} /StemV {stem} \
2743 /FontFile2 {ff2} 0 R >>",
2744 name = pdf_name,
2745 flags = std_font.descriptor_flags(),
2746 x0 = (bbox.x_min as f64 * scale) as i32,
2747 y0 = (bbox.y_min as f64 * scale) as i32,
2748 x1 = (bbox.x_max as f64 * scale) as i32,
2749 y1 = (bbox.y_max as f64 * scale) as i32,
2750 ia = if key.italic { -12 } else { 0 },
2751 asc = (face.ascender() as f64 * scale) as i32,
2752 desc = (face.descender() as f64 * scale) as i32,
2753 cap = cap_height,
2754 stem = if key.weight >= 700 { 120 } else { 80 },
2755 ff2 = fontfile2_id,
2756 );
2757 builder.objects.push(PdfObject {
2758 id: fd_id,
2759 data: fd.into_bytes(),
2760 });
2761
2762 let declared_widths: Vec<u16> = metrics
2776 .widths
2777 .iter()
2778 .enumerate()
2779 .map(|(i, &afm)| {
2780 let code = 32u8.wrapping_add(i as u8); if let Some(ch) = crate::font::winansi_to_char(code) {
2782 if let Some(gid) = face.glyph_index(ch) {
2783 if let Some(adv) = face.glyph_hor_advance(gid) {
2784 let hmtx = (adv as f64 * scale).round() as u16;
2785 if (hmtx as i32 - afm as i32).abs() > 1 {
2786 return hmtx;
2787 }
2788 }
2789 }
2790 }
2791 afm
2792 })
2793 .collect();
2794 let widths_str: String = declared_widths
2795 .iter()
2796 .map(|w| w.to_string())
2797 .collect::<Vec<_>>()
2798 .join(" ");
2799 let obj_id = builder.objects.len();
2800 let font_dict = format!(
2801 "<< /Type /Font /Subtype /TrueType /BaseFont /{name} \
2802 /Encoding /WinAnsiEncoding \
2803 /FirstChar 32 /LastChar 255 /Widths [{w}] \
2804 /FontDescriptor {fd} 0 R >>",
2805 name = pdf_name,
2806 w = widths_str,
2807 fd = fd_id,
2808 );
2809 builder.objects.push(PdfObject {
2810 id: obj_id,
2811 data: font_dict.into_bytes(),
2812 });
2813 builder.font_objects.push((key.clone(), obj_id));
2814 builder.embedded_standard_fonts.insert(key.clone());
2818 true
2819 }
2820
2821 fn register_fonts(
2822 &self,
2823 builder: &mut PdfBuilder,
2824 pages: &[LayoutPage],
2825 font_context: &FontContext,
2826 pdf_ua: bool,
2827 ) -> Result<(), FormeError> {
2828 let mut font_usage_map: HashMap<FontKey, FontUsage> = HashMap::new();
2830
2831 for page in pages {
2832 Self::collect_font_usage(&page.elements, &mut font_usage_map);
2833 }
2834
2835 let mut keys: Vec<FontKey> = font_usage_map.keys().cloned().collect();
2836
2837 keys.sort_by(|a, b| {
2839 a.family
2840 .cmp(&b.family)
2841 .then(a.weight.cmp(&b.weight))
2842 .then(a.italic.cmp(&b.italic))
2843 });
2844 keys.dedup();
2845
2846 if keys.is_empty() {
2848 keys.push(FontKey {
2849 family: "Helvetica".to_string(),
2850 weight: 400,
2851 italic: false,
2852 });
2853 }
2854
2855 for key in &keys {
2856 let font_data = font_context.resolve(&key.family, key.weight, key.italic);
2857
2858 match font_data {
2859 FontData::Standard(std_font) => {
2860 let metrics = std_font.metrics();
2861
2862 if pdf_ua {
2871 if Self::emit_pdfua_embedded_standard(
2872 builder,
2873 key,
2874 std_font,
2875 &metrics,
2876 font_context,
2877 ) {
2878 continue;
2879 }
2880 if let Some(lib) = std_font.liberation_family() {
2886 builder.warnings.push(format!(
2887 "pdfUa: font '{}' is not embedded, so the PDF will not conform to \
2888 PDF/UA (all fonts must be embedded). Install \
2889 @formepdf/fonts-standard and register its fonts \
2890 (`for (const f of standardFonts()) Font.register(f)`) — Forme \
2891 will then embed the metric-compatible {} in its place.",
2892 std_font.pdf_name(),
2893 lib,
2894 ));
2895 }
2896 }
2897
2898 let obj_id = builder.objects.len();
2899 let widths_str: String = metrics
2902 .widths
2903 .iter()
2904 .map(|w| w.to_string())
2905 .collect::<Vec<_>>()
2906 .join(" ");
2907 let font_dict = format!(
2908 "<< /Type /Font /Subtype /Type1 /BaseFont /{} \
2909 /Encoding /WinAnsiEncoding \
2910 /FirstChar 32 /LastChar 255 /Widths [{}] >>",
2911 std_font.pdf_name(),
2912 widths_str,
2913 );
2914 builder.objects.push(PdfObject {
2915 id: obj_id,
2916 data: font_dict.into_bytes(),
2917 });
2918 builder.font_objects.push((key.clone(), obj_id));
2919 }
2920 FontData::Custom { data, .. } => {
2921 let usage = font_usage_map.get(key);
2922 let used_glyph_ids = usage.map(|u| &u.glyph_ids);
2923 let used_chars = usage.map(|u| &u.chars);
2924 let glyph_to_char = usage.map(|u| &u.glyph_to_char);
2925 let type0_obj_id = Self::write_custom_font_objects(
2926 builder,
2927 key,
2928 data,
2929 used_glyph_ids.cloned().unwrap_or_default(),
2930 used_chars.cloned().unwrap_or_default(),
2931 glyph_to_char.cloned().unwrap_or_default(),
2932 )?;
2933 builder.font_objects.push((key.clone(), type0_obj_id));
2934 }
2935 }
2936 }
2937
2938 Ok(())
2939 }
2940
2941 fn collect_font_usage(
2943 elements: &[LayoutElement],
2944 font_usage: &mut HashMap<FontKey, FontUsage>,
2945 ) {
2946 for element in elements {
2947 let lines_opt = match &element.draw {
2948 DrawCommand::Text { lines, .. } => Some(lines),
2949 DrawCommand::Watermark { lines, .. } => Some(lines),
2950 _ => None,
2951 };
2952 if let Some(lines) = lines_opt {
2953 for line in lines {
2954 for glyph in &line.glyphs {
2955 let italic =
2956 matches!(glyph.font_style, FontStyle::Italic | FontStyle::Oblique);
2957 let key = FontKey {
2958 family: glyph.font_family.to_string(),
2959 weight: glyph.font_weight,
2960 italic,
2961 };
2962 let usage = font_usage.entry(key).or_insert_with(|| FontUsage {
2963 chars: HashSet::new(),
2964 glyph_ids: HashSet::new(),
2965 glyph_to_char: HashMap::new(),
2966 });
2967 usage.chars.insert(glyph.char_value);
2968 if glyph.char_value == PAGE_NUMBER_SENTINEL
2973 || glyph.char_value == TOTAL_PAGES_SENTINEL
2974 {
2975 usage.chars.extend('0'..='9');
2976 }
2977 usage.glyph_ids.insert(glyph.glyph_id);
2978 usage
2980 .glyph_to_char
2981 .entry(glyph.glyph_id)
2982 .or_insert(glyph.char_value);
2983 if let Some(ref ct) = glyph.cluster_text {
2985 if let Some(first_char) = ct.chars().next() {
2987 usage
2988 .glyph_to_char
2989 .entry(glyph.glyph_id)
2990 .or_insert(first_char);
2991 }
2992 }
2993 }
2994 }
2995 }
2996 Self::collect_font_usage(&element.children, font_usage);
2997 }
2998 }
2999
3000 fn register_shadings(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3006 for (page_idx, page) in pages.iter().enumerate() {
3007 let mut counter = 0usize;
3008 Self::collect_shadings_recursive(&page.elements, page_idx, &mut counter, builder);
3009 }
3010 }
3011
3012 fn collect_shadings_recursive(
3013 elements: &[LayoutElement],
3014 page_idx: usize,
3015 counter: &mut usize,
3016 builder: &mut PdfBuilder,
3017 ) {
3018 for element in elements {
3019 if let DrawCommand::Rect {
3020 background_gradient: Some(gradient),
3021 ..
3022 } = &element.draw
3023 {
3024 let ordinal = *counter;
3025 *counter += 1;
3026 let (obj_id, name) =
3027 Self::write_shading_objects(builder, gradient, element, ordinal);
3028 builder
3029 .shading_map
3030 .insert((page_idx, ordinal), (obj_id, name));
3031 }
3032 Self::collect_shadings_recursive(&element.children, page_idx, counter, builder);
3033 }
3034 }
3035
3036 fn write_shading_objects(
3042 builder: &mut PdfBuilder,
3043 gradient: &crate::style::Background,
3044 element: &LayoutElement,
3045 ordinal: usize,
3046 ) -> (usize, String) {
3047 use crate::style::Background;
3048 use crate::style::GradientStop;
3049
3050 let black = Color {
3054 r: 0.0,
3055 g: 0.0,
3056 b: 0.0,
3057 a: 1.0,
3058 };
3059 let stops: Vec<GradientStop> = match gradient {
3060 Background::Color(c) => vec![
3061 GradientStop {
3062 position: 0.0,
3063 color: *c,
3064 },
3065 GradientStop {
3066 position: 1.0,
3067 color: *c,
3068 },
3069 ],
3070 Background::Linear(g) => normalize_gradient_stops(&g.stops, black),
3071 Background::Radial(g) => normalize_gradient_stops(&g.stops, black),
3072 };
3073
3074 let function_id = if stops.len() <= 2 {
3078 let c0 = stops.first().map(|s| s.color).unwrap_or(black);
3079 let c1 = stops.last().map(|s| s.color).unwrap_or(c0);
3080 let id = builder.objects.len();
3081 let data = format!(
3082 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
3083 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
3084 );
3085 builder.objects.push(PdfObject {
3086 id,
3087 data: data.into_bytes(),
3088 });
3089 id
3090 } else {
3091 let mut sub_ids: Vec<usize> = Vec::with_capacity(stops.len() - 1);
3093 for window in stops.windows(2) {
3094 let c0 = window[0].color;
3095 let c1 = window[1].color;
3096 let id = builder.objects.len();
3097 let data = format!(
3098 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
3099 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
3100 );
3101 builder.objects.push(PdfObject {
3102 id,
3103 data: data.into_bytes(),
3104 });
3105 sub_ids.push(id);
3106 }
3107 let bounds: Vec<String> = stops[1..stops.len() - 1]
3111 .iter()
3112 .map(|s| format!("{:.4}", s.position))
3113 .collect();
3114 let encode: Vec<&str> = (0..sub_ids.len()).map(|_| "0 1").collect();
3115 let functions: Vec<String> = sub_ids.iter().map(|i| format!("{} 0 R", i)).collect();
3116 let id = builder.objects.len();
3117 let data = format!(
3118 "<< /FunctionType 3 /Domain [0 1] /Functions [{}] /Bounds [{}] /Encode [{}] >>",
3119 functions.join(" "),
3120 bounds.join(" "),
3121 encode.join(" "),
3122 );
3123 builder.objects.push(PdfObject {
3124 id,
3125 data: data.into_bytes(),
3126 });
3127 id
3128 };
3129
3130 let _ = element.x;
3134 let _ = element.y;
3135 let w = element.width;
3136 let h = element.height;
3137
3138 let shading_id = builder.objects.len();
3139 let shading_data = match gradient {
3140 Background::Linear(g) => {
3141 let theta = g.angle_deg.to_radians();
3151 let dx = theta.sin();
3152 let dy = theta.cos();
3153 let axis_len = w * dx.abs() + h * dy.abs();
3156 let cx_rel = w / 2.0;
3159 let cy_rel = h / 2.0;
3160 let half = axis_len / 2.0;
3161 let x0 = cx_rel - dx * half;
3162 let y0 = cy_rel - dy * half;
3163 let x1 = cx_rel + dx * half;
3164 let y1 = cy_rel + dy * half;
3165 format!(
3166 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
3167 x0, y0, x1, y1, function_id,
3168 )
3169 }
3170 Background::Radial(_) => {
3171 let cx_rel = w / 2.0;
3174 let cy_rel = h / 2.0;
3175 let r_outer = (w / 2.0).max(h / 2.0);
3176 format!(
3177 "<< /ShadingType 3 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} 0 {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
3178 cx_rel, cy_rel, cx_rel, cy_rel, r_outer, function_id,
3179 )
3180 }
3181 Background::Color(_) => {
3182 format!(
3186 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 0 0] /Function {} 0 R /Extend [true true] >>",
3187 function_id,
3188 )
3189 }
3190 };
3191 builder.objects.push(PdfObject {
3192 id: shading_id,
3193 data: shading_data.into_bytes(),
3194 });
3195 (shading_id, format!("Sh{}", ordinal))
3196 }
3197
3198 fn register_page_background_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3202 for (page_idx, page) in pages.iter().enumerate() {
3203 let Some(src) = &page.config.background_image else {
3204 continue;
3205 };
3206 if let Some(&entry) = builder.page_background_url_cache.get(src) {
3208 builder.page_background_image_map.insert(page_idx, entry);
3209 continue;
3210 }
3211 match crate::image_loader::load_image(src) {
3214 Ok(image_data) => {
3215 let img_idx = builder.image_objects.len();
3216 let dims = (img_idx, image_data.width_px, image_data.height_px);
3217 let xobj_id = Self::write_image_xobject(builder, &image_data);
3218 builder.image_objects.push(xobj_id);
3219 builder.page_background_image_map.insert(page_idx, dims);
3220 builder.page_background_url_cache.insert(src.clone(), dims);
3221 }
3222 Err(e) => {
3223 eprintln!("[forme] page background image failed to load: {}", e);
3224 }
3225 }
3226 }
3227 }
3228
3229 fn write_page_background(
3234 &self,
3235 stream: &mut String,
3236 page: &LayoutPage,
3237 page_bg: (usize, u32, u32),
3238 builder: &PdfBuilder,
3239 ) {
3240 use crate::model::{BackgroundPosition, BackgroundSize};
3241 let (img_idx, iw_px, ih_px) = page_bg;
3242 let page_w = page.width;
3243 let page_h = page.height;
3244 let iw = iw_px as f64;
3245 let ih = ih_px as f64;
3246
3247 let size = page.config.background_size.unwrap_or_default();
3248 let (dest_w, dest_h) = match size {
3249 BackgroundSize::Fill => (page_w, page_h),
3250 BackgroundSize::Cover => {
3251 let s = (page_w / iw).max(page_h / ih);
3252 (iw * s, ih * s)
3253 }
3254 BackgroundSize::Contain => {
3255 let s = (page_w / iw).min(page_h / ih);
3256 (iw * s, ih * s)
3257 }
3258 };
3259
3260 let position = page.config.background_position.unwrap_or_default();
3264 let (dest_x, dest_y) = match position {
3267 BackgroundPosition::TopLeft => (0.0, page_h - dest_h),
3268 BackgroundPosition::TopRight => (page_w - dest_w, page_h - dest_h),
3269 BackgroundPosition::BottomLeft => (0.0, 0.0),
3270 BackgroundPosition::BottomRight => (page_w - dest_w, 0.0),
3271 BackgroundPosition::Center => ((page_w - dest_w) / 2.0, (page_h - dest_h) / 2.0),
3272 };
3273
3274 let opacity = page.config.background_opacity.unwrap_or(1.0);
3276 let needs_opacity = opacity < 1.0;
3277 if needs_opacity {
3278 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
3279 let _ = writeln!(stream, "q\n/{} gs", gs_name);
3280 } else {
3281 let _ = writeln!(stream, "q");
3282 }
3283 } else {
3284 let _ = writeln!(stream, "q");
3285 }
3286 let _ = writeln!(
3289 stream,
3290 "{:.2} 0 0 {:.2} {:.2} {:.2} cm\n/Im{} Do\nQ",
3291 dest_w, dest_h, dest_x, dest_y, img_idx,
3292 );
3293 }
3294
3295 fn register_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3297 for (page_idx, page) in pages.iter().enumerate() {
3298 let mut element_counter = 0usize;
3299 Self::collect_images_recursive(&page.elements, page_idx, &mut element_counter, builder);
3300 }
3301 }
3302
3303 fn collect_images_recursive(
3304 elements: &[LayoutElement],
3305 page_idx: usize,
3306 element_counter: &mut usize,
3307 builder: &mut PdfBuilder,
3308 ) {
3309 for element in elements {
3310 match &element.draw {
3311 DrawCommand::Image { image_data } => {
3312 let elem_idx = *element_counter;
3313 *element_counter += 1;
3314
3315 let img_idx = builder.image_objects.len();
3316 let xobj_id = Self::write_image_xobject(builder, image_data);
3317 builder.image_objects.push(xobj_id);
3318 builder
3319 .image_index_map
3320 .insert((page_idx, elem_idx), img_idx);
3321 }
3322 DrawCommand::ImagePlaceholder => {
3323 *element_counter += 1;
3324 }
3325 _ => {
3326 Self::collect_images_recursive(
3327 &element.children,
3328 page_idx,
3329 element_counter,
3330 builder,
3331 );
3332 }
3333 }
3334 }
3335 }
3336
3337 fn register_ext_gstates(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3339 let mut unique_opacities: Vec<f64> = Vec::new();
3340 for page in pages {
3341 Self::collect_opacities_recursive(&page.elements, &mut unique_opacities);
3342 if let Some(o) = page.config.background_opacity {
3344 if o < 1.0 {
3345 unique_opacities.push(o);
3346 }
3347 }
3348 }
3349 unique_opacities.sort_by(|a, b| a.partial_cmp(b).unwrap());
3350 unique_opacities.dedup();
3351
3352 for (idx, &opacity) in unique_opacities.iter().enumerate() {
3353 let obj_id = builder.objects.len();
3354 let gs_name = format!("GS{}", idx);
3355 let obj_data = format!(
3356 "<< /Type /ExtGState /ca {:.4} /CA {:.4} >>",
3357 opacity, opacity
3358 );
3359 builder.objects.push(PdfObject {
3360 id: obj_id,
3361 data: obj_data.into_bytes(),
3362 });
3363 let key = opacity.to_bits();
3364 builder.ext_gstate_map.insert(key, (obj_id, gs_name));
3365 }
3366 }
3367
3368 fn collect_opacities_recursive(elements: &[LayoutElement], opacities: &mut Vec<f64>) {
3369 for element in elements {
3370 if element.opacity < 1.0 {
3377 opacities.push(element.opacity);
3378 }
3379 if let DrawCommand::Rect {
3383 box_shadow: Some(shadow),
3384 ..
3385 } = &element.draw
3386 {
3387 if shadow.color.a < 1.0 {
3388 opacities.push(shadow.color.a);
3389 }
3390 }
3391 match &element.draw {
3392 DrawCommand::Rect { opacity, .. }
3393 | DrawCommand::Text { opacity, .. }
3394 | DrawCommand::Watermark { opacity, .. }
3395 if *opacity < 1.0 =>
3396 {
3397 opacities.push(*opacity);
3398 }
3399 DrawCommand::Chart { primitives } => {
3400 for prim in primitives {
3401 if let crate::chart::ChartPrimitive::FilledPath { opacity, .. } = prim {
3402 if *opacity < 1.0 {
3403 opacities.push(*opacity);
3404 }
3405 }
3406 }
3407 }
3408 DrawCommand::Svg { commands, .. } => {
3409 for cmd in commands {
3410 if let crate::svg::SvgCommand::SetOpacity(opacity) = cmd {
3411 if *opacity < 1.0 {
3412 opacities.push(*opacity);
3413 }
3414 }
3415 }
3416 }
3417 _ => {}
3418 }
3419 Self::collect_opacities_recursive(&element.children, opacities);
3420 }
3421 }
3422
3423 fn build_ext_gstate_resource_dict(&self, builder: &PdfBuilder) -> String {
3425 if builder.ext_gstate_map.is_empty() {
3426 return String::new();
3427 }
3428 let mut entries: Vec<(&String, usize)> = builder
3429 .ext_gstate_map
3430 .values()
3431 .map(|(obj_id, name)| (name, *obj_id))
3432 .collect();
3433 entries.sort_by_key(|(name, _)| (*name).clone());
3434 entries
3435 .iter()
3436 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3437 .collect::<Vec<_>>()
3438 .join(" ")
3439 }
3440
3441 fn write_image_xobject(
3444 builder: &mut PdfBuilder,
3445 image: &crate::image_loader::LoadedImage,
3446 ) -> usize {
3447 use crate::image_loader::{ImagePixelData, JpegColorSpace};
3448
3449 match &image.pixel_data {
3450 ImagePixelData::Jpeg { data, color_space } => {
3451 let color_space_str = match color_space {
3452 JpegColorSpace::DeviceRGB => "/DeviceRGB",
3453 JpegColorSpace::DeviceGray => "/DeviceGray",
3454 };
3455
3456 let obj_id = builder.objects.len();
3457 let mut obj_data: Vec<u8> = Vec::new();
3458 let _ = write!(
3459 obj_data,
3460 "<< /Type /XObject /Subtype /Image \
3461 /Width {} /Height {} \
3462 /ColorSpace {} \
3463 /BitsPerComponent 8 \
3464 /Filter /DCTDecode \
3465 /Length {} >>\nstream\n",
3466 image.width_px,
3467 image.height_px,
3468 color_space_str,
3469 data.len()
3470 );
3471 obj_data.extend_from_slice(data);
3472 obj_data.extend_from_slice(b"\nendstream");
3473 builder.objects.push(PdfObject {
3474 id: obj_id,
3475 data: obj_data,
3476 });
3477 obj_id
3478 }
3479
3480 ImagePixelData::Decoded { rgb, alpha } => {
3481 let smask_id = alpha.as_ref().map(|alpha_data| {
3483 let compressed_alpha = compress_to_vec_zlib(alpha_data, 6);
3484 let smask_obj_id = builder.objects.len();
3485 let mut smask_data: Vec<u8> = Vec::new();
3486 let _ = write!(
3487 smask_data,
3488 "<< /Type /XObject /Subtype /Image \
3489 /Width {} /Height {} \
3490 /ColorSpace /DeviceGray \
3491 /BitsPerComponent 8 \
3492 /Filter /FlateDecode \
3493 /Length {} >>\nstream\n",
3494 image.width_px,
3495 image.height_px,
3496 compressed_alpha.len()
3497 );
3498 smask_data.extend_from_slice(&compressed_alpha);
3499 smask_data.extend_from_slice(b"\nendstream");
3500 builder.objects.push(PdfObject {
3501 id: smask_obj_id,
3502 data: smask_data,
3503 });
3504 smask_obj_id
3505 });
3506
3507 let compressed_rgb = compress_to_vec_zlib(rgb, 6);
3509 let obj_id = builder.objects.len();
3510 let mut obj_data: Vec<u8> = Vec::new();
3511
3512 let smask_ref = smask_id
3513 .map(|id| format!(" /SMask {} 0 R", id))
3514 .unwrap_or_default();
3515
3516 let _ = write!(
3517 obj_data,
3518 "<< /Type /XObject /Subtype /Image \
3519 /Width {} /Height {} \
3520 /ColorSpace /DeviceRGB \
3521 /BitsPerComponent 8 \
3522 /Filter /FlateDecode \
3523 /Length {}{} >>\nstream\n",
3524 image.width_px,
3525 image.height_px,
3526 compressed_rgb.len(),
3527 smask_ref
3528 );
3529 obj_data.extend_from_slice(&compressed_rgb);
3530 obj_data.extend_from_slice(b"\nendstream");
3531 builder.objects.push(PdfObject {
3532 id: obj_id,
3533 data: obj_data,
3534 });
3535 obj_id
3536 }
3537 }
3538 }
3539
3540 fn build_shading_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3544 let mut entries: Vec<(String, usize)> = builder
3545 .shading_map
3546 .iter()
3547 .filter(|(&(p, _), _)| p == page_idx)
3548 .map(|(_, (obj_id, name))| (name.clone(), *obj_id))
3549 .collect();
3550 if entries.is_empty() {
3551 return String::new();
3552 }
3553 entries.sort_by(|a, b| a.0.cmp(&b.0));
3554 entries
3555 .iter()
3556 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3557 .collect::<Vec<_>>()
3558 .join(" ")
3559 }
3560
3561 fn build_xobject_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3562 let mut entries: Vec<(usize, usize)> = Vec::new();
3563 for (&(pidx, _), &img_idx) in &builder.image_index_map {
3564 if pidx == page_idx {
3565 let obj_id = builder.image_objects[img_idx];
3566 entries.push((img_idx, obj_id));
3567 }
3568 }
3569 if let Some(&(img_idx, _, _)) = builder.page_background_image_map.get(&page_idx) {
3572 let obj_id = builder.image_objects[img_idx];
3573 entries.push((img_idx, obj_id));
3574 }
3575 if entries.is_empty() {
3576 return String::new();
3577 }
3578 entries.sort_by_key(|(idx, _)| *idx);
3579 entries.dedup();
3580 entries
3581 .iter()
3582 .map(|(idx, obj_id)| format!("/Im{} {} 0 R", idx, obj_id))
3583 .collect::<Vec<_>>()
3584 .join(" ")
3585 }
3586
3587 fn write_custom_font_objects(
3594 builder: &mut PdfBuilder,
3595 key: &FontKey,
3596 ttf_data: &[u8],
3597 used_glyph_ids: HashSet<u16>,
3598 used_chars: HashSet<char>,
3599 glyph_to_char_map: HashMap<u16, char>,
3600 ) -> Result<usize, FormeError> {
3601 let face = ttf_parser::Face::parse(ttf_data, 0).map_err(|e| {
3602 FormeError::FontError(format!(
3603 "Failed to parse TTF data for font '{}': {}",
3604 key.family, e
3605 ))
3606 })?;
3607
3608 let units_per_em = face.units_per_em();
3609 let ascender = face.ascender();
3610 let descender = face.descender();
3611
3612 let mut char_to_orig_gid: HashMap<char, u16> = HashMap::new();
3614 for &ch in &used_chars {
3615 if let Some(gid) = face.glyph_index(ch) {
3616 char_to_orig_gid.insert(ch, gid.0);
3617 }
3618 }
3619
3620 let mut all_orig_gids: HashSet<u16> = used_glyph_ids.clone();
3624 for &gid in char_to_orig_gid.values() {
3625 all_orig_gids.insert(gid);
3626 }
3627
3628 let (embed_ttf, gid_remap) = match subset_ttf(ttf_data, &all_orig_gids) {
3630 Ok(subset_result) => (subset_result.ttf_data, subset_result.gid_remap),
3631 Err(_) => {
3632 let identity: HashMap<u16, u16> =
3634 all_orig_gids.iter().map(|&gid| (gid, gid)).collect();
3635 (ttf_data.to_vec(), identity)
3636 }
3637 };
3638
3639 let char_to_gid: HashMap<char, u16> = char_to_orig_gid
3641 .iter()
3642 .filter_map(|(&ch, &orig_gid)| gid_remap.get(&orig_gid).map(|&new_gid| (ch, new_gid)))
3643 .collect();
3644
3645 let gid_remap_for_embed = gid_remap.clone();
3647
3648 let mut new_gid_to_char: HashMap<u16, char> = HashMap::new();
3650 for (&orig_gid, &ch) in &glyph_to_char_map {
3652 if let Some(&new_gid) = gid_remap.get(&orig_gid) {
3653 new_gid_to_char.entry(new_gid).or_insert(ch);
3654 }
3655 }
3656 for (&ch, &new_gid) in &char_to_gid {
3658 new_gid_to_char.entry(new_gid).or_insert(ch);
3659 }
3660
3661 let pdf_font_name = Self::sanitize_font_name(&key.family, key.weight, key.italic);
3662
3663 let compressed_ttf = compress_to_vec_zlib(&embed_ttf, 6);
3665 let fontfile2_id = builder.objects.len();
3666 let mut fontfile2_data: Vec<u8> = Vec::new();
3667 let _ = write!(
3668 fontfile2_data,
3669 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
3670 compressed_ttf.len(),
3671 embed_ttf.len()
3672 );
3673 fontfile2_data.extend_from_slice(&compressed_ttf);
3674 fontfile2_data.extend_from_slice(b"\nendstream");
3675 builder.objects.push(PdfObject {
3676 id: fontfile2_id,
3677 data: fontfile2_data,
3678 });
3679
3680 let subset_face = ttf_parser::Face::parse(&embed_ttf, 0).unwrap_or_else(|_| face.clone());
3682 let subset_upem = subset_face.units_per_em();
3683
3684 let font_descriptor_id = builder.objects.len();
3686 let bbox = face.global_bounding_box();
3687 let scale = 1000.0 / units_per_em as f64;
3688 let bbox_str = format!(
3689 "[{} {} {} {}]",
3690 (bbox.x_min as f64 * scale) as i32,
3691 (bbox.y_min as f64 * scale) as i32,
3692 (bbox.x_max as f64 * scale) as i32,
3693 (bbox.y_max as f64 * scale) as i32,
3694 );
3695
3696 let flags = 4u32;
3697 let cap_height = face.capital_height().unwrap_or(ascender) as f64 * scale;
3698 let stem_v = if key.weight >= 700 { 120 } else { 80 };
3699
3700 let font_descriptor_dict = format!(
3701 "<< /Type /FontDescriptor /FontName /{} /Flags {} \
3702 /FontBBox {} /ItalicAngle {} \
3703 /Ascent {} /Descent {} /CapHeight {} /StemV {} \
3704 /FontFile2 {} 0 R >>",
3705 pdf_font_name,
3706 flags,
3707 bbox_str,
3708 if key.italic { -12 } else { 0 },
3709 (ascender as f64 * scale) as i32,
3710 (descender as f64 * scale) as i32,
3711 cap_height as i32,
3712 stem_v,
3713 fontfile2_id,
3714 );
3715 builder.objects.push(PdfObject {
3716 id: font_descriptor_id,
3717 data: font_descriptor_dict.into_bytes(),
3718 });
3719
3720 let cidfont_id = builder.objects.len();
3722 let w_array = Self::build_w_array_from_gids(&gid_remap, &subset_face, subset_upem);
3724 let default_width = subset_face
3725 .glyph_hor_advance(ttf_parser::GlyphId(0))
3726 .map(|adv| (adv as f64 * 1000.0 / subset_upem as f64) as u32)
3727 .unwrap_or(1000);
3728 let cidfont_dict = format!(
3729 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{} \
3730 /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> \
3731 /FontDescriptor {} 0 R /DW {} /W {} \
3732 /CIDToGIDMap /Identity >>",
3733 pdf_font_name, font_descriptor_id, default_width, w_array,
3734 );
3735 builder.objects.push(PdfObject {
3736 id: cidfont_id,
3737 data: cidfont_dict.into_bytes(),
3738 });
3739
3740 let tounicode_id = builder.objects.len();
3742 let cmap_content = Self::build_tounicode_cmap_from_gids(&new_gid_to_char, &pdf_font_name);
3743 let compressed_cmap = compress_to_vec_zlib(cmap_content.as_bytes(), 6);
3744 let mut tounicode_data: Vec<u8> = Vec::new();
3745 let _ = write!(
3746 tounicode_data,
3747 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
3748 compressed_cmap.len()
3749 );
3750 tounicode_data.extend_from_slice(&compressed_cmap);
3751 tounicode_data.extend_from_slice(b"\nendstream");
3752 builder.objects.push(PdfObject {
3753 id: tounicode_id,
3754 data: tounicode_data,
3755 });
3756
3757 let type0_id = builder.objects.len();
3759 let type0_dict = format!(
3760 "<< /Type /Font /Subtype /Type0 /BaseFont /{} \
3761 /Encoding /Identity-H \
3762 /DescendantFonts [{} 0 R] \
3763 /ToUnicode {} 0 R >>",
3764 pdf_font_name, cidfont_id, tounicode_id,
3765 );
3766 builder.objects.push(PdfObject {
3767 id: type0_id,
3768 data: type0_dict.into_bytes(),
3769 });
3770
3771 builder.custom_font_data.insert(
3773 key.clone(),
3774 CustomFontEmbedData {
3775 ttf_data: embed_ttf,
3776 gid_remap: gid_remap_for_embed,
3777 glyph_to_char: glyph_to_char_map,
3778 char_to_gid,
3779 units_per_em,
3780 ascender,
3781 descender,
3782 },
3783 );
3784
3785 Ok(type0_id)
3786 }
3787
3788 fn build_w_array_from_gids(
3790 gid_remap: &HashMap<u16, u16>,
3791 face: &ttf_parser::Face,
3792 units_per_em: u16,
3793 ) -> String {
3794 let scale = 1000.0 / units_per_em as f64;
3795
3796 let mut entries: Vec<(u16, u32)> = Vec::new();
3797 let mut seen_gids: HashSet<u16> = HashSet::new();
3798
3799 for &new_gid in gid_remap.values() {
3800 if seen_gids.contains(&new_gid) {
3801 continue;
3802 }
3803 seen_gids.insert(new_gid);
3804 let advance = face
3805 .glyph_hor_advance(ttf_parser::GlyphId(new_gid))
3806 .unwrap_or(0);
3807 let width = (advance as f64 * scale) as u32;
3808 entries.push((new_gid, width));
3809 }
3810
3811 entries.sort_by_key(|(gid, _)| *gid);
3812
3813 let mut result = String::from("[");
3815 for (gid, width) in &entries {
3816 let _ = write!(result, " {} [{}]", gid, width);
3817 }
3818 result.push_str(" ]");
3819 result
3820 }
3821
3822 fn build_tounicode_cmap_from_gids(gid_to_char: &HashMap<u16, char>, font_name: &str) -> String {
3824 let mut gid_to_unicode: Vec<(u16, u32)> = gid_to_char
3825 .iter()
3826 .map(|(&gid, &ch)| (gid, ch as u32))
3827 .collect();
3828 gid_to_unicode.sort_by_key(|(gid, _)| *gid);
3829
3830 let mut cmap = String::new();
3831 let _ = writeln!(cmap, "/CIDInit /ProcSet findresource begin");
3832 let _ = writeln!(cmap, "12 dict begin");
3833 let _ = writeln!(cmap, "begincmap");
3834 let _ = writeln!(cmap, "/CIDSystemInfo");
3835 let _ = writeln!(
3836 cmap,
3837 "<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def"
3838 );
3839 let _ = writeln!(cmap, "/CMapName /{}-UTF16 def", font_name);
3840 let _ = writeln!(cmap, "/CMapType 2 def");
3841 let _ = writeln!(cmap, "1 begincodespacerange");
3842 let _ = writeln!(cmap, "<0000> <FFFF>");
3843 let _ = writeln!(cmap, "endcodespacerange");
3844
3845 for chunk in gid_to_unicode.chunks(100) {
3847 let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
3848 for &(gid, unicode) in chunk {
3849 let _ = writeln!(cmap, "<{:04X}> <{:04X}>", gid, unicode);
3850 }
3851 let _ = writeln!(cmap, "endbfchar");
3852 }
3853
3854 let _ = writeln!(cmap, "endcmap");
3855 let _ = writeln!(cmap, "CMapName currentdict /CMap defineresource pop");
3856 let _ = writeln!(cmap, "end");
3857 let _ = writeln!(cmap, "end");
3858
3859 cmap
3860 }
3861
3862 fn sanitize_font_name(family: &str, weight: u32, italic: bool) -> String {
3865 let mut name: String = family
3866 .chars()
3867 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
3868 .collect();
3869
3870 if weight >= 700 {
3871 name.push_str("-Bold");
3872 }
3873 if italic {
3874 name.push_str("-Italic");
3875 }
3876
3877 if name.is_empty() {
3879 name = "CustomFont".to_string();
3880 }
3881
3882 name
3883 }
3884
3885 fn build_font_resource_dict(&self, font_objects: &[(FontKey, usize)]) -> String {
3886 font_objects
3887 .iter()
3888 .enumerate()
3889 .map(|(i, (_, obj_id))| format!("/F{} {} 0 R", i, obj_id))
3890 .collect::<Vec<_>>()
3891 .join(" ")
3892 }
3893
3894 fn font_index(
3896 &self,
3897 family: &str,
3898 weight: u32,
3899 font_style: FontStyle,
3900 font_objects: &[(FontKey, usize)],
3901 ) -> usize {
3902 let italic = matches!(font_style, FontStyle::Italic | FontStyle::Oblique);
3903
3904 for (i, (key, _)) in font_objects.iter().enumerate() {
3906 if key.family == family && key.weight == weight && key.italic == italic {
3907 return i;
3908 }
3909 }
3910
3911 let snapped = if weight >= 600 { 700 } else { 400 };
3913 for (i, (key, _)) in font_objects.iter().enumerate() {
3914 if key.family == family && key.weight == snapped && key.italic == italic {
3915 return i;
3916 }
3917 }
3918
3919 for (i, (key, _)) in font_objects.iter().enumerate() {
3921 if key.family == "Helvetica" && key.weight == snapped && key.italic == italic {
3922 return i;
3923 }
3924 }
3925
3926 0
3928 }
3929
3930 fn group_glyphs_by_style(glyphs: &[PositionedGlyph]) -> Vec<Vec<&PositionedGlyph>> {
3933 if glyphs.is_empty() {
3934 return vec![];
3935 }
3936
3937 let mut groups: Vec<Vec<&PositionedGlyph>> = Vec::new();
3938 let mut current_group: Vec<&PositionedGlyph> = vec![&glyphs[0]];
3939
3940 for glyph in &glyphs[1..] {
3941 let prev = current_group.last().unwrap();
3942 let same_style = glyph.font_family == prev.font_family
3943 && glyph.font_weight == prev.font_weight
3944 && std::mem::discriminant(&glyph.font_style)
3945 == std::mem::discriminant(&prev.font_style)
3946 && (glyph.font_size - prev.font_size).abs() < 0.01
3947 && Self::colors_equal(&glyph.color, &prev.color)
3948 && std::mem::discriminant(&glyph.text_decoration)
3949 == std::mem::discriminant(&prev.text_decoration);
3950
3951 if same_style {
3952 current_group.push(glyph);
3953 } else {
3954 groups.push(current_group);
3955 current_group = vec![glyph];
3956 }
3957 }
3958 groups.push(current_group);
3959 groups
3960 }
3961
3962 fn colors_equal(a: &Option<Color>, b: &Option<Color>) -> bool {
3963 match (a, b) {
3964 (None, None) => true,
3965 (Some(ca), Some(cb)) => {
3966 (ca.r - cb.r).abs() < 0.001
3967 && (ca.g - cb.g).abs() < 0.001
3968 && (ca.b - cb.b).abs() < 0.001
3969 && (ca.a - cb.a).abs() < 0.001
3970 }
3971 _ => false,
3972 }
3973 }
3974
3975 fn collect_link_annotations(
3979 elements: &[LayoutElement],
3980 page_height: f64,
3981 annotations: &mut Vec<LinkAnnotation>,
3982 ) {
3983 for element in elements {
3984 if let Some(ref href) = element.href {
3985 if !href.is_empty() {
3986 let pdf_y = page_height - element.y - element.height;
3987 annotations.push(LinkAnnotation {
3988 x: element.x,
3989 y: pdf_y,
3990 width: element.width,
3991 height: element.height,
3992 href: href.clone(),
3993 });
3994 continue;
3996 }
3997 }
3998 Self::collect_link_annotations(&element.children, page_height, annotations);
3999 }
4000 }
4001
4002 fn collect_form_fields(
4004 elements: &[LayoutElement],
4005 page_height: f64,
4006 page_idx: usize,
4007 fields: &mut Vec<FormFieldData>,
4008 ) {
4009 for element in elements {
4010 if let DrawCommand::FormField {
4011 ref field_type,
4012 ref name,
4013 } = element.draw
4014 {
4015 let pdf_y = page_height - element.y - element.height;
4016 fields.push(FormFieldData {
4017 field_type: field_type.clone(),
4018 name: name.clone(),
4019 x: element.x,
4020 y: pdf_y,
4021 width: element.width,
4022 height: element.height,
4023 page_idx,
4024 });
4025 }
4026 Self::collect_form_fields(&element.children, page_height, page_idx, fields);
4027 }
4028 }
4029
4030 fn collect_bookmarks(
4032 elements: &[LayoutElement],
4033 page_height: f64,
4034 page_obj_id: usize,
4035 bookmarks: &mut Vec<PdfBookmark>,
4036 ) {
4037 for element in elements {
4038 if let Some(ref title) = element.bookmark {
4039 let y_pdf = page_height - element.y;
4040 bookmarks.push(PdfBookmark {
4041 title: title.clone(),
4042 page_obj_id,
4043 y_pdf,
4044 });
4045 }
4046 Self::collect_bookmarks(&element.children, page_height, page_obj_id, bookmarks);
4047 }
4048 }
4049
4050 fn write_outline_tree(&self, builder: &mut PdfBuilder, bookmarks: &[PdfBookmark]) -> usize {
4053 let outlines_id = builder.objects.len();
4055 builder.objects.push(PdfObject {
4056 id: outlines_id,
4057 data: vec![],
4058 });
4059
4060 let mut item_ids: Vec<usize> = Vec::new();
4062 for _bm in bookmarks {
4063 let item_id = builder.objects.len();
4064 builder.objects.push(PdfObject {
4065 id: item_id,
4066 data: vec![],
4067 });
4068 item_ids.push(item_id);
4069 }
4070
4071 for (i, (bm, &item_id)) in bookmarks.iter().zip(item_ids.iter()).enumerate() {
4073 let mut dict = format!(
4074 "<< /Title ({}) /Parent {} 0 R /Dest [{} 0 R /XYZ 0 {:.2} null]",
4075 Self::escape_pdf_string(&bm.title),
4076 outlines_id,
4077 bm.page_obj_id,
4078 bm.y_pdf,
4079 );
4080 if i > 0 {
4081 let _ = write!(dict, " /Prev {} 0 R", item_ids[i - 1]);
4082 }
4083 if i + 1 < item_ids.len() {
4084 let _ = write!(dict, " /Next {} 0 R", item_ids[i + 1]);
4085 }
4086 dict.push_str(" >>");
4087 builder.objects[item_id].data = dict.into_bytes();
4088 }
4089
4090 let first_id = item_ids.first().copied().unwrap_or(0);
4092 let last_id = item_ids.last().copied().unwrap_or(0);
4093 let outlines_dict = format!(
4094 "<< /Type /Outlines /First {} 0 R /Last {} 0 R /Count {} >>",
4095 first_id,
4096 last_id,
4097 bookmarks.len()
4098 );
4099 builder.objects[outlines_id].data = outlines_dict.into_bytes();
4100
4101 outlines_id
4102 }
4103
4104 fn write_svg_commands(
4106 stream: &mut String,
4107 commands: &[SvgCommand],
4108 ext_gstate_map: &HashMap<u64, (usize, String)>,
4109 ) {
4110 for cmd in commands {
4111 match cmd {
4112 SvgCommand::MoveTo(x, y) => {
4113 let _ = writeln!(stream, "{:.2} {:.2} m", x, y);
4114 }
4115 SvgCommand::LineTo(x, y) => {
4116 let _ = writeln!(stream, "{:.2} {:.2} l", x, y);
4117 }
4118 SvgCommand::CurveTo(x1, y1, x2, y2, x3, y3) => {
4119 let _ = writeln!(
4120 stream,
4121 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4122 x1, y1, x2, y2, x3, y3
4123 );
4124 }
4125 SvgCommand::ClosePath => {
4126 let _ = writeln!(stream, "h");
4127 }
4128 SvgCommand::SetFill(r, g, b) => {
4129 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", r, g, b);
4130 }
4131 SvgCommand::SetFillNone => {
4132 }
4134 SvgCommand::SetStroke(r, g, b) => {
4135 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", r, g, b);
4136 }
4137 SvgCommand::SetStrokeNone => {
4138 }
4140 SvgCommand::SetStrokeWidth(w) => {
4141 let _ = writeln!(stream, "{:.2} w", w);
4142 }
4143 SvgCommand::Fill => {
4144 let _ = writeln!(stream, "f");
4145 }
4146 SvgCommand::Stroke => {
4147 let _ = writeln!(stream, "S");
4148 }
4149 SvgCommand::FillAndStroke => {
4150 let _ = writeln!(stream, "B");
4151 }
4152 SvgCommand::SetLineCap(cap) => {
4153 let _ = writeln!(stream, "{} J", cap);
4154 }
4155 SvgCommand::SetLineJoin(join) => {
4156 let _ = writeln!(stream, "{} j", join);
4157 }
4158 SvgCommand::SaveState => {
4159 let _ = writeln!(stream, "q");
4160 }
4161 SvgCommand::RestoreState => {
4162 let _ = writeln!(stream, "Q");
4163 }
4164 SvgCommand::SetOpacity(opacity) => {
4165 if let Some((_, gs_name)) = ext_gstate_map.get(&opacity.to_bits()) {
4166 let _ = writeln!(stream, "/{} gs", gs_name);
4167 }
4168 }
4169 }
4170 }
4171 }
4172
4173 pub(crate) fn escape_pdf_string(s: &str) -> String {
4175 s.replace('\\', "\\\\")
4176 .replace('(', "\\(")
4177 .replace(')', "\\)")
4178 }
4179
4180 fn decode_attachment_src(src: &str) -> Result<Vec<u8>, FormeError> {
4183 use base64::Engine as _;
4184 let b64 = src.rsplit_once(";base64,").map(|(_, d)| d).unwrap_or(src);
4185 base64::engine::general_purpose::STANDARD
4186 .decode(b64.trim())
4187 .map_err(|e| {
4188 FormeError::RenderError(format!(
4189 "attachment src is not valid base64 (expected base64 bytes or a data: URI): {e}"
4190 ))
4191 })
4192 }
4193
4194 fn mime_to_pdf_name(mime: &str) -> String {
4197 let mut out = String::with_capacity(mime.len() + 2);
4198 for b in mime.bytes() {
4199 let regular =
4200 b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'+' | b'\'' | b'"');
4201 if regular {
4202 out.push(b as char);
4203 } else {
4204 let _ = write!(out, "#{:02X}", b);
4205 }
4206 }
4207 out
4208 }
4209
4210 fn encode_winansi_text(builder: &PdfBuilder, s: &str) -> String {
4214 let mut result = String::with_capacity(s.len());
4215 for ch in s.chars() {
4216 let b = Self::unicode_to_winansi(ch).unwrap_or_else(|| {
4217 builder.missing_glyphs.borrow_mut().insert(ch);
4218 b'?'
4219 });
4220 match b {
4221 b'\\' => result.push_str("\\\\"),
4222 b'(' => result.push_str("\\("),
4223 b')' => result.push_str("\\)"),
4224 0x20..=0x7E => result.push(b as char),
4225 _ => {
4226 let _ = write!(result, "\\{:03o}", b);
4227 }
4228 }
4229 }
4230 result
4231 }
4232
4233 fn unicode_to_winansi(ch: char) -> Option<u8> {
4235 crate::font::unicode_to_winansi(ch)
4236 }
4237
4238 fn serialize(&self, builder: &PdfBuilder, info_obj_id: Option<usize>) -> Vec<u8> {
4240 let mut output: Vec<u8> = Vec::new();
4241 let mut offsets: Vec<usize> = vec![0; builder.objects.len()];
4242
4243 output.extend_from_slice(b"%PDF-1.7\n");
4245 output.extend_from_slice(b"%\xe2\xe3\xcf\xd3\n");
4246
4247 for (i, obj) in builder.objects.iter().enumerate().skip(1) {
4248 offsets[i] = output.len();
4249 let header = format!("{} 0 obj\n", i);
4250 output.extend_from_slice(header.as_bytes());
4251 output.extend_from_slice(&obj.data);
4252 output.extend_from_slice(b"\nendobj\n\n");
4253 }
4254
4255 let xref_offset = output.len();
4256 let _ = writeln!(output, "xref\n0 {}", builder.objects.len());
4257 let _ = writeln!(output, "0000000000 65535 f ");
4258 for offset in offsets.iter().skip(1) {
4259 let _ = writeln!(output, "{:010} 00000 n ", offset);
4260 }
4261
4262 let _ = write!(
4263 output,
4264 "trailer\n<< /Size {} /Root 1 0 R",
4265 builder.objects.len()
4266 );
4267 if let Some(info_id) = info_obj_id {
4268 let _ = write!(output, " /Info {} 0 R", info_id);
4269 }
4270 {
4276 use sha2::Digest as _;
4277 let digest = sha2::Sha256::digest(&output);
4278 let mut id_hex = String::with_capacity(32);
4279 for b in &digest[..16] {
4280 let _ = write!(id_hex, "{:02X}", b);
4281 }
4282 let _ = write!(output, " /ID [<{id_hex}> <{id_hex}>]");
4283 }
4284 let _ = writeln!(output, " >>\nstartxref\n{}\n%%EOF", xref_offset);
4285
4286 output
4287 }
4288}
4289
4290fn write_chart_primitive(
4295 stream: &mut String,
4296 prim: &crate::chart::ChartPrimitive,
4297 _chart_height: f64,
4298 builder: &PdfBuilder,
4299) {
4300 use crate::chart::{ChartPrimitive, TextAnchor};
4301 use crate::font::metrics::unicode_to_winansi;
4302
4303 match prim {
4304 ChartPrimitive::Rect { x, y, w, h, fill } => {
4305 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
4306 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re f", x, y, w, h);
4307 }
4308
4309 ChartPrimitive::Line {
4310 x1,
4311 y1,
4312 x2,
4313 y2,
4314 stroke,
4315 width,
4316 } => {
4317 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
4318 let _ = writeln!(stream, "{:.2} w", width);
4319 let _ = writeln!(stream, "{:.2} {:.2} m {:.2} {:.2} l S", x1, y1, x2, y2);
4320 }
4321
4322 ChartPrimitive::Polyline {
4323 points,
4324 stroke,
4325 width,
4326 } => {
4327 if points.len() < 2 {
4328 return;
4329 }
4330 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
4331 let _ = writeln!(stream, "{:.2} w", width);
4332 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
4333 for &(px, py) in &points[1..] {
4334 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
4335 }
4336 let _ = writeln!(stream, "S");
4337 }
4338
4339 ChartPrimitive::FilledPath {
4340 points,
4341 fill,
4342 opacity,
4343 } => {
4344 if points.len() < 3 {
4345 return;
4346 }
4347 let _ = writeln!(stream, "q");
4348 if *opacity < 1.0 {
4350 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
4351 let _ = writeln!(stream, "/{} gs", gs_name);
4352 }
4353 }
4354 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
4355 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
4356 for &(px, py) in &points[1..] {
4357 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
4358 }
4359 let _ = writeln!(stream, "h f");
4360 let _ = writeln!(stream, "Q");
4361 }
4362
4363 ChartPrimitive::Circle { cx, cy, r, fill } => {
4364 let kappa: f64 = 0.5523;
4366 let kr = kappa * r;
4367 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
4368 let _ = writeln!(stream, "{:.2} {:.2} m", cx + r, cy);
4369 let _ = writeln!(
4370 stream,
4371 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4372 cx + r,
4373 cy + kr,
4374 cx + kr,
4375 cy + r,
4376 cx,
4377 cy + r
4378 );
4379 let _ = writeln!(
4380 stream,
4381 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4382 cx - kr,
4383 cy + r,
4384 cx - r,
4385 cy + kr,
4386 cx - r,
4387 cy
4388 );
4389 let _ = writeln!(
4390 stream,
4391 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4392 cx - r,
4393 cy - kr,
4394 cx - kr,
4395 cy - r,
4396 cx,
4397 cy - r
4398 );
4399 let _ = writeln!(
4400 stream,
4401 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4402 cx + kr,
4403 cy - r,
4404 cx + r,
4405 cy - kr,
4406 cx + r,
4407 cy
4408 );
4409 let _ = writeln!(stream, "f");
4410 }
4411
4412 ChartPrimitive::ArcSector {
4413 cx,
4414 cy,
4415 r,
4416 start_angle,
4417 end_angle,
4418 fill,
4419 } => {
4420 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
4421 let _ = writeln!(stream, "{:.2} {:.2} m", cx, cy);
4423 let sx = cx + r * start_angle.cos();
4425 let sy = cy + r * start_angle.sin();
4426 let _ = writeln!(stream, "{:.2} {:.2} l", sx, sy);
4427
4428 let mut angle = *start_angle;
4430 let total = end_angle - start_angle;
4431 let segments = ((total.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
4432 let step = total / segments as f64;
4433
4434 for _ in 0..segments {
4435 let a1 = angle;
4436 let a2 = angle + step;
4437 let alpha = 4.0 / 3.0 * ((a2 - a1) / 4.0).tan();
4438
4439 let p1x = cx + r * a1.cos();
4440 let p1y = cy + r * a1.sin();
4441 let p2x = cx + r * a2.cos();
4442 let p2y = cy + r * a2.sin();
4443
4444 let cp1x = p1x - alpha * r * a1.sin();
4445 let cp1y = p1y + alpha * r * a1.cos();
4446 let cp2x = p2x + alpha * r * a2.sin();
4447 let cp2y = p2y - alpha * r * a2.cos();
4448
4449 let _ = writeln!(
4450 stream,
4451 "{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c",
4452 cp1x, cp1y, cp2x, cp2y, p2x, p2y
4453 );
4454 angle = a2;
4455 }
4456
4457 let _ = writeln!(stream, "h f");
4459 }
4460
4461 ChartPrimitive::Label {
4462 text,
4463 x,
4464 y,
4465 font_size,
4466 color,
4467 anchor,
4468 } => {
4469 let metrics = crate::font::StandardFont::Helvetica.metrics();
4471 let text_width = metrics.measure_string(text, *font_size, 0.0);
4472 let x_offset = match anchor {
4473 TextAnchor::Left => 0.0,
4474 TextAnchor::Center => -text_width / 2.0,
4475 TextAnchor::Right => -text_width,
4476 };
4477
4478 let font_idx = builder
4480 .font_objects
4481 .iter()
4482 .enumerate()
4483 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
4484 .map(|(i, _)| i)
4485 .unwrap_or(0);
4486
4487 let encoded: String = text
4489 .chars()
4490 .map(|ch| {
4491 if let Some(code) = unicode_to_winansi(ch) {
4492 code as char
4493 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
4494 ch
4495 } else {
4496 builder.missing_glyphs.borrow_mut().insert(ch);
4497 '?'
4498 }
4499 })
4500 .collect();
4501 let escaped = pdf_escape_string(&encoded);
4502
4503 let _ = writeln!(stream, "q");
4505 let _ = writeln!(stream, "1 0 0 -1 {:.4} {:.4} cm", x + x_offset, *y);
4506 let _ = writeln!(
4507 stream,
4508 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
4509 font_idx, font_size, color.r, color.g, color.b, escaped
4510 );
4511 let _ = writeln!(stream, "Q");
4512 }
4513 }
4514}
4515
4516fn normalize_gradient_stops(
4523 stops: &[crate::style::GradientStop],
4524 fallback: Color,
4525) -> Vec<crate::style::GradientStop> {
4526 use crate::style::GradientStop;
4527 if stops.is_empty() {
4528 return vec![
4529 GradientStop {
4530 position: 0.0,
4531 color: fallback,
4532 },
4533 GradientStop {
4534 position: 1.0,
4535 color: fallback,
4536 },
4537 ];
4538 }
4539 let mut sorted: Vec<GradientStop> = stops
4540 .iter()
4541 .map(|s| GradientStop {
4542 position: s.position.clamp(0.0, 1.0),
4543 color: s.color,
4544 })
4545 .collect();
4546 sorted.sort_by(|a, b| {
4547 a.position
4548 .partial_cmp(&b.position)
4549 .unwrap_or(std::cmp::Ordering::Equal)
4550 });
4551 if sorted[0].position > 0.0 {
4552 sorted.insert(
4553 0,
4554 GradientStop {
4555 position: 0.0,
4556 color: sorted[0].color,
4557 },
4558 );
4559 }
4560 if sorted[sorted.len() - 1].position < 1.0 {
4561 let last = sorted[sorted.len() - 1].color;
4562 sorted.push(GradientStop {
4563 position: 1.0,
4564 color: last,
4565 });
4566 }
4567 sorted
4568}
4569
4570fn pdf_escape_string(s: &str) -> String {
4571 let mut out = String::with_capacity(s.len());
4572 for ch in s.chars() {
4573 match ch {
4574 '(' => out.push_str("\\("),
4575 ')' => out.push_str("\\)"),
4576 '\\' => out.push_str("\\\\"),
4577 _ => out.push(ch),
4578 }
4579 }
4580 out
4581}
4582
4583#[cfg(test)]
4584mod tests {
4585 use super::*;
4586 use crate::font::FontContext;
4587
4588 #[test]
4595 fn test_embedded_srgb_is_a_valid_icc_profile() {
4596 let icc: &[u8] = include_bytes!("sRGB.icc");
4597 assert!(
4598 icc.len() >= 128,
4599 "ICC shorter than its 128-byte header: {}",
4600 icc.len()
4601 );
4602 assert_ne!(
4604 icc[0], b'<',
4605 "embedded ICC starts with '<' — looks like HTML, not a profile"
4606 );
4607 assert_eq!(&icc[36..40], b"acsp", "missing ICC 'acsp' signature");
4609 let device_class = &icc[12..16];
4611 assert!(
4612 device_class == b"mntr" || device_class == b"prtr",
4613 "ICC device class {:?} is not mntr/prtr (PDF/A 6.2.3)",
4614 String::from_utf8_lossy(device_class),
4615 );
4616 assert_eq!(&icc[16..20], b"RGB ", "ICC data colour space is not RGB");
4618 }
4619
4620 #[test]
4621 fn test_escape_pdf_string() {
4622 assert_eq!(
4623 PdfWriter::escape_pdf_string("Hello (World)"),
4624 "Hello \\(World\\)"
4625 );
4626 assert_eq!(PdfWriter::escape_pdf_string("back\\slash"), "back\\\\slash");
4627 }
4628
4629 #[test]
4630 fn test_empty_document_produces_valid_pdf() {
4631 let writer = PdfWriter::new();
4632 let font_context = FontContext::new();
4633 let pages = vec![LayoutPage {
4634 width: 595.28,
4635 height: 841.89,
4636 elements: vec![],
4637 fixed_header: vec![],
4638 fixed_footer: vec![],
4639 watermarks: vec![],
4640 config: PageConfig::default(),
4641 page_name: None,
4642 }];
4643 let metadata = Metadata::default();
4644 let (bytes, _warnings) = writer
4645 .write(
4646 &pages,
4647 &metadata,
4648 &font_context,
4649 false,
4650 None,
4651 false,
4652 None,
4653 &[],
4654 None,
4655 false,
4656 )
4657 .unwrap();
4658
4659 assert!(bytes.starts_with(b"%PDF-1.7"));
4660 assert!(bytes.windows(5).any(|w| w == b"%%EOF"));
4661 assert!(bytes.windows(4).any(|w| w == b"xref"));
4662 assert!(bytes.windows(7).any(|w| w == b"trailer"));
4663 }
4664
4665 #[test]
4666 fn test_metadata_in_pdf() {
4667 let writer = PdfWriter::new();
4668 let font_context = FontContext::new();
4669 let pages = vec![LayoutPage {
4670 width: 595.28,
4671 height: 841.89,
4672 elements: vec![],
4673 fixed_header: vec![],
4674 fixed_footer: vec![],
4675 watermarks: vec![],
4676 config: PageConfig::default(),
4677 page_name: None,
4678 }];
4679 let metadata = Metadata {
4680 title: Some("Test Document".to_string()),
4681 author: Some("Forme".to_string()),
4682 subject: None,
4683 creator: None,
4684 lang: None,
4685 };
4686 let (bytes, _warnings) = writer
4687 .write(
4688 &pages,
4689 &metadata,
4690 &font_context,
4691 false,
4692 None,
4693 false,
4694 None,
4695 &[],
4696 None,
4697 false,
4698 )
4699 .unwrap();
4700 let text = String::from_utf8_lossy(&bytes);
4701
4702 assert!(text.contains("/Title (Test Document)"));
4703 assert!(text.contains("/Author (Forme)"));
4704 }
4705
4706 #[test]
4707 fn test_bold_font_registered_separately() {
4708 let writer = PdfWriter::new();
4709 let font_context = FontContext::new();
4710
4711 let pages = vec![LayoutPage {
4713 width: 595.28,
4714 height: 841.89,
4715 elements: vec![
4716 LayoutElement {
4717 x: 54.0,
4718 y: 54.0,
4719 width: 100.0,
4720 height: 16.8,
4721 draw: DrawCommand::Text {
4722 lines: vec![TextLine {
4723 x: 54.0,
4724 y: 66.0,
4725 width: 50.0,
4726 height: 16.8,
4727 glyphs: vec![PositionedGlyph {
4728 glyph_id: 65,
4729 x_offset: 0.0,
4730 y_offset: 0.0,
4731 x_advance: 8.0,
4732 font_size: 12.0,
4733 font_family: "Helvetica".into(),
4734 font_weight: 400,
4735 font_style: FontStyle::Normal,
4736 char_value: 'A',
4737 color: None,
4738 href: None,
4739 text_decoration: TextDecoration::None,
4740 letter_spacing: 0.0,
4741 cluster_text: None,
4742 }],
4743 word_spacing: 0.0,
4744 }],
4745 color: Color::BLACK,
4746 text_decoration: TextDecoration::None,
4747 opacity: 1.0,
4748 },
4749 children: vec![],
4750 node_type: None,
4751 resolved_style: None,
4752 source_location: None,
4753 href: None,
4754 bookmark: None,
4755 alt: None,
4756 is_header_row: false,
4757 col_span: 1,
4758 overflow: Overflow::default(),
4759 opacity: 1.0,
4760 },
4761 LayoutElement {
4762 x: 54.0,
4763 y: 74.0,
4764 width: 100.0,
4765 height: 16.8,
4766 draw: DrawCommand::Text {
4767 lines: vec![TextLine {
4768 x: 54.0,
4769 y: 86.0,
4770 width: 50.0,
4771 height: 16.8,
4772 glyphs: vec![PositionedGlyph {
4773 glyph_id: 65,
4774 x_offset: 0.0,
4775 y_offset: 0.0,
4776 x_advance: 8.0,
4777 font_size: 12.0,
4778 font_family: "Helvetica".into(),
4779 font_weight: 700,
4780 font_style: FontStyle::Normal,
4781 char_value: 'A',
4782 color: None,
4783 href: None,
4784 text_decoration: TextDecoration::None,
4785 letter_spacing: 0.0,
4786 cluster_text: None,
4787 }],
4788 word_spacing: 0.0,
4789 }],
4790 color: Color::BLACK,
4791 text_decoration: TextDecoration::None,
4792 opacity: 1.0,
4793 },
4794 children: vec![],
4795 node_type: None,
4796 resolved_style: None,
4797 source_location: None,
4798 href: None,
4799 bookmark: None,
4800 alt: None,
4801 is_header_row: false,
4802 col_span: 1,
4803 overflow: Overflow::default(),
4804 opacity: 1.0,
4805 },
4806 ],
4807 fixed_header: vec![],
4808 fixed_footer: vec![],
4809 watermarks: vec![],
4810 config: PageConfig::default(),
4811 page_name: None,
4812 }];
4813
4814 let metadata = Metadata::default();
4815 let (bytes, _warnings) = writer
4816 .write(
4817 &pages,
4818 &metadata,
4819 &font_context,
4820 false,
4821 None,
4822 false,
4823 None,
4824 &[],
4825 None,
4826 false,
4827 )
4828 .unwrap();
4829 let text = String::from_utf8_lossy(&bytes);
4830
4831 assert!(
4833 text.contains("Helvetica"),
4834 "Should contain regular Helvetica"
4835 );
4836 assert!(
4837 text.contains("Helvetica-Bold"),
4838 "Should contain Helvetica-Bold"
4839 );
4840 }
4841
4842 #[test]
4843 fn test_sanitize_font_name() {
4844 assert_eq!(PdfWriter::sanitize_font_name("Inter", 400, false), "Inter");
4845 assert_eq!(
4846 PdfWriter::sanitize_font_name("Inter", 700, false),
4847 "Inter-Bold"
4848 );
4849 assert_eq!(
4850 PdfWriter::sanitize_font_name("Inter", 400, true),
4851 "Inter-Italic"
4852 );
4853 assert_eq!(
4854 PdfWriter::sanitize_font_name("Inter", 700, true),
4855 "Inter-Bold-Italic"
4856 );
4857 assert_eq!(
4858 PdfWriter::sanitize_font_name("Noto Sans", 400, false),
4859 "NotoSans"
4860 );
4861 assert_eq!(
4862 PdfWriter::sanitize_font_name("Font (Display)", 400, false),
4863 "FontDisplay"
4864 );
4865 }
4866
4867 #[test]
4868 fn test_tounicode_cmap_format() {
4869 let mut glyph_to_char = HashMap::new();
4871 glyph_to_char.insert(36u16, 'A');
4872 glyph_to_char.insert(37u16, 'B');
4873
4874 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&glyph_to_char, "TestFont");
4875
4876 assert!(cmap.contains("begincmap"), "CMap should contain begincmap");
4877 assert!(cmap.contains("endcmap"), "CMap should contain endcmap");
4878 assert!(
4879 cmap.contains("beginbfchar"),
4880 "CMap should contain beginbfchar"
4881 );
4882 assert!(cmap.contains("endbfchar"), "CMap should contain endbfchar");
4883 assert!(
4884 cmap.contains("<0024> <0041>"),
4885 "Should map gid 0x0024 to Unicode 'A' 0x0041"
4886 );
4887 assert!(
4888 cmap.contains("<0025> <0042>"),
4889 "Should map gid 0x0025 to Unicode 'B' 0x0042"
4890 );
4891 assert!(
4892 cmap.contains("begincodespacerange"),
4893 "Should define codespace range"
4894 );
4895 assert!(
4896 cmap.contains("<0000> <FFFF>"),
4897 "Codespace should be 0000-FFFF"
4898 );
4899 }
4900
4901 #[test]
4902 fn test_w_array_format() {
4903 let mut char_to_gid = HashMap::new();
4904 char_to_gid.insert('A', 36u16);
4905
4906 let w_array_str = "[ 36 [600] ]";
4909 assert!(w_array_str.starts_with('['));
4910 assert!(w_array_str.ends_with(']'));
4911 }
4912
4913 #[test]
4914 fn test_hex_glyph_encoding() {
4915 let gid: u16 = 0x0041;
4917 let hex = format!("{:04X}", gid);
4918 assert_eq!(hex, "0041");
4919
4920 let gids = [0x0041u16, 0x0042, 0x0043];
4921 let hex_str: String = gids.iter().map(|g| format!("{:04X}", g)).collect();
4922 assert_eq!(hex_str, "004100420043");
4923 }
4924
4925 #[test]
4926 fn test_standard_font_still_uses_text_string() {
4927 let writer = PdfWriter::new();
4928 let font_context = FontContext::new();
4929
4930 let pages = vec![LayoutPage {
4931 width: 595.28,
4932 height: 841.89,
4933 elements: vec![LayoutElement {
4934 x: 54.0,
4935 y: 54.0,
4936 width: 100.0,
4937 height: 16.8,
4938 draw: DrawCommand::Text {
4939 lines: vec![TextLine {
4940 x: 54.0,
4941 y: 66.0,
4942 width: 50.0,
4943 height: 16.8,
4944 glyphs: vec![PositionedGlyph {
4945 glyph_id: 65,
4946 x_offset: 0.0,
4947 y_offset: 0.0,
4948 x_advance: 8.0,
4949 font_size: 12.0,
4950 font_family: "Helvetica".into(),
4951 font_weight: 400,
4952 font_style: FontStyle::Normal,
4953 char_value: 'H',
4954 color: None,
4955 href: None,
4956 text_decoration: TextDecoration::None,
4957 letter_spacing: 0.0,
4958 cluster_text: None,
4959 }],
4960 word_spacing: 0.0,
4961 }],
4962 color: Color::BLACK,
4963 text_decoration: TextDecoration::None,
4964 opacity: 1.0,
4965 },
4966 children: vec![],
4967 node_type: None,
4968 resolved_style: None,
4969 source_location: None,
4970 href: None,
4971 bookmark: None,
4972 alt: None,
4973 is_header_row: false,
4974 col_span: 1,
4975 overflow: Overflow::default(),
4976 opacity: 1.0,
4977 }],
4978 fixed_header: vec![],
4979 fixed_footer: vec![],
4980 watermarks: vec![],
4981 config: PageConfig::default(),
4982 page_name: None,
4983 }];
4984
4985 let metadata = Metadata::default();
4986 let (bytes, _warnings) = writer
4987 .write(
4988 &pages,
4989 &metadata,
4990 &font_context,
4991 false,
4992 None,
4993 false,
4994 None,
4995 &[],
4996 None,
4997 false,
4998 )
4999 .unwrap();
5000 let text = String::from_utf8_lossy(&bytes);
5001
5002 assert!(
5004 text.contains("/Type1"),
5005 "Standard font should use Type1 subtype"
5006 );
5007 assert!(
5008 !text.contains("CIDFontType2"),
5009 "Standard font should not use CIDFontType2"
5010 );
5011 }
5012}