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