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