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