1pub mod certify;
30pub mod merge;
31pub mod redaction;
32pub(crate) mod tagged;
33pub(crate) mod xmp;
34
35use std::collections::{HashMap, HashSet};
36use std::fmt::Write as FmtWrite; use std::io::Write as IoWrite; use crate::error::FormeError;
40use crate::font::subset::subset_ttf;
41use crate::font::{FontContext, FontData, FontKey};
42use crate::layout::*;
43use crate::model::*;
44use crate::style::{Color, FontStyle, Overflow, TextDecoration};
45use crate::svg::SvgCommand;
46use miniz_oxide::deflate::compress_to_vec_zlib;
47
48struct LinkAnnotation {
50 x: f64,
51 y: f64,
52 width: f64,
53 height: f64,
54 href: String,
55}
56
57struct PdfBookmark {
59 title: String,
60 page_obj_id: usize,
61 y_pdf: f64,
62}
63
64struct FormFieldData {
66 field_type: FormFieldType,
67 name: String,
68 x: f64,
69 y: f64,
70 width: f64,
71 height: f64,
72 page_idx: usize,
73}
74
75pub struct PdfWriter;
76
77#[allow(dead_code)]
79struct CustomFontEmbedData {
80 ttf_data: Vec<u8>,
81 gid_remap: HashMap<u16, u16>,
83 glyph_to_char: HashMap<u16, char>,
85 char_to_gid: HashMap<char, u16>,
87 units_per_em: u16,
88 ascender: i16,
89 descender: i16,
90}
91
92struct FontUsage {
94 chars: HashSet<char>,
96 glyph_ids: HashSet<u16>,
98 glyph_to_char: HashMap<u16, char>,
100}
101
102struct PdfBuilder {
104 objects: Vec<PdfObject>,
105 font_objects: Vec<(FontKey, usize)>,
107 custom_font_data: HashMap<FontKey, CustomFontEmbedData>,
109 image_objects: Vec<usize>,
112 image_index_map: HashMap<(usize, usize), usize>,
115 page_background_image_map: HashMap<usize, (usize, u32, u32)>,
120 page_background_url_cache: HashMap<String, (usize, u32, u32)>,
124 ext_gstate_map: HashMap<u64, (usize, String)>,
127 shading_map: HashMap<(usize, usize), (usize, String)>,
131}
132
133pub(crate) struct PdfObject {
134 #[allow(dead_code)]
135 pub(crate) id: usize,
136 pub(crate) data: Vec<u8>,
137}
138
139impl Default for PdfWriter {
140 fn default() -> Self {
141 Self::new()
142 }
143}
144
145impl PdfWriter {
146 pub fn new() -> Self {
147 Self
148 }
149
150 #[allow(clippy::too_many_arguments)]
152 pub fn write(
153 &self,
154 pages: &[LayoutPage],
155 metadata: &Metadata,
156 font_context: &FontContext,
157 tagged: bool,
158 pdfa: Option<&PdfAConformance>,
159 pdf_ua: bool,
160 embedded_data: Option<&str>,
161 flatten_forms: bool,
162 ) -> Result<Vec<u8>, FormeError> {
163 let mut builder = PdfBuilder {
164 objects: Vec::new(),
165 font_objects: Vec::new(),
166 custom_font_data: HashMap::new(),
167 image_objects: Vec::new(),
168 image_index_map: HashMap::new(),
169 page_background_image_map: HashMap::new(),
170 page_background_url_cache: HashMap::new(),
171 ext_gstate_map: HashMap::new(),
172 shading_map: HashMap::new(),
173 };
174
175 builder.objects.push(PdfObject {
181 id: 0,
182 data: vec![],
183 });
184 builder.objects.push(PdfObject {
185 id: 1,
186 data: vec![],
187 });
188 builder.objects.push(PdfObject {
189 id: 2,
190 data: vec![],
191 });
192
193 self.register_fonts(&mut builder, pages, font_context)?;
195
196 if pdfa.is_some() {
198 for (key, _) in &builder.font_objects {
199 if !builder.custom_font_data.contains_key(key) {
200 return Err(FormeError::RenderError(format!(
201 "PDF/A requires all fonts to be embedded. Register a custom font for \
202 family '{}' using Font.register().",
203 key.family
204 )));
205 }
206 }
207 }
208
209 self.register_images(&mut builder, pages);
211
212 self.register_page_background_images(&mut builder, pages);
216
217 self.register_ext_gstates(&mut builder, pages);
219
220 self.register_shadings(&mut builder, pages);
222
223 let mut tag_builder = if tagged {
225 Some(tagged::TagBuilder::new(pages.len()))
226 } else {
227 None
228 };
229
230 let mut page_obj_ids: Vec<usize> = Vec::new();
234 let mut all_bookmarks: Vec<PdfBookmark> = Vec::new();
235 let mut per_page_content_obj_ids: Vec<usize> = Vec::new();
236 let mut per_page_annotations: Vec<Vec<LinkAnnotation>> = Vec::new();
237 let mut per_page_resources: Vec<String> = Vec::new();
238 let mut all_form_fields: Vec<FormFieldData> = Vec::new();
239
240 for (page_idx, page) in pages.iter().enumerate() {
242 let content = self.build_content_stream_for_page(
243 page,
244 page_idx,
245 &builder,
246 page_idx + 1,
247 pages.len(),
248 tag_builder.as_mut(),
249 flatten_forms,
250 );
251 let compressed = compress_to_vec_zlib(content.as_bytes(), 6);
252
253 let content_obj_id = builder.objects.len();
254 let mut content_data: Vec<u8> = Vec::new();
255 let _ = write!(
256 content_data,
257 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
258 compressed.len()
259 );
260 content_data.extend_from_slice(&compressed);
261 content_data.extend_from_slice(b"\nendstream");
262 builder.objects.push(PdfObject {
263 id: content_obj_id,
264 data: content_data,
265 });
266 per_page_content_obj_ids.push(content_obj_id);
267
268 let mut annotations: Vec<LinkAnnotation> = Vec::new();
270 Self::collect_link_annotations(&page.elements, page.height, &mut annotations);
271 per_page_annotations.push(annotations);
272
273 Self::collect_form_fields(&page.elements, page.height, page_idx, &mut all_form_fields);
275
276 let page_obj_id = builder.objects.len();
278 builder.objects.push(PdfObject {
279 id: page_obj_id,
280 data: vec![],
281 });
282
283 let font_resources = self.build_font_resource_dict(&builder.font_objects);
285 let xobject_resources = self.build_xobject_resource_dict(page_idx, &builder);
286 let ext_gstate_resources = self.build_ext_gstate_resource_dict(&builder);
287 let shading_resources = self.build_shading_resource_dict(page_idx, &builder);
288 let mut resources = format!("/Font << {} >>", font_resources);
289 if !xobject_resources.is_empty() {
290 let _ = write!(resources, " /XObject << {} >>", xobject_resources);
291 }
292 if !ext_gstate_resources.is_empty() {
293 let _ = write!(resources, " /ExtGState << {} >>", ext_gstate_resources);
294 }
295 if !shading_resources.is_empty() {
296 let _ = write!(resources, " /Shading << {} >>", shading_resources);
297 }
298 per_page_resources.push(resources);
299
300 Self::collect_bookmarks(&page.elements, page.height, page_obj_id, &mut all_bookmarks);
302
303 page_obj_ids.push(page_obj_id);
304 }
305
306 for (page_idx, annotations) in per_page_annotations.iter().enumerate() {
308 let mut annot_obj_ids: Vec<usize> = Vec::new();
309 for annot in annotations {
310 let rect = format!(
311 "[{:.2} {:.2} {:.2} {:.2}]",
312 annot.x,
313 annot.y,
314 annot.x + annot.width,
315 annot.y + annot.height
316 );
317
318 if let Some(anchor) = annot.href.strip_prefix('#') {
319 if let Some(bm) = all_bookmarks.iter().find(|b| b.title == anchor) {
321 let annot_obj_id = builder.objects.len();
322 let annot_dict = format!(
323 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
324 /A << /S /GoTo /D [{} 0 R /XYZ 0 {:.2} null] >> >>",
325 rect, bm.page_obj_id, bm.y_pdf
326 );
327 builder.objects.push(PdfObject {
328 id: annot_obj_id,
329 data: annot_dict.into_bytes(),
330 });
331 annot_obj_ids.push(annot_obj_id);
332 }
333 } else {
335 let annot_obj_id = builder.objects.len();
337 let annot_dict = format!(
338 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
339 /A << /Type /Action /S /URI /URI ({}) >> >>",
340 rect,
341 Self::escape_pdf_string(&annot.href)
342 );
343 builder.objects.push(PdfObject {
344 id: annot_obj_id,
345 data: annot_dict.into_bytes(),
346 });
347 annot_obj_ids.push(annot_obj_id);
348 }
349 }
350
351 let annots_str = if annot_obj_ids.is_empty() {
352 String::new()
353 } else {
354 let refs: String = annot_obj_ids
355 .iter()
356 .map(|id| format!("{} 0 R", id))
357 .collect::<Vec<_>>()
358 .join(" ");
359 format!(" /Annots [{}]", refs)
360 };
361
362 let page_obj_id = page_obj_ids[page_idx];
363 let content_obj_id = per_page_content_obj_ids[page_idx];
364 let struct_parents_str = if tagged {
365 format!(" /StructParents {} /Tabs /S", page_idx)
366 } else {
367 String::new()
368 };
369 let page_dict = format!(
370 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {:.2} {:.2}] \
371 /Contents {} 0 R /Resources << {} >>{}{} >>",
372 pages[page_idx].width,
373 pages[page_idx].height,
374 content_obj_id,
375 per_page_resources[page_idx],
376 annots_str,
377 struct_parents_str
378 );
379 builder.objects[page_obj_id].data = page_dict.into_bytes();
380 }
381
382 let outlines_obj_id = if !all_bookmarks.is_empty() {
384 Some(self.write_outline_tree(&mut builder, &all_bookmarks))
385 } else {
386 None
387 };
388
389 let struct_tree_root_id = if let Some(ref tb) = tag_builder {
391 let (root_id, _parent_tree_id) = tb.write_objects(
392 &mut builder.objects,
393 &page_obj_ids,
394 metadata.lang.as_deref(),
395 );
396 Some(root_id)
397 } else {
398 None
399 };
400
401 let xmp_metadata_id = if pdfa.is_some() || pdf_ua {
403 let xmp_xml = xmp::generate_xmp(metadata, pdfa, pdf_ua);
404 let xmp_bytes = xmp_xml.as_bytes();
405 let xmp_obj_id = builder.objects.len();
406 let xmp_data = format!(
408 "<< /Type /Metadata /Subtype /XML /Length {} >>\nstream\n",
409 xmp_bytes.len()
410 );
411 let mut xmp_obj_data: Vec<u8> = xmp_data.into_bytes();
412 xmp_obj_data.extend_from_slice(xmp_bytes);
413 xmp_obj_data.extend_from_slice(b"\nendstream");
414 builder.objects.push(PdfObject {
415 id: xmp_obj_id,
416 data: xmp_obj_data,
417 });
418 Some(xmp_obj_id)
419 } else {
420 None
421 };
422
423 let output_intent_id = if pdfa.is_some() {
424 static SRGB_ICC: &[u8] = include_bytes!("srgb2014.icc");
426 let compressed_icc = compress_to_vec_zlib(SRGB_ICC, 6);
427
428 let icc_obj_id = builder.objects.len();
429 let mut icc_data: Vec<u8> = Vec::new();
430 let _ = write!(
431 icc_data,
432 "<< /N 3 /Length {} /Filter /FlateDecode >>\nstream\n",
433 compressed_icc.len()
434 );
435 icc_data.extend_from_slice(&compressed_icc);
436 icc_data.extend_from_slice(b"\nendstream");
437 builder.objects.push(PdfObject {
438 id: icc_obj_id,
439 data: icc_data,
440 });
441
442 let oi_obj_id = builder.objects.len();
444 let oi_data = format!(
445 "<< /Type /OutputIntent /S /GTS_PDFA1 \
446 /OutputConditionIdentifier (sRGB IEC61966-2.1) \
447 /RegistryName (http://www.color.org) \
448 /DestOutputProfile {} 0 R >>",
449 icc_obj_id
450 );
451 builder.objects.push(PdfObject {
452 id: oi_obj_id,
453 data: oi_data.into_bytes(),
454 });
455 Some(oi_obj_id)
456 } else {
457 None
458 };
459
460 let embedded_names_id = if let Some(data) = embedded_data {
462 let compressed = compress_to_vec_zlib(data.as_bytes(), 6);
463
464 let ef_obj_id = builder.objects.len();
466 let ef_data = format!(
467 "<< /Type /EmbeddedFile /Subtype /application#2Fjson /Length {} /Filter /FlateDecode >>\nstream\n",
468 compressed.len()
469 );
470 let mut ef_bytes = ef_data.into_bytes();
471 ef_bytes.extend_from_slice(&compressed);
472 ef_bytes.extend_from_slice(b"\nendstream");
473 builder.objects.push(PdfObject {
474 id: ef_obj_id,
475 data: ef_bytes,
476 });
477
478 let fs_obj_id = builder.objects.len();
480 let fs_data = format!(
481 "<< /Type /Filespec /F (forme-data.json) /UF (forme-data.json) /EF << /F {} 0 R >> /AFRelationship /Data >>",
482 ef_obj_id
483 );
484 builder.objects.push(PdfObject {
485 id: fs_obj_id,
486 data: fs_data.into_bytes(),
487 });
488
489 let names_obj_id = builder.objects.len();
491 let names_data = format!("<< /Names [(forme-data.json) {} 0 R] >>", fs_obj_id);
492 builder.objects.push(PdfObject {
493 id: names_obj_id,
494 data: names_data.into_bytes(),
495 });
496
497 Some(names_obj_id)
498 } else {
499 None
500 };
501
502 let acroform_obj_id = if !all_form_fields.is_empty() && !flatten_forms {
504 let helv_obj_id = builder
506 .font_objects
507 .iter()
508 .find(|(key, _)| key.family == "Helvetica" && key.weight == 400 && !key.italic)
509 .map(|(_, id)| *id);
510
511 let mut radio_groups: HashMap<String, Vec<usize>> = HashMap::new(); let mut non_radio_indices: Vec<usize> = Vec::new();
514 for (i, field) in all_form_fields.iter().enumerate() {
515 if matches!(field.field_type, FormFieldType::RadioButton { .. }) {
516 radio_groups.entry(field.name.clone()).or_default().push(i);
517 } else {
518 non_radio_indices.push(i);
519 }
520 }
521
522 let mut radio_parent_ids: HashMap<String, usize> = HashMap::new();
524 for group_name in radio_groups.keys() {
525 let parent_id = builder.objects.len();
526 builder.objects.push(PdfObject {
527 id: parent_id,
528 data: vec![], });
530 radio_parent_ids.insert(group_name.clone(), parent_id);
531 }
532
533 let checkbox_yes_stream_id = builder.objects.len();
536 {
537 let stream_content =
538 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";
539 let mut data: Vec<u8> = Vec::new();
540 let _ = write!(
541 data,
542 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
543 stream_content.len()
544 );
545 data.extend_from_slice(stream_content);
546 data.extend_from_slice(b"\nendstream");
547 builder.objects.push(PdfObject {
548 id: checkbox_yes_stream_id,
549 data,
550 });
551 }
552 let checkbox_off_stream_id = builder.objects.len();
554 {
555 let stream_content = b"";
556 let mut data: Vec<u8> = Vec::new();
557 let _ = write!(
558 data,
559 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
560 stream_content.len()
561 );
562 data.extend_from_slice(stream_content);
563 data.extend_from_slice(b"\nendstream");
564 builder.objects.push(PdfObject {
565 id: checkbox_off_stream_id,
566 data,
567 });
568 }
569 let radio_on_stream_id = builder.objects.len();
571 {
572 let k = 2.761; let stream_content = format!(
575 "0.2 0.2 0.2 rg\n\
576 7 12 m {:.2} 12 12 {:.2} 12 7 c\n\
577 12 {:.2} {:.2} 2 7 2 c\n\
578 {:.2} 2 2 {:.2} 2 7 c\n\
579 2 {:.2} {:.2} 12 7 12 c f\n",
580 7.0 + k,
581 7.0 + k, 7.0 - k,
583 7.0 - k, 7.0 - k,
585 7.0 - k, 7.0 + k,
587 7.0 + k, );
589 let stream_bytes = stream_content.as_bytes();
590 let mut data: Vec<u8> = Vec::new();
591 let _ = write!(
592 data,
593 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
594 stream_bytes.len()
595 );
596 data.extend_from_slice(stream_bytes);
597 data.extend_from_slice(b"\nendstream");
598 builder.objects.push(PdfObject {
599 id: radio_on_stream_id,
600 data,
601 });
602 }
603 let radio_off_stream_id = builder.objects.len();
605 {
606 let stream_content = b"";
607 let mut data: Vec<u8> = Vec::new();
608 let _ = write!(
609 data,
610 "<< /Type /XObject /Subtype /Form /BBox [0 0 14 14] /Length {} >>\nstream\n",
611 stream_content.len()
612 );
613 data.extend_from_slice(stream_content);
614 data.extend_from_slice(b"\nendstream");
615 builder.objects.push(PdfObject {
616 id: radio_off_stream_id,
617 data,
618 });
619 }
620
621 let mut acroform_field_ids: Vec<usize> = Vec::new();
623 let mut per_page_widget_ids: Vec<Vec<usize>> = vec![Vec::new(); pages.len()];
624 let mut radio_kid_ids: HashMap<String, Vec<usize>> = HashMap::new();
625
626 for field in all_form_fields.iter() {
627 let rect = format!(
628 "[{:.2} {:.2} {:.2} {:.2}]",
629 field.x,
630 field.y,
631 field.x + field.width,
632 field.y + field.height
633 );
634 let page_ref = format!("{} 0 R", page_obj_ids[field.page_idx]);
635
636 match &field.field_type {
637 FormFieldType::TextField {
638 value,
639 multiline,
640 password,
641 read_only,
642 max_length,
643 font_size,
644 ..
645 } => {
646 let mut flags: u32 = 0;
647 if *multiline {
648 flags |= 1 << 12; }
650 if *password {
651 flags |= 1 << 13; }
653 if *read_only {
654 flags |= 1; }
656 let da = if let Some(helv_id) = helv_obj_id {
657 let _ = helv_id; format!("/Helv {} Tf 0 g", font_size)
659 } else {
660 format!("/Helv {} Tf 0 g", font_size)
661 };
662 let v_str = if let Some(ref v) = value {
663 format!(
664 " /V ({}) /DV ({})",
665 Self::escape_pdf_string(v),
666 Self::escape_pdf_string(v)
667 )
668 } else {
669 String::new()
670 };
671 let max_len_str = if let Some(ml) = max_length {
672 format!(" /MaxLen {}", ml)
673 } else {
674 String::new()
675 };
676 let ap_w = field.width;
678 let ap_h = field.height;
679 let text_y = if *multiline {
680 ap_h - *font_size - 2.0
681 } else {
682 (ap_h - *font_size) / 2.0
683 };
684 let ap_content = if let Some(ref v) = value {
685 format!(
686 "1 1 1 rg 0 0 {} {} re f \
687 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S \
688 BT /Helv {} Tf 0 g 2 {} Td ({}) Tj ET",
689 ap_w,
690 ap_h,
691 ap_w,
692 ap_h,
693 font_size,
694 text_y,
695 Self::escape_pdf_string(v)
696 )
697 } else {
698 format!(
699 "1 1 1 rg 0 0 {} {} re f \
700 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S",
701 ap_w, ap_h, ap_w, ap_h
702 )
703 };
704 let ap_stream_id = builder.objects.len();
705 let ap_stream = format!(
706 "<< /Type /XObject /Subtype /Form /BBox [0 0 {} {}] \
707 /Resources << /Font << /Helv {} 0 R >> >> /Length {} >>\nstream\n{}\nendstream",
708 ap_w, ap_h,
709 helv_obj_id.unwrap_or(0),
710 ap_content.len(),
711 ap_content
712 );
713 builder.objects.push(PdfObject {
714 id: ap_stream_id,
715 data: ap_stream.into_bytes(),
716 });
717
718 let widget_obj_id = builder.objects.len();
719 let widget_dict = format!(
720 "<< /Type /Annot /Subtype /Widget /FT /Tx \
721 /T ({}) /Rect {} /P {}\
722 {} /DA ({}) /Ff {}{} \
723 /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> \
724 /AP << /N {} 0 R >> >>",
725 Self::escape_pdf_string(&field.name),
726 rect,
727 page_ref,
728 v_str,
729 da,
730 flags,
731 max_len_str,
732 ap_stream_id
733 );
734 builder.objects.push(PdfObject {
735 id: widget_obj_id,
736 data: widget_dict.into_bytes(),
737 });
738 per_page_widget_ids[field.page_idx].push(widget_obj_id);
739 acroform_field_ids.push(widget_obj_id);
740 }
741
742 FormFieldType::Checkbox {
743 checked, read_only, ..
744 } => {
745 let state = if *checked { "Yes" } else { "Off" };
746 let mut flags: u32 = 0;
747 if *read_only {
748 flags |= 1;
749 }
750 let ff_str = if flags > 0 {
751 format!(" /Ff {}", flags)
752 } else {
753 String::new()
754 };
755 let widget_obj_id = builder.objects.len();
756 let widget_dict = format!(
757 "<< /Type /Annot /Subtype /Widget /FT /Btn \
758 /T ({}) /Rect {} /P {} \
759 /V /{} /AS /{}{} \
760 /MK << /BC [0.6 0.6 0.6] /CA (4) >> \
761 /AP << /N << /Yes {} 0 R /Off {} 0 R >> >> >>",
762 Self::escape_pdf_string(&field.name),
763 rect,
764 page_ref,
765 state,
766 state,
767 ff_str,
768 checkbox_yes_stream_id,
769 checkbox_off_stream_id,
770 );
771 builder.objects.push(PdfObject {
772 id: widget_obj_id,
773 data: widget_dict.into_bytes(),
774 });
775 per_page_widget_ids[field.page_idx].push(widget_obj_id);
776 acroform_field_ids.push(widget_obj_id);
777 }
778
779 FormFieldType::Dropdown {
780 options,
781 value,
782 read_only,
783 font_size,
784 ..
785 } => {
786 let mut flags: u32 = 1 << 17; if *read_only {
788 flags |= 1;
789 }
790 let opts_str: String = options
791 .iter()
792 .map(|o| format!("({})", Self::escape_pdf_string(o)))
793 .collect::<Vec<_>>()
794 .join(" ");
795 let v_str = if let Some(ref v) = value {
796 format!(" /V ({})", Self::escape_pdf_string(v))
797 } else {
798 String::new()
799 };
800 let ap_w = field.width;
802 let ap_h = field.height;
803 let text_y = (ap_h - *font_size) / 2.0;
804 let ap_content = if let Some(ref v) = value {
805 format!(
806 "1 1 1 rg 0 0 {} {} re f \
807 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S \
808 BT /Helv {} Tf 0 g 2 {} Td ({}) Tj ET",
809 ap_w,
810 ap_h,
811 ap_w,
812 ap_h,
813 font_size,
814 text_y,
815 Self::escape_pdf_string(v)
816 )
817 } else {
818 format!(
819 "1 1 1 rg 0 0 {} {} re f \
820 0.6 0.6 0.6 RG 0.5 w 0 0 {} {} re S",
821 ap_w, ap_h, ap_w, ap_h
822 )
823 };
824 let ap_stream_id = builder.objects.len();
825 let ap_stream = format!(
826 "<< /Type /XObject /Subtype /Form /BBox [0 0 {} {}] \
827 /Resources << /Font << /Helv {} 0 R >> >> /Length {} >>\nstream\n{}\nendstream",
828 ap_w, ap_h,
829 helv_obj_id.unwrap_or(0),
830 ap_content.len(),
831 ap_content
832 );
833 builder.objects.push(PdfObject {
834 id: ap_stream_id,
835 data: ap_stream.into_bytes(),
836 });
837
838 let widget_obj_id = builder.objects.len();
839 let widget_dict = format!(
840 "<< /Type /Annot /Subtype /Widget /FT /Ch \
841 /T ({}) /Rect {} /P {} \
842 /Opt [{}]{} \
843 /DA (/Helv {} Tf 0 g) /Ff {} \
844 /MK << /BC [0.6 0.6 0.6] /BG [1 1 1] >> \
845 /AP << /N {} 0 R >> >>",
846 Self::escape_pdf_string(&field.name),
847 rect,
848 page_ref,
849 opts_str,
850 v_str,
851 font_size,
852 flags,
853 ap_stream_id
854 );
855 builder.objects.push(PdfObject {
856 id: widget_obj_id,
857 data: widget_dict.into_bytes(),
858 });
859 per_page_widget_ids[field.page_idx].push(widget_obj_id);
860 acroform_field_ids.push(widget_obj_id);
861 }
862
863 FormFieldType::RadioButton {
864 value,
865 checked,
866 read_only: _,
867 } => {
868 let parent_id = radio_parent_ids[&field.name];
870 let as_value = if *checked { value.as_str() } else { "Off" };
871 let widget_obj_id = builder.objects.len();
872 let widget_dict = format!(
873 "<< /Type /Annot /Subtype /Widget \
874 /Parent {} 0 R \
875 /Rect {} /P {} \
876 /AS /{} \
877 /AP << /N << /{} {} 0 R /Off {} 0 R >> >> \
878 /MK << /BC [0.6 0.6 0.6] >> >>",
879 parent_id,
880 rect,
881 page_ref,
882 Self::escape_pdf_string(as_value),
883 Self::escape_pdf_string(value),
884 radio_on_stream_id,
885 radio_off_stream_id,
886 );
887 builder.objects.push(PdfObject {
888 id: widget_obj_id,
889 data: widget_dict.into_bytes(),
890 });
891 per_page_widget_ids[field.page_idx].push(widget_obj_id);
892 radio_kid_ids
894 .entry(field.name.clone())
895 .or_default()
896 .push(widget_obj_id);
897 }
898 }
899 }
900
901 for (group_name, kid_indices) in &radio_kid_ids {
903 let parent_id = radio_parent_ids[group_name];
904 let checked_value = all_form_fields
906 .iter()
907 .filter(|f| f.name == *group_name)
908 .find_map(|f| {
909 if let FormFieldType::RadioButton {
910 ref value, checked, ..
911 } = f.field_type
912 {
913 if checked {
914 Some(value.clone())
915 } else {
916 None
917 }
918 } else {
919 None
920 }
921 })
922 .unwrap_or_else(|| "Off".to_string());
923
924 let kids_refs: String = kid_indices
925 .iter()
926 .map(|id| format!("{} 0 R", id))
927 .collect::<Vec<_>>()
928 .join(" ");
929
930 let mut flags: u32 = (1 << 14) | (1 << 15); let is_read_only = all_form_fields
933 .iter()
934 .filter(|f| f.name == *group_name)
935 .any(|f| {
936 matches!(
937 f.field_type,
938 FormFieldType::RadioButton {
939 read_only: true,
940 ..
941 }
942 )
943 });
944 if is_read_only {
945 flags |= 1;
946 }
947
948 let parent_dict = format!(
949 "<< /FT /Btn /T ({}) /Ff {} /Kids [{}] /V /{} >>",
950 Self::escape_pdf_string(group_name),
951 flags,
952 kids_refs,
953 Self::escape_pdf_string(&checked_value),
954 );
955 builder.objects[parent_id].data = parent_dict.into_bytes();
956 acroform_field_ids.push(parent_id);
957 }
958
959 for (page_idx, widget_ids) in per_page_widget_ids.iter().enumerate() {
963 if widget_ids.is_empty() {
964 continue;
965 }
966 let page_obj_id = page_obj_ids[page_idx];
967 let existing_page_data =
968 String::from_utf8_lossy(&builder.objects[page_obj_id].data).to_string();
969
970 let new_refs: String = widget_ids
972 .iter()
973 .map(|id| format!("{} 0 R", id))
974 .collect::<Vec<_>>()
975 .join(" ");
976
977 let updated = if let Some(pos) = existing_page_data.find("/Annots [") {
978 let bracket_end = existing_page_data[pos..].find(']').unwrap() + pos;
980 format!(
981 "{} {}{}",
982 &existing_page_data[..bracket_end],
983 new_refs,
984 &existing_page_data[bracket_end..]
985 )
986 } else {
987 let end = existing_page_data.rfind(">>").unwrap();
989 format!(
990 "{} /Annots [{}]{}",
991 &existing_page_data[..end],
992 new_refs,
993 &existing_page_data[end..]
994 )
995 };
996 builder.objects[page_obj_id].data = updated.into_bytes();
997 }
998
999 let acroform_id = builder.objects.len();
1001 let fields_refs: String = acroform_field_ids
1002 .iter()
1003 .map(|id| format!("{} 0 R", id))
1004 .collect::<Vec<_>>()
1005 .join(" ");
1006 let dr_str = if let Some(helv_id) = helv_obj_id {
1007 format!(" /DR << /Font << /Helv {} 0 R >> >>", helv_id)
1008 } else {
1009 String::new()
1010 };
1011 let acroform_dict = format!(
1012 "<< /Fields [{}] /NeedAppearances true{} /DA (/Helv 0 Tf 0 g) >>",
1013 fields_refs, dr_str
1014 );
1015 builder.objects.push(PdfObject {
1016 id: acroform_id,
1017 data: acroform_dict.into_bytes(),
1018 });
1019 Some(acroform_id)
1020 } else {
1021 None
1022 };
1023
1024 let mut catalog = String::from("<< /Type /Catalog /Pages 2 0 R");
1026 if let Some(acroform_id) = acroform_obj_id {
1027 write!(catalog, " /AcroForm {} 0 R", acroform_id).unwrap();
1028 }
1029 if let Some(outlines_id) = outlines_obj_id {
1030 write!(
1031 catalog,
1032 " /Outlines {} 0 R /PageMode /UseOutlines",
1033 outlines_id
1034 )
1035 .unwrap();
1036 }
1037 if let Some(ref lang) = metadata.lang {
1038 write!(catalog, " /Lang ({})", Self::escape_pdf_string(lang)).unwrap();
1039 }
1040 if let Some(struct_root_id) = struct_tree_root_id {
1041 write!(
1042 catalog,
1043 " /MarkInfo << /Marked true >> /StructTreeRoot {} 0 R",
1044 struct_root_id
1045 )
1046 .unwrap();
1047 }
1048 if let Some(xmp_id) = xmp_metadata_id {
1049 write!(catalog, " /Metadata {} 0 R", xmp_id).unwrap();
1050 }
1051 if let Some(oi_id) = output_intent_id {
1052 write!(catalog, " /OutputIntents [{} 0 R]", oi_id).unwrap();
1053 }
1054 if let Some(names_id) = embedded_names_id {
1055 write!(catalog, " /Names << /EmbeddedFiles {} 0 R >>", names_id).unwrap();
1056 }
1057 if pdf_ua {
1058 catalog.push_str(" /ViewerPreferences << /DisplayDocTitle true >>");
1059 }
1060 catalog.push_str(" >>");
1061 builder.objects[1].data = catalog.into_bytes();
1062
1063 let kids: String = page_obj_ids
1065 .iter()
1066 .map(|id| format!("{} 0 R", id))
1067 .collect::<Vec<_>>()
1068 .join(" ");
1069 builder.objects[2].data = format!(
1070 "<< /Type /Pages /Kids [{}] /Count {} >>",
1071 kids,
1072 page_obj_ids.len()
1073 )
1074 .into_bytes();
1075
1076 let info_obj_id = if metadata.title.is_some() || metadata.author.is_some() {
1078 let id = builder.objects.len();
1079 let mut info = String::from("<< ");
1080 if let Some(ref title) = metadata.title {
1081 let _ = write!(info, "/Title ({}) ", Self::escape_pdf_string(title));
1082 }
1083 if let Some(ref author) = metadata.author {
1084 let _ = write!(info, "/Author ({}) ", Self::escape_pdf_string(author));
1085 }
1086 if let Some(ref subject) = metadata.subject {
1087 let _ = write!(info, "/Subject ({}) ", Self::escape_pdf_string(subject));
1088 }
1089 let _ = write!(info, "/Producer (Forme 0.6) /Creator (Forme) >>");
1090 builder.objects.push(PdfObject {
1091 id,
1092 data: info.into_bytes(),
1093 });
1094 Some(id)
1095 } else {
1096 None
1097 };
1098
1099 Ok(self.serialize(&builder, info_obj_id))
1100 }
1101
1102 #[allow(clippy::too_many_arguments)]
1104 fn build_content_stream_for_page(
1105 &self,
1106 page: &LayoutPage,
1107 page_idx: usize,
1108 builder: &PdfBuilder,
1109 page_number: usize,
1110 total_pages: usize,
1111 mut tag_builder: Option<&mut tagged::TagBuilder>,
1112 flatten_forms: bool,
1113 ) -> String {
1114 let mut stream = String::new();
1115 let page_height = page.height;
1116 let mut element_counter = 0usize;
1117 let mut gradient_counter = 0usize;
1118
1119 if let Some(&img_idx) = builder.page_background_image_map.get(&page_idx) {
1125 self.write_page_background(&mut stream, page, img_idx, builder);
1126 }
1127
1128 for element in &page.elements {
1129 self.write_element(
1130 &mut stream,
1131 element,
1132 page_height,
1133 builder,
1134 page_idx,
1135 &mut element_counter,
1136 &mut gradient_counter,
1137 page_number,
1138 total_pages,
1139 tag_builder.as_deref_mut(),
1140 flatten_forms,
1141 );
1142 }
1143
1144 stream
1145 }
1146
1147 #[allow(clippy::too_many_arguments)]
1149 #[allow(clippy::too_many_arguments)]
1150 fn write_element(
1151 &self,
1152 stream: &mut String,
1153 element: &LayoutElement,
1154 page_height: f64,
1155 builder: &PdfBuilder,
1156 page_idx: usize,
1157 element_counter: &mut usize,
1158 gradient_counter: &mut usize,
1159 page_number: usize,
1160 total_pages: usize,
1161 mut tag_builder: Option<&mut tagged::TagBuilder>,
1162 flatten_forms: bool,
1163 ) {
1164 let mut is_artifact = false;
1167 let tagged_mcid = if let Some(ref mut tb) = tag_builder {
1168 if let Some(ref nt) = element.node_type {
1169 if nt == "Watermark" {
1170 let _ = writeln!(stream, "/Artifact BMC");
1172 is_artifact = true;
1173 None
1174 } else {
1175 let is_header = element.is_header_row;
1176 let mcid = tb.begin_element(nt, is_header, element.alt.as_deref(), page_idx);
1177 let role = tb.map_role_public(nt, is_header);
1178 let _ = writeln!(stream, "/{} <</MCID {}>> BDC", role, mcid);
1179 Some(mcid)
1180 }
1181 } else if !matches!(element.draw, DrawCommand::None) {
1182 let _ = writeln!(stream, "/Artifact BMC");
1184 is_artifact = true;
1185 None
1186 } else {
1187 None
1188 }
1189 } else {
1190 None
1191 };
1192
1193 let needs_element_opacity = element.opacity < 1.0;
1201 if needs_element_opacity {
1202 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&element.opacity.to_bits()) {
1203 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1204 }
1205 }
1206
1207 match &element.draw {
1208 DrawCommand::None => {}
1209
1210 DrawCommand::Rect {
1211 background,
1212 border_width,
1213 border_color,
1214 border_radius,
1215 opacity,
1216 box_shadow,
1217 background_gradient,
1218 } => {
1219 let x = element.x;
1220 let y = page_height - element.y - element.height;
1221 let w = element.width;
1222 let h = element.height;
1223
1224 let needs_opacity = *opacity < 1.0;
1226 if needs_opacity {
1227 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1228 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1229 }
1230 }
1231
1232 if let Some(shadow) = box_shadow {
1238 if shadow.color.a > 0.0 {
1239 let sx = x + shadow.offset_x;
1244 let sy = y - shadow.offset_y;
1245 let needs_shadow_alpha = shadow.color.a < 1.0;
1246 if needs_shadow_alpha {
1247 if let Some((_, gs_name)) =
1248 builder.ext_gstate_map.get(&shadow.color.a.to_bits())
1249 {
1250 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1251 } else {
1252 let _ = writeln!(stream, "q");
1253 }
1254 } else {
1255 let _ = writeln!(stream, "q");
1256 }
1257 let _ = writeln!(
1258 stream,
1259 "{:.3} {:.3} {:.3} rg",
1260 shadow.color.r, shadow.color.g, shadow.color.b
1261 );
1262 if border_radius.top_left > 0.0
1263 || border_radius.top_right > 0.0
1264 || border_radius.bottom_right > 0.0
1265 || border_radius.bottom_left > 0.0
1266 {
1267 self.write_rounded_rect(stream, sx, sy, w, h, border_radius);
1268 } else {
1269 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", sx, sy, w, h);
1270 }
1271 let _ = writeln!(stream, "f\nQ");
1272 }
1273 }
1274
1275 if background_gradient.is_some() {
1281 let key = (page_idx, *gradient_counter);
1282 *gradient_counter += 1;
1283 if let Some((_, sh_name)) = builder.shading_map.get(&key) {
1284 let _ = writeln!(stream, "q");
1285 if border_radius.top_left > 0.0
1287 || border_radius.top_right > 0.0
1288 || border_radius.bottom_right > 0.0
1289 || border_radius.bottom_left > 0.0
1290 {
1291 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1292 let _ = writeln!(stream, "W n");
1293 } else {
1294 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re W n", x, y, w, h);
1295 }
1296 let _ =
1299 writeln!(stream, "1 0 0 1 {:.3} {:.3} cm\n/{} sh\nQ", x, y, sh_name);
1300 }
1301 } else if let Some(bg) = background {
1302 if bg.a > 0.0 {
1303 let _ = writeln!(stream, "q\n{:.3} {:.3} {:.3} rg", bg.r, bg.g, bg.b);
1304
1305 if border_radius.top_left > 0.0 {
1306 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1307 } else {
1308 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1309 }
1310
1311 let _ = writeln!(stream, "f\nQ");
1312 }
1313 }
1314
1315 let bw = border_width;
1316 if bw.top > 0.0 || bw.right > 0.0 || bw.bottom > 0.0 || bw.left > 0.0 {
1317 if (bw.top - bw.right).abs() < 0.001
1318 && (bw.right - bw.bottom).abs() < 0.001
1319 && (bw.bottom - bw.left).abs() < 0.001
1320 {
1321 let bc = &border_color.top;
1322 let _ = writeln!(
1323 stream,
1324 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w",
1325 bc.r, bc.g, bc.b, bw.top
1326 );
1327
1328 if border_radius.top_left > 0.0 {
1329 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1330 } else {
1331 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1332 }
1333
1334 let _ = writeln!(stream, "S\nQ");
1335 } else {
1336 self.write_border_sides(stream, x, y, w, h, bw, border_color);
1337 }
1338 }
1339
1340 if needs_opacity {
1341 let _ = writeln!(stream, "Q");
1342 }
1343 }
1344
1345 DrawCommand::Text {
1346 lines,
1347 color,
1348 text_decoration,
1349 opacity,
1350 } => {
1351 let needs_opacity = *opacity < 1.0;
1353 if needs_opacity {
1354 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1355 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1356 }
1357 }
1358
1359 for line in lines {
1360 if line.glyphs.is_empty() {
1361 continue;
1362 }
1363
1364 let groups = Self::group_glyphs_by_style(&line.glyphs);
1367 let pdf_y = page_height - line.y;
1368
1369 let _ = writeln!(stream, "BT");
1370
1371 if line.word_spacing.abs() > 0.001 {
1373 let _ = writeln!(stream, "{:.4} Tw", line.word_spacing);
1374 }
1375
1376 let mut tm_x = 0.0_f64;
1378 let mut tm_y = 0.0_f64;
1379 let mut x_cursor = line.x;
1380
1381 let mut group_spans: Vec<(f64, f64, TextDecoration, Color)> = Vec::new();
1383
1384 for group in &groups {
1385 let first = &group[0];
1386 let glyph_color = first.color.unwrap_or(*color);
1387
1388 let idx = self.font_index(
1389 &first.font_family,
1390 first.font_weight,
1391 first.font_style,
1392 &builder.font_objects,
1393 );
1394 let italic =
1395 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
1396 let font_key = FontKey {
1397 family: first.font_family.clone(),
1398 weight: first.font_weight,
1399 italic,
1400 };
1401 let font_name = format!("F{}", idx);
1402
1403 let dx = x_cursor - tm_x;
1405 let dy = pdf_y - tm_y;
1406 let _ = writeln!(
1407 stream,
1408 "{:.3} {:.3} {:.3} rg\n/{} {:.1} Tf\n{:.2} Tc\n{:.2} {:.2} Td",
1409 glyph_color.r,
1410 glyph_color.g,
1411 glyph_color.b,
1412 font_name,
1413 first.font_size,
1414 first.letter_spacing,
1415 dx,
1416 dy
1417 );
1418 tm_x = x_cursor;
1419 tm_y = pdf_y;
1420
1421 let raw_text: String = group.iter().map(|g| g.char_value).collect();
1423 let has_placeholder = raw_text.contains(PAGE_NUMBER_SENTINEL)
1424 || raw_text.contains(TOTAL_PAGES_SENTINEL);
1425
1426 let is_custom = builder.custom_font_data.contains_key(&font_key);
1427
1428 if is_custom {
1429 if let Some(embed_data) = builder.custom_font_data.get(&font_key) {
1430 let mut hex = String::new();
1431 if has_placeholder {
1432 let pn = PAGE_NUMBER_SENTINEL.to_string();
1434 let tp = TOTAL_PAGES_SENTINEL.to_string();
1435 let text_after = raw_text
1436 .replace(&pn, &page_number.to_string())
1437 .replace(&tp, &total_pages.to_string());
1438 for ch in text_after.chars() {
1439 let gid =
1440 embed_data.char_to_gid.get(&ch).copied().unwrap_or(0);
1441 let _ = write!(hex, "{:04X}", gid);
1442 }
1443 } else {
1444 for g in group.iter() {
1446 let new_gid = embed_data
1447 .gid_remap
1448 .get(&g.glyph_id)
1449 .copied()
1450 .unwrap_or_else(|| {
1451 embed_data
1453 .char_to_gid
1454 .get(&g.char_value)
1455 .copied()
1456 .unwrap_or(0)
1457 });
1458 let _ = write!(hex, "{:04X}", new_gid);
1459 }
1460 }
1461 let _ = writeln!(stream, "<{}> Tj", hex);
1462 } else {
1463 let _ = writeln!(stream, "<> Tj");
1464 }
1465 } else {
1466 let pn = PAGE_NUMBER_SENTINEL.to_string();
1467 let tp = TOTAL_PAGES_SENTINEL.to_string();
1468 let text_after = raw_text
1469 .replace(&pn, &page_number.to_string())
1470 .replace(&tp, &total_pages.to_string());
1471 let mut text_str = String::new();
1472 for ch in text_after.chars() {
1473 let b = Self::unicode_to_winansi(ch).unwrap_or(b'?');
1474 match b {
1475 b'\\' => text_str.push_str("\\\\"),
1476 b'(' => text_str.push_str("\\("),
1477 b')' => text_str.push_str("\\)"),
1478 0x20..=0x7E => text_str.push(b as char),
1479 _ => {
1480 let _ = write!(text_str, "\\{:03o}", b);
1481 }
1482 }
1483 }
1484 let _ = writeln!(stream, "({}) Tj", text_str);
1485 }
1486
1487 let group_start_x = x_cursor;
1489
1490 if let Some(last) = group.last() {
1493 let space_count_in_group =
1494 group.iter().filter(|g| g.char_value == ' ').count();
1495 x_cursor = line.x
1496 + last.x_offset
1497 + last.x_advance
1498 + space_count_in_group as f64 * line.word_spacing;
1499 }
1500
1501 let group_dec = first.text_decoration;
1503 if !matches!(group_dec, TextDecoration::None) {
1504 group_spans.push((group_start_x, x_cursor, group_dec, glyph_color));
1505 }
1506 }
1507
1508 let _ = writeln!(stream, "ET");
1509
1510 for (span_x, span_end_x, dec, dec_color) in &group_spans {
1512 match dec {
1513 TextDecoration::Underline => {
1514 let underline_y = pdf_y - 1.5;
1515 let _ = write!(
1516 stream,
1517 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1518 dec_color.r, dec_color.g, dec_color.b,
1519 span_x, underline_y,
1520 span_end_x, underline_y
1521 );
1522 }
1523 TextDecoration::LineThrough => {
1524 let first_size =
1525 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
1526 let strikethrough_y = pdf_y + first_size * 0.3;
1527 let _ = write!(
1528 stream,
1529 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1530 dec_color.r, dec_color.g, dec_color.b,
1531 span_x, strikethrough_y,
1532 span_end_x, strikethrough_y
1533 );
1534 }
1535 TextDecoration::None => {}
1536 }
1537 }
1538
1539 if group_spans.is_empty() {
1541 if matches!(text_decoration, TextDecoration::Underline) {
1542 let underline_y = pdf_y - 1.5;
1543 let _ = write!(
1544 stream,
1545 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1546 color.r, color.g, color.b,
1547 line.x, underline_y,
1548 line.x + line.width, underline_y
1549 );
1550 }
1551 if matches!(text_decoration, TextDecoration::LineThrough) {
1552 let first_size =
1553 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
1554 let strikethrough_y = pdf_y + first_size * 0.3;
1555 let _ = write!(
1556 stream,
1557 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1558 color.r, color.g, color.b,
1559 line.x, strikethrough_y,
1560 line.x + line.width, strikethrough_y
1561 );
1562 }
1563 }
1564 }
1565
1566 if needs_opacity {
1567 let _ = writeln!(stream, "Q");
1568 }
1569 }
1570
1571 DrawCommand::Image { .. } => {
1572 let elem_idx = *element_counter;
1573 *element_counter += 1;
1574 if let Some(&img_idx) = builder.image_index_map.get(&(page_idx, elem_idx)) {
1575 let x = element.x;
1576 let y = page_height - element.y - element.height;
1577 let _ = write!(
1578 stream,
1579 "q\n{:.4} 0 0 {:.4} {:.2} {:.2} cm\n/Im{} Do\nQ\n",
1580 element.width, element.height, x, y, img_idx
1581 );
1582 } else {
1583 let x = element.x;
1585 let y = page_height - element.y - element.height;
1586 let _ = write!(
1587 stream,
1588 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
1589 x, y, element.width, element.height
1590 );
1591 }
1592 if tagged_mcid.is_some() {
1593 let _ = writeln!(stream, "EMC");
1594 if let Some(ref mut tb) = tag_builder {
1595 tb.end_element();
1596 }
1597 } else if is_artifact {
1598 let _ = writeln!(stream, "EMC");
1599 }
1600 return; }
1602
1603 DrawCommand::ImagePlaceholder => {
1604 *element_counter += 1;
1605 let x = element.x;
1606 let y = page_height - element.y - element.height;
1607 let _ = write!(
1608 stream,
1609 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
1610 x, y, element.width, element.height
1611 );
1612 if tagged_mcid.is_some() {
1613 let _ = writeln!(stream, "EMC");
1614 if let Some(ref mut tb) = tag_builder {
1615 tb.end_element();
1616 }
1617 } else if is_artifact {
1618 let _ = writeln!(stream, "EMC");
1619 }
1620 return;
1621 }
1622
1623 DrawCommand::Svg {
1624 commands,
1625 width: svg_w,
1626 height: svg_h,
1627 clip,
1628 } => {
1629 let x = element.x;
1630 let y = page_height - element.y - element.height;
1631
1632 let _ = writeln!(stream, "q");
1634 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", x, y);
1635
1636 if *svg_w > 0.0 && *svg_h > 0.0 {
1638 let sx = element.width / svg_w;
1639 let sy = element.height / svg_h;
1640 let _ = writeln!(stream, "{:.4} 0 0 {:.4} 0 0 cm", sx, sy);
1641 }
1642
1643 let _ = writeln!(stream, "1 0 0 -1 0 {:.2} cm", svg_h);
1645
1646 if *clip {
1648 let _ = writeln!(stream, "0 0 {:.2} {:.2} re W n", svg_w, svg_h);
1649 }
1650
1651 Self::write_svg_commands(stream, commands, &builder.ext_gstate_map);
1652
1653 let _ = writeln!(stream, "Q");
1654 if tagged_mcid.is_some() {
1655 let _ = writeln!(stream, "EMC");
1656 if let Some(ref mut tb) = tag_builder {
1657 tb.end_element();
1658 }
1659 } else if is_artifact {
1660 let _ = writeln!(stream, "EMC");
1661 }
1662 return;
1663 }
1664
1665 DrawCommand::Barcode {
1666 bars,
1667 bar_width,
1668 height,
1669 color,
1670 } => {
1671 *element_counter += 1;
1672 let _ = writeln!(stream, "q");
1673 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1674 for (i, &bar) in bars.iter().enumerate() {
1675 if bar == 1 {
1676 let bx = element.x + i as f64 * bar_width;
1677 let by = page_height - element.y - height;
1678 let _ = writeln!(
1679 stream,
1680 "{:.2} {:.2} {:.2} {:.2} re",
1681 bx, by, bar_width, height
1682 );
1683 }
1684 }
1685 let _ = writeln!(stream, "f\nQ");
1686 if tagged_mcid.is_some() {
1687 let _ = writeln!(stream, "EMC");
1688 if let Some(ref mut tb) = tag_builder {
1689 tb.end_element();
1690 }
1691 } else if is_artifact {
1692 let _ = writeln!(stream, "EMC");
1693 }
1694 return;
1695 }
1696
1697 DrawCommand::QrCode {
1698 modules,
1699 module_size,
1700 color,
1701 } => {
1702 *element_counter += 1;
1703 let _ = writeln!(stream, "q");
1704 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1705 for (row_idx, row) in modules.iter().enumerate() {
1706 for (col_idx, &dark) in row.iter().enumerate() {
1707 if dark {
1708 let mx = element.x + col_idx as f64 * module_size;
1709 let my = page_height - element.y - (row_idx as f64 + 1.0) * module_size;
1710 let _ = writeln!(
1711 stream,
1712 "{:.2} {:.2} {:.2} {:.2} re",
1713 mx, my, module_size, module_size
1714 );
1715 }
1716 }
1717 }
1718 let _ = writeln!(stream, "f\nQ");
1719 if tagged_mcid.is_some() {
1720 let _ = writeln!(stream, "EMC");
1721 if let Some(ref mut tb) = tag_builder {
1722 tb.end_element();
1723 }
1724 } else if is_artifact {
1725 let _ = writeln!(stream, "EMC");
1726 }
1727 return;
1728 }
1729
1730 DrawCommand::Chart { primitives } => {
1731 *element_counter += 1;
1732 let _ = writeln!(stream, "q");
1733 let _ = writeln!(
1735 stream,
1736 "1 0 0 -1 {:.4} {:.4} cm",
1737 element.x,
1738 page_height - element.y
1739 );
1740
1741 for prim in primitives {
1742 write_chart_primitive(stream, prim, element.height, builder);
1743 }
1744
1745 let _ = writeln!(stream, "Q");
1746 if tagged_mcid.is_some() {
1747 let _ = writeln!(stream, "EMC");
1748 if let Some(ref mut tb) = tag_builder {
1749 tb.end_element();
1750 }
1751 } else if is_artifact {
1752 let _ = writeln!(stream, "EMC");
1753 }
1754 return;
1755 }
1756
1757 DrawCommand::Watermark {
1758 lines,
1759 color,
1760 opacity,
1761 angle_rad,
1762 font_family: _,
1763 } => {
1764 let _ = writeln!(stream, "q");
1765 if *opacity < 1.0 {
1767 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1768 let _ = writeln!(stream, "/{} gs", gs_name);
1769 }
1770 }
1771 let pdf_cx = element.x;
1773 let pdf_cy = page_height - element.y;
1774 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", pdf_cx, pdf_cy);
1775 let cos_a = angle_rad.cos();
1777 let sin_a = angle_rad.sin();
1778 let _ = writeln!(
1779 stream,
1780 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
1781 cos_a, sin_a, -sin_a, cos_a
1782 );
1783 let _ = writeln!(stream, "BT");
1785 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1786 if let Some(line) = lines.first() {
1787 let groups = Self::group_glyphs_by_style(&line.glyphs);
1788 let text_width = line.width;
1789 let cap_height = line.height * 0.7;
1790 let _ = writeln!(
1791 stream,
1792 "{:.2} {:.2} Td",
1793 -text_width / 2.0,
1794 -cap_height / 2.0
1795 );
1796 for group in &groups {
1797 let first = &group[0];
1798 let italic =
1799 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
1800 let fk = FontKey {
1801 family: first.font_family.clone(),
1802 weight: first.font_weight,
1803 italic,
1804 };
1805 let idx = self.font_index(
1806 &first.font_family,
1807 first.font_weight,
1808 first.font_style,
1809 &builder.font_objects,
1810 );
1811 let font_name = format!("F{}", idx);
1812 let _ = writeln!(stream, "/{} {:.1} Tf", font_name, first.font_size);
1813 let is_custom = builder.custom_font_data.contains_key(&fk);
1814 if is_custom {
1815 if let Some(embed_data) = builder.custom_font_data.get(&fk) {
1816 let mut hex = String::new();
1817 for g in group.iter() {
1818 let gid =
1819 embed_data.gid_remap.get(&g.glyph_id).copied().unwrap_or(0);
1820 let _ = write!(hex, "{:04X}", gid);
1821 }
1822 let _ = writeln!(stream, "<{}> Tj", hex);
1823 }
1824 } else {
1825 let hex_str: String = group
1826 .iter()
1827 .map(|g| format!("{:02X}", g.glyph_id as u8))
1828 .collect();
1829 let _ = writeln!(stream, "<{}> Tj", hex_str);
1830 }
1831 }
1832 }
1833 let _ = writeln!(stream, "ET");
1834 let _ = writeln!(stream, "Q");
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::FormField { field_type, .. } => {
1847 let pdf_x = element.x;
1851 let pdf_y = page_height - element.y - element.height;
1852 let w = element.width;
1853 let h = element.height;
1854 let _ = writeln!(stream, "q");
1855 match field_type {
1856 FormFieldType::Checkbox { checked, .. } => {
1857 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
1860 let _ =
1861 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
1862 if *checked {
1863 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
1865 let sx = w / 14.0;
1866 let sy = h / 14.0;
1867 let _ = writeln!(
1868 stream,
1869 "{:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l f",
1870 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
1871 pdf_x + 5.5 * sx, pdf_y + 2.0 * sy,
1872 pdf_x + 12.0 * sx, pdf_y + 11.0 * sy,
1873 pdf_x + 11.0 * sx, pdf_y + 12.0 * sy,
1874 pdf_x + 5.5 * sx, pdf_y + 4.5 * sy,
1875 pdf_x + 3.0 * sx, pdf_y + 7.0 * sy,
1876 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
1877 );
1878 }
1879 }
1880 FormFieldType::RadioButton { checked, .. } => {
1881 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
1884 let _ =
1885 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
1886 if *checked {
1887 let cx = pdf_x + w / 2.0;
1889 let cy = pdf_y + h / 2.0;
1890 let r = (w.min(h) / 2.0) * 0.6;
1891 let k = r * 0.5523;
1892 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
1893 let _ = writeln!(
1894 stream,
1895 "{:.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",
1896 cx, cy + r,
1897 cx + k, cy + r, cx + r, cy + k, cx + r, cy,
1898 cx + r, cy - k, cx + k, cy - r, cx, cy - r,
1899 cx - k, cy - r, cx - r, cy - k, cx - r, cy,
1900 cx - r, cy + k, cx - k, cy + r, cx, cy + r,
1901 );
1902 }
1903 }
1904 FormFieldType::TextField {
1905 value,
1906 placeholder,
1907 font_size,
1908 multiline,
1909 password,
1910 ..
1911 } => {
1912 let _ = writeln!(stream, "1 1 1 rg");
1914 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
1915 let _ = writeln!(stream, "0.5 w");
1916 let _ =
1917 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
1918 if flatten_forms {
1920 let has_value = value.as_ref().is_some_and(|v| !v.is_empty());
1921 if has_value {
1922 let val = value.as_ref().unwrap();
1923 let display_text = if *password {
1924 "\u{2022}".repeat(val.len())
1925 } else {
1926 val.clone()
1927 };
1928 let font_idx = builder
1929 .font_objects
1930 .iter()
1931 .enumerate()
1932 .find(|(_, (key, _))| {
1933 key.family == "Helvetica"
1934 && key.weight == 400
1935 && !key.italic
1936 })
1937 .map(|(i, _)| i)
1938 .unwrap_or(0);
1939 if *multiline {
1940 let metrics = crate::font::StandardFont::Helvetica.metrics();
1942 let max_w = w - 4.0;
1943 let mut lines: Vec<String> = Vec::new();
1944 for paragraph in display_text.split('\n') {
1945 let mut line = String::new();
1946 let mut line_w = 0.0;
1947 for word in paragraph.split_whitespace() {
1948 let word_w =
1949 metrics.measure_string(word, *font_size, 0.0);
1950 let space_w = if line.is_empty() {
1951 0.0
1952 } else {
1953 metrics.measure_string(" ", *font_size, 0.0)
1954 };
1955 if word_w > max_w {
1957 let mut char_line = String::new();
1958 let mut char_w = 0.0;
1959 for ch in word.chars() {
1960 let cw = metrics.char_width(ch, *font_size);
1961 if !char_line.is_empty() && char_w + cw > max_w
1962 {
1963 if !line.is_empty() {
1964 lines.push(line.clone());
1965 line.clear();
1966 line_w = 0.0;
1967 }
1968 lines.push(char_line.clone());
1969 char_line.clear();
1970 char_w = 0.0;
1971 }
1972 char_line.push(ch);
1973 char_w += cw;
1974 }
1975 if !char_line.is_empty() {
1977 if !line.is_empty() {
1978 line.push(' ');
1979 line_w += metrics
1980 .measure_string(" ", *font_size, 0.0);
1981 }
1982 line.push_str(&char_line);
1983 line_w += char_w;
1984 }
1985 continue;
1986 }
1987 if !line.is_empty() && line_w + space_w + word_w > max_w
1988 {
1989 lines.push(line.clone());
1990 line.clear();
1991 line_w = 0.0;
1992 }
1993 if !line.is_empty() {
1994 line.push(' ');
1995 line_w += space_w;
1996 }
1997 line.push_str(word);
1998 line_w += word_w;
1999 }
2000 if !line.is_empty() {
2001 lines.push(line);
2002 }
2003 }
2004 let text_y = pdf_y + h - font_size - 2.0;
2005 for (i, line_text) in lines.iter().enumerate() {
2006 let ly = text_y - (i as f64) * (font_size * 1.2);
2007 if ly < pdf_y {
2008 break;
2009 }
2010 let esc = Self::encode_winansi_text(line_text);
2011 let _ = writeln!(
2012 stream,
2013 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2014 font_idx,
2015 font_size,
2016 pdf_x + 2.0,
2017 ly,
2018 esc
2019 );
2020 }
2021 } else {
2022 let escaped = Self::encode_winansi_text(&display_text);
2023 let text_y = pdf_y + (h - font_size) / 2.0;
2024 let _ = writeln!(
2025 stream,
2026 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2027 font_idx,
2028 font_size,
2029 pdf_x + 2.0,
2030 text_y,
2031 escaped
2032 );
2033 }
2034 } else if let Some(ref ph) = placeholder {
2035 if !ph.is_empty() {
2036 let font_idx = builder
2038 .font_objects
2039 .iter()
2040 .enumerate()
2041 .find(|(_, (key, _))| {
2042 key.family == "Helvetica"
2043 && key.weight == 400
2044 && !key.italic
2045 })
2046 .map(|(i, _)| i)
2047 .unwrap_or(0);
2048 let escaped = Self::encode_winansi_text(ph);
2049 let text_y = pdf_y + (h - font_size) / 2.0;
2050 let _ = writeln!(
2051 stream,
2052 "BT /F{} {:.1} Tf 0.6 g {:.2} {:.2} Td ({}) Tj ET",
2053 font_idx,
2054 font_size,
2055 pdf_x + 2.0,
2056 text_y,
2057 escaped
2058 );
2059 }
2060 }
2061 }
2062 }
2063 FormFieldType::Dropdown {
2064 value, font_size, ..
2065 } => {
2066 let _ = writeln!(stream, "1 1 1 rg");
2068 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2069 let _ = writeln!(stream, "0.5 w");
2070 let _ =
2071 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2072 if flatten_forms {
2074 if let Some(ref val) = value {
2075 if !val.is_empty() {
2076 let font_idx = builder
2077 .font_objects
2078 .iter()
2079 .enumerate()
2080 .find(|(_, (key, _))| {
2081 key.family == "Helvetica"
2082 && key.weight == 400
2083 && !key.italic
2084 })
2085 .map(|(i, _)| i)
2086 .unwrap_or(0);
2087 let escaped = Self::encode_winansi_text(val);
2088 let text_y = pdf_y + (h - font_size) / 2.0;
2089 let _ = writeln!(
2090 stream,
2091 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2092 font_idx,
2093 font_size,
2094 pdf_x + 2.0,
2095 text_y,
2096 escaped
2097 );
2098 }
2099 }
2100 }
2101 }
2102 }
2103 let _ = writeln!(stream, "Q");
2104 }
2105 }
2106
2107 let clip_overflow = matches!(element.overflow, Overflow::Hidden);
2112 if clip_overflow {
2113 let clip_x = element.x;
2114 let clip_y = page_height - element.y - element.height;
2115 let clip_w = element.width;
2116 let clip_h = element.height;
2117 let radius = if let DrawCommand::Rect { border_radius, .. } = &element.draw {
2121 Some(border_radius)
2122 } else {
2123 None
2124 };
2125 let has_rounded_corners = radius.is_some_and(|r| {
2126 r.top_left > 0.0 || r.top_right > 0.0 || r.bottom_right > 0.0 || r.bottom_left > 0.0
2127 });
2128 let _ = writeln!(stream, "q");
2129 if has_rounded_corners {
2130 self.write_rounded_rect(stream, clip_x, clip_y, clip_w, clip_h, radius.unwrap());
2131 let _ = writeln!(stream, "W n");
2132 } else {
2133 let _ = writeln!(
2134 stream,
2135 "{:.2} {:.2} {:.2} {:.2} re W n",
2136 clip_x, clip_y, clip_w, clip_h
2137 );
2138 }
2139 }
2140
2141 for child in &element.children {
2142 self.write_element(
2143 stream,
2144 child,
2145 page_height,
2146 builder,
2147 page_idx,
2148 element_counter,
2149 gradient_counter,
2150 page_number,
2151 total_pages,
2152 tag_builder.as_deref_mut(),
2153 flatten_forms,
2154 );
2155 }
2156
2157 if clip_overflow {
2158 let _ = writeln!(stream, "Q");
2159 }
2160
2161 if needs_element_opacity {
2164 let _ = writeln!(stream, "Q");
2165 }
2166
2167 if tagged_mcid.is_some() {
2169 let _ = writeln!(stream, "EMC");
2170 if let Some(ref mut tb) = tag_builder {
2171 tb.end_element();
2172 }
2173 } else if is_artifact {
2174 let _ = writeln!(stream, "EMC");
2175 }
2176 }
2177
2178 fn write_rounded_rect(
2179 &self,
2180 stream: &mut String,
2181 x: f64,
2182 y: f64,
2183 w: f64,
2184 h: f64,
2185 r: &crate::style::CornerValues,
2186 ) {
2187 let k = 0.5522847498;
2188
2189 let tl = r.top_left.min(w / 2.0).min(h / 2.0);
2190 let tr = r.top_right.min(w / 2.0).min(h / 2.0);
2191 let br = r.bottom_right.min(w / 2.0).min(h / 2.0);
2192 let bl = r.bottom_left.min(w / 2.0).min(h / 2.0);
2193
2194 let _ = writeln!(stream, "{:.2} {:.2} m", x + bl, y);
2195
2196 let _ = writeln!(stream, "{:.2} {:.2} l", x + w - br, y);
2197 if br > 0.0 {
2198 let _ = writeln!(
2199 stream,
2200 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2201 x + w - br + br * k,
2202 y,
2203 x + w,
2204 y + br - br * k,
2205 x + w,
2206 y + br
2207 );
2208 }
2209
2210 let _ = writeln!(stream, "{:.2} {:.2} l", x + w, y + h - tr);
2211 if tr > 0.0 {
2212 let _ = writeln!(
2213 stream,
2214 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2215 x + w,
2216 y + h - tr + tr * k,
2217 x + w - tr + tr * k,
2218 y + h,
2219 x + w - tr,
2220 y + h
2221 );
2222 }
2223
2224 let _ = writeln!(stream, "{:.2} {:.2} l", x + tl, y + h);
2225 if tl > 0.0 {
2226 let _ = writeln!(
2227 stream,
2228 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2229 x + tl - tl * k,
2230 y + h,
2231 x,
2232 y + h - tl + tl * k,
2233 x,
2234 y + h - tl
2235 );
2236 }
2237
2238 let _ = writeln!(stream, "{:.2} {:.2} l", x, y + bl);
2239 if bl > 0.0 {
2240 let _ = writeln!(
2241 stream,
2242 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2243 x,
2244 y + bl - bl * k,
2245 x + bl - bl * k,
2246 y,
2247 x + bl,
2248 y
2249 );
2250 }
2251
2252 let _ = writeln!(stream, "h");
2253 }
2254
2255 #[allow(clippy::too_many_arguments)]
2256 fn write_border_sides(
2257 &self,
2258 stream: &mut String,
2259 x: f64,
2260 y: f64,
2261 w: f64,
2262 h: f64,
2263 bw: &Edges,
2264 bc: &crate::style::EdgeValues<Color>,
2265 ) {
2266 if bw.top > 0.0 {
2267 let _ = write!(
2268 stream,
2269 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2270 bc.top.r,
2271 bc.top.g,
2272 bc.top.b,
2273 bw.top,
2274 x,
2275 y + h,
2276 x + w,
2277 y + h
2278 );
2279 }
2280 if bw.bottom > 0.0 {
2281 let _ = write!(
2282 stream,
2283 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2284 bc.bottom.r,
2285 bc.bottom.g,
2286 bc.bottom.b,
2287 bw.bottom,
2288 x,
2289 y,
2290 x + w,
2291 y
2292 );
2293 }
2294 if bw.left > 0.0 {
2295 let _ = write!(
2296 stream,
2297 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2298 bc.left.r,
2299 bc.left.g,
2300 bc.left.b,
2301 bw.left,
2302 x,
2303 y,
2304 x,
2305 y + h
2306 );
2307 }
2308 if bw.right > 0.0 {
2309 let _ = write!(
2310 stream,
2311 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2312 bc.right.r,
2313 bc.right.g,
2314 bc.right.b,
2315 bw.right,
2316 x + w,
2317 y,
2318 x + w,
2319 y + h
2320 );
2321 }
2322 }
2323
2324 fn register_fonts(
2327 &self,
2328 builder: &mut PdfBuilder,
2329 pages: &[LayoutPage],
2330 font_context: &FontContext,
2331 ) -> Result<(), FormeError> {
2332 let mut font_usage_map: HashMap<FontKey, FontUsage> = HashMap::new();
2334
2335 for page in pages {
2336 Self::collect_font_usage(&page.elements, &mut font_usage_map);
2337 }
2338
2339 let mut keys: Vec<FontKey> = font_usage_map.keys().cloned().collect();
2340
2341 keys.sort_by(|a, b| {
2343 a.family
2344 .cmp(&b.family)
2345 .then(a.weight.cmp(&b.weight))
2346 .then(a.italic.cmp(&b.italic))
2347 });
2348 keys.dedup();
2349
2350 if keys.is_empty() {
2352 keys.push(FontKey {
2353 family: "Helvetica".to_string(),
2354 weight: 400,
2355 italic: false,
2356 });
2357 }
2358
2359 for key in &keys {
2360 let font_data = font_context.resolve(&key.family, key.weight, key.italic);
2361
2362 match font_data {
2363 FontData::Standard(std_font) => {
2364 let obj_id = builder.objects.len();
2365 let metrics = std_font.metrics();
2368 let widths_str: String = metrics
2369 .widths
2370 .iter()
2371 .map(|w| w.to_string())
2372 .collect::<Vec<_>>()
2373 .join(" ");
2374 let font_dict = format!(
2375 "<< /Type /Font /Subtype /Type1 /BaseFont /{} \
2376 /Encoding /WinAnsiEncoding \
2377 /FirstChar 32 /LastChar 255 /Widths [{}] >>",
2378 std_font.pdf_name(),
2379 widths_str,
2380 );
2381 builder.objects.push(PdfObject {
2382 id: obj_id,
2383 data: font_dict.into_bytes(),
2384 });
2385 builder.font_objects.push((key.clone(), obj_id));
2386 }
2387 FontData::Custom { data, .. } => {
2388 let usage = font_usage_map.get(key);
2389 let used_glyph_ids = usage.map(|u| &u.glyph_ids);
2390 let used_chars = usage.map(|u| &u.chars);
2391 let glyph_to_char = usage.map(|u| &u.glyph_to_char);
2392 let type0_obj_id = Self::write_custom_font_objects(
2393 builder,
2394 key,
2395 data,
2396 used_glyph_ids.cloned().unwrap_or_default(),
2397 used_chars.cloned().unwrap_or_default(),
2398 glyph_to_char.cloned().unwrap_or_default(),
2399 )?;
2400 builder.font_objects.push((key.clone(), type0_obj_id));
2401 }
2402 }
2403 }
2404
2405 Ok(())
2406 }
2407
2408 fn collect_font_usage(
2410 elements: &[LayoutElement],
2411 font_usage: &mut HashMap<FontKey, FontUsage>,
2412 ) {
2413 for element in elements {
2414 let lines_opt = match &element.draw {
2415 DrawCommand::Text { lines, .. } => Some(lines),
2416 DrawCommand::Watermark { lines, .. } => Some(lines),
2417 _ => None,
2418 };
2419 if let Some(lines) = lines_opt {
2420 for line in lines {
2421 for glyph in &line.glyphs {
2422 let italic =
2423 matches!(glyph.font_style, FontStyle::Italic | FontStyle::Oblique);
2424 let key = FontKey {
2425 family: glyph.font_family.clone(),
2426 weight: glyph.font_weight,
2427 italic,
2428 };
2429 let usage = font_usage.entry(key).or_insert_with(|| FontUsage {
2430 chars: HashSet::new(),
2431 glyph_ids: HashSet::new(),
2432 glyph_to_char: HashMap::new(),
2433 });
2434 usage.chars.insert(glyph.char_value);
2435 usage.glyph_ids.insert(glyph.glyph_id);
2436 usage
2438 .glyph_to_char
2439 .entry(glyph.glyph_id)
2440 .or_insert(glyph.char_value);
2441 if let Some(ref ct) = glyph.cluster_text {
2443 if let Some(first_char) = ct.chars().next() {
2445 usage
2446 .glyph_to_char
2447 .entry(glyph.glyph_id)
2448 .or_insert(first_char);
2449 }
2450 }
2451 }
2452 }
2453 }
2454 Self::collect_font_usage(&element.children, font_usage);
2455 }
2456 }
2457
2458 fn register_shadings(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2464 for (page_idx, page) in pages.iter().enumerate() {
2465 let mut counter = 0usize;
2466 Self::collect_shadings_recursive(&page.elements, page_idx, &mut counter, builder);
2467 }
2468 }
2469
2470 fn collect_shadings_recursive(
2471 elements: &[LayoutElement],
2472 page_idx: usize,
2473 counter: &mut usize,
2474 builder: &mut PdfBuilder,
2475 ) {
2476 for element in elements {
2477 if let DrawCommand::Rect {
2478 background_gradient: Some(gradient),
2479 ..
2480 } = &element.draw
2481 {
2482 let ordinal = *counter;
2483 *counter += 1;
2484 let (obj_id, name) =
2485 Self::write_shading_objects(builder, gradient, element, ordinal);
2486 builder
2487 .shading_map
2488 .insert((page_idx, ordinal), (obj_id, name));
2489 }
2490 Self::collect_shadings_recursive(&element.children, page_idx, counter, builder);
2491 }
2492 }
2493
2494 fn write_shading_objects(
2500 builder: &mut PdfBuilder,
2501 gradient: &crate::style::Background,
2502 element: &LayoutElement,
2503 ordinal: usize,
2504 ) -> (usize, String) {
2505 use crate::style::Background;
2506 use crate::style::GradientStop;
2507
2508 let black = Color {
2512 r: 0.0,
2513 g: 0.0,
2514 b: 0.0,
2515 a: 1.0,
2516 };
2517 let stops: Vec<GradientStop> = match gradient {
2518 Background::Color(c) => vec![
2519 GradientStop {
2520 position: 0.0,
2521 color: *c,
2522 },
2523 GradientStop {
2524 position: 1.0,
2525 color: *c,
2526 },
2527 ],
2528 Background::Linear(g) => normalize_gradient_stops(&g.stops, black),
2529 Background::Radial(g) => normalize_gradient_stops(&g.stops, black),
2530 };
2531
2532 let function_id = if stops.len() <= 2 {
2536 let c0 = stops.first().map(|s| s.color).unwrap_or(black);
2537 let c1 = stops.last().map(|s| s.color).unwrap_or(c0);
2538 let id = builder.objects.len();
2539 let data = format!(
2540 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
2541 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
2542 );
2543 builder.objects.push(PdfObject {
2544 id,
2545 data: data.into_bytes(),
2546 });
2547 id
2548 } else {
2549 let mut sub_ids: Vec<usize> = Vec::with_capacity(stops.len() - 1);
2551 for window in stops.windows(2) {
2552 let c0 = window[0].color;
2553 let c1 = window[1].color;
2554 let id = builder.objects.len();
2555 let data = format!(
2556 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
2557 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
2558 );
2559 builder.objects.push(PdfObject {
2560 id,
2561 data: data.into_bytes(),
2562 });
2563 sub_ids.push(id);
2564 }
2565 let bounds: Vec<String> = stops[1..stops.len() - 1]
2569 .iter()
2570 .map(|s| format!("{:.4}", s.position))
2571 .collect();
2572 let encode: Vec<&str> = (0..sub_ids.len()).map(|_| "0 1").collect();
2573 let functions: Vec<String> = sub_ids.iter().map(|i| format!("{} 0 R", i)).collect();
2574 let id = builder.objects.len();
2575 let data = format!(
2576 "<< /FunctionType 3 /Domain [0 1] /Functions [{}] /Bounds [{}] /Encode [{}] >>",
2577 functions.join(" "),
2578 bounds.join(" "),
2579 encode.join(" "),
2580 );
2581 builder.objects.push(PdfObject {
2582 id,
2583 data: data.into_bytes(),
2584 });
2585 id
2586 };
2587
2588 let _ = element.x;
2592 let _ = element.y;
2593 let w = element.width;
2594 let h = element.height;
2595
2596 let shading_id = builder.objects.len();
2597 let shading_data = match gradient {
2598 Background::Linear(g) => {
2599 let theta = g.angle_deg.to_radians();
2609 let dx = theta.sin();
2610 let dy = theta.cos();
2611 let axis_len = w * dx.abs() + h * dy.abs();
2614 let cx_rel = w / 2.0;
2617 let cy_rel = h / 2.0;
2618 let half = axis_len / 2.0;
2619 let x0 = cx_rel - dx * half;
2620 let y0 = cy_rel - dy * half;
2621 let x1 = cx_rel + dx * half;
2622 let y1 = cy_rel + dy * half;
2623 format!(
2624 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
2625 x0, y0, x1, y1, function_id,
2626 )
2627 }
2628 Background::Radial(_) => {
2629 let cx_rel = w / 2.0;
2632 let cy_rel = h / 2.0;
2633 let r_outer = (w / 2.0).max(h / 2.0);
2634 format!(
2635 "<< /ShadingType 3 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} 0 {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
2636 cx_rel, cy_rel, cx_rel, cy_rel, r_outer, function_id,
2637 )
2638 }
2639 Background::Color(_) => {
2640 format!(
2644 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 0 0] /Function {} 0 R /Extend [true true] >>",
2645 function_id,
2646 )
2647 }
2648 };
2649 builder.objects.push(PdfObject {
2650 id: shading_id,
2651 data: shading_data.into_bytes(),
2652 });
2653 (shading_id, format!("Sh{}", ordinal))
2654 }
2655
2656 fn register_page_background_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2660 for (page_idx, page) in pages.iter().enumerate() {
2661 let Some(src) = &page.config.background_image else {
2662 continue;
2663 };
2664 if let Some(&entry) = builder.page_background_url_cache.get(src) {
2666 builder.page_background_image_map.insert(page_idx, entry);
2667 continue;
2668 }
2669 match crate::image_loader::load_image(src) {
2672 Ok(image_data) => {
2673 let img_idx = builder.image_objects.len();
2674 let dims = (img_idx, image_data.width_px, image_data.height_px);
2675 let xobj_id = Self::write_image_xobject(builder, &image_data);
2676 builder.image_objects.push(xobj_id);
2677 builder.page_background_image_map.insert(page_idx, dims);
2678 builder.page_background_url_cache.insert(src.clone(), dims);
2679 }
2680 Err(e) => {
2681 eprintln!("[forme] page background image failed to load: {}", e);
2682 }
2683 }
2684 }
2685 }
2686
2687 fn write_page_background(
2692 &self,
2693 stream: &mut String,
2694 page: &LayoutPage,
2695 page_bg: (usize, u32, u32),
2696 builder: &PdfBuilder,
2697 ) {
2698 use crate::model::{BackgroundPosition, BackgroundSize};
2699 let (img_idx, iw_px, ih_px) = page_bg;
2700 let page_w = page.width;
2701 let page_h = page.height;
2702 let iw = iw_px as f64;
2703 let ih = ih_px as f64;
2704
2705 let size = page.config.background_size.unwrap_or_default();
2706 let (dest_w, dest_h) = match size {
2707 BackgroundSize::Fill => (page_w, page_h),
2708 BackgroundSize::Cover => {
2709 let s = (page_w / iw).max(page_h / ih);
2710 (iw * s, ih * s)
2711 }
2712 BackgroundSize::Contain => {
2713 let s = (page_w / iw).min(page_h / ih);
2714 (iw * s, ih * s)
2715 }
2716 };
2717
2718 let position = page.config.background_position.unwrap_or_default();
2722 let (dest_x, dest_y) = match position {
2725 BackgroundPosition::TopLeft => (0.0, page_h - dest_h),
2726 BackgroundPosition::TopRight => (page_w - dest_w, page_h - dest_h),
2727 BackgroundPosition::BottomLeft => (0.0, 0.0),
2728 BackgroundPosition::BottomRight => (page_w - dest_w, 0.0),
2729 BackgroundPosition::Center => ((page_w - dest_w) / 2.0, (page_h - dest_h) / 2.0),
2730 };
2731
2732 let opacity = page.config.background_opacity.unwrap_or(1.0);
2734 let needs_opacity = opacity < 1.0;
2735 if needs_opacity {
2736 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
2737 let _ = writeln!(stream, "q\n/{} gs", gs_name);
2738 } else {
2739 let _ = writeln!(stream, "q");
2740 }
2741 } else {
2742 let _ = writeln!(stream, "q");
2743 }
2744 let _ = writeln!(
2747 stream,
2748 "{:.2} 0 0 {:.2} {:.2} {:.2} cm\n/Im{} Do\nQ",
2749 dest_w, dest_h, dest_x, dest_y, img_idx,
2750 );
2751 }
2752
2753 fn register_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2755 for (page_idx, page) in pages.iter().enumerate() {
2756 let mut element_counter = 0usize;
2757 Self::collect_images_recursive(&page.elements, page_idx, &mut element_counter, builder);
2758 }
2759 }
2760
2761 fn collect_images_recursive(
2762 elements: &[LayoutElement],
2763 page_idx: usize,
2764 element_counter: &mut usize,
2765 builder: &mut PdfBuilder,
2766 ) {
2767 for element in elements {
2768 match &element.draw {
2769 DrawCommand::Image { image_data } => {
2770 let elem_idx = *element_counter;
2771 *element_counter += 1;
2772
2773 let img_idx = builder.image_objects.len();
2774 let xobj_id = Self::write_image_xobject(builder, image_data);
2775 builder.image_objects.push(xobj_id);
2776 builder
2777 .image_index_map
2778 .insert((page_idx, elem_idx), img_idx);
2779 }
2780 DrawCommand::ImagePlaceholder => {
2781 *element_counter += 1;
2782 }
2783 _ => {
2784 Self::collect_images_recursive(
2785 &element.children,
2786 page_idx,
2787 element_counter,
2788 builder,
2789 );
2790 }
2791 }
2792 }
2793 }
2794
2795 fn register_ext_gstates(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2797 let mut unique_opacities: Vec<f64> = Vec::new();
2798 for page in pages {
2799 Self::collect_opacities_recursive(&page.elements, &mut unique_opacities);
2800 if let Some(o) = page.config.background_opacity {
2802 if o < 1.0 {
2803 unique_opacities.push(o);
2804 }
2805 }
2806 }
2807 unique_opacities.sort_by(|a, b| a.partial_cmp(b).unwrap());
2808 unique_opacities.dedup();
2809
2810 for (idx, &opacity) in unique_opacities.iter().enumerate() {
2811 let obj_id = builder.objects.len();
2812 let gs_name = format!("GS{}", idx);
2813 let obj_data = format!(
2814 "<< /Type /ExtGState /ca {:.4} /CA {:.4} >>",
2815 opacity, opacity
2816 );
2817 builder.objects.push(PdfObject {
2818 id: obj_id,
2819 data: obj_data.into_bytes(),
2820 });
2821 let key = opacity.to_bits();
2822 builder.ext_gstate_map.insert(key, (obj_id, gs_name));
2823 }
2824 }
2825
2826 fn collect_opacities_recursive(elements: &[LayoutElement], opacities: &mut Vec<f64>) {
2827 for element in elements {
2828 if element.opacity < 1.0 {
2835 opacities.push(element.opacity);
2836 }
2837 if let DrawCommand::Rect {
2841 box_shadow: Some(shadow),
2842 ..
2843 } = &element.draw
2844 {
2845 if shadow.color.a < 1.0 {
2846 opacities.push(shadow.color.a);
2847 }
2848 }
2849 match &element.draw {
2850 DrawCommand::Rect { opacity, .. }
2851 | DrawCommand::Text { opacity, .. }
2852 | DrawCommand::Watermark { opacity, .. }
2853 if *opacity < 1.0 =>
2854 {
2855 opacities.push(*opacity);
2856 }
2857 DrawCommand::Chart { primitives } => {
2858 for prim in primitives {
2859 if let crate::chart::ChartPrimitive::FilledPath { opacity, .. } = prim {
2860 if *opacity < 1.0 {
2861 opacities.push(*opacity);
2862 }
2863 }
2864 }
2865 }
2866 DrawCommand::Svg { commands, .. } => {
2867 for cmd in commands {
2868 if let crate::svg::SvgCommand::SetOpacity(opacity) = cmd {
2869 if *opacity < 1.0 {
2870 opacities.push(*opacity);
2871 }
2872 }
2873 }
2874 }
2875 _ => {}
2876 }
2877 Self::collect_opacities_recursive(&element.children, opacities);
2878 }
2879 }
2880
2881 fn build_ext_gstate_resource_dict(&self, builder: &PdfBuilder) -> String {
2883 if builder.ext_gstate_map.is_empty() {
2884 return String::new();
2885 }
2886 let mut entries: Vec<(&String, usize)> = builder
2887 .ext_gstate_map
2888 .values()
2889 .map(|(obj_id, name)| (name, *obj_id))
2890 .collect();
2891 entries.sort_by_key(|(name, _)| (*name).clone());
2892 entries
2893 .iter()
2894 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
2895 .collect::<Vec<_>>()
2896 .join(" ")
2897 }
2898
2899 fn write_image_xobject(
2902 builder: &mut PdfBuilder,
2903 image: &crate::image_loader::LoadedImage,
2904 ) -> usize {
2905 use crate::image_loader::{ImagePixelData, JpegColorSpace};
2906
2907 match &image.pixel_data {
2908 ImagePixelData::Jpeg { data, color_space } => {
2909 let color_space_str = match color_space {
2910 JpegColorSpace::DeviceRGB => "/DeviceRGB",
2911 JpegColorSpace::DeviceGray => "/DeviceGray",
2912 };
2913
2914 let obj_id = builder.objects.len();
2915 let mut obj_data: Vec<u8> = Vec::new();
2916 let _ = write!(
2917 obj_data,
2918 "<< /Type /XObject /Subtype /Image \
2919 /Width {} /Height {} \
2920 /ColorSpace {} \
2921 /BitsPerComponent 8 \
2922 /Filter /DCTDecode \
2923 /Length {} >>\nstream\n",
2924 image.width_px,
2925 image.height_px,
2926 color_space_str,
2927 data.len()
2928 );
2929 obj_data.extend_from_slice(data);
2930 obj_data.extend_from_slice(b"\nendstream");
2931 builder.objects.push(PdfObject {
2932 id: obj_id,
2933 data: obj_data,
2934 });
2935 obj_id
2936 }
2937
2938 ImagePixelData::Decoded { rgb, alpha } => {
2939 let smask_id = alpha.as_ref().map(|alpha_data| {
2941 let compressed_alpha = compress_to_vec_zlib(alpha_data, 6);
2942 let smask_obj_id = builder.objects.len();
2943 let mut smask_data: Vec<u8> = Vec::new();
2944 let _ = write!(
2945 smask_data,
2946 "<< /Type /XObject /Subtype /Image \
2947 /Width {} /Height {} \
2948 /ColorSpace /DeviceGray \
2949 /BitsPerComponent 8 \
2950 /Filter /FlateDecode \
2951 /Length {} >>\nstream\n",
2952 image.width_px,
2953 image.height_px,
2954 compressed_alpha.len()
2955 );
2956 smask_data.extend_from_slice(&compressed_alpha);
2957 smask_data.extend_from_slice(b"\nendstream");
2958 builder.objects.push(PdfObject {
2959 id: smask_obj_id,
2960 data: smask_data,
2961 });
2962 smask_obj_id
2963 });
2964
2965 let compressed_rgb = compress_to_vec_zlib(rgb, 6);
2967 let obj_id = builder.objects.len();
2968 let mut obj_data: Vec<u8> = Vec::new();
2969
2970 let smask_ref = smask_id
2971 .map(|id| format!(" /SMask {} 0 R", id))
2972 .unwrap_or_default();
2973
2974 let _ = write!(
2975 obj_data,
2976 "<< /Type /XObject /Subtype /Image \
2977 /Width {} /Height {} \
2978 /ColorSpace /DeviceRGB \
2979 /BitsPerComponent 8 \
2980 /Filter /FlateDecode \
2981 /Length {}{} >>\nstream\n",
2982 image.width_px,
2983 image.height_px,
2984 compressed_rgb.len(),
2985 smask_ref
2986 );
2987 obj_data.extend_from_slice(&compressed_rgb);
2988 obj_data.extend_from_slice(b"\nendstream");
2989 builder.objects.push(PdfObject {
2990 id: obj_id,
2991 data: obj_data,
2992 });
2993 obj_id
2994 }
2995 }
2996 }
2997
2998 fn build_shading_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3002 let mut entries: Vec<(String, usize)> = builder
3003 .shading_map
3004 .iter()
3005 .filter(|(&(p, _), _)| p == page_idx)
3006 .map(|(_, (obj_id, name))| (name.clone(), *obj_id))
3007 .collect();
3008 if entries.is_empty() {
3009 return String::new();
3010 }
3011 entries.sort_by(|a, b| a.0.cmp(&b.0));
3012 entries
3013 .iter()
3014 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3015 .collect::<Vec<_>>()
3016 .join(" ")
3017 }
3018
3019 fn build_xobject_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3020 let mut entries: Vec<(usize, usize)> = Vec::new();
3021 for (&(pidx, _), &img_idx) in &builder.image_index_map {
3022 if pidx == page_idx {
3023 let obj_id = builder.image_objects[img_idx];
3024 entries.push((img_idx, obj_id));
3025 }
3026 }
3027 if let Some(&(img_idx, _, _)) = builder.page_background_image_map.get(&page_idx) {
3030 let obj_id = builder.image_objects[img_idx];
3031 entries.push((img_idx, obj_id));
3032 }
3033 if entries.is_empty() {
3034 return String::new();
3035 }
3036 entries.sort_by_key(|(idx, _)| *idx);
3037 entries.dedup();
3038 entries
3039 .iter()
3040 .map(|(idx, obj_id)| format!("/Im{} {} 0 R", idx, obj_id))
3041 .collect::<Vec<_>>()
3042 .join(" ")
3043 }
3044
3045 fn write_custom_font_objects(
3052 builder: &mut PdfBuilder,
3053 key: &FontKey,
3054 ttf_data: &[u8],
3055 used_glyph_ids: HashSet<u16>,
3056 used_chars: HashSet<char>,
3057 glyph_to_char_map: HashMap<u16, char>,
3058 ) -> Result<usize, FormeError> {
3059 let face = ttf_parser::Face::parse(ttf_data, 0).map_err(|e| {
3060 FormeError::FontError(format!(
3061 "Failed to parse TTF data for font '{}': {}",
3062 key.family, e
3063 ))
3064 })?;
3065
3066 let units_per_em = face.units_per_em();
3067 let ascender = face.ascender();
3068 let descender = face.descender();
3069
3070 let mut char_to_orig_gid: HashMap<char, u16> = HashMap::new();
3072 for &ch in &used_chars {
3073 if let Some(gid) = face.glyph_index(ch) {
3074 char_to_orig_gid.insert(ch, gid.0);
3075 }
3076 }
3077
3078 let mut all_orig_gids: HashSet<u16> = used_glyph_ids.clone();
3082 for &gid in char_to_orig_gid.values() {
3083 all_orig_gids.insert(gid);
3084 }
3085
3086 let (embed_ttf, gid_remap) = match subset_ttf(ttf_data, &all_orig_gids) {
3088 Ok(subset_result) => (subset_result.ttf_data, subset_result.gid_remap),
3089 Err(_) => {
3090 let identity: HashMap<u16, u16> =
3092 all_orig_gids.iter().map(|&gid| (gid, gid)).collect();
3093 (ttf_data.to_vec(), identity)
3094 }
3095 };
3096
3097 let char_to_gid: HashMap<char, u16> = char_to_orig_gid
3099 .iter()
3100 .filter_map(|(&ch, &orig_gid)| gid_remap.get(&orig_gid).map(|&new_gid| (ch, new_gid)))
3101 .collect();
3102
3103 let gid_remap_for_embed = gid_remap.clone();
3105
3106 let mut new_gid_to_char: HashMap<u16, char> = HashMap::new();
3108 for (&orig_gid, &ch) in &glyph_to_char_map {
3110 if let Some(&new_gid) = gid_remap.get(&orig_gid) {
3111 new_gid_to_char.entry(new_gid).or_insert(ch);
3112 }
3113 }
3114 for (&ch, &new_gid) in &char_to_gid {
3116 new_gid_to_char.entry(new_gid).or_insert(ch);
3117 }
3118
3119 let pdf_font_name = Self::sanitize_font_name(&key.family, key.weight, key.italic);
3120
3121 let compressed_ttf = compress_to_vec_zlib(&embed_ttf, 6);
3123 let fontfile2_id = builder.objects.len();
3124 let mut fontfile2_data: Vec<u8> = Vec::new();
3125 let _ = write!(
3126 fontfile2_data,
3127 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
3128 compressed_ttf.len(),
3129 embed_ttf.len()
3130 );
3131 fontfile2_data.extend_from_slice(&compressed_ttf);
3132 fontfile2_data.extend_from_slice(b"\nendstream");
3133 builder.objects.push(PdfObject {
3134 id: fontfile2_id,
3135 data: fontfile2_data,
3136 });
3137
3138 let subset_face = ttf_parser::Face::parse(&embed_ttf, 0).unwrap_or_else(|_| face.clone());
3140 let subset_upem = subset_face.units_per_em();
3141
3142 let font_descriptor_id = builder.objects.len();
3144 let bbox = face.global_bounding_box();
3145 let scale = 1000.0 / units_per_em as f64;
3146 let bbox_str = format!(
3147 "[{} {} {} {}]",
3148 (bbox.x_min as f64 * scale) as i32,
3149 (bbox.y_min as f64 * scale) as i32,
3150 (bbox.x_max as f64 * scale) as i32,
3151 (bbox.y_max as f64 * scale) as i32,
3152 );
3153
3154 let flags = 4u32;
3155 let cap_height = face.capital_height().unwrap_or(ascender) as f64 * scale;
3156 let stem_v = if key.weight >= 700 { 120 } else { 80 };
3157
3158 let font_descriptor_dict = format!(
3159 "<< /Type /FontDescriptor /FontName /{} /Flags {} \
3160 /FontBBox {} /ItalicAngle {} \
3161 /Ascent {} /Descent {} /CapHeight {} /StemV {} \
3162 /FontFile2 {} 0 R >>",
3163 pdf_font_name,
3164 flags,
3165 bbox_str,
3166 if key.italic { -12 } else { 0 },
3167 (ascender as f64 * scale) as i32,
3168 (descender as f64 * scale) as i32,
3169 cap_height as i32,
3170 stem_v,
3171 fontfile2_id,
3172 );
3173 builder.objects.push(PdfObject {
3174 id: font_descriptor_id,
3175 data: font_descriptor_dict.into_bytes(),
3176 });
3177
3178 let cidfont_id = builder.objects.len();
3180 let w_array = Self::build_w_array_from_gids(&gid_remap, &subset_face, subset_upem);
3182 let default_width = subset_face
3183 .glyph_hor_advance(ttf_parser::GlyphId(0))
3184 .map(|adv| (adv as f64 * 1000.0 / subset_upem as f64) as u32)
3185 .unwrap_or(1000);
3186 let cidfont_dict = format!(
3187 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{} \
3188 /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> \
3189 /FontDescriptor {} 0 R /DW {} /W {} \
3190 /CIDToGIDMap /Identity >>",
3191 pdf_font_name, font_descriptor_id, default_width, w_array,
3192 );
3193 builder.objects.push(PdfObject {
3194 id: cidfont_id,
3195 data: cidfont_dict.into_bytes(),
3196 });
3197
3198 let tounicode_id = builder.objects.len();
3200 let cmap_content = Self::build_tounicode_cmap_from_gids(&new_gid_to_char, &pdf_font_name);
3201 let compressed_cmap = compress_to_vec_zlib(cmap_content.as_bytes(), 6);
3202 let mut tounicode_data: Vec<u8> = Vec::new();
3203 let _ = write!(
3204 tounicode_data,
3205 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
3206 compressed_cmap.len()
3207 );
3208 tounicode_data.extend_from_slice(&compressed_cmap);
3209 tounicode_data.extend_from_slice(b"\nendstream");
3210 builder.objects.push(PdfObject {
3211 id: tounicode_id,
3212 data: tounicode_data,
3213 });
3214
3215 let type0_id = builder.objects.len();
3217 let type0_dict = format!(
3218 "<< /Type /Font /Subtype /Type0 /BaseFont /{} \
3219 /Encoding /Identity-H \
3220 /DescendantFonts [{} 0 R] \
3221 /ToUnicode {} 0 R >>",
3222 pdf_font_name, cidfont_id, tounicode_id,
3223 );
3224 builder.objects.push(PdfObject {
3225 id: type0_id,
3226 data: type0_dict.into_bytes(),
3227 });
3228
3229 builder.custom_font_data.insert(
3231 key.clone(),
3232 CustomFontEmbedData {
3233 ttf_data: embed_ttf,
3234 gid_remap: gid_remap_for_embed,
3235 glyph_to_char: glyph_to_char_map,
3236 char_to_gid,
3237 units_per_em,
3238 ascender,
3239 descender,
3240 },
3241 );
3242
3243 Ok(type0_id)
3244 }
3245
3246 fn build_w_array_from_gids(
3248 gid_remap: &HashMap<u16, u16>,
3249 face: &ttf_parser::Face,
3250 units_per_em: u16,
3251 ) -> String {
3252 let scale = 1000.0 / units_per_em as f64;
3253
3254 let mut entries: Vec<(u16, u32)> = Vec::new();
3255 let mut seen_gids: HashSet<u16> = HashSet::new();
3256
3257 for &new_gid in gid_remap.values() {
3258 if seen_gids.contains(&new_gid) {
3259 continue;
3260 }
3261 seen_gids.insert(new_gid);
3262 let advance = face
3263 .glyph_hor_advance(ttf_parser::GlyphId(new_gid))
3264 .unwrap_or(0);
3265 let width = (advance as f64 * scale) as u32;
3266 entries.push((new_gid, width));
3267 }
3268
3269 entries.sort_by_key(|(gid, _)| *gid);
3270
3271 let mut result = String::from("[");
3273 for (gid, width) in &entries {
3274 let _ = write!(result, " {} [{}]", gid, width);
3275 }
3276 result.push_str(" ]");
3277 result
3278 }
3279
3280 fn build_tounicode_cmap_from_gids(gid_to_char: &HashMap<u16, char>, font_name: &str) -> String {
3282 let mut gid_to_unicode: Vec<(u16, u32)> = gid_to_char
3283 .iter()
3284 .map(|(&gid, &ch)| (gid, ch as u32))
3285 .collect();
3286 gid_to_unicode.sort_by_key(|(gid, _)| *gid);
3287
3288 let mut cmap = String::new();
3289 let _ = writeln!(cmap, "/CIDInit /ProcSet findresource begin");
3290 let _ = writeln!(cmap, "12 dict begin");
3291 let _ = writeln!(cmap, "begincmap");
3292 let _ = writeln!(cmap, "/CIDSystemInfo");
3293 let _ = writeln!(
3294 cmap,
3295 "<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def"
3296 );
3297 let _ = writeln!(cmap, "/CMapName /{}-UTF16 def", font_name);
3298 let _ = writeln!(cmap, "/CMapType 2 def");
3299 let _ = writeln!(cmap, "1 begincodespacerange");
3300 let _ = writeln!(cmap, "<0000> <FFFF>");
3301 let _ = writeln!(cmap, "endcodespacerange");
3302
3303 for chunk in gid_to_unicode.chunks(100) {
3305 let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
3306 for &(gid, unicode) in chunk {
3307 let _ = writeln!(cmap, "<{:04X}> <{:04X}>", gid, unicode);
3308 }
3309 let _ = writeln!(cmap, "endbfchar");
3310 }
3311
3312 let _ = writeln!(cmap, "endcmap");
3313 let _ = writeln!(cmap, "CMapName currentdict /CMap defineresource pop");
3314 let _ = writeln!(cmap, "end");
3315 let _ = writeln!(cmap, "end");
3316
3317 cmap
3318 }
3319
3320 fn sanitize_font_name(family: &str, weight: u32, italic: bool) -> String {
3323 let mut name: String = family
3324 .chars()
3325 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
3326 .collect();
3327
3328 if weight >= 700 {
3329 name.push_str("-Bold");
3330 }
3331 if italic {
3332 name.push_str("-Italic");
3333 }
3334
3335 if name.is_empty() {
3337 name = "CustomFont".to_string();
3338 }
3339
3340 name
3341 }
3342
3343 fn build_font_resource_dict(&self, font_objects: &[(FontKey, usize)]) -> String {
3344 font_objects
3345 .iter()
3346 .enumerate()
3347 .map(|(i, (_, obj_id))| format!("/F{} {} 0 R", i, obj_id))
3348 .collect::<Vec<_>>()
3349 .join(" ")
3350 }
3351
3352 fn font_index(
3354 &self,
3355 family: &str,
3356 weight: u32,
3357 font_style: FontStyle,
3358 font_objects: &[(FontKey, usize)],
3359 ) -> usize {
3360 let italic = matches!(font_style, FontStyle::Italic | FontStyle::Oblique);
3361
3362 for (i, (key, _)) in font_objects.iter().enumerate() {
3364 if key.family == family && key.weight == weight && key.italic == italic {
3365 return i;
3366 }
3367 }
3368
3369 let snapped = if weight >= 600 { 700 } else { 400 };
3371 for (i, (key, _)) in font_objects.iter().enumerate() {
3372 if key.family == family && key.weight == snapped && key.italic == italic {
3373 return i;
3374 }
3375 }
3376
3377 for (i, (key, _)) in font_objects.iter().enumerate() {
3379 if key.family == "Helvetica" && key.weight == snapped && key.italic == italic {
3380 return i;
3381 }
3382 }
3383
3384 0
3386 }
3387
3388 fn group_glyphs_by_style(glyphs: &[PositionedGlyph]) -> Vec<Vec<&PositionedGlyph>> {
3391 if glyphs.is_empty() {
3392 return vec![];
3393 }
3394
3395 let mut groups: Vec<Vec<&PositionedGlyph>> = Vec::new();
3396 let mut current_group: Vec<&PositionedGlyph> = vec![&glyphs[0]];
3397
3398 for glyph in &glyphs[1..] {
3399 let prev = current_group.last().unwrap();
3400 let same_style = glyph.font_family == prev.font_family
3401 && glyph.font_weight == prev.font_weight
3402 && std::mem::discriminant(&glyph.font_style)
3403 == std::mem::discriminant(&prev.font_style)
3404 && (glyph.font_size - prev.font_size).abs() < 0.01
3405 && Self::colors_equal(&glyph.color, &prev.color)
3406 && std::mem::discriminant(&glyph.text_decoration)
3407 == std::mem::discriminant(&prev.text_decoration);
3408
3409 if same_style {
3410 current_group.push(glyph);
3411 } else {
3412 groups.push(current_group);
3413 current_group = vec![glyph];
3414 }
3415 }
3416 groups.push(current_group);
3417 groups
3418 }
3419
3420 fn colors_equal(a: &Option<Color>, b: &Option<Color>) -> bool {
3421 match (a, b) {
3422 (None, None) => true,
3423 (Some(ca), Some(cb)) => {
3424 (ca.r - cb.r).abs() < 0.001
3425 && (ca.g - cb.g).abs() < 0.001
3426 && (ca.b - cb.b).abs() < 0.001
3427 && (ca.a - cb.a).abs() < 0.001
3428 }
3429 _ => false,
3430 }
3431 }
3432
3433 fn collect_link_annotations(
3437 elements: &[LayoutElement],
3438 page_height: f64,
3439 annotations: &mut Vec<LinkAnnotation>,
3440 ) {
3441 for element in elements {
3442 if let Some(ref href) = element.href {
3443 if !href.is_empty() {
3444 let pdf_y = page_height - element.y - element.height;
3445 annotations.push(LinkAnnotation {
3446 x: element.x,
3447 y: pdf_y,
3448 width: element.width,
3449 height: element.height,
3450 href: href.clone(),
3451 });
3452 continue;
3454 }
3455 }
3456 Self::collect_link_annotations(&element.children, page_height, annotations);
3457 }
3458 }
3459
3460 fn collect_form_fields(
3462 elements: &[LayoutElement],
3463 page_height: f64,
3464 page_idx: usize,
3465 fields: &mut Vec<FormFieldData>,
3466 ) {
3467 for element in elements {
3468 if let DrawCommand::FormField {
3469 ref field_type,
3470 ref name,
3471 } = element.draw
3472 {
3473 let pdf_y = page_height - element.y - element.height;
3474 fields.push(FormFieldData {
3475 field_type: field_type.clone(),
3476 name: name.clone(),
3477 x: element.x,
3478 y: pdf_y,
3479 width: element.width,
3480 height: element.height,
3481 page_idx,
3482 });
3483 }
3484 Self::collect_form_fields(&element.children, page_height, page_idx, fields);
3485 }
3486 }
3487
3488 fn collect_bookmarks(
3490 elements: &[LayoutElement],
3491 page_height: f64,
3492 page_obj_id: usize,
3493 bookmarks: &mut Vec<PdfBookmark>,
3494 ) {
3495 for element in elements {
3496 if let Some(ref title) = element.bookmark {
3497 let y_pdf = page_height - element.y;
3498 bookmarks.push(PdfBookmark {
3499 title: title.clone(),
3500 page_obj_id,
3501 y_pdf,
3502 });
3503 }
3504 Self::collect_bookmarks(&element.children, page_height, page_obj_id, bookmarks);
3505 }
3506 }
3507
3508 fn write_outline_tree(&self, builder: &mut PdfBuilder, bookmarks: &[PdfBookmark]) -> usize {
3511 let outlines_id = builder.objects.len();
3513 builder.objects.push(PdfObject {
3514 id: outlines_id,
3515 data: vec![],
3516 });
3517
3518 let mut item_ids: Vec<usize> = Vec::new();
3520 for _bm in bookmarks {
3521 let item_id = builder.objects.len();
3522 builder.objects.push(PdfObject {
3523 id: item_id,
3524 data: vec![],
3525 });
3526 item_ids.push(item_id);
3527 }
3528
3529 for (i, (bm, &item_id)) in bookmarks.iter().zip(item_ids.iter()).enumerate() {
3531 let mut dict = format!(
3532 "<< /Title ({}) /Parent {} 0 R /Dest [{} 0 R /XYZ 0 {:.2} null]",
3533 Self::escape_pdf_string(&bm.title),
3534 outlines_id,
3535 bm.page_obj_id,
3536 bm.y_pdf,
3537 );
3538 if i > 0 {
3539 let _ = write!(dict, " /Prev {} 0 R", item_ids[i - 1]);
3540 }
3541 if i + 1 < item_ids.len() {
3542 let _ = write!(dict, " /Next {} 0 R", item_ids[i + 1]);
3543 }
3544 dict.push_str(" >>");
3545 builder.objects[item_id].data = dict.into_bytes();
3546 }
3547
3548 let first_id = item_ids.first().copied().unwrap_or(0);
3550 let last_id = item_ids.last().copied().unwrap_or(0);
3551 let outlines_dict = format!(
3552 "<< /Type /Outlines /First {} 0 R /Last {} 0 R /Count {} >>",
3553 first_id,
3554 last_id,
3555 bookmarks.len()
3556 );
3557 builder.objects[outlines_id].data = outlines_dict.into_bytes();
3558
3559 outlines_id
3560 }
3561
3562 fn write_svg_commands(
3564 stream: &mut String,
3565 commands: &[SvgCommand],
3566 ext_gstate_map: &HashMap<u64, (usize, String)>,
3567 ) {
3568 for cmd in commands {
3569 match cmd {
3570 SvgCommand::MoveTo(x, y) => {
3571 let _ = writeln!(stream, "{:.2} {:.2} m", x, y);
3572 }
3573 SvgCommand::LineTo(x, y) => {
3574 let _ = writeln!(stream, "{:.2} {:.2} l", x, y);
3575 }
3576 SvgCommand::CurveTo(x1, y1, x2, y2, x3, y3) => {
3577 let _ = writeln!(
3578 stream,
3579 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3580 x1, y1, x2, y2, x3, y3
3581 );
3582 }
3583 SvgCommand::ClosePath => {
3584 let _ = writeln!(stream, "h");
3585 }
3586 SvgCommand::SetFill(r, g, b) => {
3587 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", r, g, b);
3588 }
3589 SvgCommand::SetFillNone => {
3590 }
3592 SvgCommand::SetStroke(r, g, b) => {
3593 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", r, g, b);
3594 }
3595 SvgCommand::SetStrokeNone => {
3596 }
3598 SvgCommand::SetStrokeWidth(w) => {
3599 let _ = writeln!(stream, "{:.2} w", w);
3600 }
3601 SvgCommand::Fill => {
3602 let _ = writeln!(stream, "f");
3603 }
3604 SvgCommand::Stroke => {
3605 let _ = writeln!(stream, "S");
3606 }
3607 SvgCommand::FillAndStroke => {
3608 let _ = writeln!(stream, "B");
3609 }
3610 SvgCommand::SetLineCap(cap) => {
3611 let _ = writeln!(stream, "{} J", cap);
3612 }
3613 SvgCommand::SetLineJoin(join) => {
3614 let _ = writeln!(stream, "{} j", join);
3615 }
3616 SvgCommand::SaveState => {
3617 let _ = writeln!(stream, "q");
3618 }
3619 SvgCommand::RestoreState => {
3620 let _ = writeln!(stream, "Q");
3621 }
3622 SvgCommand::SetOpacity(opacity) => {
3623 if let Some((_, gs_name)) = ext_gstate_map.get(&opacity.to_bits()) {
3624 let _ = writeln!(stream, "/{} gs", gs_name);
3625 }
3626 }
3627 }
3628 }
3629 }
3630
3631 pub(crate) fn escape_pdf_string(s: &str) -> String {
3633 s.replace('\\', "\\\\")
3634 .replace('(', "\\(")
3635 .replace(')', "\\)")
3636 }
3637
3638 fn encode_winansi_text(s: &str) -> String {
3641 let mut result = String::with_capacity(s.len());
3642 for ch in s.chars() {
3643 let b = Self::unicode_to_winansi(ch).unwrap_or(b'?');
3644 match b {
3645 b'\\' => result.push_str("\\\\"),
3646 b'(' => result.push_str("\\("),
3647 b')' => result.push_str("\\)"),
3648 0x20..=0x7E => result.push(b as char),
3649 _ => {
3650 let _ = write!(result, "\\{:03o}", b);
3651 }
3652 }
3653 }
3654 result
3655 }
3656
3657 fn unicode_to_winansi(ch: char) -> Option<u8> {
3659 crate::font::unicode_to_winansi(ch)
3660 }
3661
3662 fn serialize(&self, builder: &PdfBuilder, info_obj_id: Option<usize>) -> Vec<u8> {
3664 let mut output: Vec<u8> = Vec::new();
3665 let mut offsets: Vec<usize> = vec![0; builder.objects.len()];
3666
3667 output.extend_from_slice(b"%PDF-1.7\n");
3669 output.extend_from_slice(b"%\xe2\xe3\xcf\xd3\n");
3670
3671 for (i, obj) in builder.objects.iter().enumerate().skip(1) {
3672 offsets[i] = output.len();
3673 let header = format!("{} 0 obj\n", i);
3674 output.extend_from_slice(header.as_bytes());
3675 output.extend_from_slice(&obj.data);
3676 output.extend_from_slice(b"\nendobj\n\n");
3677 }
3678
3679 let xref_offset = output.len();
3680 let _ = writeln!(output, "xref\n0 {}", builder.objects.len());
3681 let _ = writeln!(output, "0000000000 65535 f ");
3682 for offset in offsets.iter().skip(1) {
3683 let _ = writeln!(output, "{:010} 00000 n ", offset);
3684 }
3685
3686 let _ = write!(
3687 output,
3688 "trailer\n<< /Size {} /Root 1 0 R",
3689 builder.objects.len()
3690 );
3691 if let Some(info_id) = info_obj_id {
3692 let _ = write!(output, " /Info {} 0 R", info_id);
3693 }
3694 let _ = writeln!(output, " >>\nstartxref\n{}\n%%EOF", xref_offset);
3695
3696 output
3697 }
3698}
3699
3700fn write_chart_primitive(
3705 stream: &mut String,
3706 prim: &crate::chart::ChartPrimitive,
3707 _chart_height: f64,
3708 builder: &PdfBuilder,
3709) {
3710 use crate::chart::{ChartPrimitive, TextAnchor};
3711 use crate::font::metrics::unicode_to_winansi;
3712
3713 match prim {
3714 ChartPrimitive::Rect { x, y, w, h, fill } => {
3715 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3716 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re f", x, y, w, h);
3717 }
3718
3719 ChartPrimitive::Line {
3720 x1,
3721 y1,
3722 x2,
3723 y2,
3724 stroke,
3725 width,
3726 } => {
3727 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
3728 let _ = writeln!(stream, "{:.2} w", width);
3729 let _ = writeln!(stream, "{:.2} {:.2} m {:.2} {:.2} l S", x1, y1, x2, y2);
3730 }
3731
3732 ChartPrimitive::Polyline {
3733 points,
3734 stroke,
3735 width,
3736 } => {
3737 if points.len() < 2 {
3738 return;
3739 }
3740 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
3741 let _ = writeln!(stream, "{:.2} w", width);
3742 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
3743 for &(px, py) in &points[1..] {
3744 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
3745 }
3746 let _ = writeln!(stream, "S");
3747 }
3748
3749 ChartPrimitive::FilledPath {
3750 points,
3751 fill,
3752 opacity,
3753 } => {
3754 if points.len() < 3 {
3755 return;
3756 }
3757 let _ = writeln!(stream, "q");
3758 if *opacity < 1.0 {
3760 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
3761 let _ = writeln!(stream, "/{} gs", gs_name);
3762 }
3763 }
3764 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3765 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
3766 for &(px, py) in &points[1..] {
3767 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
3768 }
3769 let _ = writeln!(stream, "h f");
3770 let _ = writeln!(stream, "Q");
3771 }
3772
3773 ChartPrimitive::Circle { cx, cy, r, fill } => {
3774 let kappa: f64 = 0.5523;
3776 let kr = kappa * r;
3777 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3778 let _ = writeln!(stream, "{:.2} {:.2} m", cx + r, cy);
3779 let _ = writeln!(
3780 stream,
3781 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3782 cx + r,
3783 cy + kr,
3784 cx + kr,
3785 cy + r,
3786 cx,
3787 cy + r
3788 );
3789 let _ = writeln!(
3790 stream,
3791 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3792 cx - kr,
3793 cy + r,
3794 cx - r,
3795 cy + kr,
3796 cx - r,
3797 cy
3798 );
3799 let _ = writeln!(
3800 stream,
3801 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3802 cx - r,
3803 cy - kr,
3804 cx - kr,
3805 cy - r,
3806 cx,
3807 cy - r
3808 );
3809 let _ = writeln!(
3810 stream,
3811 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3812 cx + kr,
3813 cy - r,
3814 cx + r,
3815 cy - kr,
3816 cx + r,
3817 cy
3818 );
3819 let _ = writeln!(stream, "f");
3820 }
3821
3822 ChartPrimitive::ArcSector {
3823 cx,
3824 cy,
3825 r,
3826 start_angle,
3827 end_angle,
3828 fill,
3829 } => {
3830 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3831 let _ = writeln!(stream, "{:.2} {:.2} m", cx, cy);
3833 let sx = cx + r * start_angle.cos();
3835 let sy = cy + r * start_angle.sin();
3836 let _ = writeln!(stream, "{:.2} {:.2} l", sx, sy);
3837
3838 let mut angle = *start_angle;
3840 let total = end_angle - start_angle;
3841 let segments = ((total.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
3842 let step = total / segments as f64;
3843
3844 for _ in 0..segments {
3845 let a1 = angle;
3846 let a2 = angle + step;
3847 let alpha = 4.0 / 3.0 * ((a2 - a1) / 4.0).tan();
3848
3849 let p1x = cx + r * a1.cos();
3850 let p1y = cy + r * a1.sin();
3851 let p2x = cx + r * a2.cos();
3852 let p2y = cy + r * a2.sin();
3853
3854 let cp1x = p1x - alpha * r * a1.sin();
3855 let cp1y = p1y + alpha * r * a1.cos();
3856 let cp2x = p2x + alpha * r * a2.sin();
3857 let cp2y = p2y - alpha * r * a2.cos();
3858
3859 let _ = writeln!(
3860 stream,
3861 "{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c",
3862 cp1x, cp1y, cp2x, cp2y, p2x, p2y
3863 );
3864 angle = a2;
3865 }
3866
3867 let _ = writeln!(stream, "h f");
3869 }
3870
3871 ChartPrimitive::Label {
3872 text,
3873 x,
3874 y,
3875 font_size,
3876 color,
3877 anchor,
3878 } => {
3879 let metrics = crate::font::StandardFont::Helvetica.metrics();
3881 let text_width = metrics.measure_string(text, *font_size, 0.0);
3882 let x_offset = match anchor {
3883 TextAnchor::Left => 0.0,
3884 TextAnchor::Center => -text_width / 2.0,
3885 TextAnchor::Right => -text_width,
3886 };
3887
3888 let font_idx = builder
3890 .font_objects
3891 .iter()
3892 .enumerate()
3893 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
3894 .map(|(i, _)| i)
3895 .unwrap_or(0);
3896
3897 let encoded: String = text
3899 .chars()
3900 .map(|ch| {
3901 if let Some(code) = unicode_to_winansi(ch) {
3902 code as char
3903 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
3904 ch
3905 } else {
3906 '?'
3907 }
3908 })
3909 .collect();
3910 let escaped = pdf_escape_string(&encoded);
3911
3912 let _ = writeln!(stream, "q");
3914 let _ = writeln!(stream, "1 0 0 -1 {:.4} {:.4} cm", x + x_offset, *y);
3915 let _ = writeln!(
3916 stream,
3917 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
3918 font_idx, font_size, color.r, color.g, color.b, escaped
3919 );
3920 let _ = writeln!(stream, "Q");
3921 }
3922 }
3923}
3924
3925fn normalize_gradient_stops(
3932 stops: &[crate::style::GradientStop],
3933 fallback: Color,
3934) -> Vec<crate::style::GradientStop> {
3935 use crate::style::GradientStop;
3936 if stops.is_empty() {
3937 return vec![
3938 GradientStop {
3939 position: 0.0,
3940 color: fallback,
3941 },
3942 GradientStop {
3943 position: 1.0,
3944 color: fallback,
3945 },
3946 ];
3947 }
3948 let mut sorted: Vec<GradientStop> = stops
3949 .iter()
3950 .map(|s| GradientStop {
3951 position: s.position.clamp(0.0, 1.0),
3952 color: s.color,
3953 })
3954 .collect();
3955 sorted.sort_by(|a, b| {
3956 a.position
3957 .partial_cmp(&b.position)
3958 .unwrap_or(std::cmp::Ordering::Equal)
3959 });
3960 if sorted[0].position > 0.0 {
3961 sorted.insert(
3962 0,
3963 GradientStop {
3964 position: 0.0,
3965 color: sorted[0].color,
3966 },
3967 );
3968 }
3969 if sorted[sorted.len() - 1].position < 1.0 {
3970 let last = sorted[sorted.len() - 1].color;
3971 sorted.push(GradientStop {
3972 position: 1.0,
3973 color: last,
3974 });
3975 }
3976 sorted
3977}
3978
3979fn pdf_escape_string(s: &str) -> String {
3980 let mut out = String::with_capacity(s.len());
3981 for ch in s.chars() {
3982 match ch {
3983 '(' => out.push_str("\\("),
3984 ')' => out.push_str("\\)"),
3985 '\\' => out.push_str("\\\\"),
3986 _ => out.push(ch),
3987 }
3988 }
3989 out
3990}
3991
3992#[cfg(test)]
3993mod tests {
3994 use super::*;
3995 use crate::font::FontContext;
3996
3997 #[test]
3998 fn test_escape_pdf_string() {
3999 assert_eq!(
4000 PdfWriter::escape_pdf_string("Hello (World)"),
4001 "Hello \\(World\\)"
4002 );
4003 assert_eq!(PdfWriter::escape_pdf_string("back\\slash"), "back\\\\slash");
4004 }
4005
4006 #[test]
4007 fn test_empty_document_produces_valid_pdf() {
4008 let writer = PdfWriter::new();
4009 let font_context = FontContext::new();
4010 let pages = vec![LayoutPage {
4011 width: 595.28,
4012 height: 841.89,
4013 elements: vec![],
4014 fixed_header: vec![],
4015 fixed_footer: vec![],
4016 watermarks: vec![],
4017 config: PageConfig::default(),
4018 }];
4019 let metadata = Metadata::default();
4020 let bytes = writer
4021 .write(
4022 &pages,
4023 &metadata,
4024 &font_context,
4025 false,
4026 None,
4027 false,
4028 None,
4029 false,
4030 )
4031 .unwrap();
4032
4033 assert!(bytes.starts_with(b"%PDF-1.7"));
4034 assert!(bytes.windows(5).any(|w| w == b"%%EOF"));
4035 assert!(bytes.windows(4).any(|w| w == b"xref"));
4036 assert!(bytes.windows(7).any(|w| w == b"trailer"));
4037 }
4038
4039 #[test]
4040 fn test_metadata_in_pdf() {
4041 let writer = PdfWriter::new();
4042 let font_context = FontContext::new();
4043 let pages = vec![LayoutPage {
4044 width: 595.28,
4045 height: 841.89,
4046 elements: vec![],
4047 fixed_header: vec![],
4048 fixed_footer: vec![],
4049 watermarks: vec![],
4050 config: PageConfig::default(),
4051 }];
4052 let metadata = Metadata {
4053 title: Some("Test Document".to_string()),
4054 author: Some("Forme".to_string()),
4055 subject: None,
4056 creator: None,
4057 lang: None,
4058 };
4059 let bytes = writer
4060 .write(
4061 &pages,
4062 &metadata,
4063 &font_context,
4064 false,
4065 None,
4066 false,
4067 None,
4068 false,
4069 )
4070 .unwrap();
4071 let text = String::from_utf8_lossy(&bytes);
4072
4073 assert!(text.contains("/Title (Test Document)"));
4074 assert!(text.contains("/Author (Forme)"));
4075 }
4076
4077 #[test]
4078 fn test_bold_font_registered_separately() {
4079 let writer = PdfWriter::new();
4080 let font_context = FontContext::new();
4081
4082 let pages = vec![LayoutPage {
4084 width: 595.28,
4085 height: 841.89,
4086 elements: vec![
4087 LayoutElement {
4088 x: 54.0,
4089 y: 54.0,
4090 width: 100.0,
4091 height: 16.8,
4092 draw: DrawCommand::Text {
4093 lines: vec![TextLine {
4094 x: 54.0,
4095 y: 66.0,
4096 width: 50.0,
4097 height: 16.8,
4098 glyphs: vec![PositionedGlyph {
4099 glyph_id: 65,
4100 x_offset: 0.0,
4101 y_offset: 0.0,
4102 x_advance: 8.0,
4103 font_size: 12.0,
4104 font_family: "Helvetica".to_string(),
4105 font_weight: 400,
4106 font_style: FontStyle::Normal,
4107 char_value: 'A',
4108 color: None,
4109 href: None,
4110 text_decoration: TextDecoration::None,
4111 letter_spacing: 0.0,
4112 cluster_text: None,
4113 }],
4114 word_spacing: 0.0,
4115 }],
4116 color: Color::BLACK,
4117 text_decoration: TextDecoration::None,
4118 opacity: 1.0,
4119 },
4120 children: vec![],
4121 node_type: None,
4122 resolved_style: None,
4123 source_location: None,
4124 href: None,
4125 bookmark: None,
4126 alt: None,
4127 is_header_row: false,
4128 overflow: Overflow::default(),
4129 opacity: 1.0,
4130 },
4131 LayoutElement {
4132 x: 54.0,
4133 y: 74.0,
4134 width: 100.0,
4135 height: 16.8,
4136 draw: DrawCommand::Text {
4137 lines: vec![TextLine {
4138 x: 54.0,
4139 y: 86.0,
4140 width: 50.0,
4141 height: 16.8,
4142 glyphs: vec![PositionedGlyph {
4143 glyph_id: 65,
4144 x_offset: 0.0,
4145 y_offset: 0.0,
4146 x_advance: 8.0,
4147 font_size: 12.0,
4148 font_family: "Helvetica".to_string(),
4149 font_weight: 700,
4150 font_style: FontStyle::Normal,
4151 char_value: 'A',
4152 color: None,
4153 href: None,
4154 text_decoration: TextDecoration::None,
4155 letter_spacing: 0.0,
4156 cluster_text: None,
4157 }],
4158 word_spacing: 0.0,
4159 }],
4160 color: Color::BLACK,
4161 text_decoration: TextDecoration::None,
4162 opacity: 1.0,
4163 },
4164 children: vec![],
4165 node_type: None,
4166 resolved_style: None,
4167 source_location: None,
4168 href: None,
4169 bookmark: None,
4170 alt: None,
4171 is_header_row: false,
4172 overflow: Overflow::default(),
4173 opacity: 1.0,
4174 },
4175 ],
4176 fixed_header: vec![],
4177 fixed_footer: vec![],
4178 watermarks: vec![],
4179 config: PageConfig::default(),
4180 }];
4181
4182 let metadata = Metadata::default();
4183 let bytes = writer
4184 .write(
4185 &pages,
4186 &metadata,
4187 &font_context,
4188 false,
4189 None,
4190 false,
4191 None,
4192 false,
4193 )
4194 .unwrap();
4195 let text = String::from_utf8_lossy(&bytes);
4196
4197 assert!(
4199 text.contains("Helvetica"),
4200 "Should contain regular Helvetica"
4201 );
4202 assert!(
4203 text.contains("Helvetica-Bold"),
4204 "Should contain Helvetica-Bold"
4205 );
4206 }
4207
4208 #[test]
4209 fn test_sanitize_font_name() {
4210 assert_eq!(PdfWriter::sanitize_font_name("Inter", 400, false), "Inter");
4211 assert_eq!(
4212 PdfWriter::sanitize_font_name("Inter", 700, false),
4213 "Inter-Bold"
4214 );
4215 assert_eq!(
4216 PdfWriter::sanitize_font_name("Inter", 400, true),
4217 "Inter-Italic"
4218 );
4219 assert_eq!(
4220 PdfWriter::sanitize_font_name("Inter", 700, true),
4221 "Inter-Bold-Italic"
4222 );
4223 assert_eq!(
4224 PdfWriter::sanitize_font_name("Noto Sans", 400, false),
4225 "NotoSans"
4226 );
4227 assert_eq!(
4228 PdfWriter::sanitize_font_name("Font (Display)", 400, false),
4229 "FontDisplay"
4230 );
4231 }
4232
4233 #[test]
4234 fn test_tounicode_cmap_format() {
4235 let mut glyph_to_char = HashMap::new();
4237 glyph_to_char.insert(36u16, 'A');
4238 glyph_to_char.insert(37u16, 'B');
4239
4240 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&glyph_to_char, "TestFont");
4241
4242 assert!(cmap.contains("begincmap"), "CMap should contain begincmap");
4243 assert!(cmap.contains("endcmap"), "CMap should contain endcmap");
4244 assert!(
4245 cmap.contains("beginbfchar"),
4246 "CMap should contain beginbfchar"
4247 );
4248 assert!(cmap.contains("endbfchar"), "CMap should contain endbfchar");
4249 assert!(
4250 cmap.contains("<0024> <0041>"),
4251 "Should map gid 0x0024 to Unicode 'A' 0x0041"
4252 );
4253 assert!(
4254 cmap.contains("<0025> <0042>"),
4255 "Should map gid 0x0025 to Unicode 'B' 0x0042"
4256 );
4257 assert!(
4258 cmap.contains("begincodespacerange"),
4259 "Should define codespace range"
4260 );
4261 assert!(
4262 cmap.contains("<0000> <FFFF>"),
4263 "Codespace should be 0000-FFFF"
4264 );
4265 }
4266
4267 #[test]
4268 fn test_w_array_format() {
4269 let mut char_to_gid = HashMap::new();
4270 char_to_gid.insert('A', 36u16);
4271
4272 let w_array_str = "[ 36 [600] ]";
4275 assert!(w_array_str.starts_with('['));
4276 assert!(w_array_str.ends_with(']'));
4277 }
4278
4279 #[test]
4280 fn test_hex_glyph_encoding() {
4281 let gid: u16 = 0x0041;
4283 let hex = format!("{:04X}", gid);
4284 assert_eq!(hex, "0041");
4285
4286 let gids = [0x0041u16, 0x0042, 0x0043];
4287 let hex_str: String = gids.iter().map(|g| format!("{:04X}", g)).collect();
4288 assert_eq!(hex_str, "004100420043");
4289 }
4290
4291 #[test]
4292 fn test_standard_font_still_uses_text_string() {
4293 let writer = PdfWriter::new();
4294 let font_context = FontContext::new();
4295
4296 let pages = vec![LayoutPage {
4297 width: 595.28,
4298 height: 841.89,
4299 elements: vec![LayoutElement {
4300 x: 54.0,
4301 y: 54.0,
4302 width: 100.0,
4303 height: 16.8,
4304 draw: DrawCommand::Text {
4305 lines: vec![TextLine {
4306 x: 54.0,
4307 y: 66.0,
4308 width: 50.0,
4309 height: 16.8,
4310 glyphs: vec![PositionedGlyph {
4311 glyph_id: 65,
4312 x_offset: 0.0,
4313 y_offset: 0.0,
4314 x_advance: 8.0,
4315 font_size: 12.0,
4316 font_family: "Helvetica".to_string(),
4317 font_weight: 400,
4318 font_style: FontStyle::Normal,
4319 char_value: 'H',
4320 color: None,
4321 href: None,
4322 text_decoration: TextDecoration::None,
4323 letter_spacing: 0.0,
4324 cluster_text: None,
4325 }],
4326 word_spacing: 0.0,
4327 }],
4328 color: Color::BLACK,
4329 text_decoration: TextDecoration::None,
4330 opacity: 1.0,
4331 },
4332 children: vec![],
4333 node_type: None,
4334 resolved_style: None,
4335 source_location: None,
4336 href: None,
4337 bookmark: None,
4338 alt: None,
4339 is_header_row: false,
4340 overflow: Overflow::default(),
4341 opacity: 1.0,
4342 }],
4343 fixed_header: vec![],
4344 fixed_footer: vec![],
4345 watermarks: vec![],
4346 config: PageConfig::default(),
4347 }];
4348
4349 let metadata = Metadata::default();
4350 let bytes = writer
4351 .write(
4352 &pages,
4353 &metadata,
4354 &font_context,
4355 false,
4356 None,
4357 false,
4358 None,
4359 false,
4360 )
4361 .unwrap();
4362 let text = String::from_utf8_lossy(&bytes);
4363
4364 assert!(
4366 text.contains("/Type1"),
4367 "Standard font should use Type1 subtype"
4368 );
4369 assert!(
4370 !text.contains("CIDFontType2"),
4371 "Standard font should not use CIDFontType2"
4372 );
4373 }
4374}