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 for element in &page.elements {
1353 self.write_element(
1354 &mut stream,
1355 element,
1356 page_height,
1357 builder,
1358 page_idx,
1359 &mut element_counter,
1360 &mut gradient_counter,
1361 page_number,
1362 total_pages,
1363 tag_builder.as_deref_mut(),
1364 flatten_forms,
1365 );
1366 }
1367
1368 stream
1369 }
1370
1371 #[allow(clippy::too_many_arguments)]
1373 #[allow(clippy::too_many_arguments)]
1374 fn write_element(
1375 &self,
1376 stream: &mut String,
1377 element: &LayoutElement,
1378 page_height: f64,
1379 builder: &PdfBuilder,
1380 page_idx: usize,
1381 element_counter: &mut usize,
1382 gradient_counter: &mut usize,
1383 page_number: usize,
1384 total_pages: usize,
1385 mut tag_builder: Option<&mut tagged::TagBuilder>,
1386 flatten_forms: bool,
1387 ) {
1388 let mut is_artifact = false;
1391 let tagged_mcid = if let Some(ref mut tb) = tag_builder {
1392 if let Some(ref nt) = element.node_type {
1393 if nt == "Watermark" {
1394 let _ = writeln!(stream, "/Artifact BMC");
1396 is_artifact = true;
1397 None
1398 } else {
1399 let is_header = element.is_header_row;
1400 let href = element.href.as_deref();
1401 let mcid = tb.begin_element(
1402 nt,
1403 is_header,
1404 element.alt.as_deref(),
1405 page_idx,
1406 href,
1407 element.col_span,
1408 );
1409 let role = if href.is_some() {
1412 "Link"
1413 } else {
1414 tb.map_role_public(nt, is_header)
1415 };
1416 let _ = writeln!(stream, "/{} <</MCID {}>> BDC", role, mcid);
1417 Some(mcid)
1418 }
1419 } else if !matches!(element.draw, DrawCommand::None) {
1420 let _ = writeln!(stream, "/Artifact BMC");
1422 is_artifact = true;
1423 None
1424 } else {
1425 None
1426 }
1427 } else {
1428 None
1429 };
1430
1431 let needs_element_opacity = element.opacity < 1.0;
1439 if needs_element_opacity {
1440 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&element.opacity.to_bits()) {
1441 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1442 }
1443 }
1444
1445 let transform_ops: &[TransformOp] = element
1451 .resolved_style
1452 .as_ref()
1453 .map(|s| s.transform.as_slice())
1454 .unwrap_or(&[]);
1455 let has_transform = !transform_ops.is_empty();
1456 if has_transform {
1457 let rs = element.resolved_style.as_ref().unwrap();
1458 let pdf_x = element.x;
1459 let pdf_y_bottom = page_height - element.y - element.height;
1460 let (ox_frac, oy_frac) = rs.transform_origin;
1461 let origin_x = pdf_x + element.width * ox_frac;
1462 let origin_y = pdf_y_bottom + (1.0 - oy_frac) * element.height;
1465
1466 let _ = writeln!(stream, "q");
1467 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", -origin_x, -origin_y);
1469 for op in transform_ops.iter().rev() {
1477 match op {
1478 TransformOp::Rotate { deg } => {
1479 let theta = (-deg).to_radians();
1484 let c = theta.cos();
1485 let s = theta.sin();
1486 let _ = writeln!(stream, "{:.6} {:.6} {:.6} {:.6} 0 0 cm", c, s, -s, c);
1487 }
1488 TransformOp::Scale { x, y } => {
1489 let _ = writeln!(stream, "{:.6} 0 0 {:.6} 0 0 cm", x, y);
1490 }
1491 TransformOp::Translate { x, y } => {
1492 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", x, -y);
1494 }
1495 }
1496 }
1497 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", origin_x, origin_y);
1499 }
1500
1501 match &element.draw {
1502 DrawCommand::None => {}
1503
1504 DrawCommand::Rect {
1505 background,
1506 border_width,
1507 border_color,
1508 border_style,
1509 border_radius,
1510 opacity,
1511 box_shadow,
1512 background_gradient,
1513 } => {
1514 let x = element.x;
1515 let y = page_height - element.y - element.height;
1516 let w = element.width;
1517 let h = element.height;
1518
1519 let needs_opacity = *opacity < 1.0;
1521 if needs_opacity {
1522 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1523 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1524 }
1525 }
1526
1527 if let Some(shadow) = box_shadow {
1533 if shadow.color.a > 0.0 {
1534 let sx = x + shadow.offset_x;
1539 let sy = y - shadow.offset_y;
1540 let needs_shadow_alpha = shadow.color.a < 1.0;
1541 if needs_shadow_alpha {
1542 if let Some((_, gs_name)) =
1543 builder.ext_gstate_map.get(&shadow.color.a.to_bits())
1544 {
1545 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1546 } else {
1547 let _ = writeln!(stream, "q");
1548 }
1549 } else {
1550 let _ = writeln!(stream, "q");
1551 }
1552 let _ = writeln!(
1553 stream,
1554 "{:.3} {:.3} {:.3} rg",
1555 shadow.color.r, shadow.color.g, shadow.color.b
1556 );
1557 if border_radius.top_left > 0.0
1558 || border_radius.top_right > 0.0
1559 || border_radius.bottom_right > 0.0
1560 || border_radius.bottom_left > 0.0
1561 {
1562 self.write_rounded_rect(stream, sx, sy, w, h, border_radius);
1563 } else {
1564 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", sx, sy, w, h);
1565 }
1566 let _ = writeln!(stream, "f\nQ");
1567 }
1568 }
1569
1570 if background_gradient.is_some() {
1576 let key = (page_idx, *gradient_counter);
1577 *gradient_counter += 1;
1578 if let Some((_, sh_name)) = builder.shading_map.get(&key) {
1579 let _ = writeln!(stream, "q");
1580 if border_radius.top_left > 0.0
1582 || border_radius.top_right > 0.0
1583 || border_radius.bottom_right > 0.0
1584 || border_radius.bottom_left > 0.0
1585 {
1586 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1587 let _ = writeln!(stream, "W n");
1588 } else {
1589 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re W n", x, y, w, h);
1590 }
1591 let _ =
1594 writeln!(stream, "1 0 0 1 {:.3} {:.3} cm\n/{} sh\nQ", x, y, sh_name);
1595 }
1596 } else if let Some(bg) = background {
1597 if bg.a > 0.0 {
1598 let _ = writeln!(stream, "q\n{:.3} {:.3} {:.3} rg", bg.r, bg.g, bg.b);
1599
1600 if border_radius.top_left > 0.0 {
1601 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1602 } else {
1603 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1604 }
1605
1606 let _ = writeln!(stream, "f\nQ");
1607 }
1608 }
1609
1610 let bw = border_width;
1611 if bw.top > 0.0 || bw.right > 0.0 || bw.bottom > 0.0 || bw.left > 0.0 {
1612 use crate::style::BorderStyle::Solid;
1613 let all_solid = border_style.top == Solid
1614 && border_style.right == Solid
1615 && border_style.bottom == Solid
1616 && border_style.left == Solid;
1617 if all_solid
1623 && (bw.top - bw.right).abs() < 0.001
1624 && (bw.right - bw.bottom).abs() < 0.001
1625 && (bw.bottom - bw.left).abs() < 0.001
1626 {
1627 let bc = &border_color.top;
1628 let _ = writeln!(
1629 stream,
1630 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w",
1631 bc.r, bc.g, bc.b, bw.top
1632 );
1633
1634 if border_radius.top_left > 0.0 {
1635 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1636 } else {
1637 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1638 }
1639
1640 let _ = writeln!(stream, "S\nQ");
1641 } else {
1642 self.write_border_sides(stream, x, y, w, h, bw, border_color, border_style);
1643 }
1644 }
1645
1646 if needs_opacity {
1647 let _ = writeln!(stream, "Q");
1648 }
1649 }
1650
1651 DrawCommand::Text {
1652 lines,
1653 color,
1654 text_decoration,
1655 opacity,
1656 } => {
1657 let needs_opacity = *opacity < 1.0;
1659 if needs_opacity {
1660 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1661 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1662 }
1663 }
1664
1665 for line in lines {
1666 if line.glyphs.is_empty() {
1667 continue;
1668 }
1669
1670 let groups = Self::group_glyphs_by_style(&line.glyphs);
1673 let pdf_y = page_height - line.y;
1674
1675 let _ = writeln!(stream, "BT");
1676
1677 if line.word_spacing.abs() > 0.001 {
1679 let _ = writeln!(stream, "{:.4} Tw", line.word_spacing);
1680 }
1681
1682 let mut tm_x = 0.0_f64;
1684 let mut tm_y = 0.0_f64;
1685 let mut x_cursor = line.x;
1686
1687 let mut group_spans: Vec<(f64, f64, TextDecoration, Color)> = Vec::new();
1689
1690 for group in &groups {
1691 let first = &group[0];
1692 let glyph_color = first.color.unwrap_or(*color);
1693
1694 let idx = self.font_index(
1695 &first.font_family,
1696 first.font_weight,
1697 first.font_style,
1698 &builder.font_objects,
1699 );
1700 let italic =
1701 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
1702 let font_key = FontKey {
1703 family: first.font_family.to_string(),
1704 weight: first.font_weight,
1705 italic,
1706 };
1707 let font_name = format!("F{}", idx);
1708
1709 let dx = x_cursor - tm_x;
1711 let dy = pdf_y - tm_y;
1712 let _ = writeln!(
1713 stream,
1714 "{:.3} {:.3} {:.3} rg\n/{} {:.1} Tf\n{:.2} Tc\n{:.2} {:.2} Td",
1715 glyph_color.r,
1716 glyph_color.g,
1717 glyph_color.b,
1718 font_name,
1719 first.font_size,
1720 first.letter_spacing,
1721 dx,
1722 dy
1723 );
1724 tm_x = x_cursor;
1725 tm_y = pdf_y;
1726
1727 let raw_text: String = group.iter().map(|g| g.char_value).collect();
1729 let has_placeholder = raw_text.contains(PAGE_NUMBER_SENTINEL)
1730 || raw_text.contains(TOTAL_PAGES_SENTINEL);
1731
1732 let is_custom = builder.custom_font_data.contains_key(&font_key);
1733
1734 if is_custom {
1735 if let Some(embed_data) = builder.custom_font_data.get(&font_key) {
1736 let mut hex = String::new();
1737 if has_placeholder {
1738 let pn = PAGE_NUMBER_SENTINEL.to_string();
1740 let tp = TOTAL_PAGES_SENTINEL.to_string();
1741 let text_after = raw_text
1742 .replace(&pn, &page_number.to_string())
1743 .replace(&tp, &total_pages.to_string());
1744 for ch in text_after.chars() {
1745 let gid =
1746 embed_data.char_to_gid.get(&ch).copied().unwrap_or(0);
1747 let _ = write!(hex, "{:04X}", gid);
1748 }
1749 } else {
1750 for g in group.iter() {
1752 let new_gid = embed_data
1753 .gid_remap
1754 .get(&g.glyph_id)
1755 .copied()
1756 .unwrap_or_else(|| {
1757 embed_data
1759 .char_to_gid
1760 .get(&g.char_value)
1761 .copied()
1762 .unwrap_or(0)
1763 });
1764 let _ = write!(hex, "{:04X}", new_gid);
1765 }
1766 }
1767 let _ = writeln!(stream, "<{}> Tj", hex);
1768 } else {
1769 let _ = writeln!(stream, "<> Tj");
1770 }
1771 } else {
1772 let pn = PAGE_NUMBER_SENTINEL.to_string();
1773 let tp = TOTAL_PAGES_SENTINEL.to_string();
1774 let text_after = raw_text
1775 .replace(&pn, &page_number.to_string())
1776 .replace(&tp, &total_pages.to_string());
1777 let mut text_str = String::new();
1778 for ch in text_after.chars() {
1779 let b = Self::unicode_to_winansi(ch).unwrap_or(b'?');
1780 match b {
1781 b'\\' => text_str.push_str("\\\\"),
1782 b'(' => text_str.push_str("\\("),
1783 b')' => text_str.push_str("\\)"),
1784 0x20..=0x7E => text_str.push(b as char),
1785 _ => {
1786 let _ = write!(text_str, "\\{:03o}", b);
1787 }
1788 }
1789 }
1790 let _ = writeln!(stream, "({}) Tj", text_str);
1791 }
1792
1793 let group_start_x = x_cursor;
1795
1796 if let Some(last) = group.last() {
1799 let space_count_in_group =
1800 group.iter().filter(|g| g.char_value == ' ').count();
1801 x_cursor = line.x
1802 + last.x_offset
1803 + last.x_advance
1804 + space_count_in_group as f64 * line.word_spacing;
1805 }
1806
1807 let group_dec = first.text_decoration;
1809 if !matches!(group_dec, TextDecoration::None) {
1810 group_spans.push((group_start_x, x_cursor, group_dec, glyph_color));
1811 }
1812 }
1813
1814 let _ = writeln!(stream, "ET");
1815
1816 for (span_x, span_end_x, dec, dec_color) in &group_spans {
1818 match dec {
1819 TextDecoration::Underline => {
1820 let underline_y = pdf_y - 1.5;
1821 let _ = write!(
1822 stream,
1823 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1824 dec_color.r, dec_color.g, dec_color.b,
1825 span_x, underline_y,
1826 span_end_x, underline_y
1827 );
1828 }
1829 TextDecoration::LineThrough => {
1830 let first_size =
1831 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
1832 let strikethrough_y = pdf_y + first_size * 0.3;
1833 let _ = write!(
1834 stream,
1835 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1836 dec_color.r, dec_color.g, dec_color.b,
1837 span_x, strikethrough_y,
1838 span_end_x, strikethrough_y
1839 );
1840 }
1841 TextDecoration::None => {}
1842 }
1843 }
1844
1845 if group_spans.is_empty() {
1847 if matches!(text_decoration, TextDecoration::Underline) {
1848 let underline_y = pdf_y - 1.5;
1849 let _ = write!(
1850 stream,
1851 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1852 color.r, color.g, color.b,
1853 line.x, underline_y,
1854 line.x + line.width, underline_y
1855 );
1856 }
1857 if matches!(text_decoration, TextDecoration::LineThrough) {
1858 let first_size =
1859 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
1860 let strikethrough_y = pdf_y + first_size * 0.3;
1861 let _ = write!(
1862 stream,
1863 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1864 color.r, color.g, color.b,
1865 line.x, strikethrough_y,
1866 line.x + line.width, strikethrough_y
1867 );
1868 }
1869 }
1870 }
1871
1872 if needs_opacity {
1873 let _ = writeln!(stream, "Q");
1874 }
1875 }
1876
1877 DrawCommand::Image { .. } => {
1878 let elem_idx = *element_counter;
1879 *element_counter += 1;
1880 if let Some(&img_idx) = builder.image_index_map.get(&(page_idx, elem_idx)) {
1881 let x = element.x;
1882 let y = page_height - element.y - element.height;
1883 let _ = write!(
1884 stream,
1885 "q\n{:.4} 0 0 {:.4} {:.2} {:.2} cm\n/Im{} Do\nQ\n",
1886 element.width, element.height, x, y, img_idx
1887 );
1888 } else {
1889 let x = element.x;
1891 let y = page_height - element.y - element.height;
1892 let _ = write!(
1893 stream,
1894 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
1895 x, y, element.width, element.height
1896 );
1897 }
1898 if tagged_mcid.is_some() {
1899 let _ = writeln!(stream, "EMC");
1900 if let Some(ref mut tb) = tag_builder {
1901 tb.end_element();
1902 }
1903 } else if is_artifact {
1904 let _ = writeln!(stream, "EMC");
1905 }
1906 return; }
1908
1909 DrawCommand::ImagePlaceholder => {
1910 *element_counter += 1;
1911 let x = element.x;
1912 let y = page_height - element.y - element.height;
1913 let _ = write!(
1914 stream,
1915 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
1916 x, y, element.width, element.height
1917 );
1918 if tagged_mcid.is_some() {
1919 let _ = writeln!(stream, "EMC");
1920 if let Some(ref mut tb) = tag_builder {
1921 tb.end_element();
1922 }
1923 } else if is_artifact {
1924 let _ = writeln!(stream, "EMC");
1925 }
1926 return;
1927 }
1928
1929 DrawCommand::Svg {
1930 commands,
1931 width: _svg_w,
1932 height: _svg_h,
1933 viewbox_min_x,
1934 viewbox_min_y,
1935 viewbox_width,
1936 viewbox_height,
1937 clip,
1938 } => {
1939 let x = element.x;
1940 let y = page_height - element.y - element.height;
1941
1942 let _ = writeln!(stream, "q");
1944 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", x, y);
1945
1946 if *viewbox_width > 0.0 && *viewbox_height > 0.0 {
1952 let raw_sx = element.width / *viewbox_width;
1953 let raw_sy = element.height / *viewbox_height;
1954 let s = raw_sx.min(raw_sy);
1955 let tx = (element.width - s * *viewbox_width) / 2.0;
1956 let ty = (element.height - s * *viewbox_height) / 2.0;
1957 let _ = writeln!(stream, "{:.4} 0 0 {:.4} {:.2} {:.2} cm", s, s, tx, ty);
1958 }
1959
1960 let _ = writeln!(stream, "1 0 0 -1 0 {:.2} cm", *viewbox_height);
1963
1964 if *viewbox_min_x != 0.0 || *viewbox_min_y != 0.0 {
1966 let _ = writeln!(
1967 stream,
1968 "1 0 0 1 {:.2} {:.2} cm",
1969 -*viewbox_min_x, -*viewbox_min_y
1970 );
1971 }
1972
1973 if *clip {
1975 let _ = writeln!(
1976 stream,
1977 "{:.2} {:.2} {:.2} {:.2} re W n",
1978 *viewbox_min_x, *viewbox_min_y, *viewbox_width, *viewbox_height
1979 );
1980 }
1981
1982 Self::write_svg_commands(stream, commands, &builder.ext_gstate_map);
1983
1984 let _ = writeln!(stream, "Q");
1985 if tagged_mcid.is_some() {
1986 let _ = writeln!(stream, "EMC");
1987 if let Some(ref mut tb) = tag_builder {
1988 tb.end_element();
1989 }
1990 } else if is_artifact {
1991 let _ = writeln!(stream, "EMC");
1992 }
1993 return;
1994 }
1995
1996 DrawCommand::Barcode {
1997 bars,
1998 bar_width,
1999 height,
2000 color,
2001 } => {
2002 *element_counter += 1;
2003 let _ = writeln!(stream, "q");
2004 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2005 for (i, &bar) in bars.iter().enumerate() {
2006 if bar == 1 {
2007 let bx = element.x + i as f64 * bar_width;
2008 let by = page_height - element.y - height;
2009 let _ = writeln!(
2010 stream,
2011 "{:.2} {:.2} {:.2} {:.2} re",
2012 bx, by, bar_width, height
2013 );
2014 }
2015 }
2016 let _ = writeln!(stream, "f\nQ");
2017 if tagged_mcid.is_some() {
2018 let _ = writeln!(stream, "EMC");
2019 if let Some(ref mut tb) = tag_builder {
2020 tb.end_element();
2021 }
2022 } else if is_artifact {
2023 let _ = writeln!(stream, "EMC");
2024 }
2025 return;
2026 }
2027
2028 DrawCommand::QrCode {
2029 modules,
2030 module_size,
2031 color,
2032 } => {
2033 *element_counter += 1;
2034 let _ = writeln!(stream, "q");
2035 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2036 for (row_idx, row) in modules.iter().enumerate() {
2037 for (col_idx, &dark) in row.iter().enumerate() {
2038 if dark {
2039 let mx = element.x + col_idx as f64 * module_size;
2040 let my = page_height - element.y - (row_idx as f64 + 1.0) * module_size;
2041 let _ = writeln!(
2042 stream,
2043 "{:.2} {:.2} {:.2} {:.2} re",
2044 mx, my, module_size, module_size
2045 );
2046 }
2047 }
2048 }
2049 let _ = writeln!(stream, "f\nQ");
2050 if tagged_mcid.is_some() {
2051 let _ = writeln!(stream, "EMC");
2052 if let Some(ref mut tb) = tag_builder {
2053 tb.end_element();
2054 }
2055 } else if is_artifact {
2056 let _ = writeln!(stream, "EMC");
2057 }
2058 return;
2059 }
2060
2061 DrawCommand::Chart { primitives } => {
2062 *element_counter += 1;
2063 let _ = writeln!(stream, "q");
2064 let _ = writeln!(
2066 stream,
2067 "1 0 0 -1 {:.4} {:.4} cm",
2068 element.x,
2069 page_height - element.y
2070 );
2071
2072 for prim in primitives {
2073 write_chart_primitive(stream, prim, element.height, builder);
2074 }
2075
2076 let _ = writeln!(stream, "Q");
2077 if tagged_mcid.is_some() {
2078 let _ = writeln!(stream, "EMC");
2079 if let Some(ref mut tb) = tag_builder {
2080 tb.end_element();
2081 }
2082 } else if is_artifact {
2083 let _ = writeln!(stream, "EMC");
2084 }
2085 return;
2086 }
2087
2088 DrawCommand::Watermark {
2089 lines,
2090 color,
2091 opacity,
2092 angle_rad,
2093 font_family: _,
2094 } => {
2095 let _ = writeln!(stream, "q");
2096 if *opacity < 1.0 {
2098 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
2099 let _ = writeln!(stream, "/{} gs", gs_name);
2100 }
2101 }
2102 let pdf_cx = element.x;
2104 let pdf_cy = page_height - element.y;
2105 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", pdf_cx, pdf_cy);
2106 let cos_a = angle_rad.cos();
2108 let sin_a = angle_rad.sin();
2109 let _ = writeln!(
2110 stream,
2111 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
2112 cos_a, sin_a, -sin_a, cos_a
2113 );
2114 let _ = writeln!(stream, "BT");
2116 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
2117 if let Some(line) = lines.first() {
2118 let groups = Self::group_glyphs_by_style(&line.glyphs);
2119 let text_width = line.width;
2120 let cap_height = line.height * 0.7;
2121 let _ = writeln!(
2122 stream,
2123 "{:.2} {:.2} Td",
2124 -text_width / 2.0,
2125 -cap_height / 2.0
2126 );
2127 for group in &groups {
2128 let first = &group[0];
2129 let italic =
2130 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
2131 let fk = FontKey {
2132 family: first.font_family.to_string(),
2133 weight: first.font_weight,
2134 italic,
2135 };
2136 let idx = self.font_index(
2137 &first.font_family,
2138 first.font_weight,
2139 first.font_style,
2140 &builder.font_objects,
2141 );
2142 let font_name = format!("F{}", idx);
2143 let _ = writeln!(stream, "/{} {:.1} Tf", font_name, first.font_size);
2144 let is_custom = builder.custom_font_data.contains_key(&fk);
2145 if is_custom {
2146 if let Some(embed_data) = builder.custom_font_data.get(&fk) {
2147 let mut hex = String::new();
2148 for g in group.iter() {
2149 let gid =
2150 embed_data.gid_remap.get(&g.glyph_id).copied().unwrap_or(0);
2151 let _ = write!(hex, "{:04X}", gid);
2152 }
2153 let _ = writeln!(stream, "<{}> Tj", hex);
2154 }
2155 } else {
2156 let hex_str: String = group
2157 .iter()
2158 .map(|g| format!("{:02X}", g.glyph_id as u8))
2159 .collect();
2160 let _ = writeln!(stream, "<{}> Tj", hex_str);
2161 }
2162 }
2163 }
2164 let _ = writeln!(stream, "ET");
2165 let _ = writeln!(stream, "Q");
2166 if tagged_mcid.is_some() {
2167 let _ = writeln!(stream, "EMC");
2168 if let Some(ref mut tb) = tag_builder {
2169 tb.end_element();
2170 }
2171 } else if is_artifact {
2172 let _ = writeln!(stream, "EMC");
2173 }
2174 return;
2175 }
2176
2177 DrawCommand::FormField { field_type, .. } => {
2178 let pdf_x = element.x;
2182 let pdf_y = page_height - element.y - element.height;
2183 let w = element.width;
2184 let h = element.height;
2185 let _ = writeln!(stream, "q");
2186 match field_type {
2187 FormFieldType::Checkbox { checked, .. } => {
2188 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
2191 let _ =
2192 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
2193 if *checked {
2194 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
2196 let sx = w / 14.0;
2197 let sy = h / 14.0;
2198 let _ = writeln!(
2199 stream,
2200 "{:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l f",
2201 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
2202 pdf_x + 5.5 * sx, pdf_y + 2.0 * sy,
2203 pdf_x + 12.0 * sx, pdf_y + 11.0 * sy,
2204 pdf_x + 11.0 * sx, pdf_y + 12.0 * sy,
2205 pdf_x + 5.5 * sx, pdf_y + 4.5 * sy,
2206 pdf_x + 3.0 * sx, pdf_y + 7.0 * sy,
2207 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
2208 );
2209 }
2210 }
2211 FormFieldType::RadioButton { checked, .. } => {
2212 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
2215 let _ =
2216 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
2217 if *checked {
2218 let cx = pdf_x + w / 2.0;
2220 let cy = pdf_y + h / 2.0;
2221 let r = (w.min(h) / 2.0) * 0.6;
2222 let k = r * 0.5523;
2223 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
2224 let _ = writeln!(
2225 stream,
2226 "{:.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",
2227 cx, cy + r,
2228 cx + k, cy + r, cx + r, cy + k, cx + r, cy,
2229 cx + r, cy - k, cx + k, cy - r, cx, cy - r,
2230 cx - k, cy - r, cx - r, cy - k, cx - r, cy,
2231 cx - r, cy + k, cx - k, cy + r, cx, cy + r,
2232 );
2233 }
2234 }
2235 FormFieldType::TextField {
2236 value,
2237 placeholder,
2238 font_size,
2239 multiline,
2240 password,
2241 ..
2242 } => {
2243 let _ = writeln!(stream, "1 1 1 rg");
2245 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2246 let _ = writeln!(stream, "0.5 w");
2247 let _ =
2248 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2249 if flatten_forms {
2251 let has_value = value.as_ref().is_some_and(|v| !v.is_empty());
2252 if has_value {
2253 let val = value.as_ref().unwrap();
2254 let display_text = if *password {
2255 "\u{2022}".repeat(val.len())
2256 } else {
2257 val.clone()
2258 };
2259 let font_idx = builder
2260 .font_objects
2261 .iter()
2262 .enumerate()
2263 .find(|(_, (key, _))| {
2264 key.family == "Helvetica"
2265 && key.weight == 400
2266 && !key.italic
2267 })
2268 .map(|(i, _)| i)
2269 .unwrap_or(0);
2270 if *multiline {
2271 let metrics = crate::font::StandardFont::Helvetica.metrics();
2273 let max_w = w - 4.0;
2274 let mut lines: Vec<String> = Vec::new();
2275 for paragraph in display_text.split('\n') {
2276 let mut line = String::new();
2277 let mut line_w = 0.0;
2278 for word in paragraph.split_whitespace() {
2279 let word_w =
2280 metrics.measure_string(word, *font_size, 0.0);
2281 let space_w = if line.is_empty() {
2282 0.0
2283 } else {
2284 metrics.measure_string(" ", *font_size, 0.0)
2285 };
2286 if word_w > max_w {
2288 let mut char_line = String::new();
2289 let mut char_w = 0.0;
2290 for ch in word.chars() {
2291 let cw = metrics.char_width(ch, *font_size);
2292 if !char_line.is_empty() && char_w + cw > max_w
2293 {
2294 if !line.is_empty() {
2295 lines.push(line.clone());
2296 line.clear();
2297 line_w = 0.0;
2298 }
2299 lines.push(char_line.clone());
2300 char_line.clear();
2301 char_w = 0.0;
2302 }
2303 char_line.push(ch);
2304 char_w += cw;
2305 }
2306 if !char_line.is_empty() {
2308 if !line.is_empty() {
2309 line.push(' ');
2310 line_w += metrics
2311 .measure_string(" ", *font_size, 0.0);
2312 }
2313 line.push_str(&char_line);
2314 line_w += char_w;
2315 }
2316 continue;
2317 }
2318 if !line.is_empty() && line_w + space_w + word_w > max_w
2319 {
2320 lines.push(line.clone());
2321 line.clear();
2322 line_w = 0.0;
2323 }
2324 if !line.is_empty() {
2325 line.push(' ');
2326 line_w += space_w;
2327 }
2328 line.push_str(word);
2329 line_w += word_w;
2330 }
2331 if !line.is_empty() {
2332 lines.push(line);
2333 }
2334 }
2335 let text_y = pdf_y + h - font_size - 2.0;
2336 for (i, line_text) in lines.iter().enumerate() {
2337 let ly = text_y - (i as f64) * (font_size * 1.2);
2338 if ly < pdf_y {
2339 break;
2340 }
2341 let esc = Self::encode_winansi_text(line_text);
2342 let _ = writeln!(
2343 stream,
2344 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2345 font_idx,
2346 font_size,
2347 pdf_x + 2.0,
2348 ly,
2349 esc
2350 );
2351 }
2352 } else {
2353 let escaped = Self::encode_winansi_text(&display_text);
2354 let text_y = pdf_y + (h - font_size) / 2.0;
2355 let _ = writeln!(
2356 stream,
2357 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2358 font_idx,
2359 font_size,
2360 pdf_x + 2.0,
2361 text_y,
2362 escaped
2363 );
2364 }
2365 } else if let Some(ref ph) = placeholder {
2366 if !ph.is_empty() {
2367 let font_idx = builder
2369 .font_objects
2370 .iter()
2371 .enumerate()
2372 .find(|(_, (key, _))| {
2373 key.family == "Helvetica"
2374 && key.weight == 400
2375 && !key.italic
2376 })
2377 .map(|(i, _)| i)
2378 .unwrap_or(0);
2379 let escaped = Self::encode_winansi_text(ph);
2380 let text_y = pdf_y + (h - font_size) / 2.0;
2381 let _ = writeln!(
2382 stream,
2383 "BT /F{} {:.1} Tf 0.6 g {:.2} {:.2} Td ({}) Tj ET",
2384 font_idx,
2385 font_size,
2386 pdf_x + 2.0,
2387 text_y,
2388 escaped
2389 );
2390 }
2391 }
2392 }
2393 }
2394 FormFieldType::Dropdown {
2395 value, font_size, ..
2396 } => {
2397 let _ = writeln!(stream, "1 1 1 rg");
2399 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2400 let _ = writeln!(stream, "0.5 w");
2401 let _ =
2402 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2403 if flatten_forms {
2405 if let Some(ref val) = value {
2406 if !val.is_empty() {
2407 let font_idx = builder
2408 .font_objects
2409 .iter()
2410 .enumerate()
2411 .find(|(_, (key, _))| {
2412 key.family == "Helvetica"
2413 && key.weight == 400
2414 && !key.italic
2415 })
2416 .map(|(i, _)| i)
2417 .unwrap_or(0);
2418 let escaped = Self::encode_winansi_text(val);
2419 let text_y = pdf_y + (h - font_size) / 2.0;
2420 let _ = writeln!(
2421 stream,
2422 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2423 font_idx,
2424 font_size,
2425 pdf_x + 2.0,
2426 text_y,
2427 escaped
2428 );
2429 }
2430 }
2431 }
2432 }
2433 }
2434 let _ = writeln!(stream, "Q");
2435 }
2436 }
2437
2438 let clip_overflow = matches!(element.overflow, Overflow::Hidden);
2443 if clip_overflow {
2444 let clip_x = element.x;
2445 let clip_y = page_height - element.y - element.height;
2446 let clip_w = element.width;
2447 let clip_h = element.height;
2448 let radius = if let DrawCommand::Rect { border_radius, .. } = &element.draw {
2452 Some(border_radius)
2453 } else {
2454 None
2455 };
2456 let has_rounded_corners = radius.is_some_and(|r| {
2457 r.top_left > 0.0 || r.top_right > 0.0 || r.bottom_right > 0.0 || r.bottom_left > 0.0
2458 });
2459 let _ = writeln!(stream, "q");
2460 if has_rounded_corners {
2461 self.write_rounded_rect(stream, clip_x, clip_y, clip_w, clip_h, radius.unwrap());
2462 let _ = writeln!(stream, "W n");
2463 } else {
2464 let _ = writeln!(
2465 stream,
2466 "{:.2} {:.2} {:.2} {:.2} re W n",
2467 clip_x, clip_y, clip_w, clip_h
2468 );
2469 }
2470 }
2471
2472 for child in &element.children {
2473 self.write_element(
2474 stream,
2475 child,
2476 page_height,
2477 builder,
2478 page_idx,
2479 element_counter,
2480 gradient_counter,
2481 page_number,
2482 total_pages,
2483 tag_builder.as_deref_mut(),
2484 flatten_forms,
2485 );
2486 }
2487
2488 if clip_overflow {
2489 let _ = writeln!(stream, "Q");
2490 }
2491
2492 if has_transform {
2494 let _ = writeln!(stream, "Q");
2495 }
2496
2497 if needs_element_opacity {
2500 let _ = writeln!(stream, "Q");
2501 }
2502
2503 if tagged_mcid.is_some() {
2505 let _ = writeln!(stream, "EMC");
2506 if let Some(ref mut tb) = tag_builder {
2507 tb.end_element();
2508 }
2509 } else if is_artifact {
2510 let _ = writeln!(stream, "EMC");
2511 }
2512 }
2513
2514 fn write_rounded_rect(
2515 &self,
2516 stream: &mut String,
2517 x: f64,
2518 y: f64,
2519 w: f64,
2520 h: f64,
2521 r: &crate::style::CornerValues,
2522 ) {
2523 let k = 0.5522847498;
2524
2525 let tl = r.top_left.min(w / 2.0).min(h / 2.0);
2526 let tr = r.top_right.min(w / 2.0).min(h / 2.0);
2527 let br = r.bottom_right.min(w / 2.0).min(h / 2.0);
2528 let bl = r.bottom_left.min(w / 2.0).min(h / 2.0);
2529
2530 let _ = writeln!(stream, "{:.2} {:.2} m", x + bl, y);
2531
2532 let _ = writeln!(stream, "{:.2} {:.2} l", x + w - br, y);
2533 if br > 0.0 {
2534 let _ = writeln!(
2535 stream,
2536 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2537 x + w - br + br * k,
2538 y,
2539 x + w,
2540 y + br - br * k,
2541 x + w,
2542 y + br
2543 );
2544 }
2545
2546 let _ = writeln!(stream, "{:.2} {:.2} l", x + w, y + h - tr);
2547 if tr > 0.0 {
2548 let _ = writeln!(
2549 stream,
2550 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2551 x + w,
2552 y + h - tr + tr * k,
2553 x + w - tr + tr * k,
2554 y + h,
2555 x + w - tr,
2556 y + h
2557 );
2558 }
2559
2560 let _ = writeln!(stream, "{:.2} {:.2} l", x + tl, y + h);
2561 if tl > 0.0 {
2562 let _ = writeln!(
2563 stream,
2564 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2565 x + tl - tl * k,
2566 y + h,
2567 x,
2568 y + h - tl + tl * k,
2569 x,
2570 y + h - tl
2571 );
2572 }
2573
2574 let _ = writeln!(stream, "{:.2} {:.2} l", x, y + bl);
2575 if bl > 0.0 {
2576 let _ = writeln!(
2577 stream,
2578 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2579 x,
2580 y + bl - bl * k,
2581 x + bl - bl * k,
2582 y,
2583 x + bl,
2584 y
2585 );
2586 }
2587
2588 let _ = writeln!(stream, "h");
2589 }
2590
2591 #[allow(clippy::too_many_arguments)]
2592 fn write_border_sides(
2593 &self,
2594 stream: &mut String,
2595 x: f64,
2596 y: f64,
2597 w: f64,
2598 h: f64,
2599 bw: &Edges,
2600 bc: &crate::style::EdgeValues<Color>,
2601 bs: &crate::style::EdgeValues<crate::style::BorderStyle>,
2602 ) {
2603 fn dash_ops(style: crate::style::BorderStyle, width: f64) -> String {
2608 use crate::style::BorderStyle::*;
2609 match style {
2610 Solid => String::new(),
2611 Dashed => format!("[{:.2} {:.2}] 0 d\n", width * 2.0, width),
2612 Dotted => format!("1 J\n[0 {:.2}] 0 d\n", width * 2.0),
2613 }
2614 }
2615 let sides = [
2617 (bc.top, bw.top, bs.top, x, y + h, x + w, y + h),
2618 (bc.bottom, bw.bottom, bs.bottom, x, y, x + w, y),
2619 (bc.left, bw.left, bs.left, x, y, x, y + h),
2620 (bc.right, bw.right, bs.right, x + w, y, x + w, y + h),
2621 ];
2622 for (color, width, style, x0, y0, x1, y1) in sides {
2623 if width <= 0.0 {
2624 continue;
2625 }
2626 let _ = write!(
2627 stream,
2628 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{}{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2629 color.r,
2630 color.g,
2631 color.b,
2632 width,
2633 dash_ops(style, width),
2634 x0,
2635 y0,
2636 x1,
2637 y1
2638 );
2639 }
2640 }
2641
2642 fn emit_pdfua_embedded_standard(
2654 builder: &mut PdfBuilder,
2655 key: &FontKey,
2656 std_font: &crate::font::StandardFont,
2657 metrics: &crate::font::StandardFontMetrics,
2658 font_context: &FontContext,
2659 ) -> bool {
2660 let lib_family = match std_font.liberation_family() {
2661 Some(f) => f,
2662 None => return false, };
2664 let lib_bytes: &[u8] = match font_context.resolve(lib_family, key.weight, key.italic) {
2667 FontData::Custom { data, .. } => data,
2668 FontData::Standard(_) => return false,
2669 };
2670 let face = match ttf_parser::Face::parse(lib_bytes, 0) {
2671 Ok(f) => f,
2672 Err(_) => return false,
2673 };
2674 let scale = 1000.0 / face.units_per_em() as f64;
2675 let bbox = face.global_bounding_box();
2676 let pdf_name = Self::sanitize_font_name(lib_family, key.weight, key.italic);
2677
2678 let compressed = compress_to_vec_zlib(lib_bytes, 6);
2680 let fontfile2_id = builder.objects.len();
2681 let mut ff2: Vec<u8> = Vec::new();
2682 let _ = write!(
2683 ff2,
2684 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
2685 compressed.len(),
2686 lib_bytes.len()
2687 );
2688 ff2.extend_from_slice(&compressed);
2689 ff2.extend_from_slice(b"\nendstream");
2690 builder.objects.push(PdfObject {
2691 id: fontfile2_id,
2692 data: ff2,
2693 });
2694
2695 let fd_id = builder.objects.len();
2697 let cap_height =
2698 (face.capital_height().unwrap_or_else(|| face.ascender()) as f64 * scale) as i32;
2699 let fd = format!(
2700 "<< /Type /FontDescriptor /FontName /{name} /Flags {flags} \
2701 /FontBBox [{x0} {y0} {x1} {y1}] /ItalicAngle {ia} \
2702 /Ascent {asc} /Descent {desc} /CapHeight {cap} /StemV {stem} \
2703 /FontFile2 {ff2} 0 R >>",
2704 name = pdf_name,
2705 flags = std_font.descriptor_flags(),
2706 x0 = (bbox.x_min as f64 * scale) as i32,
2707 y0 = (bbox.y_min as f64 * scale) as i32,
2708 x1 = (bbox.x_max as f64 * scale) as i32,
2709 y1 = (bbox.y_max as f64 * scale) as i32,
2710 ia = if key.italic { -12 } else { 0 },
2711 asc = (face.ascender() as f64 * scale) as i32,
2712 desc = (face.descender() as f64 * scale) as i32,
2713 cap = cap_height,
2714 stem = if key.weight >= 700 { 120 } else { 80 },
2715 ff2 = fontfile2_id,
2716 );
2717 builder.objects.push(PdfObject {
2718 id: fd_id,
2719 data: fd.into_bytes(),
2720 });
2721
2722 let declared_widths: Vec<u16> = metrics
2736 .widths
2737 .iter()
2738 .enumerate()
2739 .map(|(i, &afm)| {
2740 let code = 32u8.wrapping_add(i as u8); if let Some(ch) = crate::font::winansi_to_char(code) {
2742 if let Some(gid) = face.glyph_index(ch) {
2743 if let Some(adv) = face.glyph_hor_advance(gid) {
2744 let hmtx = (adv as f64 * scale).round() as u16;
2745 if (hmtx as i32 - afm as i32).abs() > 1 {
2746 return hmtx;
2747 }
2748 }
2749 }
2750 }
2751 afm
2752 })
2753 .collect();
2754 let widths_str: String = declared_widths
2755 .iter()
2756 .map(|w| w.to_string())
2757 .collect::<Vec<_>>()
2758 .join(" ");
2759 let obj_id = builder.objects.len();
2760 let font_dict = format!(
2761 "<< /Type /Font /Subtype /TrueType /BaseFont /{name} \
2762 /Encoding /WinAnsiEncoding \
2763 /FirstChar 32 /LastChar 255 /Widths [{w}] \
2764 /FontDescriptor {fd} 0 R >>",
2765 name = pdf_name,
2766 w = widths_str,
2767 fd = fd_id,
2768 );
2769 builder.objects.push(PdfObject {
2770 id: obj_id,
2771 data: font_dict.into_bytes(),
2772 });
2773 builder.font_objects.push((key.clone(), obj_id));
2774 builder.embedded_standard_fonts.insert(key.clone());
2778 true
2779 }
2780
2781 fn register_fonts(
2782 &self,
2783 builder: &mut PdfBuilder,
2784 pages: &[LayoutPage],
2785 font_context: &FontContext,
2786 pdf_ua: bool,
2787 ) -> Result<(), FormeError> {
2788 let mut font_usage_map: HashMap<FontKey, FontUsage> = HashMap::new();
2790
2791 for page in pages {
2792 Self::collect_font_usage(&page.elements, &mut font_usage_map);
2793 }
2794
2795 let mut keys: Vec<FontKey> = font_usage_map.keys().cloned().collect();
2796
2797 keys.sort_by(|a, b| {
2799 a.family
2800 .cmp(&b.family)
2801 .then(a.weight.cmp(&b.weight))
2802 .then(a.italic.cmp(&b.italic))
2803 });
2804 keys.dedup();
2805
2806 if keys.is_empty() {
2808 keys.push(FontKey {
2809 family: "Helvetica".to_string(),
2810 weight: 400,
2811 italic: false,
2812 });
2813 }
2814
2815 for key in &keys {
2816 let font_data = font_context.resolve(&key.family, key.weight, key.italic);
2817
2818 match font_data {
2819 FontData::Standard(std_font) => {
2820 let metrics = std_font.metrics();
2821
2822 if pdf_ua {
2831 if Self::emit_pdfua_embedded_standard(
2832 builder,
2833 key,
2834 std_font,
2835 &metrics,
2836 font_context,
2837 ) {
2838 continue;
2839 }
2840 if let Some(lib) = std_font.liberation_family() {
2846 builder.warnings.push(format!(
2847 "pdfUa: font '{}' is not embedded, so the PDF will not conform to \
2848 PDF/UA (all fonts must be embedded). Install \
2849 @formepdf/fonts-standard and register its fonts \
2850 (`for (const f of standardFonts()) Font.register(f)`) — Forme \
2851 will then embed the metric-compatible {} in its place.",
2852 std_font.pdf_name(),
2853 lib,
2854 ));
2855 }
2856 }
2857
2858 let obj_id = builder.objects.len();
2859 let widths_str: String = metrics
2862 .widths
2863 .iter()
2864 .map(|w| w.to_string())
2865 .collect::<Vec<_>>()
2866 .join(" ");
2867 let font_dict = format!(
2868 "<< /Type /Font /Subtype /Type1 /BaseFont /{} \
2869 /Encoding /WinAnsiEncoding \
2870 /FirstChar 32 /LastChar 255 /Widths [{}] >>",
2871 std_font.pdf_name(),
2872 widths_str,
2873 );
2874 builder.objects.push(PdfObject {
2875 id: obj_id,
2876 data: font_dict.into_bytes(),
2877 });
2878 builder.font_objects.push((key.clone(), obj_id));
2879 }
2880 FontData::Custom { data, .. } => {
2881 let usage = font_usage_map.get(key);
2882 let used_glyph_ids = usage.map(|u| &u.glyph_ids);
2883 let used_chars = usage.map(|u| &u.chars);
2884 let glyph_to_char = usage.map(|u| &u.glyph_to_char);
2885 let type0_obj_id = Self::write_custom_font_objects(
2886 builder,
2887 key,
2888 data,
2889 used_glyph_ids.cloned().unwrap_or_default(),
2890 used_chars.cloned().unwrap_or_default(),
2891 glyph_to_char.cloned().unwrap_or_default(),
2892 )?;
2893 builder.font_objects.push((key.clone(), type0_obj_id));
2894 }
2895 }
2896 }
2897
2898 Ok(())
2899 }
2900
2901 fn collect_font_usage(
2903 elements: &[LayoutElement],
2904 font_usage: &mut HashMap<FontKey, FontUsage>,
2905 ) {
2906 for element in elements {
2907 let lines_opt = match &element.draw {
2908 DrawCommand::Text { lines, .. } => Some(lines),
2909 DrawCommand::Watermark { lines, .. } => Some(lines),
2910 _ => None,
2911 };
2912 if let Some(lines) = lines_opt {
2913 for line in lines {
2914 for glyph in &line.glyphs {
2915 let italic =
2916 matches!(glyph.font_style, FontStyle::Italic | FontStyle::Oblique);
2917 let key = FontKey {
2918 family: glyph.font_family.to_string(),
2919 weight: glyph.font_weight,
2920 italic,
2921 };
2922 let usage = font_usage.entry(key).or_insert_with(|| FontUsage {
2923 chars: HashSet::new(),
2924 glyph_ids: HashSet::new(),
2925 glyph_to_char: HashMap::new(),
2926 });
2927 usage.chars.insert(glyph.char_value);
2928 if glyph.char_value == PAGE_NUMBER_SENTINEL
2933 || glyph.char_value == TOTAL_PAGES_SENTINEL
2934 {
2935 usage.chars.extend('0'..='9');
2936 }
2937 usage.glyph_ids.insert(glyph.glyph_id);
2938 usage
2940 .glyph_to_char
2941 .entry(glyph.glyph_id)
2942 .or_insert(glyph.char_value);
2943 if let Some(ref ct) = glyph.cluster_text {
2945 if let Some(first_char) = ct.chars().next() {
2947 usage
2948 .glyph_to_char
2949 .entry(glyph.glyph_id)
2950 .or_insert(first_char);
2951 }
2952 }
2953 }
2954 }
2955 }
2956 Self::collect_font_usage(&element.children, font_usage);
2957 }
2958 }
2959
2960 fn register_shadings(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2966 for (page_idx, page) in pages.iter().enumerate() {
2967 let mut counter = 0usize;
2968 Self::collect_shadings_recursive(&page.elements, page_idx, &mut counter, builder);
2969 }
2970 }
2971
2972 fn collect_shadings_recursive(
2973 elements: &[LayoutElement],
2974 page_idx: usize,
2975 counter: &mut usize,
2976 builder: &mut PdfBuilder,
2977 ) {
2978 for element in elements {
2979 if let DrawCommand::Rect {
2980 background_gradient: Some(gradient),
2981 ..
2982 } = &element.draw
2983 {
2984 let ordinal = *counter;
2985 *counter += 1;
2986 let (obj_id, name) =
2987 Self::write_shading_objects(builder, gradient, element, ordinal);
2988 builder
2989 .shading_map
2990 .insert((page_idx, ordinal), (obj_id, name));
2991 }
2992 Self::collect_shadings_recursive(&element.children, page_idx, counter, builder);
2993 }
2994 }
2995
2996 fn write_shading_objects(
3002 builder: &mut PdfBuilder,
3003 gradient: &crate::style::Background,
3004 element: &LayoutElement,
3005 ordinal: usize,
3006 ) -> (usize, String) {
3007 use crate::style::Background;
3008 use crate::style::GradientStop;
3009
3010 let black = Color {
3014 r: 0.0,
3015 g: 0.0,
3016 b: 0.0,
3017 a: 1.0,
3018 };
3019 let stops: Vec<GradientStop> = match gradient {
3020 Background::Color(c) => vec![
3021 GradientStop {
3022 position: 0.0,
3023 color: *c,
3024 },
3025 GradientStop {
3026 position: 1.0,
3027 color: *c,
3028 },
3029 ],
3030 Background::Linear(g) => normalize_gradient_stops(&g.stops, black),
3031 Background::Radial(g) => normalize_gradient_stops(&g.stops, black),
3032 };
3033
3034 let function_id = if stops.len() <= 2 {
3038 let c0 = stops.first().map(|s| s.color).unwrap_or(black);
3039 let c1 = stops.last().map(|s| s.color).unwrap_or(c0);
3040 let id = builder.objects.len();
3041 let data = format!(
3042 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
3043 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
3044 );
3045 builder.objects.push(PdfObject {
3046 id,
3047 data: data.into_bytes(),
3048 });
3049 id
3050 } else {
3051 let mut sub_ids: Vec<usize> = Vec::with_capacity(stops.len() - 1);
3053 for window in stops.windows(2) {
3054 let c0 = window[0].color;
3055 let c1 = window[1].color;
3056 let id = builder.objects.len();
3057 let data = format!(
3058 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
3059 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
3060 );
3061 builder.objects.push(PdfObject {
3062 id,
3063 data: data.into_bytes(),
3064 });
3065 sub_ids.push(id);
3066 }
3067 let bounds: Vec<String> = stops[1..stops.len() - 1]
3071 .iter()
3072 .map(|s| format!("{:.4}", s.position))
3073 .collect();
3074 let encode: Vec<&str> = (0..sub_ids.len()).map(|_| "0 1").collect();
3075 let functions: Vec<String> = sub_ids.iter().map(|i| format!("{} 0 R", i)).collect();
3076 let id = builder.objects.len();
3077 let data = format!(
3078 "<< /FunctionType 3 /Domain [0 1] /Functions [{}] /Bounds [{}] /Encode [{}] >>",
3079 functions.join(" "),
3080 bounds.join(" "),
3081 encode.join(" "),
3082 );
3083 builder.objects.push(PdfObject {
3084 id,
3085 data: data.into_bytes(),
3086 });
3087 id
3088 };
3089
3090 let _ = element.x;
3094 let _ = element.y;
3095 let w = element.width;
3096 let h = element.height;
3097
3098 let shading_id = builder.objects.len();
3099 let shading_data = match gradient {
3100 Background::Linear(g) => {
3101 let theta = g.angle_deg.to_radians();
3111 let dx = theta.sin();
3112 let dy = theta.cos();
3113 let axis_len = w * dx.abs() + h * dy.abs();
3116 let cx_rel = w / 2.0;
3119 let cy_rel = h / 2.0;
3120 let half = axis_len / 2.0;
3121 let x0 = cx_rel - dx * half;
3122 let y0 = cy_rel - dy * half;
3123 let x1 = cx_rel + dx * half;
3124 let y1 = cy_rel + dy * half;
3125 format!(
3126 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
3127 x0, y0, x1, y1, function_id,
3128 )
3129 }
3130 Background::Radial(_) => {
3131 let cx_rel = w / 2.0;
3134 let cy_rel = h / 2.0;
3135 let r_outer = (w / 2.0).max(h / 2.0);
3136 format!(
3137 "<< /ShadingType 3 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} 0 {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
3138 cx_rel, cy_rel, cx_rel, cy_rel, r_outer, function_id,
3139 )
3140 }
3141 Background::Color(_) => {
3142 format!(
3146 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 0 0] /Function {} 0 R /Extend [true true] >>",
3147 function_id,
3148 )
3149 }
3150 };
3151 builder.objects.push(PdfObject {
3152 id: shading_id,
3153 data: shading_data.into_bytes(),
3154 });
3155 (shading_id, format!("Sh{}", ordinal))
3156 }
3157
3158 fn register_page_background_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3162 for (page_idx, page) in pages.iter().enumerate() {
3163 let Some(src) = &page.config.background_image else {
3164 continue;
3165 };
3166 if let Some(&entry) = builder.page_background_url_cache.get(src) {
3168 builder.page_background_image_map.insert(page_idx, entry);
3169 continue;
3170 }
3171 match crate::image_loader::load_image(src) {
3174 Ok(image_data) => {
3175 let img_idx = builder.image_objects.len();
3176 let dims = (img_idx, image_data.width_px, image_data.height_px);
3177 let xobj_id = Self::write_image_xobject(builder, &image_data);
3178 builder.image_objects.push(xobj_id);
3179 builder.page_background_image_map.insert(page_idx, dims);
3180 builder.page_background_url_cache.insert(src.clone(), dims);
3181 }
3182 Err(e) => {
3183 eprintln!("[forme] page background image failed to load: {}", e);
3184 }
3185 }
3186 }
3187 }
3188
3189 fn write_page_background(
3194 &self,
3195 stream: &mut String,
3196 page: &LayoutPage,
3197 page_bg: (usize, u32, u32),
3198 builder: &PdfBuilder,
3199 ) {
3200 use crate::model::{BackgroundPosition, BackgroundSize};
3201 let (img_idx, iw_px, ih_px) = page_bg;
3202 let page_w = page.width;
3203 let page_h = page.height;
3204 let iw = iw_px as f64;
3205 let ih = ih_px as f64;
3206
3207 let size = page.config.background_size.unwrap_or_default();
3208 let (dest_w, dest_h) = match size {
3209 BackgroundSize::Fill => (page_w, page_h),
3210 BackgroundSize::Cover => {
3211 let s = (page_w / iw).max(page_h / ih);
3212 (iw * s, ih * s)
3213 }
3214 BackgroundSize::Contain => {
3215 let s = (page_w / iw).min(page_h / ih);
3216 (iw * s, ih * s)
3217 }
3218 };
3219
3220 let position = page.config.background_position.unwrap_or_default();
3224 let (dest_x, dest_y) = match position {
3227 BackgroundPosition::TopLeft => (0.0, page_h - dest_h),
3228 BackgroundPosition::TopRight => (page_w - dest_w, page_h - dest_h),
3229 BackgroundPosition::BottomLeft => (0.0, 0.0),
3230 BackgroundPosition::BottomRight => (page_w - dest_w, 0.0),
3231 BackgroundPosition::Center => ((page_w - dest_w) / 2.0, (page_h - dest_h) / 2.0),
3232 };
3233
3234 let opacity = page.config.background_opacity.unwrap_or(1.0);
3236 let needs_opacity = opacity < 1.0;
3237 if needs_opacity {
3238 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
3239 let _ = writeln!(stream, "q\n/{} gs", gs_name);
3240 } else {
3241 let _ = writeln!(stream, "q");
3242 }
3243 } else {
3244 let _ = writeln!(stream, "q");
3245 }
3246 let _ = writeln!(
3249 stream,
3250 "{:.2} 0 0 {:.2} {:.2} {:.2} cm\n/Im{} Do\nQ",
3251 dest_w, dest_h, dest_x, dest_y, img_idx,
3252 );
3253 }
3254
3255 fn register_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3257 for (page_idx, page) in pages.iter().enumerate() {
3258 let mut element_counter = 0usize;
3259 Self::collect_images_recursive(&page.elements, page_idx, &mut element_counter, builder);
3260 }
3261 }
3262
3263 fn collect_images_recursive(
3264 elements: &[LayoutElement],
3265 page_idx: usize,
3266 element_counter: &mut usize,
3267 builder: &mut PdfBuilder,
3268 ) {
3269 for element in elements {
3270 match &element.draw {
3271 DrawCommand::Image { image_data } => {
3272 let elem_idx = *element_counter;
3273 *element_counter += 1;
3274
3275 let img_idx = builder.image_objects.len();
3276 let xobj_id = Self::write_image_xobject(builder, image_data);
3277 builder.image_objects.push(xobj_id);
3278 builder
3279 .image_index_map
3280 .insert((page_idx, elem_idx), img_idx);
3281 }
3282 DrawCommand::ImagePlaceholder => {
3283 *element_counter += 1;
3284 }
3285 _ => {
3286 Self::collect_images_recursive(
3287 &element.children,
3288 page_idx,
3289 element_counter,
3290 builder,
3291 );
3292 }
3293 }
3294 }
3295 }
3296
3297 fn register_ext_gstates(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
3299 let mut unique_opacities: Vec<f64> = Vec::new();
3300 for page in pages {
3301 Self::collect_opacities_recursive(&page.elements, &mut unique_opacities);
3302 if let Some(o) = page.config.background_opacity {
3304 if o < 1.0 {
3305 unique_opacities.push(o);
3306 }
3307 }
3308 }
3309 unique_opacities.sort_by(|a, b| a.partial_cmp(b).unwrap());
3310 unique_opacities.dedup();
3311
3312 for (idx, &opacity) in unique_opacities.iter().enumerate() {
3313 let obj_id = builder.objects.len();
3314 let gs_name = format!("GS{}", idx);
3315 let obj_data = format!(
3316 "<< /Type /ExtGState /ca {:.4} /CA {:.4} >>",
3317 opacity, opacity
3318 );
3319 builder.objects.push(PdfObject {
3320 id: obj_id,
3321 data: obj_data.into_bytes(),
3322 });
3323 let key = opacity.to_bits();
3324 builder.ext_gstate_map.insert(key, (obj_id, gs_name));
3325 }
3326 }
3327
3328 fn collect_opacities_recursive(elements: &[LayoutElement], opacities: &mut Vec<f64>) {
3329 for element in elements {
3330 if element.opacity < 1.0 {
3337 opacities.push(element.opacity);
3338 }
3339 if let DrawCommand::Rect {
3343 box_shadow: Some(shadow),
3344 ..
3345 } = &element.draw
3346 {
3347 if shadow.color.a < 1.0 {
3348 opacities.push(shadow.color.a);
3349 }
3350 }
3351 match &element.draw {
3352 DrawCommand::Rect { opacity, .. }
3353 | DrawCommand::Text { opacity, .. }
3354 | DrawCommand::Watermark { opacity, .. }
3355 if *opacity < 1.0 =>
3356 {
3357 opacities.push(*opacity);
3358 }
3359 DrawCommand::Chart { primitives } => {
3360 for prim in primitives {
3361 if let crate::chart::ChartPrimitive::FilledPath { opacity, .. } = prim {
3362 if *opacity < 1.0 {
3363 opacities.push(*opacity);
3364 }
3365 }
3366 }
3367 }
3368 DrawCommand::Svg { commands, .. } => {
3369 for cmd in commands {
3370 if let crate::svg::SvgCommand::SetOpacity(opacity) = cmd {
3371 if *opacity < 1.0 {
3372 opacities.push(*opacity);
3373 }
3374 }
3375 }
3376 }
3377 _ => {}
3378 }
3379 Self::collect_opacities_recursive(&element.children, opacities);
3380 }
3381 }
3382
3383 fn build_ext_gstate_resource_dict(&self, builder: &PdfBuilder) -> String {
3385 if builder.ext_gstate_map.is_empty() {
3386 return String::new();
3387 }
3388 let mut entries: Vec<(&String, usize)> = builder
3389 .ext_gstate_map
3390 .values()
3391 .map(|(obj_id, name)| (name, *obj_id))
3392 .collect();
3393 entries.sort_by_key(|(name, _)| (*name).clone());
3394 entries
3395 .iter()
3396 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3397 .collect::<Vec<_>>()
3398 .join(" ")
3399 }
3400
3401 fn write_image_xobject(
3404 builder: &mut PdfBuilder,
3405 image: &crate::image_loader::LoadedImage,
3406 ) -> usize {
3407 use crate::image_loader::{ImagePixelData, JpegColorSpace};
3408
3409 match &image.pixel_data {
3410 ImagePixelData::Jpeg { data, color_space } => {
3411 let color_space_str = match color_space {
3412 JpegColorSpace::DeviceRGB => "/DeviceRGB",
3413 JpegColorSpace::DeviceGray => "/DeviceGray",
3414 };
3415
3416 let obj_id = builder.objects.len();
3417 let mut obj_data: Vec<u8> = Vec::new();
3418 let _ = write!(
3419 obj_data,
3420 "<< /Type /XObject /Subtype /Image \
3421 /Width {} /Height {} \
3422 /ColorSpace {} \
3423 /BitsPerComponent 8 \
3424 /Filter /DCTDecode \
3425 /Length {} >>\nstream\n",
3426 image.width_px,
3427 image.height_px,
3428 color_space_str,
3429 data.len()
3430 );
3431 obj_data.extend_from_slice(data);
3432 obj_data.extend_from_slice(b"\nendstream");
3433 builder.objects.push(PdfObject {
3434 id: obj_id,
3435 data: obj_data,
3436 });
3437 obj_id
3438 }
3439
3440 ImagePixelData::Decoded { rgb, alpha } => {
3441 let smask_id = alpha.as_ref().map(|alpha_data| {
3443 let compressed_alpha = compress_to_vec_zlib(alpha_data, 6);
3444 let smask_obj_id = builder.objects.len();
3445 let mut smask_data: Vec<u8> = Vec::new();
3446 let _ = write!(
3447 smask_data,
3448 "<< /Type /XObject /Subtype /Image \
3449 /Width {} /Height {} \
3450 /ColorSpace /DeviceGray \
3451 /BitsPerComponent 8 \
3452 /Filter /FlateDecode \
3453 /Length {} >>\nstream\n",
3454 image.width_px,
3455 image.height_px,
3456 compressed_alpha.len()
3457 );
3458 smask_data.extend_from_slice(&compressed_alpha);
3459 smask_data.extend_from_slice(b"\nendstream");
3460 builder.objects.push(PdfObject {
3461 id: smask_obj_id,
3462 data: smask_data,
3463 });
3464 smask_obj_id
3465 });
3466
3467 let compressed_rgb = compress_to_vec_zlib(rgb, 6);
3469 let obj_id = builder.objects.len();
3470 let mut obj_data: Vec<u8> = Vec::new();
3471
3472 let smask_ref = smask_id
3473 .map(|id| format!(" /SMask {} 0 R", id))
3474 .unwrap_or_default();
3475
3476 let _ = write!(
3477 obj_data,
3478 "<< /Type /XObject /Subtype /Image \
3479 /Width {} /Height {} \
3480 /ColorSpace /DeviceRGB \
3481 /BitsPerComponent 8 \
3482 /Filter /FlateDecode \
3483 /Length {}{} >>\nstream\n",
3484 image.width_px,
3485 image.height_px,
3486 compressed_rgb.len(),
3487 smask_ref
3488 );
3489 obj_data.extend_from_slice(&compressed_rgb);
3490 obj_data.extend_from_slice(b"\nendstream");
3491 builder.objects.push(PdfObject {
3492 id: obj_id,
3493 data: obj_data,
3494 });
3495 obj_id
3496 }
3497 }
3498 }
3499
3500 fn build_shading_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3504 let mut entries: Vec<(String, usize)> = builder
3505 .shading_map
3506 .iter()
3507 .filter(|(&(p, _), _)| p == page_idx)
3508 .map(|(_, (obj_id, name))| (name.clone(), *obj_id))
3509 .collect();
3510 if entries.is_empty() {
3511 return String::new();
3512 }
3513 entries.sort_by(|a, b| a.0.cmp(&b.0));
3514 entries
3515 .iter()
3516 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3517 .collect::<Vec<_>>()
3518 .join(" ")
3519 }
3520
3521 fn build_xobject_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3522 let mut entries: Vec<(usize, usize)> = Vec::new();
3523 for (&(pidx, _), &img_idx) in &builder.image_index_map {
3524 if pidx == page_idx {
3525 let obj_id = builder.image_objects[img_idx];
3526 entries.push((img_idx, obj_id));
3527 }
3528 }
3529 if let Some(&(img_idx, _, _)) = builder.page_background_image_map.get(&page_idx) {
3532 let obj_id = builder.image_objects[img_idx];
3533 entries.push((img_idx, obj_id));
3534 }
3535 if entries.is_empty() {
3536 return String::new();
3537 }
3538 entries.sort_by_key(|(idx, _)| *idx);
3539 entries.dedup();
3540 entries
3541 .iter()
3542 .map(|(idx, obj_id)| format!("/Im{} {} 0 R", idx, obj_id))
3543 .collect::<Vec<_>>()
3544 .join(" ")
3545 }
3546
3547 fn write_custom_font_objects(
3554 builder: &mut PdfBuilder,
3555 key: &FontKey,
3556 ttf_data: &[u8],
3557 used_glyph_ids: HashSet<u16>,
3558 used_chars: HashSet<char>,
3559 glyph_to_char_map: HashMap<u16, char>,
3560 ) -> Result<usize, FormeError> {
3561 let face = ttf_parser::Face::parse(ttf_data, 0).map_err(|e| {
3562 FormeError::FontError(format!(
3563 "Failed to parse TTF data for font '{}': {}",
3564 key.family, e
3565 ))
3566 })?;
3567
3568 let units_per_em = face.units_per_em();
3569 let ascender = face.ascender();
3570 let descender = face.descender();
3571
3572 let mut char_to_orig_gid: HashMap<char, u16> = HashMap::new();
3574 for &ch in &used_chars {
3575 if let Some(gid) = face.glyph_index(ch) {
3576 char_to_orig_gid.insert(ch, gid.0);
3577 }
3578 }
3579
3580 let mut all_orig_gids: HashSet<u16> = used_glyph_ids.clone();
3584 for &gid in char_to_orig_gid.values() {
3585 all_orig_gids.insert(gid);
3586 }
3587
3588 let (embed_ttf, gid_remap) = match subset_ttf(ttf_data, &all_orig_gids) {
3590 Ok(subset_result) => (subset_result.ttf_data, subset_result.gid_remap),
3591 Err(_) => {
3592 let identity: HashMap<u16, u16> =
3594 all_orig_gids.iter().map(|&gid| (gid, gid)).collect();
3595 (ttf_data.to_vec(), identity)
3596 }
3597 };
3598
3599 let char_to_gid: HashMap<char, u16> = char_to_orig_gid
3601 .iter()
3602 .filter_map(|(&ch, &orig_gid)| gid_remap.get(&orig_gid).map(|&new_gid| (ch, new_gid)))
3603 .collect();
3604
3605 let gid_remap_for_embed = gid_remap.clone();
3607
3608 let mut new_gid_to_char: HashMap<u16, char> = HashMap::new();
3610 for (&orig_gid, &ch) in &glyph_to_char_map {
3612 if let Some(&new_gid) = gid_remap.get(&orig_gid) {
3613 new_gid_to_char.entry(new_gid).or_insert(ch);
3614 }
3615 }
3616 for (&ch, &new_gid) in &char_to_gid {
3618 new_gid_to_char.entry(new_gid).or_insert(ch);
3619 }
3620
3621 let pdf_font_name = Self::sanitize_font_name(&key.family, key.weight, key.italic);
3622
3623 let compressed_ttf = compress_to_vec_zlib(&embed_ttf, 6);
3625 let fontfile2_id = builder.objects.len();
3626 let mut fontfile2_data: Vec<u8> = Vec::new();
3627 let _ = write!(
3628 fontfile2_data,
3629 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
3630 compressed_ttf.len(),
3631 embed_ttf.len()
3632 );
3633 fontfile2_data.extend_from_slice(&compressed_ttf);
3634 fontfile2_data.extend_from_slice(b"\nendstream");
3635 builder.objects.push(PdfObject {
3636 id: fontfile2_id,
3637 data: fontfile2_data,
3638 });
3639
3640 let subset_face = ttf_parser::Face::parse(&embed_ttf, 0).unwrap_or_else(|_| face.clone());
3642 let subset_upem = subset_face.units_per_em();
3643
3644 let font_descriptor_id = builder.objects.len();
3646 let bbox = face.global_bounding_box();
3647 let scale = 1000.0 / units_per_em as f64;
3648 let bbox_str = format!(
3649 "[{} {} {} {}]",
3650 (bbox.x_min as f64 * scale) as i32,
3651 (bbox.y_min as f64 * scale) as i32,
3652 (bbox.x_max as f64 * scale) as i32,
3653 (bbox.y_max as f64 * scale) as i32,
3654 );
3655
3656 let flags = 4u32;
3657 let cap_height = face.capital_height().unwrap_or(ascender) as f64 * scale;
3658 let stem_v = if key.weight >= 700 { 120 } else { 80 };
3659
3660 let font_descriptor_dict = format!(
3661 "<< /Type /FontDescriptor /FontName /{} /Flags {} \
3662 /FontBBox {} /ItalicAngle {} \
3663 /Ascent {} /Descent {} /CapHeight {} /StemV {} \
3664 /FontFile2 {} 0 R >>",
3665 pdf_font_name,
3666 flags,
3667 bbox_str,
3668 if key.italic { -12 } else { 0 },
3669 (ascender as f64 * scale) as i32,
3670 (descender as f64 * scale) as i32,
3671 cap_height as i32,
3672 stem_v,
3673 fontfile2_id,
3674 );
3675 builder.objects.push(PdfObject {
3676 id: font_descriptor_id,
3677 data: font_descriptor_dict.into_bytes(),
3678 });
3679
3680 let cidfont_id = builder.objects.len();
3682 let w_array = Self::build_w_array_from_gids(&gid_remap, &subset_face, subset_upem);
3684 let default_width = subset_face
3685 .glyph_hor_advance(ttf_parser::GlyphId(0))
3686 .map(|adv| (adv as f64 * 1000.0 / subset_upem as f64) as u32)
3687 .unwrap_or(1000);
3688 let cidfont_dict = format!(
3689 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{} \
3690 /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> \
3691 /FontDescriptor {} 0 R /DW {} /W {} \
3692 /CIDToGIDMap /Identity >>",
3693 pdf_font_name, font_descriptor_id, default_width, w_array,
3694 );
3695 builder.objects.push(PdfObject {
3696 id: cidfont_id,
3697 data: cidfont_dict.into_bytes(),
3698 });
3699
3700 let tounicode_id = builder.objects.len();
3702 let cmap_content = Self::build_tounicode_cmap_from_gids(&new_gid_to_char, &pdf_font_name);
3703 let compressed_cmap = compress_to_vec_zlib(cmap_content.as_bytes(), 6);
3704 let mut tounicode_data: Vec<u8> = Vec::new();
3705 let _ = write!(
3706 tounicode_data,
3707 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
3708 compressed_cmap.len()
3709 );
3710 tounicode_data.extend_from_slice(&compressed_cmap);
3711 tounicode_data.extend_from_slice(b"\nendstream");
3712 builder.objects.push(PdfObject {
3713 id: tounicode_id,
3714 data: tounicode_data,
3715 });
3716
3717 let type0_id = builder.objects.len();
3719 let type0_dict = format!(
3720 "<< /Type /Font /Subtype /Type0 /BaseFont /{} \
3721 /Encoding /Identity-H \
3722 /DescendantFonts [{} 0 R] \
3723 /ToUnicode {} 0 R >>",
3724 pdf_font_name, cidfont_id, tounicode_id,
3725 );
3726 builder.objects.push(PdfObject {
3727 id: type0_id,
3728 data: type0_dict.into_bytes(),
3729 });
3730
3731 builder.custom_font_data.insert(
3733 key.clone(),
3734 CustomFontEmbedData {
3735 ttf_data: embed_ttf,
3736 gid_remap: gid_remap_for_embed,
3737 glyph_to_char: glyph_to_char_map,
3738 char_to_gid,
3739 units_per_em,
3740 ascender,
3741 descender,
3742 },
3743 );
3744
3745 Ok(type0_id)
3746 }
3747
3748 fn build_w_array_from_gids(
3750 gid_remap: &HashMap<u16, u16>,
3751 face: &ttf_parser::Face,
3752 units_per_em: u16,
3753 ) -> String {
3754 let scale = 1000.0 / units_per_em as f64;
3755
3756 let mut entries: Vec<(u16, u32)> = Vec::new();
3757 let mut seen_gids: HashSet<u16> = HashSet::new();
3758
3759 for &new_gid in gid_remap.values() {
3760 if seen_gids.contains(&new_gid) {
3761 continue;
3762 }
3763 seen_gids.insert(new_gid);
3764 let advance = face
3765 .glyph_hor_advance(ttf_parser::GlyphId(new_gid))
3766 .unwrap_or(0);
3767 let width = (advance as f64 * scale) as u32;
3768 entries.push((new_gid, width));
3769 }
3770
3771 entries.sort_by_key(|(gid, _)| *gid);
3772
3773 let mut result = String::from("[");
3775 for (gid, width) in &entries {
3776 let _ = write!(result, " {} [{}]", gid, width);
3777 }
3778 result.push_str(" ]");
3779 result
3780 }
3781
3782 fn build_tounicode_cmap_from_gids(gid_to_char: &HashMap<u16, char>, font_name: &str) -> String {
3784 let mut gid_to_unicode: Vec<(u16, u32)> = gid_to_char
3785 .iter()
3786 .map(|(&gid, &ch)| (gid, ch as u32))
3787 .collect();
3788 gid_to_unicode.sort_by_key(|(gid, _)| *gid);
3789
3790 let mut cmap = String::new();
3791 let _ = writeln!(cmap, "/CIDInit /ProcSet findresource begin");
3792 let _ = writeln!(cmap, "12 dict begin");
3793 let _ = writeln!(cmap, "begincmap");
3794 let _ = writeln!(cmap, "/CIDSystemInfo");
3795 let _ = writeln!(
3796 cmap,
3797 "<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def"
3798 );
3799 let _ = writeln!(cmap, "/CMapName /{}-UTF16 def", font_name);
3800 let _ = writeln!(cmap, "/CMapType 2 def");
3801 let _ = writeln!(cmap, "1 begincodespacerange");
3802 let _ = writeln!(cmap, "<0000> <FFFF>");
3803 let _ = writeln!(cmap, "endcodespacerange");
3804
3805 for chunk in gid_to_unicode.chunks(100) {
3807 let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
3808 for &(gid, unicode) in chunk {
3809 let _ = writeln!(cmap, "<{:04X}> <{:04X}>", gid, unicode);
3810 }
3811 let _ = writeln!(cmap, "endbfchar");
3812 }
3813
3814 let _ = writeln!(cmap, "endcmap");
3815 let _ = writeln!(cmap, "CMapName currentdict /CMap defineresource pop");
3816 let _ = writeln!(cmap, "end");
3817 let _ = writeln!(cmap, "end");
3818
3819 cmap
3820 }
3821
3822 fn sanitize_font_name(family: &str, weight: u32, italic: bool) -> String {
3825 let mut name: String = family
3826 .chars()
3827 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
3828 .collect();
3829
3830 if weight >= 700 {
3831 name.push_str("-Bold");
3832 }
3833 if italic {
3834 name.push_str("-Italic");
3835 }
3836
3837 if name.is_empty() {
3839 name = "CustomFont".to_string();
3840 }
3841
3842 name
3843 }
3844
3845 fn build_font_resource_dict(&self, font_objects: &[(FontKey, usize)]) -> String {
3846 font_objects
3847 .iter()
3848 .enumerate()
3849 .map(|(i, (_, obj_id))| format!("/F{} {} 0 R", i, obj_id))
3850 .collect::<Vec<_>>()
3851 .join(" ")
3852 }
3853
3854 fn font_index(
3856 &self,
3857 family: &str,
3858 weight: u32,
3859 font_style: FontStyle,
3860 font_objects: &[(FontKey, usize)],
3861 ) -> usize {
3862 let italic = matches!(font_style, FontStyle::Italic | FontStyle::Oblique);
3863
3864 for (i, (key, _)) in font_objects.iter().enumerate() {
3866 if key.family == family && key.weight == weight && key.italic == italic {
3867 return i;
3868 }
3869 }
3870
3871 let snapped = if weight >= 600 { 700 } else { 400 };
3873 for (i, (key, _)) in font_objects.iter().enumerate() {
3874 if key.family == family && key.weight == snapped && key.italic == italic {
3875 return i;
3876 }
3877 }
3878
3879 for (i, (key, _)) in font_objects.iter().enumerate() {
3881 if key.family == "Helvetica" && key.weight == snapped && key.italic == italic {
3882 return i;
3883 }
3884 }
3885
3886 0
3888 }
3889
3890 fn group_glyphs_by_style(glyphs: &[PositionedGlyph]) -> Vec<Vec<&PositionedGlyph>> {
3893 if glyphs.is_empty() {
3894 return vec![];
3895 }
3896
3897 let mut groups: Vec<Vec<&PositionedGlyph>> = Vec::new();
3898 let mut current_group: Vec<&PositionedGlyph> = vec![&glyphs[0]];
3899
3900 for glyph in &glyphs[1..] {
3901 let prev = current_group.last().unwrap();
3902 let same_style = glyph.font_family == prev.font_family
3903 && glyph.font_weight == prev.font_weight
3904 && std::mem::discriminant(&glyph.font_style)
3905 == std::mem::discriminant(&prev.font_style)
3906 && (glyph.font_size - prev.font_size).abs() < 0.01
3907 && Self::colors_equal(&glyph.color, &prev.color)
3908 && std::mem::discriminant(&glyph.text_decoration)
3909 == std::mem::discriminant(&prev.text_decoration);
3910
3911 if same_style {
3912 current_group.push(glyph);
3913 } else {
3914 groups.push(current_group);
3915 current_group = vec![glyph];
3916 }
3917 }
3918 groups.push(current_group);
3919 groups
3920 }
3921
3922 fn colors_equal(a: &Option<Color>, b: &Option<Color>) -> bool {
3923 match (a, b) {
3924 (None, None) => true,
3925 (Some(ca), Some(cb)) => {
3926 (ca.r - cb.r).abs() < 0.001
3927 && (ca.g - cb.g).abs() < 0.001
3928 && (ca.b - cb.b).abs() < 0.001
3929 && (ca.a - cb.a).abs() < 0.001
3930 }
3931 _ => false,
3932 }
3933 }
3934
3935 fn collect_link_annotations(
3939 elements: &[LayoutElement],
3940 page_height: f64,
3941 annotations: &mut Vec<LinkAnnotation>,
3942 ) {
3943 for element in elements {
3944 if let Some(ref href) = element.href {
3945 if !href.is_empty() {
3946 let pdf_y = page_height - element.y - element.height;
3947 annotations.push(LinkAnnotation {
3948 x: element.x,
3949 y: pdf_y,
3950 width: element.width,
3951 height: element.height,
3952 href: href.clone(),
3953 });
3954 continue;
3956 }
3957 }
3958 Self::collect_link_annotations(&element.children, page_height, annotations);
3959 }
3960 }
3961
3962 fn collect_form_fields(
3964 elements: &[LayoutElement],
3965 page_height: f64,
3966 page_idx: usize,
3967 fields: &mut Vec<FormFieldData>,
3968 ) {
3969 for element in elements {
3970 if let DrawCommand::FormField {
3971 ref field_type,
3972 ref name,
3973 } = element.draw
3974 {
3975 let pdf_y = page_height - element.y - element.height;
3976 fields.push(FormFieldData {
3977 field_type: field_type.clone(),
3978 name: name.clone(),
3979 x: element.x,
3980 y: pdf_y,
3981 width: element.width,
3982 height: element.height,
3983 page_idx,
3984 });
3985 }
3986 Self::collect_form_fields(&element.children, page_height, page_idx, fields);
3987 }
3988 }
3989
3990 fn collect_bookmarks(
3992 elements: &[LayoutElement],
3993 page_height: f64,
3994 page_obj_id: usize,
3995 bookmarks: &mut Vec<PdfBookmark>,
3996 ) {
3997 for element in elements {
3998 if let Some(ref title) = element.bookmark {
3999 let y_pdf = page_height - element.y;
4000 bookmarks.push(PdfBookmark {
4001 title: title.clone(),
4002 page_obj_id,
4003 y_pdf,
4004 });
4005 }
4006 Self::collect_bookmarks(&element.children, page_height, page_obj_id, bookmarks);
4007 }
4008 }
4009
4010 fn write_outline_tree(&self, builder: &mut PdfBuilder, bookmarks: &[PdfBookmark]) -> usize {
4013 let outlines_id = builder.objects.len();
4015 builder.objects.push(PdfObject {
4016 id: outlines_id,
4017 data: vec![],
4018 });
4019
4020 let mut item_ids: Vec<usize> = Vec::new();
4022 for _bm in bookmarks {
4023 let item_id = builder.objects.len();
4024 builder.objects.push(PdfObject {
4025 id: item_id,
4026 data: vec![],
4027 });
4028 item_ids.push(item_id);
4029 }
4030
4031 for (i, (bm, &item_id)) in bookmarks.iter().zip(item_ids.iter()).enumerate() {
4033 let mut dict = format!(
4034 "<< /Title ({}) /Parent {} 0 R /Dest [{} 0 R /XYZ 0 {:.2} null]",
4035 Self::escape_pdf_string(&bm.title),
4036 outlines_id,
4037 bm.page_obj_id,
4038 bm.y_pdf,
4039 );
4040 if i > 0 {
4041 let _ = write!(dict, " /Prev {} 0 R", item_ids[i - 1]);
4042 }
4043 if i + 1 < item_ids.len() {
4044 let _ = write!(dict, " /Next {} 0 R", item_ids[i + 1]);
4045 }
4046 dict.push_str(" >>");
4047 builder.objects[item_id].data = dict.into_bytes();
4048 }
4049
4050 let first_id = item_ids.first().copied().unwrap_or(0);
4052 let last_id = item_ids.last().copied().unwrap_or(0);
4053 let outlines_dict = format!(
4054 "<< /Type /Outlines /First {} 0 R /Last {} 0 R /Count {} >>",
4055 first_id,
4056 last_id,
4057 bookmarks.len()
4058 );
4059 builder.objects[outlines_id].data = outlines_dict.into_bytes();
4060
4061 outlines_id
4062 }
4063
4064 fn write_svg_commands(
4066 stream: &mut String,
4067 commands: &[SvgCommand],
4068 ext_gstate_map: &HashMap<u64, (usize, String)>,
4069 ) {
4070 for cmd in commands {
4071 match cmd {
4072 SvgCommand::MoveTo(x, y) => {
4073 let _ = writeln!(stream, "{:.2} {:.2} m", x, y);
4074 }
4075 SvgCommand::LineTo(x, y) => {
4076 let _ = writeln!(stream, "{:.2} {:.2} l", x, y);
4077 }
4078 SvgCommand::CurveTo(x1, y1, x2, y2, x3, y3) => {
4079 let _ = writeln!(
4080 stream,
4081 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4082 x1, y1, x2, y2, x3, y3
4083 );
4084 }
4085 SvgCommand::ClosePath => {
4086 let _ = writeln!(stream, "h");
4087 }
4088 SvgCommand::SetFill(r, g, b) => {
4089 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", r, g, b);
4090 }
4091 SvgCommand::SetFillNone => {
4092 }
4094 SvgCommand::SetStroke(r, g, b) => {
4095 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", r, g, b);
4096 }
4097 SvgCommand::SetStrokeNone => {
4098 }
4100 SvgCommand::SetStrokeWidth(w) => {
4101 let _ = writeln!(stream, "{:.2} w", w);
4102 }
4103 SvgCommand::Fill => {
4104 let _ = writeln!(stream, "f");
4105 }
4106 SvgCommand::Stroke => {
4107 let _ = writeln!(stream, "S");
4108 }
4109 SvgCommand::FillAndStroke => {
4110 let _ = writeln!(stream, "B");
4111 }
4112 SvgCommand::SetLineCap(cap) => {
4113 let _ = writeln!(stream, "{} J", cap);
4114 }
4115 SvgCommand::SetLineJoin(join) => {
4116 let _ = writeln!(stream, "{} j", join);
4117 }
4118 SvgCommand::SaveState => {
4119 let _ = writeln!(stream, "q");
4120 }
4121 SvgCommand::RestoreState => {
4122 let _ = writeln!(stream, "Q");
4123 }
4124 SvgCommand::SetOpacity(opacity) => {
4125 if let Some((_, gs_name)) = ext_gstate_map.get(&opacity.to_bits()) {
4126 let _ = writeln!(stream, "/{} gs", gs_name);
4127 }
4128 }
4129 }
4130 }
4131 }
4132
4133 pub(crate) fn escape_pdf_string(s: &str) -> String {
4135 s.replace('\\', "\\\\")
4136 .replace('(', "\\(")
4137 .replace(')', "\\)")
4138 }
4139
4140 fn decode_attachment_src(src: &str) -> Result<Vec<u8>, FormeError> {
4143 use base64::Engine as _;
4144 let b64 = src.rsplit_once(";base64,").map(|(_, d)| d).unwrap_or(src);
4145 base64::engine::general_purpose::STANDARD
4146 .decode(b64.trim())
4147 .map_err(|e| {
4148 FormeError::RenderError(format!(
4149 "attachment src is not valid base64 (expected base64 bytes or a data: URI): {e}"
4150 ))
4151 })
4152 }
4153
4154 fn mime_to_pdf_name(mime: &str) -> String {
4157 let mut out = String::with_capacity(mime.len() + 2);
4158 for b in mime.bytes() {
4159 let regular =
4160 b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'+' | b'\'' | b'"');
4161 if regular {
4162 out.push(b as char);
4163 } else {
4164 let _ = write!(out, "#{:02X}", b);
4165 }
4166 }
4167 out
4168 }
4169
4170 fn encode_winansi_text(s: &str) -> String {
4173 let mut result = String::with_capacity(s.len());
4174 for ch in s.chars() {
4175 let b = Self::unicode_to_winansi(ch).unwrap_or(b'?');
4176 match b {
4177 b'\\' => result.push_str("\\\\"),
4178 b'(' => result.push_str("\\("),
4179 b')' => result.push_str("\\)"),
4180 0x20..=0x7E => result.push(b as char),
4181 _ => {
4182 let _ = write!(result, "\\{:03o}", b);
4183 }
4184 }
4185 }
4186 result
4187 }
4188
4189 fn unicode_to_winansi(ch: char) -> Option<u8> {
4191 crate::font::unicode_to_winansi(ch)
4192 }
4193
4194 fn serialize(&self, builder: &PdfBuilder, info_obj_id: Option<usize>) -> Vec<u8> {
4196 let mut output: Vec<u8> = Vec::new();
4197 let mut offsets: Vec<usize> = vec![0; builder.objects.len()];
4198
4199 output.extend_from_slice(b"%PDF-1.7\n");
4201 output.extend_from_slice(b"%\xe2\xe3\xcf\xd3\n");
4202
4203 for (i, obj) in builder.objects.iter().enumerate().skip(1) {
4204 offsets[i] = output.len();
4205 let header = format!("{} 0 obj\n", i);
4206 output.extend_from_slice(header.as_bytes());
4207 output.extend_from_slice(&obj.data);
4208 output.extend_from_slice(b"\nendobj\n\n");
4209 }
4210
4211 let xref_offset = output.len();
4212 let _ = writeln!(output, "xref\n0 {}", builder.objects.len());
4213 let _ = writeln!(output, "0000000000 65535 f ");
4214 for offset in offsets.iter().skip(1) {
4215 let _ = writeln!(output, "{:010} 00000 n ", offset);
4216 }
4217
4218 let _ = write!(
4219 output,
4220 "trailer\n<< /Size {} /Root 1 0 R",
4221 builder.objects.len()
4222 );
4223 if let Some(info_id) = info_obj_id {
4224 let _ = write!(output, " /Info {} 0 R", info_id);
4225 }
4226 {
4232 use sha2::Digest as _;
4233 let digest = sha2::Sha256::digest(&output);
4234 let mut id_hex = String::with_capacity(32);
4235 for b in &digest[..16] {
4236 let _ = write!(id_hex, "{:02X}", b);
4237 }
4238 let _ = write!(output, " /ID [<{id_hex}> <{id_hex}>]");
4239 }
4240 let _ = writeln!(output, " >>\nstartxref\n{}\n%%EOF", xref_offset);
4241
4242 output
4243 }
4244}
4245
4246fn write_chart_primitive(
4251 stream: &mut String,
4252 prim: &crate::chart::ChartPrimitive,
4253 _chart_height: f64,
4254 builder: &PdfBuilder,
4255) {
4256 use crate::chart::{ChartPrimitive, TextAnchor};
4257 use crate::font::metrics::unicode_to_winansi;
4258
4259 match prim {
4260 ChartPrimitive::Rect { x, y, w, h, fill } => {
4261 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
4262 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re f", x, y, w, h);
4263 }
4264
4265 ChartPrimitive::Line {
4266 x1,
4267 y1,
4268 x2,
4269 y2,
4270 stroke,
4271 width,
4272 } => {
4273 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
4274 let _ = writeln!(stream, "{:.2} w", width);
4275 let _ = writeln!(stream, "{:.2} {:.2} m {:.2} {:.2} l S", x1, y1, x2, y2);
4276 }
4277
4278 ChartPrimitive::Polyline {
4279 points,
4280 stroke,
4281 width,
4282 } => {
4283 if points.len() < 2 {
4284 return;
4285 }
4286 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
4287 let _ = writeln!(stream, "{:.2} w", width);
4288 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
4289 for &(px, py) in &points[1..] {
4290 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
4291 }
4292 let _ = writeln!(stream, "S");
4293 }
4294
4295 ChartPrimitive::FilledPath {
4296 points,
4297 fill,
4298 opacity,
4299 } => {
4300 if points.len() < 3 {
4301 return;
4302 }
4303 let _ = writeln!(stream, "q");
4304 if *opacity < 1.0 {
4306 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
4307 let _ = writeln!(stream, "/{} gs", gs_name);
4308 }
4309 }
4310 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
4311 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
4312 for &(px, py) in &points[1..] {
4313 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
4314 }
4315 let _ = writeln!(stream, "h f");
4316 let _ = writeln!(stream, "Q");
4317 }
4318
4319 ChartPrimitive::Circle { cx, cy, r, fill } => {
4320 let kappa: f64 = 0.5523;
4322 let kr = kappa * r;
4323 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
4324 let _ = writeln!(stream, "{:.2} {:.2} m", cx + r, cy);
4325 let _ = writeln!(
4326 stream,
4327 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4328 cx + r,
4329 cy + kr,
4330 cx + kr,
4331 cy + r,
4332 cx,
4333 cy + r
4334 );
4335 let _ = writeln!(
4336 stream,
4337 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4338 cx - kr,
4339 cy + r,
4340 cx - r,
4341 cy + kr,
4342 cx - r,
4343 cy
4344 );
4345 let _ = writeln!(
4346 stream,
4347 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4348 cx - r,
4349 cy - kr,
4350 cx - kr,
4351 cy - r,
4352 cx,
4353 cy - r
4354 );
4355 let _ = writeln!(
4356 stream,
4357 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
4358 cx + kr,
4359 cy - r,
4360 cx + r,
4361 cy - kr,
4362 cx + r,
4363 cy
4364 );
4365 let _ = writeln!(stream, "f");
4366 }
4367
4368 ChartPrimitive::ArcSector {
4369 cx,
4370 cy,
4371 r,
4372 start_angle,
4373 end_angle,
4374 fill,
4375 } => {
4376 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
4377 let _ = writeln!(stream, "{:.2} {:.2} m", cx, cy);
4379 let sx = cx + r * start_angle.cos();
4381 let sy = cy + r * start_angle.sin();
4382 let _ = writeln!(stream, "{:.2} {:.2} l", sx, sy);
4383
4384 let mut angle = *start_angle;
4386 let total = end_angle - start_angle;
4387 let segments = ((total.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
4388 let step = total / segments as f64;
4389
4390 for _ in 0..segments {
4391 let a1 = angle;
4392 let a2 = angle + step;
4393 let alpha = 4.0 / 3.0 * ((a2 - a1) / 4.0).tan();
4394
4395 let p1x = cx + r * a1.cos();
4396 let p1y = cy + r * a1.sin();
4397 let p2x = cx + r * a2.cos();
4398 let p2y = cy + r * a2.sin();
4399
4400 let cp1x = p1x - alpha * r * a1.sin();
4401 let cp1y = p1y + alpha * r * a1.cos();
4402 let cp2x = p2x + alpha * r * a2.sin();
4403 let cp2y = p2y - alpha * r * a2.cos();
4404
4405 let _ = writeln!(
4406 stream,
4407 "{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c",
4408 cp1x, cp1y, cp2x, cp2y, p2x, p2y
4409 );
4410 angle = a2;
4411 }
4412
4413 let _ = writeln!(stream, "h f");
4415 }
4416
4417 ChartPrimitive::Label {
4418 text,
4419 x,
4420 y,
4421 font_size,
4422 color,
4423 anchor,
4424 } => {
4425 let metrics = crate::font::StandardFont::Helvetica.metrics();
4427 let text_width = metrics.measure_string(text, *font_size, 0.0);
4428 let x_offset = match anchor {
4429 TextAnchor::Left => 0.0,
4430 TextAnchor::Center => -text_width / 2.0,
4431 TextAnchor::Right => -text_width,
4432 };
4433
4434 let font_idx = builder
4436 .font_objects
4437 .iter()
4438 .enumerate()
4439 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
4440 .map(|(i, _)| i)
4441 .unwrap_or(0);
4442
4443 let encoded: String = text
4445 .chars()
4446 .map(|ch| {
4447 if let Some(code) = unicode_to_winansi(ch) {
4448 code as char
4449 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
4450 ch
4451 } else {
4452 '?'
4453 }
4454 })
4455 .collect();
4456 let escaped = pdf_escape_string(&encoded);
4457
4458 let _ = writeln!(stream, "q");
4460 let _ = writeln!(stream, "1 0 0 -1 {:.4} {:.4} cm", x + x_offset, *y);
4461 let _ = writeln!(
4462 stream,
4463 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
4464 font_idx, font_size, color.r, color.g, color.b, escaped
4465 );
4466 let _ = writeln!(stream, "Q");
4467 }
4468 }
4469}
4470
4471fn normalize_gradient_stops(
4478 stops: &[crate::style::GradientStop],
4479 fallback: Color,
4480) -> Vec<crate::style::GradientStop> {
4481 use crate::style::GradientStop;
4482 if stops.is_empty() {
4483 return vec![
4484 GradientStop {
4485 position: 0.0,
4486 color: fallback,
4487 },
4488 GradientStop {
4489 position: 1.0,
4490 color: fallback,
4491 },
4492 ];
4493 }
4494 let mut sorted: Vec<GradientStop> = stops
4495 .iter()
4496 .map(|s| GradientStop {
4497 position: s.position.clamp(0.0, 1.0),
4498 color: s.color,
4499 })
4500 .collect();
4501 sorted.sort_by(|a, b| {
4502 a.position
4503 .partial_cmp(&b.position)
4504 .unwrap_or(std::cmp::Ordering::Equal)
4505 });
4506 if sorted[0].position > 0.0 {
4507 sorted.insert(
4508 0,
4509 GradientStop {
4510 position: 0.0,
4511 color: sorted[0].color,
4512 },
4513 );
4514 }
4515 if sorted[sorted.len() - 1].position < 1.0 {
4516 let last = sorted[sorted.len() - 1].color;
4517 sorted.push(GradientStop {
4518 position: 1.0,
4519 color: last,
4520 });
4521 }
4522 sorted
4523}
4524
4525fn pdf_escape_string(s: &str) -> String {
4526 let mut out = String::with_capacity(s.len());
4527 for ch in s.chars() {
4528 match ch {
4529 '(' => out.push_str("\\("),
4530 ')' => out.push_str("\\)"),
4531 '\\' => out.push_str("\\\\"),
4532 _ => out.push(ch),
4533 }
4534 }
4535 out
4536}
4537
4538#[cfg(test)]
4539mod tests {
4540 use super::*;
4541 use crate::font::FontContext;
4542
4543 #[test]
4550 fn test_embedded_srgb_is_a_valid_icc_profile() {
4551 let icc: &[u8] = include_bytes!("sRGB.icc");
4552 assert!(
4553 icc.len() >= 128,
4554 "ICC shorter than its 128-byte header: {}",
4555 icc.len()
4556 );
4557 assert_ne!(
4559 icc[0], b'<',
4560 "embedded ICC starts with '<' — looks like HTML, not a profile"
4561 );
4562 assert_eq!(&icc[36..40], b"acsp", "missing ICC 'acsp' signature");
4564 let device_class = &icc[12..16];
4566 assert!(
4567 device_class == b"mntr" || device_class == b"prtr",
4568 "ICC device class {:?} is not mntr/prtr (PDF/A 6.2.3)",
4569 String::from_utf8_lossy(device_class),
4570 );
4571 assert_eq!(&icc[16..20], b"RGB ", "ICC data colour space is not RGB");
4573 }
4574
4575 #[test]
4576 fn test_escape_pdf_string() {
4577 assert_eq!(
4578 PdfWriter::escape_pdf_string("Hello (World)"),
4579 "Hello \\(World\\)"
4580 );
4581 assert_eq!(PdfWriter::escape_pdf_string("back\\slash"), "back\\\\slash");
4582 }
4583
4584 #[test]
4585 fn test_empty_document_produces_valid_pdf() {
4586 let writer = PdfWriter::new();
4587 let font_context = FontContext::new();
4588 let pages = vec![LayoutPage {
4589 width: 595.28,
4590 height: 841.89,
4591 elements: vec![],
4592 fixed_header: vec![],
4593 fixed_footer: vec![],
4594 watermarks: vec![],
4595 config: PageConfig::default(),
4596 page_name: None,
4597 }];
4598 let metadata = Metadata::default();
4599 let (bytes, _warnings) = writer
4600 .write(
4601 &pages,
4602 &metadata,
4603 &font_context,
4604 false,
4605 None,
4606 false,
4607 None,
4608 &[],
4609 None,
4610 false,
4611 )
4612 .unwrap();
4613
4614 assert!(bytes.starts_with(b"%PDF-1.7"));
4615 assert!(bytes.windows(5).any(|w| w == b"%%EOF"));
4616 assert!(bytes.windows(4).any(|w| w == b"xref"));
4617 assert!(bytes.windows(7).any(|w| w == b"trailer"));
4618 }
4619
4620 #[test]
4621 fn test_metadata_in_pdf() {
4622 let writer = PdfWriter::new();
4623 let font_context = FontContext::new();
4624 let pages = vec![LayoutPage {
4625 width: 595.28,
4626 height: 841.89,
4627 elements: vec![],
4628 fixed_header: vec![],
4629 fixed_footer: vec![],
4630 watermarks: vec![],
4631 config: PageConfig::default(),
4632 page_name: None,
4633 }];
4634 let metadata = Metadata {
4635 title: Some("Test Document".to_string()),
4636 author: Some("Forme".to_string()),
4637 subject: None,
4638 creator: None,
4639 lang: None,
4640 };
4641 let (bytes, _warnings) = writer
4642 .write(
4643 &pages,
4644 &metadata,
4645 &font_context,
4646 false,
4647 None,
4648 false,
4649 None,
4650 &[],
4651 None,
4652 false,
4653 )
4654 .unwrap();
4655 let text = String::from_utf8_lossy(&bytes);
4656
4657 assert!(text.contains("/Title (Test Document)"));
4658 assert!(text.contains("/Author (Forme)"));
4659 }
4660
4661 #[test]
4662 fn test_bold_font_registered_separately() {
4663 let writer = PdfWriter::new();
4664 let font_context = FontContext::new();
4665
4666 let pages = vec![LayoutPage {
4668 width: 595.28,
4669 height: 841.89,
4670 elements: vec![
4671 LayoutElement {
4672 x: 54.0,
4673 y: 54.0,
4674 width: 100.0,
4675 height: 16.8,
4676 draw: DrawCommand::Text {
4677 lines: vec![TextLine {
4678 x: 54.0,
4679 y: 66.0,
4680 width: 50.0,
4681 height: 16.8,
4682 glyphs: vec![PositionedGlyph {
4683 glyph_id: 65,
4684 x_offset: 0.0,
4685 y_offset: 0.0,
4686 x_advance: 8.0,
4687 font_size: 12.0,
4688 font_family: "Helvetica".into(),
4689 font_weight: 400,
4690 font_style: FontStyle::Normal,
4691 char_value: 'A',
4692 color: None,
4693 href: None,
4694 text_decoration: TextDecoration::None,
4695 letter_spacing: 0.0,
4696 cluster_text: None,
4697 }],
4698 word_spacing: 0.0,
4699 }],
4700 color: Color::BLACK,
4701 text_decoration: TextDecoration::None,
4702 opacity: 1.0,
4703 },
4704 children: vec![],
4705 node_type: None,
4706 resolved_style: None,
4707 source_location: None,
4708 href: None,
4709 bookmark: None,
4710 alt: None,
4711 is_header_row: false,
4712 col_span: 1,
4713 overflow: Overflow::default(),
4714 opacity: 1.0,
4715 },
4716 LayoutElement {
4717 x: 54.0,
4718 y: 74.0,
4719 width: 100.0,
4720 height: 16.8,
4721 draw: DrawCommand::Text {
4722 lines: vec![TextLine {
4723 x: 54.0,
4724 y: 86.0,
4725 width: 50.0,
4726 height: 16.8,
4727 glyphs: vec![PositionedGlyph {
4728 glyph_id: 65,
4729 x_offset: 0.0,
4730 y_offset: 0.0,
4731 x_advance: 8.0,
4732 font_size: 12.0,
4733 font_family: "Helvetica".into(),
4734 font_weight: 700,
4735 font_style: FontStyle::Normal,
4736 char_value: 'A',
4737 color: None,
4738 href: None,
4739 text_decoration: TextDecoration::None,
4740 letter_spacing: 0.0,
4741 cluster_text: None,
4742 }],
4743 word_spacing: 0.0,
4744 }],
4745 color: Color::BLACK,
4746 text_decoration: TextDecoration::None,
4747 opacity: 1.0,
4748 },
4749 children: vec![],
4750 node_type: None,
4751 resolved_style: None,
4752 source_location: None,
4753 href: None,
4754 bookmark: None,
4755 alt: None,
4756 is_header_row: false,
4757 col_span: 1,
4758 overflow: Overflow::default(),
4759 opacity: 1.0,
4760 },
4761 ],
4762 fixed_header: vec![],
4763 fixed_footer: vec![],
4764 watermarks: vec![],
4765 config: PageConfig::default(),
4766 page_name: None,
4767 }];
4768
4769 let metadata = Metadata::default();
4770 let (bytes, _warnings) = writer
4771 .write(
4772 &pages,
4773 &metadata,
4774 &font_context,
4775 false,
4776 None,
4777 false,
4778 None,
4779 &[],
4780 None,
4781 false,
4782 )
4783 .unwrap();
4784 let text = String::from_utf8_lossy(&bytes);
4785
4786 assert!(
4788 text.contains("Helvetica"),
4789 "Should contain regular Helvetica"
4790 );
4791 assert!(
4792 text.contains("Helvetica-Bold"),
4793 "Should contain Helvetica-Bold"
4794 );
4795 }
4796
4797 #[test]
4798 fn test_sanitize_font_name() {
4799 assert_eq!(PdfWriter::sanitize_font_name("Inter", 400, false), "Inter");
4800 assert_eq!(
4801 PdfWriter::sanitize_font_name("Inter", 700, false),
4802 "Inter-Bold"
4803 );
4804 assert_eq!(
4805 PdfWriter::sanitize_font_name("Inter", 400, true),
4806 "Inter-Italic"
4807 );
4808 assert_eq!(
4809 PdfWriter::sanitize_font_name("Inter", 700, true),
4810 "Inter-Bold-Italic"
4811 );
4812 assert_eq!(
4813 PdfWriter::sanitize_font_name("Noto Sans", 400, false),
4814 "NotoSans"
4815 );
4816 assert_eq!(
4817 PdfWriter::sanitize_font_name("Font (Display)", 400, false),
4818 "FontDisplay"
4819 );
4820 }
4821
4822 #[test]
4823 fn test_tounicode_cmap_format() {
4824 let mut glyph_to_char = HashMap::new();
4826 glyph_to_char.insert(36u16, 'A');
4827 glyph_to_char.insert(37u16, 'B');
4828
4829 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&glyph_to_char, "TestFont");
4830
4831 assert!(cmap.contains("begincmap"), "CMap should contain begincmap");
4832 assert!(cmap.contains("endcmap"), "CMap should contain endcmap");
4833 assert!(
4834 cmap.contains("beginbfchar"),
4835 "CMap should contain beginbfchar"
4836 );
4837 assert!(cmap.contains("endbfchar"), "CMap should contain endbfchar");
4838 assert!(
4839 cmap.contains("<0024> <0041>"),
4840 "Should map gid 0x0024 to Unicode 'A' 0x0041"
4841 );
4842 assert!(
4843 cmap.contains("<0025> <0042>"),
4844 "Should map gid 0x0025 to Unicode 'B' 0x0042"
4845 );
4846 assert!(
4847 cmap.contains("begincodespacerange"),
4848 "Should define codespace range"
4849 );
4850 assert!(
4851 cmap.contains("<0000> <FFFF>"),
4852 "Codespace should be 0000-FFFF"
4853 );
4854 }
4855
4856 #[test]
4857 fn test_w_array_format() {
4858 let mut char_to_gid = HashMap::new();
4859 char_to_gid.insert('A', 36u16);
4860
4861 let w_array_str = "[ 36 [600] ]";
4864 assert!(w_array_str.starts_with('['));
4865 assert!(w_array_str.ends_with(']'));
4866 }
4867
4868 #[test]
4869 fn test_hex_glyph_encoding() {
4870 let gid: u16 = 0x0041;
4872 let hex = format!("{:04X}", gid);
4873 assert_eq!(hex, "0041");
4874
4875 let gids = [0x0041u16, 0x0042, 0x0043];
4876 let hex_str: String = gids.iter().map(|g| format!("{:04X}", g)).collect();
4877 assert_eq!(hex_str, "004100420043");
4878 }
4879
4880 #[test]
4881 fn test_standard_font_still_uses_text_string() {
4882 let writer = PdfWriter::new();
4883 let font_context = FontContext::new();
4884
4885 let pages = vec![LayoutPage {
4886 width: 595.28,
4887 height: 841.89,
4888 elements: vec![LayoutElement {
4889 x: 54.0,
4890 y: 54.0,
4891 width: 100.0,
4892 height: 16.8,
4893 draw: DrawCommand::Text {
4894 lines: vec![TextLine {
4895 x: 54.0,
4896 y: 66.0,
4897 width: 50.0,
4898 height: 16.8,
4899 glyphs: vec![PositionedGlyph {
4900 glyph_id: 65,
4901 x_offset: 0.0,
4902 y_offset: 0.0,
4903 x_advance: 8.0,
4904 font_size: 12.0,
4905 font_family: "Helvetica".into(),
4906 font_weight: 400,
4907 font_style: FontStyle::Normal,
4908 char_value: 'H',
4909 color: None,
4910 href: None,
4911 text_decoration: TextDecoration::None,
4912 letter_spacing: 0.0,
4913 cluster_text: None,
4914 }],
4915 word_spacing: 0.0,
4916 }],
4917 color: Color::BLACK,
4918 text_decoration: TextDecoration::None,
4919 opacity: 1.0,
4920 },
4921 children: vec![],
4922 node_type: None,
4923 resolved_style: None,
4924 source_location: None,
4925 href: None,
4926 bookmark: None,
4927 alt: None,
4928 is_header_row: false,
4929 col_span: 1,
4930 overflow: Overflow::default(),
4931 opacity: 1.0,
4932 }],
4933 fixed_header: vec![],
4934 fixed_footer: vec![],
4935 watermarks: vec![],
4936 config: PageConfig::default(),
4937 page_name: None,
4938 }];
4939
4940 let metadata = Metadata::default();
4941 let (bytes, _warnings) = writer
4942 .write(
4943 &pages,
4944 &metadata,
4945 &font_context,
4946 false,
4947 None,
4948 false,
4949 None,
4950 &[],
4951 None,
4952 false,
4953 )
4954 .unwrap();
4955 let text = String::from_utf8_lossy(&bytes);
4956
4957 assert!(
4959 text.contains("/Type1"),
4960 "Standard font should use Type1 subtype"
4961 );
4962 assert!(
4963 !text.contains("CIDFontType2"),
4964 "Standard font should not use CIDFontType2"
4965 );
4966 }
4967}