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