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};
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 image_objects: Vec<usize>,
112 image_index_map: HashMap<(usize, usize), usize>,
115 ext_gstate_map: HashMap<u64, (usize, String)>,
118}
119
120pub(crate) struct PdfObject {
121 #[allow(dead_code)]
122 pub(crate) id: usize,
123 pub(crate) data: Vec<u8>,
124}
125
126impl Default for PdfWriter {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl PdfWriter {
133 pub fn new() -> Self {
134 Self
135 }
136
137 #[allow(clippy::too_many_arguments)]
139 pub fn write(
140 &self,
141 pages: &[LayoutPage],
142 metadata: &Metadata,
143 font_context: &FontContext,
144 tagged: bool,
145 pdfa: Option<&PdfAConformance>,
146 pdf_ua: bool,
147 embedded_data: Option<&str>,
148 flatten_forms: bool,
149 ) -> Result<Vec<u8>, FormeError> {
150 let mut builder = PdfBuilder {
151 objects: Vec::new(),
152 font_objects: Vec::new(),
153 custom_font_data: HashMap::new(),
154 image_objects: Vec::new(),
155 image_index_map: HashMap::new(),
156 ext_gstate_map: HashMap::new(),
157 };
158
159 builder.objects.push(PdfObject {
165 id: 0,
166 data: vec![],
167 });
168 builder.objects.push(PdfObject {
169 id: 1,
170 data: vec![],
171 });
172 builder.objects.push(PdfObject {
173 id: 2,
174 data: vec![],
175 });
176
177 self.register_fonts(&mut builder, pages, font_context)?;
179
180 if pdfa.is_some() {
182 for (key, _) in &builder.font_objects {
183 if !builder.custom_font_data.contains_key(key) {
184 return Err(FormeError::RenderError(format!(
185 "PDF/A requires all fonts to be embedded. Register a custom font for \
186 family '{}' using Font.register().",
187 key.family
188 )));
189 }
190 }
191 }
192
193 self.register_images(&mut builder, pages);
195
196 self.register_ext_gstates(&mut builder, pages);
198
199 let mut tag_builder = if tagged {
201 Some(tagged::TagBuilder::new(pages.len()))
202 } else {
203 None
204 };
205
206 let mut page_obj_ids: Vec<usize> = Vec::new();
210 let mut all_bookmarks: Vec<PdfBookmark> = Vec::new();
211 let mut per_page_content_obj_ids: Vec<usize> = Vec::new();
212 let mut per_page_annotations: Vec<Vec<LinkAnnotation>> = Vec::new();
213 let mut per_page_resources: Vec<String> = Vec::new();
214 let mut all_form_fields: Vec<FormFieldData> = Vec::new();
215
216 for (page_idx, page) in pages.iter().enumerate() {
218 let content = self.build_content_stream_for_page(
219 page,
220 page_idx,
221 &builder,
222 page_idx + 1,
223 pages.len(),
224 tag_builder.as_mut(),
225 flatten_forms,
226 );
227 let compressed = compress_to_vec_zlib(content.as_bytes(), 6);
228
229 let content_obj_id = builder.objects.len();
230 let mut content_data: Vec<u8> = Vec::new();
231 let _ = write!(
232 content_data,
233 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
234 compressed.len()
235 );
236 content_data.extend_from_slice(&compressed);
237 content_data.extend_from_slice(b"\nendstream");
238 builder.objects.push(PdfObject {
239 id: content_obj_id,
240 data: content_data,
241 });
242 per_page_content_obj_ids.push(content_obj_id);
243
244 let mut annotations: Vec<LinkAnnotation> = Vec::new();
246 Self::collect_link_annotations(&page.elements, page.height, &mut annotations);
247 per_page_annotations.push(annotations);
248
249 Self::collect_form_fields(&page.elements, page.height, page_idx, &mut all_form_fields);
251
252 let page_obj_id = builder.objects.len();
254 builder.objects.push(PdfObject {
255 id: page_obj_id,
256 data: vec![],
257 });
258
259 let font_resources = self.build_font_resource_dict(&builder.font_objects);
261 let xobject_resources = self.build_xobject_resource_dict(page_idx, &builder);
262 let ext_gstate_resources = self.build_ext_gstate_resource_dict(&builder);
263 let mut resources = format!("/Font << {} >>", font_resources);
264 if !xobject_resources.is_empty() {
265 let _ = write!(resources, " /XObject << {} >>", xobject_resources);
266 }
267 if !ext_gstate_resources.is_empty() {
268 let _ = write!(resources, " /ExtGState << {} >>", ext_gstate_resources);
269 }
270 per_page_resources.push(resources);
271
272 Self::collect_bookmarks(&page.elements, page.height, page_obj_id, &mut all_bookmarks);
274
275 page_obj_ids.push(page_obj_id);
276 }
277
278 for (page_idx, annotations) in per_page_annotations.iter().enumerate() {
280 let mut annot_obj_ids: Vec<usize> = Vec::new();
281 for annot in annotations {
282 let rect = format!(
283 "[{:.2} {:.2} {:.2} {:.2}]",
284 annot.x,
285 annot.y,
286 annot.x + annot.width,
287 annot.y + annot.height
288 );
289
290 if let Some(anchor) = annot.href.strip_prefix('#') {
291 if let Some(bm) = all_bookmarks.iter().find(|b| b.title == anchor) {
293 let annot_obj_id = builder.objects.len();
294 let annot_dict = format!(
295 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
296 /A << /S /GoTo /D [{} 0 R /XYZ 0 {:.2} null] >> >>",
297 rect, bm.page_obj_id, bm.y_pdf
298 );
299 builder.objects.push(PdfObject {
300 id: annot_obj_id,
301 data: annot_dict.into_bytes(),
302 });
303 annot_obj_ids.push(annot_obj_id);
304 }
305 } else {
307 let annot_obj_id = builder.objects.len();
309 let annot_dict = format!(
310 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
311 /A << /Type /Action /S /URI /URI ({}) >> >>",
312 rect,
313 Self::escape_pdf_string(&annot.href)
314 );
315 builder.objects.push(PdfObject {
316 id: annot_obj_id,
317 data: annot_dict.into_bytes(),
318 });
319 annot_obj_ids.push(annot_obj_id);
320 }
321 }
322
323 let annots_str = if annot_obj_ids.is_empty() {
324 String::new()
325 } else {
326 let refs: String = annot_obj_ids
327 .iter()
328 .map(|id| format!("{} 0 R", id))
329 .collect::<Vec<_>>()
330 .join(" ");
331 format!(" /Annots [{}]", refs)
332 };
333
334 let page_obj_id = page_obj_ids[page_idx];
335 let content_obj_id = per_page_content_obj_ids[page_idx];
336 let struct_parents_str = if tagged {
337 format!(" /StructParents {} /Tabs /S", page_idx)
338 } else {
339 String::new()
340 };
341 let page_dict = format!(
342 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {:.2} {:.2}] \
343 /Contents {} 0 R /Resources << {} >>{}{} >>",
344 pages[page_idx].width,
345 pages[page_idx].height,
346 content_obj_id,
347 per_page_resources[page_idx],
348 annots_str,
349 struct_parents_str
350 );
351 builder.objects[page_obj_id].data = page_dict.into_bytes();
352 }
353
354 let outlines_obj_id = if !all_bookmarks.is_empty() {
356 Some(self.write_outline_tree(&mut builder, &all_bookmarks))
357 } else {
358 None
359 };
360
361 let struct_tree_root_id = if let Some(ref tb) = tag_builder {
363 let (root_id, _parent_tree_id) = tb.write_objects(
364 &mut builder.objects,
365 &page_obj_ids,
366 metadata.lang.as_deref(),
367 );
368 Some(root_id)
369 } else {
370 None
371 };
372
373 let xmp_metadata_id = if pdfa.is_some() || pdf_ua {
375 let xmp_xml = xmp::generate_xmp(metadata, pdfa, pdf_ua);
376 let xmp_bytes = xmp_xml.as_bytes();
377 let xmp_obj_id = builder.objects.len();
378 let xmp_data = format!(
380 "<< /Type /Metadata /Subtype /XML /Length {} >>\nstream\n",
381 xmp_bytes.len()
382 );
383 let mut xmp_obj_data: Vec<u8> = xmp_data.into_bytes();
384 xmp_obj_data.extend_from_slice(xmp_bytes);
385 xmp_obj_data.extend_from_slice(b"\nendstream");
386 builder.objects.push(PdfObject {
387 id: xmp_obj_id,
388 data: xmp_obj_data,
389 });
390 Some(xmp_obj_id)
391 } else {
392 None
393 };
394
395 let output_intent_id = if pdfa.is_some() {
396 static SRGB_ICC: &[u8] = include_bytes!("srgb2014.icc");
398 let compressed_icc = compress_to_vec_zlib(SRGB_ICC, 6);
399
400 let icc_obj_id = builder.objects.len();
401 let mut icc_data: Vec<u8> = Vec::new();
402 let _ = write!(
403 icc_data,
404 "<< /N 3 /Length {} /Filter /FlateDecode >>\nstream\n",
405 compressed_icc.len()
406 );
407 icc_data.extend_from_slice(&compressed_icc);
408 icc_data.extend_from_slice(b"\nendstream");
409 builder.objects.push(PdfObject {
410 id: icc_obj_id,
411 data: icc_data,
412 });
413
414 let oi_obj_id = builder.objects.len();
416 let oi_data = format!(
417 "<< /Type /OutputIntent /S /GTS_PDFA1 \
418 /OutputConditionIdentifier (sRGB IEC61966-2.1) \
419 /RegistryName (http://www.color.org) \
420 /DestOutputProfile {} 0 R >>",
421 icc_obj_id
422 );
423 builder.objects.push(PdfObject {
424 id: oi_obj_id,
425 data: oi_data.into_bytes(),
426 });
427 Some(oi_obj_id)
428 } else {
429 None
430 };
431
432 let embedded_names_id = if let Some(data) = embedded_data {
434 let compressed = compress_to_vec_zlib(data.as_bytes(), 6);
435
436 let ef_obj_id = builder.objects.len();
438 let ef_data = format!(
439 "<< /Type /EmbeddedFile /Subtype /application#2Fjson /Length {} /Filter /FlateDecode >>\nstream\n",
440 compressed.len()
441 );
442 let mut ef_bytes = ef_data.into_bytes();
443 ef_bytes.extend_from_slice(&compressed);
444 ef_bytes.extend_from_slice(b"\nendstream");
445 builder.objects.push(PdfObject {
446 id: ef_obj_id,
447 data: ef_bytes,
448 });
449
450 let fs_obj_id = builder.objects.len();
452 let fs_data = format!(
453 "<< /Type /Filespec /F (forme-data.json) /UF (forme-data.json) /EF << /F {} 0 R >> /AFRelationship /Data >>",
454 ef_obj_id
455 );
456 builder.objects.push(PdfObject {
457 id: fs_obj_id,
458 data: fs_data.into_bytes(),
459 });
460
461 let names_obj_id = builder.objects.len();
463 let names_data = format!("<< /Names [(forme-data.json) {} 0 R] >>", fs_obj_id);
464 builder.objects.push(PdfObject {
465 id: names_obj_id,
466 data: names_data.into_bytes(),
467 });
468
469 Some(names_obj_id)
470 } else {
471 None
472 };
473
474 let acroform_obj_id = if !all_form_fields.is_empty() && !flatten_forms {
476 let helv_obj_id = builder
478 .font_objects
479 .iter()
480 .find(|(key, _)| key.family == "Helvetica" && key.weight == 400 && !key.italic)
481 .map(|(_, id)| *id);
482
483 let mut radio_groups: HashMap<String, Vec<usize>> = HashMap::new(); let mut non_radio_indices: Vec<usize> = Vec::new();
486 for (i, field) in all_form_fields.iter().enumerate() {
487 if matches!(field.field_type, FormFieldType::RadioButton { .. }) {
488 radio_groups.entry(field.name.clone()).or_default().push(i);
489 } else {
490 non_radio_indices.push(i);
491 }
492 }
493
494 let mut radio_parent_ids: HashMap<String, usize> = HashMap::new();
496 for group_name in radio_groups.keys() {
497 let parent_id = builder.objects.len();
498 builder.objects.push(PdfObject {
499 id: parent_id,
500 data: vec![], });
502 radio_parent_ids.insert(group_name.clone(), parent_id);
503 }
504
505 let checkbox_yes_stream_id = builder.objects.len();
508 {
509 let stream_content =
510 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";
511 let mut data: Vec<u8> = Vec::new();
512 let _ = write!(
513 data,
514 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
515 stream_content.len()
516 );
517 data.extend_from_slice(stream_content);
518 data.extend_from_slice(b"\nendstream");
519 builder.objects.push(PdfObject {
520 id: checkbox_yes_stream_id,
521 data,
522 });
523 }
524 let checkbox_off_stream_id = builder.objects.len();
526 {
527 let stream_content = b"";
528 let mut data: Vec<u8> = Vec::new();
529 let _ = write!(
530 data,
531 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
532 stream_content.len()
533 );
534 data.extend_from_slice(stream_content);
535 data.extend_from_slice(b"\nendstream");
536 builder.objects.push(PdfObject {
537 id: checkbox_off_stream_id,
538 data,
539 });
540 }
541 let radio_on_stream_id = builder.objects.len();
543 {
544 let k = 2.761; let stream_content = format!(
547 "0.2 0.2 0.2 rg\n\
548 7 12 m {:.2} 12 12 {:.2} 12 7 c\n\
549 12 {:.2} {:.2} 2 7 2 c\n\
550 {:.2} 2 2 {:.2} 2 7 c\n\
551 2 {:.2} {:.2} 12 7 12 c f\n",
552 7.0 + k,
553 7.0 + k, 7.0 - k,
555 7.0 - k, 7.0 - k,
557 7.0 - k, 7.0 + k,
559 7.0 + k, );
561 let stream_bytes = stream_content.as_bytes();
562 let mut data: Vec<u8> = Vec::new();
563 let _ = write!(
564 data,
565 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
566 stream_bytes.len()
567 );
568 data.extend_from_slice(stream_bytes);
569 data.extend_from_slice(b"\nendstream");
570 builder.objects.push(PdfObject {
571 id: radio_on_stream_id,
572 data,
573 });
574 }
575 let radio_off_stream_id = builder.objects.len();
577 {
578 let stream_content = b"";
579 let mut data: Vec<u8> = Vec::new();
580 let _ = write!(
581 data,
582 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
583 stream_content.len()
584 );
585 data.extend_from_slice(stream_content);
586 data.extend_from_slice(b"\nendstream");
587 builder.objects.push(PdfObject {
588 id: radio_off_stream_id,
589 data,
590 });
591 }
592
593 let mut acroform_field_ids: Vec<usize> = Vec::new();
595 let mut per_page_widget_ids: Vec<Vec<usize>> = vec![Vec::new(); pages.len()];
596 let mut radio_kid_ids: HashMap<String, Vec<usize>> = HashMap::new();
597
598 for field in all_form_fields.iter() {
599 let rect = format!(
600 "[{:.2} {:.2} {:.2} {:.2}]",
601 field.x,
602 field.y,
603 field.x + field.width,
604 field.y + field.height
605 );
606 let page_ref = format!("{} 0 R", page_obj_ids[field.page_idx]);
607
608 match &field.field_type {
609 FormFieldType::TextField {
610 value,
611 multiline,
612 password,
613 read_only,
614 max_length,
615 font_size,
616 ..
617 } => {
618 let mut flags: u32 = 0;
619 if *multiline {
620 flags |= 1 << 12; }
622 if *password {
623 flags |= 1 << 13; }
625 if *read_only {
626 flags |= 1; }
628 let da = if let Some(helv_id) = helv_obj_id {
629 let _ = helv_id; format!("/Helv {} Tf 0 g", font_size)
631 } else {
632 format!("/Helv {} Tf 0 g", font_size)
633 };
634 let v_str = if let Some(ref v) = value {
635 format!(
636 " /V ({}) /DV ({})",
637 Self::escape_pdf_string(v),
638 Self::escape_pdf_string(v)
639 )
640 } else {
641 String::new()
642 };
643 let max_len_str = if let Some(ml) = max_length {
644 format!(" /MaxLen {}", ml)
645 } else {
646 String::new()
647 };
648 let ap_w = field.width;
650 let ap_h = field.height;
651 let text_y = if *multiline {
652 ap_h - *font_size - 2.0
653 } else {
654 (ap_h - *font_size) / 2.0
655 };
656 let ap_content = if let Some(ref v) = value {
657 format!(
658 "1 1 1 rg 0 0 {} {} re f \
659 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S \
660 BT /Helv {} Tf 0 g 2 {} Td ({}) Tj ET",
661 ap_w,
662 ap_h,
663 ap_w,
664 ap_h,
665 font_size,
666 text_y,
667 Self::escape_pdf_string(v)
668 )
669 } else {
670 format!(
671 "1 1 1 rg 0 0 {} {} re f \
672 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S",
673 ap_w, ap_h, ap_w, ap_h
674 )
675 };
676 let ap_stream_id = builder.objects.len();
677 let ap_stream = format!(
678 "<< /Type /XObject /Subtype /Form /BBox [0 0 {} {}] \
679 /Resources << /Font << /Helv {} 0 R >> >> /Length {} >>\nstream\n{}\nendstream",
680 ap_w, ap_h,
681 helv_obj_id.unwrap_or(0),
682 ap_content.len(),
683 ap_content
684 );
685 builder.objects.push(PdfObject {
686 id: ap_stream_id,
687 data: ap_stream.into_bytes(),
688 });
689
690 let widget_obj_id = builder.objects.len();
691 let widget_dict = format!(
692 "<< /Type /Annot /Subtype /Widget /FT /Tx \
693 /T ({}) /Rect {} /P {}\
694 {} /DA ({}) /Ff {}{} \
695 /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> \
696 /AP << /N {} 0 R >> >>",
697 Self::escape_pdf_string(&field.name),
698 rect,
699 page_ref,
700 v_str,
701 da,
702 flags,
703 max_len_str,
704 ap_stream_id
705 );
706 builder.objects.push(PdfObject {
707 id: widget_obj_id,
708 data: widget_dict.into_bytes(),
709 });
710 per_page_widget_ids[field.page_idx].push(widget_obj_id);
711 acroform_field_ids.push(widget_obj_id);
712 }
713
714 FormFieldType::Checkbox {
715 checked, read_only, ..
716 } => {
717 let state = if *checked { "Yes" } else { "Off" };
718 let mut flags: u32 = 0;
719 if *read_only {
720 flags |= 1;
721 }
722 let ff_str = if flags > 0 {
723 format!(" /Ff {}", flags)
724 } else {
725 String::new()
726 };
727 let widget_obj_id = builder.objects.len();
728 let widget_dict = format!(
729 "<< /Type /Annot /Subtype /Widget /FT /Btn \
730 /T ({}) /Rect {} /P {} \
731 /V /{} /AS /{}{} \
732 /MK << /BC [0.6 0.6 0.6] /CA (4) >> \
733 /AP << /N << /Yes {} 0 R /Off {} 0 R >> >> >>",
734 Self::escape_pdf_string(&field.name),
735 rect,
736 page_ref,
737 state,
738 state,
739 ff_str,
740 checkbox_yes_stream_id,
741 checkbox_off_stream_id,
742 );
743 builder.objects.push(PdfObject {
744 id: widget_obj_id,
745 data: widget_dict.into_bytes(),
746 });
747 per_page_widget_ids[field.page_idx].push(widget_obj_id);
748 acroform_field_ids.push(widget_obj_id);
749 }
750
751 FormFieldType::Dropdown {
752 options,
753 value,
754 read_only,
755 font_size,
756 ..
757 } => {
758 let mut flags: u32 = 1 << 17; if *read_only {
760 flags |= 1;
761 }
762 let opts_str: String = options
763 .iter()
764 .map(|o| format!("({})", Self::escape_pdf_string(o)))
765 .collect::<Vec<_>>()
766 .join(" ");
767 let v_str = if let Some(ref v) = value {
768 format!(" /V ({})", Self::escape_pdf_string(v))
769 } else {
770 String::new()
771 };
772 let ap_w = field.width;
774 let ap_h = field.height;
775 let text_y = (ap_h - *font_size) / 2.0;
776 let ap_content = if let Some(ref v) = value {
777 format!(
778 "1 1 1 rg 0 0 {} {} re f \
779 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S \
780 BT /Helv {} Tf 0 g 2 {} Td ({}) Tj ET",
781 ap_w,
782 ap_h,
783 ap_w,
784 ap_h,
785 font_size,
786 text_y,
787 Self::escape_pdf_string(v)
788 )
789 } else {
790 format!(
791 "1 1 1 rg 0 0 {} {} re f \
792 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S",
793 ap_w, ap_h, ap_w, ap_h
794 )
795 };
796 let ap_stream_id = builder.objects.len();
797 let ap_stream = format!(
798 "<< /Type /XObject /Subtype /Form /BBox [0 0 {} {}] \
799 /Resources << /Font << /Helv {} 0 R >> >> /Length {} >>\nstream\n{}\nendstream",
800 ap_w, ap_h,
801 helv_obj_id.unwrap_or(0),
802 ap_content.len(),
803 ap_content
804 );
805 builder.objects.push(PdfObject {
806 id: ap_stream_id,
807 data: ap_stream.into_bytes(),
808 });
809
810 let widget_obj_id = builder.objects.len();
811 let widget_dict = format!(
812 "<< /Type /Annot /Subtype /Widget /FT /Ch \
813 /T ({}) /Rect {} /P {} \
814 /Opt [{}]{} \
815 /DA (/Helv {} Tf 0 g) /Ff {} \
816 /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> \
817 /AP << /N {} 0 R >> >>",
818 Self::escape_pdf_string(&field.name),
819 rect,
820 page_ref,
821 opts_str,
822 v_str,
823 font_size,
824 flags,
825 ap_stream_id
826 );
827 builder.objects.push(PdfObject {
828 id: widget_obj_id,
829 data: widget_dict.into_bytes(),
830 });
831 per_page_widget_ids[field.page_idx].push(widget_obj_id);
832 acroform_field_ids.push(widget_obj_id);
833 }
834
835 FormFieldType::RadioButton {
836 value,
837 checked,
838 read_only: _,
839 } => {
840 let parent_id = radio_parent_ids[&field.name];
842 let as_value = if *checked { value.as_str() } else { "Off" };
843 let widget_obj_id = builder.objects.len();
844 let widget_dict = format!(
845 "<< /Type /Annot /Subtype /Widget \
846 /Parent {} 0 R \
847 /Rect {} /P {} \
848 /AS /{} \
849 /AP << /N << /{} {} 0 R /Off {} 0 R >> >> \
850 /MK << /BC [0.6 0.6 0.6] >> >>",
851 parent_id,
852 rect,
853 page_ref,
854 Self::escape_pdf_string(as_value),
855 Self::escape_pdf_string(value),
856 radio_on_stream_id,
857 radio_off_stream_id,
858 );
859 builder.objects.push(PdfObject {
860 id: widget_obj_id,
861 data: widget_dict.into_bytes(),
862 });
863 per_page_widget_ids[field.page_idx].push(widget_obj_id);
864 radio_kid_ids
866 .entry(field.name.clone())
867 .or_default()
868 .push(widget_obj_id);
869 }
870 }
871 }
872
873 for (group_name, kid_indices) in &radio_kid_ids {
875 let parent_id = radio_parent_ids[group_name];
876 let checked_value = all_form_fields
878 .iter()
879 .filter(|f| f.name == *group_name)
880 .find_map(|f| {
881 if let FormFieldType::RadioButton {
882 ref value, checked, ..
883 } = f.field_type
884 {
885 if checked {
886 Some(value.clone())
887 } else {
888 None
889 }
890 } else {
891 None
892 }
893 })
894 .unwrap_or_else(|| "Off".to_string());
895
896 let kids_refs: String = kid_indices
897 .iter()
898 .map(|id| format!("{} 0 R", id))
899 .collect::<Vec<_>>()
900 .join(" ");
901
902 let mut flags: u32 = (1 << 14) | (1 << 15); let is_read_only = all_form_fields
905 .iter()
906 .filter(|f| f.name == *group_name)
907 .any(|f| {
908 matches!(
909 f.field_type,
910 FormFieldType::RadioButton {
911 read_only: true,
912 ..
913 }
914 )
915 });
916 if is_read_only {
917 flags |= 1;
918 }
919
920 let parent_dict = format!(
921 "<< /FT /Btn /T ({}) /Ff {} /Kids [{}] /V /{} >>",
922 Self::escape_pdf_string(group_name),
923 flags,
924 kids_refs,
925 Self::escape_pdf_string(&checked_value),
926 );
927 builder.objects[parent_id].data = parent_dict.into_bytes();
928 acroform_field_ids.push(parent_id);
929 }
930
931 for (page_idx, widget_ids) in per_page_widget_ids.iter().enumerate() {
935 if widget_ids.is_empty() {
936 continue;
937 }
938 let page_obj_id = page_obj_ids[page_idx];
939 let existing_page_data =
940 String::from_utf8_lossy(&builder.objects[page_obj_id].data).to_string();
941
942 let new_refs: String = widget_ids
944 .iter()
945 .map(|id| format!("{} 0 R", id))
946 .collect::<Vec<_>>()
947 .join(" ");
948
949 let updated = if let Some(pos) = existing_page_data.find("/Annots [") {
950 let bracket_end = existing_page_data[pos..].find(']').unwrap() + pos;
952 format!(
953 "{} {}{}",
954 &existing_page_data[..bracket_end],
955 new_refs,
956 &existing_page_data[bracket_end..]
957 )
958 } else {
959 let end = existing_page_data.rfind(">>").unwrap();
961 format!(
962 "{} /Annots [{}]{}",
963 &existing_page_data[..end],
964 new_refs,
965 &existing_page_data[end..]
966 )
967 };
968 builder.objects[page_obj_id].data = updated.into_bytes();
969 }
970
971 let acroform_id = builder.objects.len();
973 let fields_refs: String = acroform_field_ids
974 .iter()
975 .map(|id| format!("{} 0 R", id))
976 .collect::<Vec<_>>()
977 .join(" ");
978 let dr_str = if let Some(helv_id) = helv_obj_id {
979 format!(" /DR << /Font << /Helv {} 0 R >> >>", helv_id)
980 } else {
981 String::new()
982 };
983 let acroform_dict = format!(
984 "<< /Fields [{}] /NeedAppearances true{} /DA (/Helv 0 Tf 0 g) >>",
985 fields_refs, dr_str
986 );
987 builder.objects.push(PdfObject {
988 id: acroform_id,
989 data: acroform_dict.into_bytes(),
990 });
991 Some(acroform_id)
992 } else {
993 None
994 };
995
996 let mut catalog = String::from("<< /Type /Catalog /Pages 2 0 R");
998 if let Some(acroform_id) = acroform_obj_id {
999 write!(catalog, " /AcroForm {} 0 R", acroform_id).unwrap();
1000 }
1001 if let Some(outlines_id) = outlines_obj_id {
1002 write!(
1003 catalog,
1004 " /Outlines {} 0 R /PageMode /UseOutlines",
1005 outlines_id
1006 )
1007 .unwrap();
1008 }
1009 if let Some(ref lang) = metadata.lang {
1010 write!(catalog, " /Lang ({})", Self::escape_pdf_string(lang)).unwrap();
1011 }
1012 if let Some(struct_root_id) = struct_tree_root_id {
1013 write!(
1014 catalog,
1015 " /MarkInfo << /Marked true >> /StructTreeRoot {} 0 R",
1016 struct_root_id
1017 )
1018 .unwrap();
1019 }
1020 if let Some(xmp_id) = xmp_metadata_id {
1021 write!(catalog, " /Metadata {} 0 R", xmp_id).unwrap();
1022 }
1023 if let Some(oi_id) = output_intent_id {
1024 write!(catalog, " /OutputIntents [{} 0 R]", oi_id).unwrap();
1025 }
1026 if let Some(names_id) = embedded_names_id {
1027 write!(catalog, " /Names << /EmbeddedFiles {} 0 R >>", names_id).unwrap();
1028 }
1029 if pdf_ua {
1030 catalog.push_str(" /ViewerPreferences << /DisplayDocTitle true >>");
1031 }
1032 catalog.push_str(" >>");
1033 builder.objects[1].data = catalog.into_bytes();
1034
1035 let kids: String = page_obj_ids
1037 .iter()
1038 .map(|id| format!("{} 0 R", id))
1039 .collect::<Vec<_>>()
1040 .join(" ");
1041 builder.objects[2].data = format!(
1042 "<< /Type /Pages /Kids [{}] /Count {} >>",
1043 kids,
1044 page_obj_ids.len()
1045 )
1046 .into_bytes();
1047
1048 let info_obj_id = if metadata.title.is_some() || metadata.author.is_some() {
1050 let id = builder.objects.len();
1051 let mut info = String::from("<< ");
1052 if let Some(ref title) = metadata.title {
1053 let _ = write!(info, "/Title ({}) ", Self::escape_pdf_string(title));
1054 }
1055 if let Some(ref author) = metadata.author {
1056 let _ = write!(info, "/Author ({}) ", Self::escape_pdf_string(author));
1057 }
1058 if let Some(ref subject) = metadata.subject {
1059 let _ = write!(info, "/Subject ({}) ", Self::escape_pdf_string(subject));
1060 }
1061 let _ = write!(info, "/Producer (Forme 0.6) /Creator (Forme) >>");
1062 builder.objects.push(PdfObject {
1063 id,
1064 data: info.into_bytes(),
1065 });
1066 Some(id)
1067 } else {
1068 None
1069 };
1070
1071 Ok(self.serialize(&builder, info_obj_id))
1072 }
1073
1074 #[allow(clippy::too_many_arguments)]
1076 fn build_content_stream_for_page(
1077 &self,
1078 page: &LayoutPage,
1079 page_idx: usize,
1080 builder: &PdfBuilder,
1081 page_number: usize,
1082 total_pages: usize,
1083 mut tag_builder: Option<&mut tagged::TagBuilder>,
1084 flatten_forms: bool,
1085 ) -> String {
1086 let mut stream = String::new();
1087 let page_height = page.height;
1088 let mut element_counter = 0usize;
1089
1090 for element in &page.elements {
1091 self.write_element(
1092 &mut stream,
1093 element,
1094 page_height,
1095 builder,
1096 page_idx,
1097 &mut element_counter,
1098 page_number,
1099 total_pages,
1100 tag_builder.as_deref_mut(),
1101 flatten_forms,
1102 );
1103 }
1104
1105 stream
1106 }
1107
1108 #[allow(clippy::too_many_arguments)]
1110 fn write_element(
1111 &self,
1112 stream: &mut String,
1113 element: &LayoutElement,
1114 page_height: f64,
1115 builder: &PdfBuilder,
1116 page_idx: usize,
1117 element_counter: &mut usize,
1118 page_number: usize,
1119 total_pages: usize,
1120 mut tag_builder: Option<&mut tagged::TagBuilder>,
1121 flatten_forms: bool,
1122 ) {
1123 let mut is_artifact = false;
1126 let tagged_mcid = if let Some(ref mut tb) = tag_builder {
1127 if let Some(ref nt) = element.node_type {
1128 if nt == "Watermark" {
1129 let _ = writeln!(stream, "/Artifact BMC");
1131 is_artifact = true;
1132 None
1133 } else {
1134 let is_header = element.is_header_row;
1135 let mcid = tb.begin_element(nt, is_header, element.alt.as_deref(), page_idx);
1136 let role = tb.map_role_public(nt, is_header);
1137 let _ = writeln!(stream, "/{} <</MCID {}>> BDC", role, mcid);
1138 Some(mcid)
1139 }
1140 } else if !matches!(element.draw, DrawCommand::None) {
1141 let _ = writeln!(stream, "/Artifact BMC");
1143 is_artifact = true;
1144 None
1145 } else {
1146 None
1147 }
1148 } else {
1149 None
1150 };
1151
1152 match &element.draw {
1153 DrawCommand::None => {}
1154
1155 DrawCommand::Rect {
1156 background,
1157 border_width,
1158 border_color,
1159 border_radius,
1160 opacity,
1161 } => {
1162 let x = element.x;
1163 let y = page_height - element.y - element.height;
1164 let w = element.width;
1165 let h = element.height;
1166
1167 let needs_opacity = *opacity < 1.0;
1169 if needs_opacity {
1170 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1171 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1172 }
1173 }
1174
1175 if let Some(bg) = background {
1176 if bg.a > 0.0 {
1177 let _ = writeln!(stream, "q\n{:.3} {:.3} {:.3} rg", bg.r, bg.g, bg.b);
1178
1179 if border_radius.top_left > 0.0 {
1180 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1181 } else {
1182 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1183 }
1184
1185 let _ = writeln!(stream, "f\nQ");
1186 }
1187 }
1188
1189 let bw = border_width;
1190 if bw.top > 0.0 || bw.right > 0.0 || bw.bottom > 0.0 || bw.left > 0.0 {
1191 if (bw.top - bw.right).abs() < 0.001
1192 && (bw.right - bw.bottom).abs() < 0.001
1193 && (bw.bottom - bw.left).abs() < 0.001
1194 {
1195 let bc = &border_color.top;
1196 let _ = writeln!(
1197 stream,
1198 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w",
1199 bc.r, bc.g, bc.b, bw.top
1200 );
1201
1202 if border_radius.top_left > 0.0 {
1203 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1204 } else {
1205 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1206 }
1207
1208 let _ = writeln!(stream, "S\nQ");
1209 } else {
1210 self.write_border_sides(stream, x, y, w, h, bw, border_color);
1211 }
1212 }
1213
1214 if needs_opacity {
1215 let _ = writeln!(stream, "Q");
1216 }
1217 }
1218
1219 DrawCommand::Text {
1220 lines,
1221 color,
1222 text_decoration,
1223 opacity,
1224 } => {
1225 let needs_opacity = *opacity < 1.0;
1227 if needs_opacity {
1228 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1229 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1230 }
1231 }
1232
1233 for line in lines {
1234 if line.glyphs.is_empty() {
1235 continue;
1236 }
1237
1238 let groups = Self::group_glyphs_by_style(&line.glyphs);
1241 let pdf_y = page_height - line.y;
1242
1243 let _ = writeln!(stream, "BT");
1244
1245 if line.word_spacing.abs() > 0.001 {
1247 let _ = writeln!(stream, "{:.4} Tw", line.word_spacing);
1248 }
1249
1250 let mut tm_x = 0.0_f64;
1252 let mut tm_y = 0.0_f64;
1253 let mut x_cursor = line.x;
1254
1255 let mut group_spans: Vec<(f64, f64, TextDecoration, Color)> = Vec::new();
1257
1258 for group in &groups {
1259 let first = &group[0];
1260 let glyph_color = first.color.unwrap_or(*color);
1261
1262 let idx = self.font_index(
1263 &first.font_family,
1264 first.font_weight,
1265 first.font_style,
1266 &builder.font_objects,
1267 );
1268 let italic =
1269 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
1270 let font_key = FontKey {
1271 family: first.font_family.clone(),
1272 weight: first.font_weight,
1273 italic,
1274 };
1275 let font_name = format!("F{}", idx);
1276
1277 let dx = x_cursor - tm_x;
1279 let dy = pdf_y - tm_y;
1280 let _ = writeln!(
1281 stream,
1282 "{:.3} {:.3} {:.3} rg\n/{} {:.1} Tf\n{:.2} Tc\n{:.2} {:.2} Td",
1283 glyph_color.r,
1284 glyph_color.g,
1285 glyph_color.b,
1286 font_name,
1287 first.font_size,
1288 first.letter_spacing,
1289 dx,
1290 dy
1291 );
1292 tm_x = x_cursor;
1293 tm_y = pdf_y;
1294
1295 let raw_text: String = group.iter().map(|g| g.char_value).collect();
1297 let has_placeholder = raw_text.contains(PAGE_NUMBER_SENTINEL)
1298 || raw_text.contains(TOTAL_PAGES_SENTINEL);
1299
1300 let is_custom = builder.custom_font_data.contains_key(&font_key);
1301
1302 if is_custom {
1303 if let Some(embed_data) = builder.custom_font_data.get(&font_key) {
1304 let mut hex = String::new();
1305 if has_placeholder {
1306 let pn = PAGE_NUMBER_SENTINEL.to_string();
1308 let tp = TOTAL_PAGES_SENTINEL.to_string();
1309 let text_after = raw_text
1310 .replace(&pn, &page_number.to_string())
1311 .replace(&tp, &total_pages.to_string());
1312 for ch in text_after.chars() {
1313 let gid =
1314 embed_data.char_to_gid.get(&ch).copied().unwrap_or(0);
1315 let _ = write!(hex, "{:04X}", gid);
1316 }
1317 } else {
1318 for g in group.iter() {
1320 let new_gid = embed_data
1321 .gid_remap
1322 .get(&g.glyph_id)
1323 .copied()
1324 .unwrap_or_else(|| {
1325 embed_data
1327 .char_to_gid
1328 .get(&g.char_value)
1329 .copied()
1330 .unwrap_or(0)
1331 });
1332 let _ = write!(hex, "{:04X}", new_gid);
1333 }
1334 }
1335 let _ = writeln!(stream, "<{}> Tj", hex);
1336 } else {
1337 let _ = writeln!(stream, "<> Tj");
1338 }
1339 } else {
1340 let pn = PAGE_NUMBER_SENTINEL.to_string();
1341 let tp = TOTAL_PAGES_SENTINEL.to_string();
1342 let text_after = raw_text
1343 .replace(&pn, &page_number.to_string())
1344 .replace(&tp, &total_pages.to_string());
1345 let mut text_str = String::new();
1346 for ch in text_after.chars() {
1347 let b = Self::unicode_to_winansi(ch).unwrap_or(b'?');
1348 match b {
1349 b'\\' => text_str.push_str("\\\\"),
1350 b'(' => text_str.push_str("\\("),
1351 b')' => text_str.push_str("\\)"),
1352 0x20..=0x7E => text_str.push(b as char),
1353 _ => {
1354 let _ = write!(text_str, "\\{:03o}", b);
1355 }
1356 }
1357 }
1358 let _ = writeln!(stream, "({}) Tj", text_str);
1359 }
1360
1361 let group_start_x = x_cursor;
1363
1364 if let Some(last) = group.last() {
1367 let space_count_in_group =
1368 group.iter().filter(|g| g.char_value == ' ').count();
1369 x_cursor = line.x
1370 + last.x_offset
1371 + last.x_advance
1372 + space_count_in_group as f64 * line.word_spacing;
1373 }
1374
1375 let group_dec = first.text_decoration;
1377 if !matches!(group_dec, TextDecoration::None) {
1378 group_spans.push((group_start_x, x_cursor, group_dec, glyph_color));
1379 }
1380 }
1381
1382 let _ = writeln!(stream, "ET");
1383
1384 for (span_x, span_end_x, dec, dec_color) in &group_spans {
1386 match dec {
1387 TextDecoration::Underline => {
1388 let underline_y = pdf_y - 1.5;
1389 let _ = write!(
1390 stream,
1391 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1392 dec_color.r, dec_color.g, dec_color.b,
1393 span_x, underline_y,
1394 span_end_x, underline_y
1395 );
1396 }
1397 TextDecoration::LineThrough => {
1398 let first_size =
1399 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
1400 let strikethrough_y = pdf_y + first_size * 0.3;
1401 let _ = write!(
1402 stream,
1403 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1404 dec_color.r, dec_color.g, dec_color.b,
1405 span_x, strikethrough_y,
1406 span_end_x, strikethrough_y
1407 );
1408 }
1409 TextDecoration::None => {}
1410 }
1411 }
1412
1413 if group_spans.is_empty() {
1415 if matches!(text_decoration, TextDecoration::Underline) {
1416 let underline_y = pdf_y - 1.5;
1417 let _ = write!(
1418 stream,
1419 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1420 color.r, color.g, color.b,
1421 line.x, underline_y,
1422 line.x + line.width, underline_y
1423 );
1424 }
1425 if matches!(text_decoration, TextDecoration::LineThrough) {
1426 let first_size =
1427 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
1428 let strikethrough_y = pdf_y + first_size * 0.3;
1429 let _ = write!(
1430 stream,
1431 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1432 color.r, color.g, color.b,
1433 line.x, strikethrough_y,
1434 line.x + line.width, strikethrough_y
1435 );
1436 }
1437 }
1438 }
1439
1440 if needs_opacity {
1441 let _ = writeln!(stream, "Q");
1442 }
1443 }
1444
1445 DrawCommand::Image { .. } => {
1446 let elem_idx = *element_counter;
1447 *element_counter += 1;
1448 if let Some(&img_idx) = builder.image_index_map.get(&(page_idx, elem_idx)) {
1449 let x = element.x;
1450 let y = page_height - element.y - element.height;
1451 let _ = write!(
1452 stream,
1453 "q\n{:.4} 0 0 {:.4} {:.2} {:.2} cm\n/Im{} Do\nQ\n",
1454 element.width, element.height, x, y, img_idx
1455 );
1456 } else {
1457 let x = element.x;
1459 let y = page_height - element.y - element.height;
1460 let _ = write!(
1461 stream,
1462 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
1463 x, y, element.width, element.height
1464 );
1465 }
1466 if tagged_mcid.is_some() {
1467 let _ = writeln!(stream, "EMC");
1468 if let Some(ref mut tb) = tag_builder {
1469 tb.end_element();
1470 }
1471 } else if is_artifact {
1472 let _ = writeln!(stream, "EMC");
1473 }
1474 return; }
1476
1477 DrawCommand::ImagePlaceholder => {
1478 *element_counter += 1;
1479 let x = element.x;
1480 let y = page_height - element.y - element.height;
1481 let _ = write!(
1482 stream,
1483 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
1484 x, y, element.width, element.height
1485 );
1486 if tagged_mcid.is_some() {
1487 let _ = writeln!(stream, "EMC");
1488 if let Some(ref mut tb) = tag_builder {
1489 tb.end_element();
1490 }
1491 } else if is_artifact {
1492 let _ = writeln!(stream, "EMC");
1493 }
1494 return;
1495 }
1496
1497 DrawCommand::Svg {
1498 commands,
1499 width: svg_w,
1500 height: svg_h,
1501 clip,
1502 } => {
1503 let x = element.x;
1504 let y = page_height - element.y - element.height;
1505
1506 let _ = writeln!(stream, "q");
1508 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", x, y);
1509
1510 if *svg_w > 0.0 && *svg_h > 0.0 {
1512 let sx = element.width / svg_w;
1513 let sy = element.height / svg_h;
1514 let _ = writeln!(stream, "{:.4} 0 0 {:.4} 0 0 cm", sx, sy);
1515 }
1516
1517 let _ = writeln!(stream, "1 0 0 -1 0 {:.2} cm", svg_h);
1519
1520 if *clip {
1522 let _ = writeln!(stream, "0 0 {:.2} {:.2} re W n", svg_w, svg_h);
1523 }
1524
1525 Self::write_svg_commands(stream, commands, &builder.ext_gstate_map);
1526
1527 let _ = writeln!(stream, "Q");
1528 if tagged_mcid.is_some() {
1529 let _ = writeln!(stream, "EMC");
1530 if let Some(ref mut tb) = tag_builder {
1531 tb.end_element();
1532 }
1533 } else if is_artifact {
1534 let _ = writeln!(stream, "EMC");
1535 }
1536 return;
1537 }
1538
1539 DrawCommand::Barcode {
1540 bars,
1541 bar_width,
1542 height,
1543 color,
1544 } => {
1545 *element_counter += 1;
1546 let _ = writeln!(stream, "q");
1547 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1548 for (i, &bar) in bars.iter().enumerate() {
1549 if bar == 1 {
1550 let bx = element.x + i as f64 * bar_width;
1551 let by = page_height - element.y - height;
1552 let _ = writeln!(
1553 stream,
1554 "{:.2} {:.2} {:.2} {:.2} re",
1555 bx, by, bar_width, height
1556 );
1557 }
1558 }
1559 let _ = writeln!(stream, "f\nQ");
1560 if tagged_mcid.is_some() {
1561 let _ = writeln!(stream, "EMC");
1562 if let Some(ref mut tb) = tag_builder {
1563 tb.end_element();
1564 }
1565 } else if is_artifact {
1566 let _ = writeln!(stream, "EMC");
1567 }
1568 return;
1569 }
1570
1571 DrawCommand::QrCode {
1572 modules,
1573 module_size,
1574 color,
1575 } => {
1576 *element_counter += 1;
1577 let _ = writeln!(stream, "q");
1578 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1579 for (row_idx, row) in modules.iter().enumerate() {
1580 for (col_idx, &dark) in row.iter().enumerate() {
1581 if dark {
1582 let mx = element.x + col_idx as f64 * module_size;
1583 let my = page_height - element.y - (row_idx as f64 + 1.0) * module_size;
1584 let _ = writeln!(
1585 stream,
1586 "{:.2} {:.2} {:.2} {:.2} re",
1587 mx, my, module_size, module_size
1588 );
1589 }
1590 }
1591 }
1592 let _ = writeln!(stream, "f\nQ");
1593 if tagged_mcid.is_some() {
1594 let _ = writeln!(stream, "EMC");
1595 if let Some(ref mut tb) = tag_builder {
1596 tb.end_element();
1597 }
1598 } else if is_artifact {
1599 let _ = writeln!(stream, "EMC");
1600 }
1601 return;
1602 }
1603
1604 DrawCommand::Chart { primitives } => {
1605 *element_counter += 1;
1606 let _ = writeln!(stream, "q");
1607 let _ = writeln!(
1609 stream,
1610 "1 0 0 -1 {:.4} {:.4} cm",
1611 element.x,
1612 page_height - element.y
1613 );
1614
1615 for prim in primitives {
1616 write_chart_primitive(stream, prim, element.height, builder);
1617 }
1618
1619 let _ = writeln!(stream, "Q");
1620 if tagged_mcid.is_some() {
1621 let _ = writeln!(stream, "EMC");
1622 if let Some(ref mut tb) = tag_builder {
1623 tb.end_element();
1624 }
1625 } else if is_artifact {
1626 let _ = writeln!(stream, "EMC");
1627 }
1628 return;
1629 }
1630
1631 DrawCommand::Watermark {
1632 lines,
1633 color,
1634 opacity,
1635 angle_rad,
1636 font_family: _,
1637 } => {
1638 let _ = writeln!(stream, "q");
1639 if *opacity < 1.0 {
1641 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1642 let _ = writeln!(stream, "/{} gs", gs_name);
1643 }
1644 }
1645 let pdf_cx = element.x;
1647 let pdf_cy = page_height - element.y;
1648 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", pdf_cx, pdf_cy);
1649 let cos_a = angle_rad.cos();
1651 let sin_a = angle_rad.sin();
1652 let _ = writeln!(
1653 stream,
1654 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
1655 cos_a, sin_a, -sin_a, cos_a
1656 );
1657 let _ = writeln!(stream, "BT");
1659 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1660 if let Some(line) = lines.first() {
1661 let groups = Self::group_glyphs_by_style(&line.glyphs);
1662 let text_width = line.width;
1663 let cap_height = line.height * 0.7;
1664 let _ = writeln!(
1665 stream,
1666 "{:.2} {:.2} Td",
1667 -text_width / 2.0,
1668 -cap_height / 2.0
1669 );
1670 for group in &groups {
1671 let first = &group[0];
1672 let italic =
1673 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
1674 let fk = FontKey {
1675 family: first.font_family.clone(),
1676 weight: first.font_weight,
1677 italic,
1678 };
1679 let idx = self.font_index(
1680 &first.font_family,
1681 first.font_weight,
1682 first.font_style,
1683 &builder.font_objects,
1684 );
1685 let font_name = format!("F{}", idx);
1686 let _ = writeln!(stream, "/{} {:.1} Tf", font_name, first.font_size);
1687 let is_custom = builder.custom_font_data.contains_key(&fk);
1688 if is_custom {
1689 if let Some(embed_data) = builder.custom_font_data.get(&fk) {
1690 let mut hex = String::new();
1691 for g in group.iter() {
1692 let gid =
1693 embed_data.gid_remap.get(&g.glyph_id).copied().unwrap_or(0);
1694 let _ = write!(hex, "{:04X}", gid);
1695 }
1696 let _ = writeln!(stream, "<{}> Tj", hex);
1697 }
1698 } else {
1699 let hex_str: String = group
1700 .iter()
1701 .map(|g| format!("{:02X}", g.glyph_id as u8))
1702 .collect();
1703 let _ = writeln!(stream, "<{}> Tj", hex_str);
1704 }
1705 }
1706 }
1707 let _ = writeln!(stream, "ET");
1708 let _ = writeln!(stream, "Q");
1709 if tagged_mcid.is_some() {
1710 let _ = writeln!(stream, "EMC");
1711 if let Some(ref mut tb) = tag_builder {
1712 tb.end_element();
1713 }
1714 } else if is_artifact {
1715 let _ = writeln!(stream, "EMC");
1716 }
1717 return;
1718 }
1719
1720 DrawCommand::FormField { field_type, .. } => {
1721 let pdf_x = element.x;
1725 let pdf_y = page_height - element.y - element.height;
1726 let w = element.width;
1727 let h = element.height;
1728 let _ = writeln!(stream, "q");
1729 match field_type {
1730 FormFieldType::Checkbox { checked, .. } => {
1731 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
1734 let _ =
1735 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
1736 if *checked {
1737 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
1739 let sx = w / 14.0;
1740 let sy = h / 14.0;
1741 let _ = writeln!(
1742 stream,
1743 "{:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l f",
1744 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
1745 pdf_x + 5.5 * sx, pdf_y + 2.0 * sy,
1746 pdf_x + 12.0 * sx, pdf_y + 11.0 * sy,
1747 pdf_x + 11.0 * sx, pdf_y + 12.0 * sy,
1748 pdf_x + 5.5 * sx, pdf_y + 4.5 * sy,
1749 pdf_x + 3.0 * sx, pdf_y + 7.0 * sy,
1750 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
1751 );
1752 }
1753 }
1754 FormFieldType::RadioButton { checked, .. } => {
1755 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
1758 let _ =
1759 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
1760 if *checked {
1761 let cx = pdf_x + w / 2.0;
1763 let cy = pdf_y + h / 2.0;
1764 let r = (w.min(h) / 2.0) * 0.6;
1765 let k = r * 0.5523;
1766 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
1767 let _ = writeln!(
1768 stream,
1769 "{:.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",
1770 cx, cy + r,
1771 cx + k, cy + r, cx + r, cy + k, cx + r, cy,
1772 cx + r, cy - k, cx + k, cy - r, cx, cy - r,
1773 cx - k, cy - r, cx - r, cy - k, cx - r, cy,
1774 cx - r, cy + k, cx - k, cy + r, cx, cy + r,
1775 );
1776 }
1777 }
1778 FormFieldType::TextField {
1779 value,
1780 placeholder,
1781 font_size,
1782 multiline,
1783 password,
1784 ..
1785 } => {
1786 let _ = writeln!(stream, "1 1 1 rg");
1788 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
1789 let _ = writeln!(stream, "0.5 w");
1790 let _ =
1791 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
1792 if flatten_forms {
1794 let has_value = value.as_ref().is_some_and(|v| !v.is_empty());
1795 if has_value {
1796 let val = value.as_ref().unwrap();
1797 let display_text = if *password {
1798 "\u{2022}".repeat(val.len())
1799 } else {
1800 val.clone()
1801 };
1802 let font_idx = builder
1803 .font_objects
1804 .iter()
1805 .enumerate()
1806 .find(|(_, (key, _))| {
1807 key.family == "Helvetica"
1808 && key.weight == 400
1809 && !key.italic
1810 })
1811 .map(|(i, _)| i)
1812 .unwrap_or(0);
1813 if *multiline {
1814 let metrics = crate::font::StandardFont::Helvetica.metrics();
1816 let max_w = w - 4.0;
1817 let mut lines: Vec<String> = Vec::new();
1818 for paragraph in display_text.split('\n') {
1819 let mut line = String::new();
1820 let mut line_w = 0.0;
1821 for word in paragraph.split_whitespace() {
1822 let word_w =
1823 metrics.measure_string(word, *font_size, 0.0);
1824 let space_w = if line.is_empty() {
1825 0.0
1826 } else {
1827 metrics.measure_string(" ", *font_size, 0.0)
1828 };
1829 if word_w > max_w {
1831 let mut char_line = String::new();
1832 let mut char_w = 0.0;
1833 for ch in word.chars() {
1834 let cw = metrics.char_width(ch, *font_size);
1835 if !char_line.is_empty() && char_w + cw > max_w
1836 {
1837 if !line.is_empty() {
1838 lines.push(line.clone());
1839 line.clear();
1840 line_w = 0.0;
1841 }
1842 lines.push(char_line.clone());
1843 char_line.clear();
1844 char_w = 0.0;
1845 }
1846 char_line.push(ch);
1847 char_w += cw;
1848 }
1849 if !char_line.is_empty() {
1851 if !line.is_empty() {
1852 line.push(' ');
1853 line_w += metrics
1854 .measure_string(" ", *font_size, 0.0);
1855 }
1856 line.push_str(&char_line);
1857 line_w += char_w;
1858 }
1859 continue;
1860 }
1861 if !line.is_empty() && line_w + space_w + word_w > max_w
1862 {
1863 lines.push(line.clone());
1864 line.clear();
1865 line_w = 0.0;
1866 }
1867 if !line.is_empty() {
1868 line.push(' ');
1869 line_w += space_w;
1870 }
1871 line.push_str(word);
1872 line_w += word_w;
1873 }
1874 if !line.is_empty() {
1875 lines.push(line);
1876 }
1877 }
1878 let text_y = pdf_y + h - font_size - 2.0;
1879 for (i, line_text) in lines.iter().enumerate() {
1880 let ly = text_y - (i as f64) * (font_size * 1.2);
1881 if ly < pdf_y {
1882 break;
1883 }
1884 let esc = Self::encode_winansi_text(line_text);
1885 let _ = writeln!(
1886 stream,
1887 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
1888 font_idx,
1889 font_size,
1890 pdf_x + 2.0,
1891 ly,
1892 esc
1893 );
1894 }
1895 } else {
1896 let escaped = Self::encode_winansi_text(&display_text);
1897 let text_y = pdf_y + (h - font_size) / 2.0;
1898 let _ = writeln!(
1899 stream,
1900 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
1901 font_idx,
1902 font_size,
1903 pdf_x + 2.0,
1904 text_y,
1905 escaped
1906 );
1907 }
1908 } else if let Some(ref ph) = placeholder {
1909 if !ph.is_empty() {
1910 let font_idx = builder
1912 .font_objects
1913 .iter()
1914 .enumerate()
1915 .find(|(_, (key, _))| {
1916 key.family == "Helvetica"
1917 && key.weight == 400
1918 && !key.italic
1919 })
1920 .map(|(i, _)| i)
1921 .unwrap_or(0);
1922 let escaped = Self::encode_winansi_text(ph);
1923 let text_y = pdf_y + (h - font_size) / 2.0;
1924 let _ = writeln!(
1925 stream,
1926 "BT /F{} {:.1} Tf 0.6 g {:.2} {:.2} Td ({}) Tj ET",
1927 font_idx,
1928 font_size,
1929 pdf_x + 2.0,
1930 text_y,
1931 escaped
1932 );
1933 }
1934 }
1935 }
1936 }
1937 FormFieldType::Dropdown {
1938 value, font_size, ..
1939 } => {
1940 let _ = writeln!(stream, "1 1 1 rg");
1942 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
1943 let _ = writeln!(stream, "0.5 w");
1944 let _ =
1945 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
1946 if flatten_forms {
1948 if let Some(ref val) = value {
1949 if !val.is_empty() {
1950 let font_idx = builder
1951 .font_objects
1952 .iter()
1953 .enumerate()
1954 .find(|(_, (key, _))| {
1955 key.family == "Helvetica"
1956 && key.weight == 400
1957 && !key.italic
1958 })
1959 .map(|(i, _)| i)
1960 .unwrap_or(0);
1961 let escaped = Self::encode_winansi_text(val);
1962 let text_y = pdf_y + (h - font_size) / 2.0;
1963 let _ = writeln!(
1964 stream,
1965 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
1966 font_idx,
1967 font_size,
1968 pdf_x + 2.0,
1969 text_y,
1970 escaped
1971 );
1972 }
1973 }
1974 }
1975 }
1976 }
1977 let _ = writeln!(stream, "Q");
1978 }
1979 }
1980
1981 let clip_overflow = matches!(element.overflow, Overflow::Hidden);
1983 if clip_overflow {
1984 let clip_x = element.x;
1985 let clip_y = page_height - element.y - element.height;
1986 let clip_w = element.width;
1987 let clip_h = element.height;
1988 let _ = writeln!(
1989 stream,
1990 "q\n{:.2} {:.2} {:.2} {:.2} re W n",
1991 clip_x, clip_y, clip_w, clip_h
1992 );
1993 }
1994
1995 for child in &element.children {
1996 self.write_element(
1997 stream,
1998 child,
1999 page_height,
2000 builder,
2001 page_idx,
2002 element_counter,
2003 page_number,
2004 total_pages,
2005 tag_builder.as_deref_mut(),
2006 flatten_forms,
2007 );
2008 }
2009
2010 if clip_overflow {
2011 let _ = writeln!(stream, "Q");
2012 }
2013
2014 if tagged_mcid.is_some() {
2016 let _ = writeln!(stream, "EMC");
2017 if let Some(ref mut tb) = tag_builder {
2018 tb.end_element();
2019 }
2020 } else if is_artifact {
2021 let _ = writeln!(stream, "EMC");
2022 }
2023 }
2024
2025 fn write_rounded_rect(
2026 &self,
2027 stream: &mut String,
2028 x: f64,
2029 y: f64,
2030 w: f64,
2031 h: f64,
2032 r: &crate::style::CornerValues,
2033 ) {
2034 let k = 0.5522847498;
2035
2036 let tl = r.top_left.min(w / 2.0).min(h / 2.0);
2037 let tr = r.top_right.min(w / 2.0).min(h / 2.0);
2038 let br = r.bottom_right.min(w / 2.0).min(h / 2.0);
2039 let bl = r.bottom_left.min(w / 2.0).min(h / 2.0);
2040
2041 let _ = writeln!(stream, "{:.2} {:.2} m", x + bl, y);
2042
2043 let _ = writeln!(stream, "{:.2} {:.2} l", x + w - br, y);
2044 if br > 0.0 {
2045 let _ = writeln!(
2046 stream,
2047 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2048 x + w - br + br * k,
2049 y,
2050 x + w,
2051 y + br - br * k,
2052 x + w,
2053 y + br
2054 );
2055 }
2056
2057 let _ = writeln!(stream, "{:.2} {:.2} l", x + w, y + h - tr);
2058 if tr > 0.0 {
2059 let _ = writeln!(
2060 stream,
2061 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2062 x + w,
2063 y + h - tr + tr * k,
2064 x + w - tr + tr * k,
2065 y + h,
2066 x + w - tr,
2067 y + h
2068 );
2069 }
2070
2071 let _ = writeln!(stream, "{:.2} {:.2} l", x + tl, y + h);
2072 if tl > 0.0 {
2073 let _ = writeln!(
2074 stream,
2075 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2076 x + tl - tl * k,
2077 y + h,
2078 x,
2079 y + h - tl + tl * k,
2080 x,
2081 y + h - tl
2082 );
2083 }
2084
2085 let _ = writeln!(stream, "{:.2} {:.2} l", x, y + bl);
2086 if bl > 0.0 {
2087 let _ = writeln!(
2088 stream,
2089 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2090 x,
2091 y + bl - bl * k,
2092 x + bl - bl * k,
2093 y,
2094 x + bl,
2095 y
2096 );
2097 }
2098
2099 let _ = writeln!(stream, "h");
2100 }
2101
2102 #[allow(clippy::too_many_arguments)]
2103 fn write_border_sides(
2104 &self,
2105 stream: &mut String,
2106 x: f64,
2107 y: f64,
2108 w: f64,
2109 h: f64,
2110 bw: &Edges,
2111 bc: &crate::style::EdgeValues<Color>,
2112 ) {
2113 if bw.top > 0.0 {
2114 let _ = write!(
2115 stream,
2116 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2117 bc.top.r,
2118 bc.top.g,
2119 bc.top.b,
2120 bw.top,
2121 x,
2122 y + h,
2123 x + w,
2124 y + h
2125 );
2126 }
2127 if bw.bottom > 0.0 {
2128 let _ = write!(
2129 stream,
2130 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2131 bc.bottom.r,
2132 bc.bottom.g,
2133 bc.bottom.b,
2134 bw.bottom,
2135 x,
2136 y,
2137 x + w,
2138 y
2139 );
2140 }
2141 if bw.left > 0.0 {
2142 let _ = write!(
2143 stream,
2144 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2145 bc.left.r,
2146 bc.left.g,
2147 bc.left.b,
2148 bw.left,
2149 x,
2150 y,
2151 x,
2152 y + h
2153 );
2154 }
2155 if bw.right > 0.0 {
2156 let _ = write!(
2157 stream,
2158 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2159 bc.right.r,
2160 bc.right.g,
2161 bc.right.b,
2162 bw.right,
2163 x + w,
2164 y,
2165 x + w,
2166 y + h
2167 );
2168 }
2169 }
2170
2171 fn register_fonts(
2174 &self,
2175 builder: &mut PdfBuilder,
2176 pages: &[LayoutPage],
2177 font_context: &FontContext,
2178 ) -> Result<(), FormeError> {
2179 let mut font_usage_map: HashMap<FontKey, FontUsage> = HashMap::new();
2181
2182 for page in pages {
2183 Self::collect_font_usage(&page.elements, &mut font_usage_map);
2184 }
2185
2186 let mut keys: Vec<FontKey> = font_usage_map.keys().cloned().collect();
2187
2188 keys.sort_by(|a, b| {
2190 a.family
2191 .cmp(&b.family)
2192 .then(a.weight.cmp(&b.weight))
2193 .then(a.italic.cmp(&b.italic))
2194 });
2195 keys.dedup();
2196
2197 if keys.is_empty() {
2199 keys.push(FontKey {
2200 family: "Helvetica".to_string(),
2201 weight: 400,
2202 italic: false,
2203 });
2204 }
2205
2206 for key in &keys {
2207 let font_data = font_context.resolve(&key.family, key.weight, key.italic);
2208
2209 match font_data {
2210 FontData::Standard(std_font) => {
2211 let obj_id = builder.objects.len();
2212 let metrics = std_font.metrics();
2215 let widths_str: String = metrics
2216 .widths
2217 .iter()
2218 .map(|w| w.to_string())
2219 .collect::<Vec<_>>()
2220 .join(" ");
2221 let font_dict = format!(
2222 "<< /Type /Font /Subtype /Type1 /BaseFont /{} \
2223 /Encoding /WinAnsiEncoding \
2224 /FirstChar 32 /LastChar 255 /Widths [{}] >>",
2225 std_font.pdf_name(),
2226 widths_str,
2227 );
2228 builder.objects.push(PdfObject {
2229 id: obj_id,
2230 data: font_dict.into_bytes(),
2231 });
2232 builder.font_objects.push((key.clone(), obj_id));
2233 }
2234 FontData::Custom { data, .. } => {
2235 let usage = font_usage_map.get(key);
2236 let used_glyph_ids = usage.map(|u| &u.glyph_ids);
2237 let used_chars = usage.map(|u| &u.chars);
2238 let glyph_to_char = usage.map(|u| &u.glyph_to_char);
2239 let type0_obj_id = Self::write_custom_font_objects(
2240 builder,
2241 key,
2242 data,
2243 used_glyph_ids.cloned().unwrap_or_default(),
2244 used_chars.cloned().unwrap_or_default(),
2245 glyph_to_char.cloned().unwrap_or_default(),
2246 )?;
2247 builder.font_objects.push((key.clone(), type0_obj_id));
2248 }
2249 }
2250 }
2251
2252 Ok(())
2253 }
2254
2255 fn collect_font_usage(
2257 elements: &[LayoutElement],
2258 font_usage: &mut HashMap<FontKey, FontUsage>,
2259 ) {
2260 for element in elements {
2261 let lines_opt = match &element.draw {
2262 DrawCommand::Text { lines, .. } => Some(lines),
2263 DrawCommand::Watermark { lines, .. } => Some(lines),
2264 _ => None,
2265 };
2266 if let Some(lines) = lines_opt {
2267 for line in lines {
2268 for glyph in &line.glyphs {
2269 let italic =
2270 matches!(glyph.font_style, FontStyle::Italic | FontStyle::Oblique);
2271 let key = FontKey {
2272 family: glyph.font_family.clone(),
2273 weight: glyph.font_weight,
2274 italic,
2275 };
2276 let usage = font_usage.entry(key).or_insert_with(|| FontUsage {
2277 chars: HashSet::new(),
2278 glyph_ids: HashSet::new(),
2279 glyph_to_char: HashMap::new(),
2280 });
2281 usage.chars.insert(glyph.char_value);
2282 usage.glyph_ids.insert(glyph.glyph_id);
2283 usage
2285 .glyph_to_char
2286 .entry(glyph.glyph_id)
2287 .or_insert(glyph.char_value);
2288 if let Some(ref ct) = glyph.cluster_text {
2290 if let Some(first_char) = ct.chars().next() {
2292 usage
2293 .glyph_to_char
2294 .entry(glyph.glyph_id)
2295 .or_insert(first_char);
2296 }
2297 }
2298 }
2299 }
2300 }
2301 Self::collect_font_usage(&element.children, font_usage);
2302 }
2303 }
2304
2305 fn register_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2308 for (page_idx, page) in pages.iter().enumerate() {
2309 let mut element_counter = 0usize;
2310 Self::collect_images_recursive(&page.elements, page_idx, &mut element_counter, builder);
2311 }
2312 }
2313
2314 fn collect_images_recursive(
2315 elements: &[LayoutElement],
2316 page_idx: usize,
2317 element_counter: &mut usize,
2318 builder: &mut PdfBuilder,
2319 ) {
2320 for element in elements {
2321 match &element.draw {
2322 DrawCommand::Image { image_data } => {
2323 let elem_idx = *element_counter;
2324 *element_counter += 1;
2325
2326 let img_idx = builder.image_objects.len();
2327 let xobj_id = Self::write_image_xobject(builder, image_data);
2328 builder.image_objects.push(xobj_id);
2329 builder
2330 .image_index_map
2331 .insert((page_idx, elem_idx), img_idx);
2332 }
2333 DrawCommand::ImagePlaceholder => {
2334 *element_counter += 1;
2335 }
2336 _ => {
2337 Self::collect_images_recursive(
2338 &element.children,
2339 page_idx,
2340 element_counter,
2341 builder,
2342 );
2343 }
2344 }
2345 }
2346 }
2347
2348 fn register_ext_gstates(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2350 let mut unique_opacities: Vec<f64> = Vec::new();
2351 for page in pages {
2352 Self::collect_opacities_recursive(&page.elements, &mut unique_opacities);
2353 }
2354 unique_opacities.sort_by(|a, b| a.partial_cmp(b).unwrap());
2355 unique_opacities.dedup();
2356
2357 for (idx, &opacity) in unique_opacities.iter().enumerate() {
2358 let obj_id = builder.objects.len();
2359 let gs_name = format!("GS{}", idx);
2360 let obj_data = format!(
2361 "<< /Type /ExtGState /ca {:.4} /CA {:.4} >>",
2362 opacity, opacity
2363 );
2364 builder.objects.push(PdfObject {
2365 id: obj_id,
2366 data: obj_data.into_bytes(),
2367 });
2368 let key = opacity.to_bits();
2369 builder.ext_gstate_map.insert(key, (obj_id, gs_name));
2370 }
2371 }
2372
2373 fn collect_opacities_recursive(elements: &[LayoutElement], opacities: &mut Vec<f64>) {
2374 for element in elements {
2375 match &element.draw {
2376 DrawCommand::Rect { opacity, .. }
2377 | DrawCommand::Text { opacity, .. }
2378 | DrawCommand::Watermark { opacity, .. }
2379 if *opacity < 1.0 =>
2380 {
2381 opacities.push(*opacity);
2382 }
2383 DrawCommand::Chart { primitives } => {
2384 for prim in primitives {
2385 if let crate::chart::ChartPrimitive::FilledPath { opacity, .. } = prim {
2386 if *opacity < 1.0 {
2387 opacities.push(*opacity);
2388 }
2389 }
2390 }
2391 }
2392 DrawCommand::Svg { commands, .. } => {
2393 for cmd in commands {
2394 if let crate::svg::SvgCommand::SetOpacity(opacity) = cmd {
2395 if *opacity < 1.0 {
2396 opacities.push(*opacity);
2397 }
2398 }
2399 }
2400 }
2401 _ => {}
2402 }
2403 Self::collect_opacities_recursive(&element.children, opacities);
2404 }
2405 }
2406
2407 fn build_ext_gstate_resource_dict(&self, builder: &PdfBuilder) -> String {
2409 if builder.ext_gstate_map.is_empty() {
2410 return String::new();
2411 }
2412 let mut entries: Vec<(&String, usize)> = builder
2413 .ext_gstate_map
2414 .values()
2415 .map(|(obj_id, name)| (name, *obj_id))
2416 .collect();
2417 entries.sort_by_key(|(name, _)| (*name).clone());
2418 entries
2419 .iter()
2420 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
2421 .collect::<Vec<_>>()
2422 .join(" ")
2423 }
2424
2425 fn write_image_xobject(
2428 builder: &mut PdfBuilder,
2429 image: &crate::image_loader::LoadedImage,
2430 ) -> usize {
2431 use crate::image_loader::{ImagePixelData, JpegColorSpace};
2432
2433 match &image.pixel_data {
2434 ImagePixelData::Jpeg { data, color_space } => {
2435 let color_space_str = match color_space {
2436 JpegColorSpace::DeviceRGB => "/DeviceRGB",
2437 JpegColorSpace::DeviceGray => "/DeviceGray",
2438 };
2439
2440 let obj_id = builder.objects.len();
2441 let mut obj_data: Vec<u8> = Vec::new();
2442 let _ = write!(
2443 obj_data,
2444 "<< /Type /XObject /Subtype /Image \
2445 /Width {} /Height {} \
2446 /ColorSpace {} \
2447 /BitsPerComponent 8 \
2448 /Filter /DCTDecode \
2449 /Length {} >>\nstream\n",
2450 image.width_px,
2451 image.height_px,
2452 color_space_str,
2453 data.len()
2454 );
2455 obj_data.extend_from_slice(data);
2456 obj_data.extend_from_slice(b"\nendstream");
2457 builder.objects.push(PdfObject {
2458 id: obj_id,
2459 data: obj_data,
2460 });
2461 obj_id
2462 }
2463
2464 ImagePixelData::Decoded { rgb, alpha } => {
2465 let smask_id = alpha.as_ref().map(|alpha_data| {
2467 let compressed_alpha = compress_to_vec_zlib(alpha_data, 6);
2468 let smask_obj_id = builder.objects.len();
2469 let mut smask_data: Vec<u8> = Vec::new();
2470 let _ = write!(
2471 smask_data,
2472 "<< /Type /XObject /Subtype /Image \
2473 /Width {} /Height {} \
2474 /ColorSpace /DeviceGray \
2475 /BitsPerComponent 8 \
2476 /Filter /FlateDecode \
2477 /Length {} >>\nstream\n",
2478 image.width_px,
2479 image.height_px,
2480 compressed_alpha.len()
2481 );
2482 smask_data.extend_from_slice(&compressed_alpha);
2483 smask_data.extend_from_slice(b"\nendstream");
2484 builder.objects.push(PdfObject {
2485 id: smask_obj_id,
2486 data: smask_data,
2487 });
2488 smask_obj_id
2489 });
2490
2491 let compressed_rgb = compress_to_vec_zlib(rgb, 6);
2493 let obj_id = builder.objects.len();
2494 let mut obj_data: Vec<u8> = Vec::new();
2495
2496 let smask_ref = smask_id
2497 .map(|id| format!(" /SMask {} 0 R", id))
2498 .unwrap_or_default();
2499
2500 let _ = write!(
2501 obj_data,
2502 "<< /Type /XObject /Subtype /Image \
2503 /Width {} /Height {} \
2504 /ColorSpace /DeviceRGB \
2505 /BitsPerComponent 8 \
2506 /Filter /FlateDecode \
2507 /Length {}{} >>\nstream\n",
2508 image.width_px,
2509 image.height_px,
2510 compressed_rgb.len(),
2511 smask_ref
2512 );
2513 obj_data.extend_from_slice(&compressed_rgb);
2514 obj_data.extend_from_slice(b"\nendstream");
2515 builder.objects.push(PdfObject {
2516 id: obj_id,
2517 data: obj_data,
2518 });
2519 obj_id
2520 }
2521 }
2522 }
2523
2524 fn build_xobject_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
2526 let mut entries: Vec<(usize, usize)> = Vec::new();
2527 for (&(pidx, _), &img_idx) in &builder.image_index_map {
2528 if pidx == page_idx {
2529 let obj_id = builder.image_objects[img_idx];
2530 entries.push((img_idx, obj_id));
2531 }
2532 }
2533 if entries.is_empty() {
2534 return String::new();
2535 }
2536 entries.sort_by_key(|(idx, _)| *idx);
2537 entries.dedup();
2538 entries
2539 .iter()
2540 .map(|(idx, obj_id)| format!("/Im{} {} 0 R", idx, obj_id))
2541 .collect::<Vec<_>>()
2542 .join(" ")
2543 }
2544
2545 fn write_custom_font_objects(
2552 builder: &mut PdfBuilder,
2553 key: &FontKey,
2554 ttf_data: &[u8],
2555 used_glyph_ids: HashSet<u16>,
2556 used_chars: HashSet<char>,
2557 glyph_to_char_map: HashMap<u16, char>,
2558 ) -> Result<usize, FormeError> {
2559 let face = ttf_parser::Face::parse(ttf_data, 0).map_err(|e| {
2560 FormeError::FontError(format!(
2561 "Failed to parse TTF data for font '{}': {}",
2562 key.family, e
2563 ))
2564 })?;
2565
2566 let units_per_em = face.units_per_em();
2567 let ascender = face.ascender();
2568 let descender = face.descender();
2569
2570 let mut char_to_orig_gid: HashMap<char, u16> = HashMap::new();
2572 for &ch in &used_chars {
2573 if let Some(gid) = face.glyph_index(ch) {
2574 char_to_orig_gid.insert(ch, gid.0);
2575 }
2576 }
2577
2578 let mut all_orig_gids: HashSet<u16> = used_glyph_ids.clone();
2582 for &gid in char_to_orig_gid.values() {
2583 all_orig_gids.insert(gid);
2584 }
2585
2586 let (embed_ttf, gid_remap) = match subset_ttf(ttf_data, &all_orig_gids) {
2588 Ok(subset_result) => (subset_result.ttf_data, subset_result.gid_remap),
2589 Err(_) => {
2590 let identity: HashMap<u16, u16> =
2592 all_orig_gids.iter().map(|&gid| (gid, gid)).collect();
2593 (ttf_data.to_vec(), identity)
2594 }
2595 };
2596
2597 let char_to_gid: HashMap<char, u16> = char_to_orig_gid
2599 .iter()
2600 .filter_map(|(&ch, &orig_gid)| gid_remap.get(&orig_gid).map(|&new_gid| (ch, new_gid)))
2601 .collect();
2602
2603 let gid_remap_for_embed = gid_remap.clone();
2605
2606 let mut new_gid_to_char: HashMap<u16, char> = HashMap::new();
2608 for (&orig_gid, &ch) in &glyph_to_char_map {
2610 if let Some(&new_gid) = gid_remap.get(&orig_gid) {
2611 new_gid_to_char.entry(new_gid).or_insert(ch);
2612 }
2613 }
2614 for (&ch, &new_gid) in &char_to_gid {
2616 new_gid_to_char.entry(new_gid).or_insert(ch);
2617 }
2618
2619 let pdf_font_name = Self::sanitize_font_name(&key.family, key.weight, key.italic);
2620
2621 let compressed_ttf = compress_to_vec_zlib(&embed_ttf, 6);
2623 let fontfile2_id = builder.objects.len();
2624 let mut fontfile2_data: Vec<u8> = Vec::new();
2625 let _ = write!(
2626 fontfile2_data,
2627 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
2628 compressed_ttf.len(),
2629 embed_ttf.len()
2630 );
2631 fontfile2_data.extend_from_slice(&compressed_ttf);
2632 fontfile2_data.extend_from_slice(b"\nendstream");
2633 builder.objects.push(PdfObject {
2634 id: fontfile2_id,
2635 data: fontfile2_data,
2636 });
2637
2638 let subset_face = ttf_parser::Face::parse(&embed_ttf, 0).unwrap_or_else(|_| face.clone());
2640 let subset_upem = subset_face.units_per_em();
2641
2642 let font_descriptor_id = builder.objects.len();
2644 let bbox = face.global_bounding_box();
2645 let scale = 1000.0 / units_per_em as f64;
2646 let bbox_str = format!(
2647 "[{} {} {} {}]",
2648 (bbox.x_min as f64 * scale) as i32,
2649 (bbox.y_min as f64 * scale) as i32,
2650 (bbox.x_max as f64 * scale) as i32,
2651 (bbox.y_max as f64 * scale) as i32,
2652 );
2653
2654 let flags = 4u32;
2655 let cap_height = face.capital_height().unwrap_or(ascender) as f64 * scale;
2656 let stem_v = if key.weight >= 700 { 120 } else { 80 };
2657
2658 let font_descriptor_dict = format!(
2659 "<< /Type /FontDescriptor /FontName /{} /Flags {} \
2660 /FontBBox {} /ItalicAngle {} \
2661 /Ascent {} /Descent {} /CapHeight {} /StemV {} \
2662 /FontFile2 {} 0 R >>",
2663 pdf_font_name,
2664 flags,
2665 bbox_str,
2666 if key.italic { -12 } else { 0 },
2667 (ascender as f64 * scale) as i32,
2668 (descender as f64 * scale) as i32,
2669 cap_height as i32,
2670 stem_v,
2671 fontfile2_id,
2672 );
2673 builder.objects.push(PdfObject {
2674 id: font_descriptor_id,
2675 data: font_descriptor_dict.into_bytes(),
2676 });
2677
2678 let cidfont_id = builder.objects.len();
2680 let w_array = Self::build_w_array_from_gids(&gid_remap, &subset_face, subset_upem);
2682 let default_width = subset_face
2683 .glyph_hor_advance(ttf_parser::GlyphId(0))
2684 .map(|adv| (adv as f64 * 1000.0 / subset_upem as f64) as u32)
2685 .unwrap_or(1000);
2686 let cidfont_dict = format!(
2687 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{} \
2688 /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> \
2689 /FontDescriptor {} 0 R /DW {} /W {} \
2690 /CIDToGIDMap /Identity >>",
2691 pdf_font_name, font_descriptor_id, default_width, w_array,
2692 );
2693 builder.objects.push(PdfObject {
2694 id: cidfont_id,
2695 data: cidfont_dict.into_bytes(),
2696 });
2697
2698 let tounicode_id = builder.objects.len();
2700 let cmap_content = Self::build_tounicode_cmap_from_gids(&new_gid_to_char, &pdf_font_name);
2701 let compressed_cmap = compress_to_vec_zlib(cmap_content.as_bytes(), 6);
2702 let mut tounicode_data: Vec<u8> = Vec::new();
2703 let _ = write!(
2704 tounicode_data,
2705 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
2706 compressed_cmap.len()
2707 );
2708 tounicode_data.extend_from_slice(&compressed_cmap);
2709 tounicode_data.extend_from_slice(b"\nendstream");
2710 builder.objects.push(PdfObject {
2711 id: tounicode_id,
2712 data: tounicode_data,
2713 });
2714
2715 let type0_id = builder.objects.len();
2717 let type0_dict = format!(
2718 "<< /Type /Font /Subtype /Type0 /BaseFont /{} \
2719 /Encoding /Identity-H \
2720 /DescendantFonts [{} 0 R] \
2721 /ToUnicode {} 0 R >>",
2722 pdf_font_name, cidfont_id, tounicode_id,
2723 );
2724 builder.objects.push(PdfObject {
2725 id: type0_id,
2726 data: type0_dict.into_bytes(),
2727 });
2728
2729 builder.custom_font_data.insert(
2731 key.clone(),
2732 CustomFontEmbedData {
2733 ttf_data: embed_ttf,
2734 gid_remap: gid_remap_for_embed,
2735 glyph_to_char: glyph_to_char_map,
2736 char_to_gid,
2737 units_per_em,
2738 ascender,
2739 descender,
2740 },
2741 );
2742
2743 Ok(type0_id)
2744 }
2745
2746 fn build_w_array_from_gids(
2748 gid_remap: &HashMap<u16, u16>,
2749 face: &ttf_parser::Face,
2750 units_per_em: u16,
2751 ) -> String {
2752 let scale = 1000.0 / units_per_em as f64;
2753
2754 let mut entries: Vec<(u16, u32)> = Vec::new();
2755 let mut seen_gids: HashSet<u16> = HashSet::new();
2756
2757 for &new_gid in gid_remap.values() {
2758 if seen_gids.contains(&new_gid) {
2759 continue;
2760 }
2761 seen_gids.insert(new_gid);
2762 let advance = face
2763 .glyph_hor_advance(ttf_parser::GlyphId(new_gid))
2764 .unwrap_or(0);
2765 let width = (advance as f64 * scale) as u32;
2766 entries.push((new_gid, width));
2767 }
2768
2769 entries.sort_by_key(|(gid, _)| *gid);
2770
2771 let mut result = String::from("[");
2773 for (gid, width) in &entries {
2774 let _ = write!(result, " {} [{}]", gid, width);
2775 }
2776 result.push_str(" ]");
2777 result
2778 }
2779
2780 fn build_tounicode_cmap_from_gids(gid_to_char: &HashMap<u16, char>, font_name: &str) -> String {
2782 let mut gid_to_unicode: Vec<(u16, u32)> = gid_to_char
2783 .iter()
2784 .map(|(&gid, &ch)| (gid, ch as u32))
2785 .collect();
2786 gid_to_unicode.sort_by_key(|(gid, _)| *gid);
2787
2788 let mut cmap = String::new();
2789 let _ = writeln!(cmap, "/CIDInit /ProcSet findresource begin");
2790 let _ = writeln!(cmap, "12 dict begin");
2791 let _ = writeln!(cmap, "begincmap");
2792 let _ = writeln!(cmap, "/CIDSystemInfo");
2793 let _ = writeln!(
2794 cmap,
2795 "<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def"
2796 );
2797 let _ = writeln!(cmap, "/CMapName /{}-UTF16 def", font_name);
2798 let _ = writeln!(cmap, "/CMapType 2 def");
2799 let _ = writeln!(cmap, "1 begincodespacerange");
2800 let _ = writeln!(cmap, "<0000> <FFFF>");
2801 let _ = writeln!(cmap, "endcodespacerange");
2802
2803 for chunk in gid_to_unicode.chunks(100) {
2805 let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
2806 for &(gid, unicode) in chunk {
2807 let _ = writeln!(cmap, "<{:04X}> <{:04X}>", gid, unicode);
2808 }
2809 let _ = writeln!(cmap, "endbfchar");
2810 }
2811
2812 let _ = writeln!(cmap, "endcmap");
2813 let _ = writeln!(cmap, "CMapName currentdict /CMap defineresource pop");
2814 let _ = writeln!(cmap, "end");
2815 let _ = writeln!(cmap, "end");
2816
2817 cmap
2818 }
2819
2820 fn sanitize_font_name(family: &str, weight: u32, italic: bool) -> String {
2823 let mut name: String = family
2824 .chars()
2825 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
2826 .collect();
2827
2828 if weight >= 700 {
2829 name.push_str("-Bold");
2830 }
2831 if italic {
2832 name.push_str("-Italic");
2833 }
2834
2835 if name.is_empty() {
2837 name = "CustomFont".to_string();
2838 }
2839
2840 name
2841 }
2842
2843 fn build_font_resource_dict(&self, font_objects: &[(FontKey, usize)]) -> String {
2844 font_objects
2845 .iter()
2846 .enumerate()
2847 .map(|(i, (_, obj_id))| format!("/F{} {} 0 R", i, obj_id))
2848 .collect::<Vec<_>>()
2849 .join(" ")
2850 }
2851
2852 fn font_index(
2854 &self,
2855 family: &str,
2856 weight: u32,
2857 font_style: FontStyle,
2858 font_objects: &[(FontKey, usize)],
2859 ) -> usize {
2860 let italic = matches!(font_style, FontStyle::Italic | FontStyle::Oblique);
2861
2862 for (i, (key, _)) in font_objects.iter().enumerate() {
2864 if key.family == family && key.weight == weight && key.italic == italic {
2865 return i;
2866 }
2867 }
2868
2869 let snapped = if weight >= 600 { 700 } else { 400 };
2871 for (i, (key, _)) in font_objects.iter().enumerate() {
2872 if key.family == family && key.weight == snapped && key.italic == italic {
2873 return i;
2874 }
2875 }
2876
2877 for (i, (key, _)) in font_objects.iter().enumerate() {
2879 if key.family == "Helvetica" && key.weight == snapped && key.italic == italic {
2880 return i;
2881 }
2882 }
2883
2884 0
2886 }
2887
2888 fn group_glyphs_by_style(glyphs: &[PositionedGlyph]) -> Vec<Vec<&PositionedGlyph>> {
2891 if glyphs.is_empty() {
2892 return vec![];
2893 }
2894
2895 let mut groups: Vec<Vec<&PositionedGlyph>> = Vec::new();
2896 let mut current_group: Vec<&PositionedGlyph> = vec![&glyphs[0]];
2897
2898 for glyph in &glyphs[1..] {
2899 let prev = current_group.last().unwrap();
2900 let same_style = glyph.font_family == prev.font_family
2901 && glyph.font_weight == prev.font_weight
2902 && std::mem::discriminant(&glyph.font_style)
2903 == std::mem::discriminant(&prev.font_style)
2904 && (glyph.font_size - prev.font_size).abs() < 0.01
2905 && Self::colors_equal(&glyph.color, &prev.color);
2906
2907 if same_style {
2908 current_group.push(glyph);
2909 } else {
2910 groups.push(current_group);
2911 current_group = vec![glyph];
2912 }
2913 }
2914 groups.push(current_group);
2915 groups
2916 }
2917
2918 fn colors_equal(a: &Option<Color>, b: &Option<Color>) -> bool {
2919 match (a, b) {
2920 (None, None) => true,
2921 (Some(ca), Some(cb)) => {
2922 (ca.r - cb.r).abs() < 0.001
2923 && (ca.g - cb.g).abs() < 0.001
2924 && (ca.b - cb.b).abs() < 0.001
2925 && (ca.a - cb.a).abs() < 0.001
2926 }
2927 _ => false,
2928 }
2929 }
2930
2931 fn collect_link_annotations(
2935 elements: &[LayoutElement],
2936 page_height: f64,
2937 annotations: &mut Vec<LinkAnnotation>,
2938 ) {
2939 for element in elements {
2940 if let Some(ref href) = element.href {
2941 if !href.is_empty() {
2942 let pdf_y = page_height - element.y - element.height;
2943 annotations.push(LinkAnnotation {
2944 x: element.x,
2945 y: pdf_y,
2946 width: element.width,
2947 height: element.height,
2948 href: href.clone(),
2949 });
2950 continue;
2952 }
2953 }
2954 Self::collect_link_annotations(&element.children, page_height, annotations);
2955 }
2956 }
2957
2958 fn collect_form_fields(
2960 elements: &[LayoutElement],
2961 page_height: f64,
2962 page_idx: usize,
2963 fields: &mut Vec<FormFieldData>,
2964 ) {
2965 for element in elements {
2966 if let DrawCommand::FormField {
2967 ref field_type,
2968 ref name,
2969 } = element.draw
2970 {
2971 let pdf_y = page_height - element.y - element.height;
2972 fields.push(FormFieldData {
2973 field_type: field_type.clone(),
2974 name: name.clone(),
2975 x: element.x,
2976 y: pdf_y,
2977 width: element.width,
2978 height: element.height,
2979 page_idx,
2980 });
2981 }
2982 Self::collect_form_fields(&element.children, page_height, page_idx, fields);
2983 }
2984 }
2985
2986 fn collect_bookmarks(
2988 elements: &[LayoutElement],
2989 page_height: f64,
2990 page_obj_id: usize,
2991 bookmarks: &mut Vec<PdfBookmark>,
2992 ) {
2993 for element in elements {
2994 if let Some(ref title) = element.bookmark {
2995 let y_pdf = page_height - element.y;
2996 bookmarks.push(PdfBookmark {
2997 title: title.clone(),
2998 page_obj_id,
2999 y_pdf,
3000 });
3001 }
3002 Self::collect_bookmarks(&element.children, page_height, page_obj_id, bookmarks);
3003 }
3004 }
3005
3006 fn write_outline_tree(&self, builder: &mut PdfBuilder, bookmarks: &[PdfBookmark]) -> usize {
3009 let outlines_id = builder.objects.len();
3011 builder.objects.push(PdfObject {
3012 id: outlines_id,
3013 data: vec![],
3014 });
3015
3016 let mut item_ids: Vec<usize> = Vec::new();
3018 for _bm in bookmarks {
3019 let item_id = builder.objects.len();
3020 builder.objects.push(PdfObject {
3021 id: item_id,
3022 data: vec![],
3023 });
3024 item_ids.push(item_id);
3025 }
3026
3027 for (i, (bm, &item_id)) in bookmarks.iter().zip(item_ids.iter()).enumerate() {
3029 let mut dict = format!(
3030 "<< /Title ({}) /Parent {} 0 R /Dest [{} 0 R /XYZ 0 {:.2} null]",
3031 Self::escape_pdf_string(&bm.title),
3032 outlines_id,
3033 bm.page_obj_id,
3034 bm.y_pdf,
3035 );
3036 if i > 0 {
3037 let _ = write!(dict, " /Prev {} 0 R", item_ids[i - 1]);
3038 }
3039 if i + 1 < item_ids.len() {
3040 let _ = write!(dict, " /Next {} 0 R", item_ids[i + 1]);
3041 }
3042 dict.push_str(" >>");
3043 builder.objects[item_id].data = dict.into_bytes();
3044 }
3045
3046 let first_id = item_ids.first().copied().unwrap_or(0);
3048 let last_id = item_ids.last().copied().unwrap_or(0);
3049 let outlines_dict = format!(
3050 "<< /Type /Outlines /First {} 0 R /Last {} 0 R /Count {} >>",
3051 first_id,
3052 last_id,
3053 bookmarks.len()
3054 );
3055 builder.objects[outlines_id].data = outlines_dict.into_bytes();
3056
3057 outlines_id
3058 }
3059
3060 fn write_svg_commands(
3062 stream: &mut String,
3063 commands: &[SvgCommand],
3064 ext_gstate_map: &HashMap<u64, (usize, String)>,
3065 ) {
3066 for cmd in commands {
3067 match cmd {
3068 SvgCommand::MoveTo(x, y) => {
3069 let _ = writeln!(stream, "{:.2} {:.2} m", x, y);
3070 }
3071 SvgCommand::LineTo(x, y) => {
3072 let _ = writeln!(stream, "{:.2} {:.2} l", x, y);
3073 }
3074 SvgCommand::CurveTo(x1, y1, x2, y2, x3, y3) => {
3075 let _ = writeln!(
3076 stream,
3077 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3078 x1, y1, x2, y2, x3, y3
3079 );
3080 }
3081 SvgCommand::ClosePath => {
3082 let _ = writeln!(stream, "h");
3083 }
3084 SvgCommand::SetFill(r, g, b) => {
3085 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", r, g, b);
3086 }
3087 SvgCommand::SetFillNone => {
3088 }
3090 SvgCommand::SetStroke(r, g, b) => {
3091 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", r, g, b);
3092 }
3093 SvgCommand::SetStrokeNone => {
3094 }
3096 SvgCommand::SetStrokeWidth(w) => {
3097 let _ = writeln!(stream, "{:.2} w", w);
3098 }
3099 SvgCommand::Fill => {
3100 let _ = writeln!(stream, "f");
3101 }
3102 SvgCommand::Stroke => {
3103 let _ = writeln!(stream, "S");
3104 }
3105 SvgCommand::FillAndStroke => {
3106 let _ = writeln!(stream, "B");
3107 }
3108 SvgCommand::SetLineCap(cap) => {
3109 let _ = writeln!(stream, "{} J", cap);
3110 }
3111 SvgCommand::SetLineJoin(join) => {
3112 let _ = writeln!(stream, "{} j", join);
3113 }
3114 SvgCommand::SaveState => {
3115 let _ = writeln!(stream, "q");
3116 }
3117 SvgCommand::RestoreState => {
3118 let _ = writeln!(stream, "Q");
3119 }
3120 SvgCommand::SetOpacity(opacity) => {
3121 if let Some((_, gs_name)) = ext_gstate_map.get(&opacity.to_bits()) {
3122 let _ = writeln!(stream, "/{} gs", gs_name);
3123 }
3124 }
3125 }
3126 }
3127 }
3128
3129 pub(crate) fn escape_pdf_string(s: &str) -> String {
3131 s.replace('\\', "\\\\")
3132 .replace('(', "\\(")
3133 .replace(')', "\\)")
3134 }
3135
3136 fn encode_winansi_text(s: &str) -> String {
3139 let mut result = String::with_capacity(s.len());
3140 for ch in s.chars() {
3141 let b = Self::unicode_to_winansi(ch).unwrap_or(b'?');
3142 match b {
3143 b'\\' => result.push_str("\\\\"),
3144 b'(' => result.push_str("\\("),
3145 b')' => result.push_str("\\)"),
3146 0x20..=0x7E => result.push(b as char),
3147 _ => {
3148 let _ = write!(result, "\\{:03o}", b);
3149 }
3150 }
3151 }
3152 result
3153 }
3154
3155 fn unicode_to_winansi(ch: char) -> Option<u8> {
3157 crate::font::unicode_to_winansi(ch)
3158 }
3159
3160 fn serialize(&self, builder: &PdfBuilder, info_obj_id: Option<usize>) -> Vec<u8> {
3162 let mut output: Vec<u8> = Vec::new();
3163 let mut offsets: Vec<usize> = vec![0; builder.objects.len()];
3164
3165 output.extend_from_slice(b"%PDF-1.7\n");
3167 output.extend_from_slice(b"%\xe2\xe3\xcf\xd3\n");
3168
3169 for (i, obj) in builder.objects.iter().enumerate().skip(1) {
3170 offsets[i] = output.len();
3171 let header = format!("{} 0 obj\n", i);
3172 output.extend_from_slice(header.as_bytes());
3173 output.extend_from_slice(&obj.data);
3174 output.extend_from_slice(b"\nendobj\n\n");
3175 }
3176
3177 let xref_offset = output.len();
3178 let _ = writeln!(output, "xref\n0 {}", builder.objects.len());
3179 let _ = writeln!(output, "0000000000 65535 f ");
3180 for offset in offsets.iter().skip(1) {
3181 let _ = writeln!(output, "{:010} 00000 n ", offset);
3182 }
3183
3184 let _ = write!(
3185 output,
3186 "trailer\n<< /Size {} /Root 1 0 R",
3187 builder.objects.len()
3188 );
3189 if let Some(info_id) = info_obj_id {
3190 let _ = write!(output, " /Info {} 0 R", info_id);
3191 }
3192 let _ = writeln!(output, " >>\nstartxref\n{}\n%%EOF", xref_offset);
3193
3194 output
3195 }
3196}
3197
3198fn write_chart_primitive(
3203 stream: &mut String,
3204 prim: &crate::chart::ChartPrimitive,
3205 _chart_height: f64,
3206 builder: &PdfBuilder,
3207) {
3208 use crate::chart::{ChartPrimitive, TextAnchor};
3209 use crate::font::metrics::unicode_to_winansi;
3210
3211 match prim {
3212 ChartPrimitive::Rect { x, y, w, h, fill } => {
3213 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3214 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re f", x, y, w, h);
3215 }
3216
3217 ChartPrimitive::Line {
3218 x1,
3219 y1,
3220 x2,
3221 y2,
3222 stroke,
3223 width,
3224 } => {
3225 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
3226 let _ = writeln!(stream, "{:.2} w", width);
3227 let _ = writeln!(stream, "{:.2} {:.2} m {:.2} {:.2} l S", x1, y1, x2, y2);
3228 }
3229
3230 ChartPrimitive::Polyline {
3231 points,
3232 stroke,
3233 width,
3234 } => {
3235 if points.len() < 2 {
3236 return;
3237 }
3238 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
3239 let _ = writeln!(stream, "{:.2} w", width);
3240 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
3241 for &(px, py) in &points[1..] {
3242 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
3243 }
3244 let _ = writeln!(stream, "S");
3245 }
3246
3247 ChartPrimitive::FilledPath {
3248 points,
3249 fill,
3250 opacity,
3251 } => {
3252 if points.len() < 3 {
3253 return;
3254 }
3255 let _ = writeln!(stream, "q");
3256 if *opacity < 1.0 {
3258 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
3259 let _ = writeln!(stream, "/{} gs", gs_name);
3260 }
3261 }
3262 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3263 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
3264 for &(px, py) in &points[1..] {
3265 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
3266 }
3267 let _ = writeln!(stream, "h f");
3268 let _ = writeln!(stream, "Q");
3269 }
3270
3271 ChartPrimitive::Circle { cx, cy, r, fill } => {
3272 let kappa: f64 = 0.5523;
3274 let kr = kappa * r;
3275 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3276 let _ = writeln!(stream, "{:.2} {:.2} m", cx + r, cy);
3277 let _ = writeln!(
3278 stream,
3279 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3280 cx + r,
3281 cy + kr,
3282 cx + kr,
3283 cy + r,
3284 cx,
3285 cy + r
3286 );
3287 let _ = writeln!(
3288 stream,
3289 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3290 cx - kr,
3291 cy + r,
3292 cx - r,
3293 cy + kr,
3294 cx - r,
3295 cy
3296 );
3297 let _ = writeln!(
3298 stream,
3299 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3300 cx - r,
3301 cy - kr,
3302 cx - kr,
3303 cy - r,
3304 cx,
3305 cy - r
3306 );
3307 let _ = writeln!(
3308 stream,
3309 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3310 cx + kr,
3311 cy - r,
3312 cx + r,
3313 cy - kr,
3314 cx + r,
3315 cy
3316 );
3317 let _ = writeln!(stream, "f");
3318 }
3319
3320 ChartPrimitive::ArcSector {
3321 cx,
3322 cy,
3323 r,
3324 start_angle,
3325 end_angle,
3326 fill,
3327 } => {
3328 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3329 let _ = writeln!(stream, "{:.2} {:.2} m", cx, cy);
3331 let sx = cx + r * start_angle.cos();
3333 let sy = cy + r * start_angle.sin();
3334 let _ = writeln!(stream, "{:.2} {:.2} l", sx, sy);
3335
3336 let mut angle = *start_angle;
3338 let total = end_angle - start_angle;
3339 let segments = ((total.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
3340 let step = total / segments as f64;
3341
3342 for _ in 0..segments {
3343 let a1 = angle;
3344 let a2 = angle + step;
3345 let alpha = 4.0 / 3.0 * ((a2 - a1) / 4.0).tan();
3346
3347 let p1x = cx + r * a1.cos();
3348 let p1y = cy + r * a1.sin();
3349 let p2x = cx + r * a2.cos();
3350 let p2y = cy + r * a2.sin();
3351
3352 let cp1x = p1x - alpha * r * a1.sin();
3353 let cp1y = p1y + alpha * r * a1.cos();
3354 let cp2x = p2x + alpha * r * a2.sin();
3355 let cp2y = p2y - alpha * r * a2.cos();
3356
3357 let _ = writeln!(
3358 stream,
3359 "{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c",
3360 cp1x, cp1y, cp2x, cp2y, p2x, p2y
3361 );
3362 angle = a2;
3363 }
3364
3365 let _ = writeln!(stream, "h f");
3367 }
3368
3369 ChartPrimitive::Label {
3370 text,
3371 x,
3372 y,
3373 font_size,
3374 color,
3375 anchor,
3376 } => {
3377 let metrics = crate::font::StandardFont::Helvetica.metrics();
3379 let text_width = metrics.measure_string(text, *font_size, 0.0);
3380 let x_offset = match anchor {
3381 TextAnchor::Left => 0.0,
3382 TextAnchor::Center => -text_width / 2.0,
3383 TextAnchor::Right => -text_width,
3384 };
3385
3386 let font_idx = builder
3388 .font_objects
3389 .iter()
3390 .enumerate()
3391 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
3392 .map(|(i, _)| i)
3393 .unwrap_or(0);
3394
3395 let encoded: String = text
3397 .chars()
3398 .map(|ch| {
3399 if let Some(code) = unicode_to_winansi(ch) {
3400 code as char
3401 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
3402 ch
3403 } else {
3404 '?'
3405 }
3406 })
3407 .collect();
3408 let escaped = pdf_escape_string(&encoded);
3409
3410 let _ = writeln!(stream, "q");
3412 let _ = writeln!(stream, "1 0 0 -1 {:.4} {:.4} cm", x + x_offset, *y);
3413 let _ = writeln!(
3414 stream,
3415 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
3416 font_idx, font_size, color.r, color.g, color.b, escaped
3417 );
3418 let _ = writeln!(stream, "Q");
3419 }
3420 }
3421}
3422
3423fn pdf_escape_string(s: &str) -> String {
3425 let mut out = String::with_capacity(s.len());
3426 for ch in s.chars() {
3427 match ch {
3428 '(' => out.push_str("\\("),
3429 ')' => out.push_str("\\)"),
3430 '\\' => out.push_str("\\\\"),
3431 _ => out.push(ch),
3432 }
3433 }
3434 out
3435}
3436
3437#[cfg(test)]
3438mod tests {
3439 use super::*;
3440 use crate::font::FontContext;
3441
3442 #[test]
3443 fn test_escape_pdf_string() {
3444 assert_eq!(
3445 PdfWriter::escape_pdf_string("Hello (World)"),
3446 "Hello \\(World\\)"
3447 );
3448 assert_eq!(PdfWriter::escape_pdf_string("back\\slash"), "back\\\\slash");
3449 }
3450
3451 #[test]
3452 fn test_empty_document_produces_valid_pdf() {
3453 let writer = PdfWriter::new();
3454 let font_context = FontContext::new();
3455 let pages = vec![LayoutPage {
3456 width: 595.28,
3457 height: 841.89,
3458 elements: vec![],
3459 fixed_header: vec![],
3460 fixed_footer: vec![],
3461 watermarks: vec![],
3462 config: PageConfig::default(),
3463 }];
3464 let metadata = Metadata::default();
3465 let bytes = writer
3466 .write(
3467 &pages,
3468 &metadata,
3469 &font_context,
3470 false,
3471 None,
3472 false,
3473 None,
3474 false,
3475 )
3476 .unwrap();
3477
3478 assert!(bytes.starts_with(b"%PDF-1.7"));
3479 assert!(bytes.windows(5).any(|w| w == b"%%EOF"));
3480 assert!(bytes.windows(4).any(|w| w == b"xref"));
3481 assert!(bytes.windows(7).any(|w| w == b"trailer"));
3482 }
3483
3484 #[test]
3485 fn test_metadata_in_pdf() {
3486 let writer = PdfWriter::new();
3487 let font_context = FontContext::new();
3488 let pages = vec![LayoutPage {
3489 width: 595.28,
3490 height: 841.89,
3491 elements: vec![],
3492 fixed_header: vec![],
3493 fixed_footer: vec![],
3494 watermarks: vec![],
3495 config: PageConfig::default(),
3496 }];
3497 let metadata = Metadata {
3498 title: Some("Test Document".to_string()),
3499 author: Some("Forme".to_string()),
3500 subject: None,
3501 creator: None,
3502 lang: None,
3503 };
3504 let bytes = writer
3505 .write(
3506 &pages,
3507 &metadata,
3508 &font_context,
3509 false,
3510 None,
3511 false,
3512 None,
3513 false,
3514 )
3515 .unwrap();
3516 let text = String::from_utf8_lossy(&bytes);
3517
3518 assert!(text.contains("/Title (Test Document)"));
3519 assert!(text.contains("/Author (Forme)"));
3520 }
3521
3522 #[test]
3523 fn test_bold_font_registered_separately() {
3524 let writer = PdfWriter::new();
3525 let font_context = FontContext::new();
3526
3527 let pages = vec![LayoutPage {
3529 width: 595.28,
3530 height: 841.89,
3531 elements: vec![
3532 LayoutElement {
3533 x: 54.0,
3534 y: 54.0,
3535 width: 100.0,
3536 height: 16.8,
3537 draw: DrawCommand::Text {
3538 lines: vec![TextLine {
3539 x: 54.0,
3540 y: 66.0,
3541 width: 50.0,
3542 height: 16.8,
3543 glyphs: vec![PositionedGlyph {
3544 glyph_id: 65,
3545 x_offset: 0.0,
3546 y_offset: 0.0,
3547 x_advance: 8.0,
3548 font_size: 12.0,
3549 font_family: "Helvetica".to_string(),
3550 font_weight: 400,
3551 font_style: FontStyle::Normal,
3552 char_value: 'A',
3553 color: None,
3554 href: None,
3555 text_decoration: TextDecoration::None,
3556 letter_spacing: 0.0,
3557 cluster_text: None,
3558 }],
3559 word_spacing: 0.0,
3560 }],
3561 color: Color::BLACK,
3562 text_decoration: TextDecoration::None,
3563 opacity: 1.0,
3564 },
3565 children: vec![],
3566 node_type: None,
3567 resolved_style: None,
3568 source_location: None,
3569 href: None,
3570 bookmark: None,
3571 alt: None,
3572 is_header_row: false,
3573 overflow: Overflow::default(),
3574 },
3575 LayoutElement {
3576 x: 54.0,
3577 y: 74.0,
3578 width: 100.0,
3579 height: 16.8,
3580 draw: DrawCommand::Text {
3581 lines: vec![TextLine {
3582 x: 54.0,
3583 y: 86.0,
3584 width: 50.0,
3585 height: 16.8,
3586 glyphs: vec![PositionedGlyph {
3587 glyph_id: 65,
3588 x_offset: 0.0,
3589 y_offset: 0.0,
3590 x_advance: 8.0,
3591 font_size: 12.0,
3592 font_family: "Helvetica".to_string(),
3593 font_weight: 700,
3594 font_style: FontStyle::Normal,
3595 char_value: 'A',
3596 color: None,
3597 href: None,
3598 text_decoration: TextDecoration::None,
3599 letter_spacing: 0.0,
3600 cluster_text: None,
3601 }],
3602 word_spacing: 0.0,
3603 }],
3604 color: Color::BLACK,
3605 text_decoration: TextDecoration::None,
3606 opacity: 1.0,
3607 },
3608 children: vec![],
3609 node_type: None,
3610 resolved_style: None,
3611 source_location: None,
3612 href: None,
3613 bookmark: None,
3614 alt: None,
3615 is_header_row: false,
3616 overflow: Overflow::default(),
3617 },
3618 ],
3619 fixed_header: vec![],
3620 fixed_footer: vec![],
3621 watermarks: vec![],
3622 config: PageConfig::default(),
3623 }];
3624
3625 let metadata = Metadata::default();
3626 let bytes = writer
3627 .write(
3628 &pages,
3629 &metadata,
3630 &font_context,
3631 false,
3632 None,
3633 false,
3634 None,
3635 false,
3636 )
3637 .unwrap();
3638 let text = String::from_utf8_lossy(&bytes);
3639
3640 assert!(
3642 text.contains("Helvetica"),
3643 "Should contain regular Helvetica"
3644 );
3645 assert!(
3646 text.contains("Helvetica-Bold"),
3647 "Should contain Helvetica-Bold"
3648 );
3649 }
3650
3651 #[test]
3652 fn test_sanitize_font_name() {
3653 assert_eq!(PdfWriter::sanitize_font_name("Inter", 400, false), "Inter");
3654 assert_eq!(
3655 PdfWriter::sanitize_font_name("Inter", 700, false),
3656 "Inter-Bold"
3657 );
3658 assert_eq!(
3659 PdfWriter::sanitize_font_name("Inter", 400, true),
3660 "Inter-Italic"
3661 );
3662 assert_eq!(
3663 PdfWriter::sanitize_font_name("Inter", 700, true),
3664 "Inter-Bold-Italic"
3665 );
3666 assert_eq!(
3667 PdfWriter::sanitize_font_name("Noto Sans", 400, false),
3668 "NotoSans"
3669 );
3670 assert_eq!(
3671 PdfWriter::sanitize_font_name("Font (Display)", 400, false),
3672 "FontDisplay"
3673 );
3674 }
3675
3676 #[test]
3677 fn test_tounicode_cmap_format() {
3678 let mut glyph_to_char = HashMap::new();
3680 glyph_to_char.insert(36u16, 'A');
3681 glyph_to_char.insert(37u16, 'B');
3682
3683 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&glyph_to_char, "TestFont");
3684
3685 assert!(cmap.contains("begincmap"), "CMap should contain begincmap");
3686 assert!(cmap.contains("endcmap"), "CMap should contain endcmap");
3687 assert!(
3688 cmap.contains("beginbfchar"),
3689 "CMap should contain beginbfchar"
3690 );
3691 assert!(cmap.contains("endbfchar"), "CMap should contain endbfchar");
3692 assert!(
3693 cmap.contains("<0024> <0041>"),
3694 "Should map gid 0x0024 to Unicode 'A' 0x0041"
3695 );
3696 assert!(
3697 cmap.contains("<0025> <0042>"),
3698 "Should map gid 0x0025 to Unicode 'B' 0x0042"
3699 );
3700 assert!(
3701 cmap.contains("begincodespacerange"),
3702 "Should define codespace range"
3703 );
3704 assert!(
3705 cmap.contains("<0000> <FFFF>"),
3706 "Codespace should be 0000-FFFF"
3707 );
3708 }
3709
3710 #[test]
3711 fn test_w_array_format() {
3712 let mut char_to_gid = HashMap::new();
3713 char_to_gid.insert('A', 36u16);
3714
3715 let w_array_str = "[ 36 [600] ]";
3718 assert!(w_array_str.starts_with('['));
3719 assert!(w_array_str.ends_with(']'));
3720 }
3721
3722 #[test]
3723 fn test_hex_glyph_encoding() {
3724 let gid: u16 = 0x0041;
3726 let hex = format!("{:04X}", gid);
3727 assert_eq!(hex, "0041");
3728
3729 let gids = [0x0041u16, 0x0042, 0x0043];
3730 let hex_str: String = gids.iter().map(|g| format!("{:04X}", g)).collect();
3731 assert_eq!(hex_str, "004100420043");
3732 }
3733
3734 #[test]
3735 fn test_standard_font_still_uses_text_string() {
3736 let writer = PdfWriter::new();
3737 let font_context = FontContext::new();
3738
3739 let pages = vec![LayoutPage {
3740 width: 595.28,
3741 height: 841.89,
3742 elements: vec![LayoutElement {
3743 x: 54.0,
3744 y: 54.0,
3745 width: 100.0,
3746 height: 16.8,
3747 draw: DrawCommand::Text {
3748 lines: vec![TextLine {
3749 x: 54.0,
3750 y: 66.0,
3751 width: 50.0,
3752 height: 16.8,
3753 glyphs: vec![PositionedGlyph {
3754 glyph_id: 65,
3755 x_offset: 0.0,
3756 y_offset: 0.0,
3757 x_advance: 8.0,
3758 font_size: 12.0,
3759 font_family: "Helvetica".to_string(),
3760 font_weight: 400,
3761 font_style: FontStyle::Normal,
3762 char_value: 'H',
3763 color: None,
3764 href: None,
3765 text_decoration: TextDecoration::None,
3766 letter_spacing: 0.0,
3767 cluster_text: None,
3768 }],
3769 word_spacing: 0.0,
3770 }],
3771 color: Color::BLACK,
3772 text_decoration: TextDecoration::None,
3773 opacity: 1.0,
3774 },
3775 children: vec![],
3776 node_type: None,
3777 resolved_style: None,
3778 source_location: None,
3779 href: None,
3780 bookmark: None,
3781 alt: None,
3782 is_header_row: false,
3783 overflow: Overflow::default(),
3784 }],
3785 fixed_header: vec![],
3786 fixed_footer: vec![],
3787 watermarks: vec![],
3788 config: PageConfig::default(),
3789 }];
3790
3791 let metadata = Metadata::default();
3792 let bytes = writer
3793 .write(
3794 &pages,
3795 &metadata,
3796 &font_context,
3797 false,
3798 None,
3799 false,
3800 None,
3801 false,
3802 )
3803 .unwrap();
3804 let text = String::from_utf8_lossy(&bytes);
3805
3806 assert!(
3808 text.contains("/Type1"),
3809 "Standard font should use Type1 subtype"
3810 );
3811 assert!(
3812 !text.contains("CIDFontType2"),
3813 "Standard font should not use CIDFontType2"
3814 );
3815 }
3816}