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 viewbox_min_x,
1628 viewbox_min_y,
1629 viewbox_width,
1630 viewbox_height,
1631 clip,
1632 } => {
1633 let x = element.x;
1634 let y = page_height - element.y - element.height;
1635
1636 let _ = writeln!(stream, "q");
1638 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", x, y);
1639
1640 if *viewbox_width > 0.0 && *viewbox_height > 0.0 {
1646 let raw_sx = element.width / *viewbox_width;
1647 let raw_sy = element.height / *viewbox_height;
1648 let s = raw_sx.min(raw_sy);
1649 let tx = (element.width - s * *viewbox_width) / 2.0;
1650 let ty = (element.height - s * *viewbox_height) / 2.0;
1651 let _ = writeln!(stream, "{:.4} 0 0 {:.4} {:.2} {:.2} cm", s, s, tx, ty);
1652 }
1653
1654 let _ = writeln!(stream, "1 0 0 -1 0 {:.2} cm", *viewbox_height);
1657
1658 if *viewbox_min_x != 0.0 || *viewbox_min_y != 0.0 {
1660 let _ = writeln!(
1661 stream,
1662 "1 0 0 1 {:.2} {:.2} cm",
1663 -*viewbox_min_x, -*viewbox_min_y
1664 );
1665 }
1666
1667 if *clip {
1669 let _ = writeln!(
1670 stream,
1671 "{:.2} {:.2} {:.2} {:.2} re W n",
1672 *viewbox_min_x, *viewbox_min_y, *viewbox_width, *viewbox_height
1673 );
1674 }
1675
1676 Self::write_svg_commands(stream, commands, &builder.ext_gstate_map);
1677
1678 let _ = writeln!(stream, "Q");
1679 if tagged_mcid.is_some() {
1680 let _ = writeln!(stream, "EMC");
1681 if let Some(ref mut tb) = tag_builder {
1682 tb.end_element();
1683 }
1684 } else if is_artifact {
1685 let _ = writeln!(stream, "EMC");
1686 }
1687 return;
1688 }
1689
1690 DrawCommand::Barcode {
1691 bars,
1692 bar_width,
1693 height,
1694 color,
1695 } => {
1696 *element_counter += 1;
1697 let _ = writeln!(stream, "q");
1698 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1699 for (i, &bar) in bars.iter().enumerate() {
1700 if bar == 1 {
1701 let bx = element.x + i as f64 * bar_width;
1702 let by = page_height - element.y - height;
1703 let _ = writeln!(
1704 stream,
1705 "{:.2} {:.2} {:.2} {:.2} re",
1706 bx, by, bar_width, height
1707 );
1708 }
1709 }
1710 let _ = writeln!(stream, "f\nQ");
1711 if tagged_mcid.is_some() {
1712 let _ = writeln!(stream, "EMC");
1713 if let Some(ref mut tb) = tag_builder {
1714 tb.end_element();
1715 }
1716 } else if is_artifact {
1717 let _ = writeln!(stream, "EMC");
1718 }
1719 return;
1720 }
1721
1722 DrawCommand::QrCode {
1723 modules,
1724 module_size,
1725 color,
1726 } => {
1727 *element_counter += 1;
1728 let _ = writeln!(stream, "q");
1729 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1730 for (row_idx, row) in modules.iter().enumerate() {
1731 for (col_idx, &dark) in row.iter().enumerate() {
1732 if dark {
1733 let mx = element.x + col_idx as f64 * module_size;
1734 let my = page_height - element.y - (row_idx as f64 + 1.0) * module_size;
1735 let _ = writeln!(
1736 stream,
1737 "{:.2} {:.2} {:.2} {:.2} re",
1738 mx, my, module_size, module_size
1739 );
1740 }
1741 }
1742 }
1743 let _ = writeln!(stream, "f\nQ");
1744 if tagged_mcid.is_some() {
1745 let _ = writeln!(stream, "EMC");
1746 if let Some(ref mut tb) = tag_builder {
1747 tb.end_element();
1748 }
1749 } else if is_artifact {
1750 let _ = writeln!(stream, "EMC");
1751 }
1752 return;
1753 }
1754
1755 DrawCommand::Chart { primitives } => {
1756 *element_counter += 1;
1757 let _ = writeln!(stream, "q");
1758 let _ = writeln!(
1760 stream,
1761 "1 0 0 -1 {:.4} {:.4} cm",
1762 element.x,
1763 page_height - element.y
1764 );
1765
1766 for prim in primitives {
1767 write_chart_primitive(stream, prim, element.height, builder);
1768 }
1769
1770 let _ = writeln!(stream, "Q");
1771 if tagged_mcid.is_some() {
1772 let _ = writeln!(stream, "EMC");
1773 if let Some(ref mut tb) = tag_builder {
1774 tb.end_element();
1775 }
1776 } else if is_artifact {
1777 let _ = writeln!(stream, "EMC");
1778 }
1779 return;
1780 }
1781
1782 DrawCommand::Watermark {
1783 lines,
1784 color,
1785 opacity,
1786 angle_rad,
1787 font_family: _,
1788 } => {
1789 let _ = writeln!(stream, "q");
1790 if *opacity < 1.0 {
1792 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1793 let _ = writeln!(stream, "/{} gs", gs_name);
1794 }
1795 }
1796 let pdf_cx = element.x;
1798 let pdf_cy = page_height - element.y;
1799 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", pdf_cx, pdf_cy);
1800 let cos_a = angle_rad.cos();
1802 let sin_a = angle_rad.sin();
1803 let _ = writeln!(
1804 stream,
1805 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
1806 cos_a, sin_a, -sin_a, cos_a
1807 );
1808 let _ = writeln!(stream, "BT");
1810 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1811 if let Some(line) = lines.first() {
1812 let groups = Self::group_glyphs_by_style(&line.glyphs);
1813 let text_width = line.width;
1814 let cap_height = line.height * 0.7;
1815 let _ = writeln!(
1816 stream,
1817 "{:.2} {:.2} Td",
1818 -text_width / 2.0,
1819 -cap_height / 2.0
1820 );
1821 for group in &groups {
1822 let first = &group[0];
1823 let italic =
1824 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
1825 let fk = FontKey {
1826 family: first.font_family.clone(),
1827 weight: first.font_weight,
1828 italic,
1829 };
1830 let idx = self.font_index(
1831 &first.font_family,
1832 first.font_weight,
1833 first.font_style,
1834 &builder.font_objects,
1835 );
1836 let font_name = format!("F{}", idx);
1837 let _ = writeln!(stream, "/{} {:.1} Tf", font_name, first.font_size);
1838 let is_custom = builder.custom_font_data.contains_key(&fk);
1839 if is_custom {
1840 if let Some(embed_data) = builder.custom_font_data.get(&fk) {
1841 let mut hex = String::new();
1842 for g in group.iter() {
1843 let gid =
1844 embed_data.gid_remap.get(&g.glyph_id).copied().unwrap_or(0);
1845 let _ = write!(hex, "{:04X}", gid);
1846 }
1847 let _ = writeln!(stream, "<{}> Tj", hex);
1848 }
1849 } else {
1850 let hex_str: String = group
1851 .iter()
1852 .map(|g| format!("{:02X}", g.glyph_id as u8))
1853 .collect();
1854 let _ = writeln!(stream, "<{}> Tj", hex_str);
1855 }
1856 }
1857 }
1858 let _ = writeln!(stream, "ET");
1859 let _ = writeln!(stream, "Q");
1860 if tagged_mcid.is_some() {
1861 let _ = writeln!(stream, "EMC");
1862 if let Some(ref mut tb) = tag_builder {
1863 tb.end_element();
1864 }
1865 } else if is_artifact {
1866 let _ = writeln!(stream, "EMC");
1867 }
1868 return;
1869 }
1870
1871 DrawCommand::FormField { field_type, .. } => {
1872 let pdf_x = element.x;
1876 let pdf_y = page_height - element.y - element.height;
1877 let w = element.width;
1878 let h = element.height;
1879 let _ = writeln!(stream, "q");
1880 match field_type {
1881 FormFieldType::Checkbox { checked, .. } => {
1882 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
1885 let _ =
1886 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
1887 if *checked {
1888 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
1890 let sx = w / 14.0;
1891 let sy = h / 14.0;
1892 let _ = writeln!(
1893 stream,
1894 "{:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l f",
1895 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
1896 pdf_x + 5.5 * sx, pdf_y + 2.0 * sy,
1897 pdf_x + 12.0 * sx, pdf_y + 11.0 * sy,
1898 pdf_x + 11.0 * sx, pdf_y + 12.0 * sy,
1899 pdf_x + 5.5 * sx, pdf_y + 4.5 * sy,
1900 pdf_x + 3.0 * sx, pdf_y + 7.0 * sy,
1901 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
1902 );
1903 }
1904 }
1905 FormFieldType::RadioButton { checked, .. } => {
1906 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
1909 let _ =
1910 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
1911 if *checked {
1912 let cx = pdf_x + w / 2.0;
1914 let cy = pdf_y + h / 2.0;
1915 let r = (w.min(h) / 2.0) * 0.6;
1916 let k = r * 0.5523;
1917 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
1918 let _ = writeln!(
1919 stream,
1920 "{:.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",
1921 cx, cy + r,
1922 cx + k, cy + r, cx + r, cy + k, cx + r, cy,
1923 cx + r, cy - k, cx + k, cy - r, cx, cy - r,
1924 cx - k, cy - r, cx - r, cy - k, cx - r, cy,
1925 cx - r, cy + k, cx - k, cy + r, cx, cy + r,
1926 );
1927 }
1928 }
1929 FormFieldType::TextField {
1930 value,
1931 placeholder,
1932 font_size,
1933 multiline,
1934 password,
1935 ..
1936 } => {
1937 let _ = writeln!(stream, "1 1 1 rg");
1939 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
1940 let _ = writeln!(stream, "0.5 w");
1941 let _ =
1942 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
1943 if flatten_forms {
1945 let has_value = value.as_ref().is_some_and(|v| !v.is_empty());
1946 if has_value {
1947 let val = value.as_ref().unwrap();
1948 let display_text = if *password {
1949 "\u{2022}".repeat(val.len())
1950 } else {
1951 val.clone()
1952 };
1953 let font_idx = builder
1954 .font_objects
1955 .iter()
1956 .enumerate()
1957 .find(|(_, (key, _))| {
1958 key.family == "Helvetica"
1959 && key.weight == 400
1960 && !key.italic
1961 })
1962 .map(|(i, _)| i)
1963 .unwrap_or(0);
1964 if *multiline {
1965 let metrics = crate::font::StandardFont::Helvetica.metrics();
1967 let max_w = w - 4.0;
1968 let mut lines: Vec<String> = Vec::new();
1969 for paragraph in display_text.split('\n') {
1970 let mut line = String::new();
1971 let mut line_w = 0.0;
1972 for word in paragraph.split_whitespace() {
1973 let word_w =
1974 metrics.measure_string(word, *font_size, 0.0);
1975 let space_w = if line.is_empty() {
1976 0.0
1977 } else {
1978 metrics.measure_string(" ", *font_size, 0.0)
1979 };
1980 if word_w > max_w {
1982 let mut char_line = String::new();
1983 let mut char_w = 0.0;
1984 for ch in word.chars() {
1985 let cw = metrics.char_width(ch, *font_size);
1986 if !char_line.is_empty() && char_w + cw > max_w
1987 {
1988 if !line.is_empty() {
1989 lines.push(line.clone());
1990 line.clear();
1991 line_w = 0.0;
1992 }
1993 lines.push(char_line.clone());
1994 char_line.clear();
1995 char_w = 0.0;
1996 }
1997 char_line.push(ch);
1998 char_w += cw;
1999 }
2000 if !char_line.is_empty() {
2002 if !line.is_empty() {
2003 line.push(' ');
2004 line_w += metrics
2005 .measure_string(" ", *font_size, 0.0);
2006 }
2007 line.push_str(&char_line);
2008 line_w += char_w;
2009 }
2010 continue;
2011 }
2012 if !line.is_empty() && line_w + space_w + word_w > max_w
2013 {
2014 lines.push(line.clone());
2015 line.clear();
2016 line_w = 0.0;
2017 }
2018 if !line.is_empty() {
2019 line.push(' ');
2020 line_w += space_w;
2021 }
2022 line.push_str(word);
2023 line_w += word_w;
2024 }
2025 if !line.is_empty() {
2026 lines.push(line);
2027 }
2028 }
2029 let text_y = pdf_y + h - font_size - 2.0;
2030 for (i, line_text) in lines.iter().enumerate() {
2031 let ly = text_y - (i as f64) * (font_size * 1.2);
2032 if ly < pdf_y {
2033 break;
2034 }
2035 let esc = Self::encode_winansi_text(line_text);
2036 let _ = writeln!(
2037 stream,
2038 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2039 font_idx,
2040 font_size,
2041 pdf_x + 2.0,
2042 ly,
2043 esc
2044 );
2045 }
2046 } else {
2047 let escaped = Self::encode_winansi_text(&display_text);
2048 let text_y = pdf_y + (h - font_size) / 2.0;
2049 let _ = writeln!(
2050 stream,
2051 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2052 font_idx,
2053 font_size,
2054 pdf_x + 2.0,
2055 text_y,
2056 escaped
2057 );
2058 }
2059 } else if let Some(ref ph) = placeholder {
2060 if !ph.is_empty() {
2061 let font_idx = builder
2063 .font_objects
2064 .iter()
2065 .enumerate()
2066 .find(|(_, (key, _))| {
2067 key.family == "Helvetica"
2068 && key.weight == 400
2069 && !key.italic
2070 })
2071 .map(|(i, _)| i)
2072 .unwrap_or(0);
2073 let escaped = Self::encode_winansi_text(ph);
2074 let text_y = pdf_y + (h - font_size) / 2.0;
2075 let _ = writeln!(
2076 stream,
2077 "BT /F{} {:.1} Tf 0.6 g {:.2} {:.2} Td ({}) Tj ET",
2078 font_idx,
2079 font_size,
2080 pdf_x + 2.0,
2081 text_y,
2082 escaped
2083 );
2084 }
2085 }
2086 }
2087 }
2088 FormFieldType::Dropdown {
2089 value, font_size, ..
2090 } => {
2091 let _ = writeln!(stream, "1 1 1 rg");
2093 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2094 let _ = writeln!(stream, "0.5 w");
2095 let _ =
2096 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2097 if flatten_forms {
2099 if let Some(ref val) = value {
2100 if !val.is_empty() {
2101 let font_idx = builder
2102 .font_objects
2103 .iter()
2104 .enumerate()
2105 .find(|(_, (key, _))| {
2106 key.family == "Helvetica"
2107 && key.weight == 400
2108 && !key.italic
2109 })
2110 .map(|(i, _)| i)
2111 .unwrap_or(0);
2112 let escaped = Self::encode_winansi_text(val);
2113 let text_y = pdf_y + (h - font_size) / 2.0;
2114 let _ = writeln!(
2115 stream,
2116 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2117 font_idx,
2118 font_size,
2119 pdf_x + 2.0,
2120 text_y,
2121 escaped
2122 );
2123 }
2124 }
2125 }
2126 }
2127 }
2128 let _ = writeln!(stream, "Q");
2129 }
2130 }
2131
2132 let clip_overflow = matches!(element.overflow, Overflow::Hidden);
2137 if clip_overflow {
2138 let clip_x = element.x;
2139 let clip_y = page_height - element.y - element.height;
2140 let clip_w = element.width;
2141 let clip_h = element.height;
2142 let radius = if let DrawCommand::Rect { border_radius, .. } = &element.draw {
2146 Some(border_radius)
2147 } else {
2148 None
2149 };
2150 let has_rounded_corners = radius.is_some_and(|r| {
2151 r.top_left > 0.0 || r.top_right > 0.0 || r.bottom_right > 0.0 || r.bottom_left > 0.0
2152 });
2153 let _ = writeln!(stream, "q");
2154 if has_rounded_corners {
2155 self.write_rounded_rect(stream, clip_x, clip_y, clip_w, clip_h, radius.unwrap());
2156 let _ = writeln!(stream, "W n");
2157 } else {
2158 let _ = writeln!(
2159 stream,
2160 "{:.2} {:.2} {:.2} {:.2} re W n",
2161 clip_x, clip_y, clip_w, clip_h
2162 );
2163 }
2164 }
2165
2166 for child in &element.children {
2167 self.write_element(
2168 stream,
2169 child,
2170 page_height,
2171 builder,
2172 page_idx,
2173 element_counter,
2174 gradient_counter,
2175 page_number,
2176 total_pages,
2177 tag_builder.as_deref_mut(),
2178 flatten_forms,
2179 );
2180 }
2181
2182 if clip_overflow {
2183 let _ = writeln!(stream, "Q");
2184 }
2185
2186 if needs_element_opacity {
2189 let _ = writeln!(stream, "Q");
2190 }
2191
2192 if tagged_mcid.is_some() {
2194 let _ = writeln!(stream, "EMC");
2195 if let Some(ref mut tb) = tag_builder {
2196 tb.end_element();
2197 }
2198 } else if is_artifact {
2199 let _ = writeln!(stream, "EMC");
2200 }
2201 }
2202
2203 fn write_rounded_rect(
2204 &self,
2205 stream: &mut String,
2206 x: f64,
2207 y: f64,
2208 w: f64,
2209 h: f64,
2210 r: &crate::style::CornerValues,
2211 ) {
2212 let k = 0.5522847498;
2213
2214 let tl = r.top_left.min(w / 2.0).min(h / 2.0);
2215 let tr = r.top_right.min(w / 2.0).min(h / 2.0);
2216 let br = r.bottom_right.min(w / 2.0).min(h / 2.0);
2217 let bl = r.bottom_left.min(w / 2.0).min(h / 2.0);
2218
2219 let _ = writeln!(stream, "{:.2} {:.2} m", x + bl, y);
2220
2221 let _ = writeln!(stream, "{:.2} {:.2} l", x + w - br, y);
2222 if br > 0.0 {
2223 let _ = writeln!(
2224 stream,
2225 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2226 x + w - br + br * k,
2227 y,
2228 x + w,
2229 y + br - br * k,
2230 x + w,
2231 y + br
2232 );
2233 }
2234
2235 let _ = writeln!(stream, "{:.2} {:.2} l", x + w, y + h - tr);
2236 if tr > 0.0 {
2237 let _ = writeln!(
2238 stream,
2239 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2240 x + w,
2241 y + h - tr + tr * k,
2242 x + w - tr + tr * k,
2243 y + h,
2244 x + w - tr,
2245 y + h
2246 );
2247 }
2248
2249 let _ = writeln!(stream, "{:.2} {:.2} l", x + tl, y + h);
2250 if tl > 0.0 {
2251 let _ = writeln!(
2252 stream,
2253 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2254 x + tl - tl * k,
2255 y + h,
2256 x,
2257 y + h - tl + tl * k,
2258 x,
2259 y + h - tl
2260 );
2261 }
2262
2263 let _ = writeln!(stream, "{:.2} {:.2} l", x, y + bl);
2264 if bl > 0.0 {
2265 let _ = writeln!(
2266 stream,
2267 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2268 x,
2269 y + bl - bl * k,
2270 x + bl - bl * k,
2271 y,
2272 x + bl,
2273 y
2274 );
2275 }
2276
2277 let _ = writeln!(stream, "h");
2278 }
2279
2280 #[allow(clippy::too_many_arguments)]
2281 fn write_border_sides(
2282 &self,
2283 stream: &mut String,
2284 x: f64,
2285 y: f64,
2286 w: f64,
2287 h: f64,
2288 bw: &Edges,
2289 bc: &crate::style::EdgeValues<Color>,
2290 ) {
2291 if bw.top > 0.0 {
2292 let _ = write!(
2293 stream,
2294 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2295 bc.top.r,
2296 bc.top.g,
2297 bc.top.b,
2298 bw.top,
2299 x,
2300 y + h,
2301 x + w,
2302 y + h
2303 );
2304 }
2305 if bw.bottom > 0.0 {
2306 let _ = write!(
2307 stream,
2308 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2309 bc.bottom.r,
2310 bc.bottom.g,
2311 bc.bottom.b,
2312 bw.bottom,
2313 x,
2314 y,
2315 x + w,
2316 y
2317 );
2318 }
2319 if bw.left > 0.0 {
2320 let _ = write!(
2321 stream,
2322 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2323 bc.left.r,
2324 bc.left.g,
2325 bc.left.b,
2326 bw.left,
2327 x,
2328 y,
2329 x,
2330 y + h
2331 );
2332 }
2333 if bw.right > 0.0 {
2334 let _ = write!(
2335 stream,
2336 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2337 bc.right.r,
2338 bc.right.g,
2339 bc.right.b,
2340 bw.right,
2341 x + w,
2342 y,
2343 x + w,
2344 y + h
2345 );
2346 }
2347 }
2348
2349 fn register_fonts(
2352 &self,
2353 builder: &mut PdfBuilder,
2354 pages: &[LayoutPage],
2355 font_context: &FontContext,
2356 ) -> Result<(), FormeError> {
2357 let mut font_usage_map: HashMap<FontKey, FontUsage> = HashMap::new();
2359
2360 for page in pages {
2361 Self::collect_font_usage(&page.elements, &mut font_usage_map);
2362 }
2363
2364 let mut keys: Vec<FontKey> = font_usage_map.keys().cloned().collect();
2365
2366 keys.sort_by(|a, b| {
2368 a.family
2369 .cmp(&b.family)
2370 .then(a.weight.cmp(&b.weight))
2371 .then(a.italic.cmp(&b.italic))
2372 });
2373 keys.dedup();
2374
2375 if keys.is_empty() {
2377 keys.push(FontKey {
2378 family: "Helvetica".to_string(),
2379 weight: 400,
2380 italic: false,
2381 });
2382 }
2383
2384 for key in &keys {
2385 let font_data = font_context.resolve(&key.family, key.weight, key.italic);
2386
2387 match font_data {
2388 FontData::Standard(std_font) => {
2389 let obj_id = builder.objects.len();
2390 let metrics = std_font.metrics();
2393 let widths_str: String = metrics
2394 .widths
2395 .iter()
2396 .map(|w| w.to_string())
2397 .collect::<Vec<_>>()
2398 .join(" ");
2399 let font_dict = format!(
2400 "<< /Type /Font /Subtype /Type1 /BaseFont /{} \
2401 /Encoding /WinAnsiEncoding \
2402 /FirstChar 32 /LastChar 255 /Widths [{}] >>",
2403 std_font.pdf_name(),
2404 widths_str,
2405 );
2406 builder.objects.push(PdfObject {
2407 id: obj_id,
2408 data: font_dict.into_bytes(),
2409 });
2410 builder.font_objects.push((key.clone(), obj_id));
2411 }
2412 FontData::Custom { data, .. } => {
2413 let usage = font_usage_map.get(key);
2414 let used_glyph_ids = usage.map(|u| &u.glyph_ids);
2415 let used_chars = usage.map(|u| &u.chars);
2416 let glyph_to_char = usage.map(|u| &u.glyph_to_char);
2417 let type0_obj_id = Self::write_custom_font_objects(
2418 builder,
2419 key,
2420 data,
2421 used_glyph_ids.cloned().unwrap_or_default(),
2422 used_chars.cloned().unwrap_or_default(),
2423 glyph_to_char.cloned().unwrap_or_default(),
2424 )?;
2425 builder.font_objects.push((key.clone(), type0_obj_id));
2426 }
2427 }
2428 }
2429
2430 Ok(())
2431 }
2432
2433 fn collect_font_usage(
2435 elements: &[LayoutElement],
2436 font_usage: &mut HashMap<FontKey, FontUsage>,
2437 ) {
2438 for element in elements {
2439 let lines_opt = match &element.draw {
2440 DrawCommand::Text { lines, .. } => Some(lines),
2441 DrawCommand::Watermark { lines, .. } => Some(lines),
2442 _ => None,
2443 };
2444 if let Some(lines) = lines_opt {
2445 for line in lines {
2446 for glyph in &line.glyphs {
2447 let italic =
2448 matches!(glyph.font_style, FontStyle::Italic | FontStyle::Oblique);
2449 let key = FontKey {
2450 family: glyph.font_family.clone(),
2451 weight: glyph.font_weight,
2452 italic,
2453 };
2454 let usage = font_usage.entry(key).or_insert_with(|| FontUsage {
2455 chars: HashSet::new(),
2456 glyph_ids: HashSet::new(),
2457 glyph_to_char: HashMap::new(),
2458 });
2459 usage.chars.insert(glyph.char_value);
2460 usage.glyph_ids.insert(glyph.glyph_id);
2461 usage
2463 .glyph_to_char
2464 .entry(glyph.glyph_id)
2465 .or_insert(glyph.char_value);
2466 if let Some(ref ct) = glyph.cluster_text {
2468 if let Some(first_char) = ct.chars().next() {
2470 usage
2471 .glyph_to_char
2472 .entry(glyph.glyph_id)
2473 .or_insert(first_char);
2474 }
2475 }
2476 }
2477 }
2478 }
2479 Self::collect_font_usage(&element.children, font_usage);
2480 }
2481 }
2482
2483 fn register_shadings(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2489 for (page_idx, page) in pages.iter().enumerate() {
2490 let mut counter = 0usize;
2491 Self::collect_shadings_recursive(&page.elements, page_idx, &mut counter, builder);
2492 }
2493 }
2494
2495 fn collect_shadings_recursive(
2496 elements: &[LayoutElement],
2497 page_idx: usize,
2498 counter: &mut usize,
2499 builder: &mut PdfBuilder,
2500 ) {
2501 for element in elements {
2502 if let DrawCommand::Rect {
2503 background_gradient: Some(gradient),
2504 ..
2505 } = &element.draw
2506 {
2507 let ordinal = *counter;
2508 *counter += 1;
2509 let (obj_id, name) =
2510 Self::write_shading_objects(builder, gradient, element, ordinal);
2511 builder
2512 .shading_map
2513 .insert((page_idx, ordinal), (obj_id, name));
2514 }
2515 Self::collect_shadings_recursive(&element.children, page_idx, counter, builder);
2516 }
2517 }
2518
2519 fn write_shading_objects(
2525 builder: &mut PdfBuilder,
2526 gradient: &crate::style::Background,
2527 element: &LayoutElement,
2528 ordinal: usize,
2529 ) -> (usize, String) {
2530 use crate::style::Background;
2531 use crate::style::GradientStop;
2532
2533 let black = Color {
2537 r: 0.0,
2538 g: 0.0,
2539 b: 0.0,
2540 a: 1.0,
2541 };
2542 let stops: Vec<GradientStop> = match gradient {
2543 Background::Color(c) => vec![
2544 GradientStop {
2545 position: 0.0,
2546 color: *c,
2547 },
2548 GradientStop {
2549 position: 1.0,
2550 color: *c,
2551 },
2552 ],
2553 Background::Linear(g) => normalize_gradient_stops(&g.stops, black),
2554 Background::Radial(g) => normalize_gradient_stops(&g.stops, black),
2555 };
2556
2557 let function_id = if stops.len() <= 2 {
2561 let c0 = stops.first().map(|s| s.color).unwrap_or(black);
2562 let c1 = stops.last().map(|s| s.color).unwrap_or(c0);
2563 let id = builder.objects.len();
2564 let data = format!(
2565 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
2566 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
2567 );
2568 builder.objects.push(PdfObject {
2569 id,
2570 data: data.into_bytes(),
2571 });
2572 id
2573 } else {
2574 let mut sub_ids: Vec<usize> = Vec::with_capacity(stops.len() - 1);
2576 for window in stops.windows(2) {
2577 let c0 = window[0].color;
2578 let c1 = window[1].color;
2579 let id = builder.objects.len();
2580 let data = format!(
2581 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
2582 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
2583 );
2584 builder.objects.push(PdfObject {
2585 id,
2586 data: data.into_bytes(),
2587 });
2588 sub_ids.push(id);
2589 }
2590 let bounds: Vec<String> = stops[1..stops.len() - 1]
2594 .iter()
2595 .map(|s| format!("{:.4}", s.position))
2596 .collect();
2597 let encode: Vec<&str> = (0..sub_ids.len()).map(|_| "0 1").collect();
2598 let functions: Vec<String> = sub_ids.iter().map(|i| format!("{} 0 R", i)).collect();
2599 let id = builder.objects.len();
2600 let data = format!(
2601 "<< /FunctionType 3 /Domain [0 1] /Functions [{}] /Bounds [{}] /Encode [{}] >>",
2602 functions.join(" "),
2603 bounds.join(" "),
2604 encode.join(" "),
2605 );
2606 builder.objects.push(PdfObject {
2607 id,
2608 data: data.into_bytes(),
2609 });
2610 id
2611 };
2612
2613 let _ = element.x;
2617 let _ = element.y;
2618 let w = element.width;
2619 let h = element.height;
2620
2621 let shading_id = builder.objects.len();
2622 let shading_data = match gradient {
2623 Background::Linear(g) => {
2624 let theta = g.angle_deg.to_radians();
2634 let dx = theta.sin();
2635 let dy = theta.cos();
2636 let axis_len = w * dx.abs() + h * dy.abs();
2639 let cx_rel = w / 2.0;
2642 let cy_rel = h / 2.0;
2643 let half = axis_len / 2.0;
2644 let x0 = cx_rel - dx * half;
2645 let y0 = cy_rel - dy * half;
2646 let x1 = cx_rel + dx * half;
2647 let y1 = cy_rel + dy * half;
2648 format!(
2649 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
2650 x0, y0, x1, y1, function_id,
2651 )
2652 }
2653 Background::Radial(_) => {
2654 let cx_rel = w / 2.0;
2657 let cy_rel = h / 2.0;
2658 let r_outer = (w / 2.0).max(h / 2.0);
2659 format!(
2660 "<< /ShadingType 3 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} 0 {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
2661 cx_rel, cy_rel, cx_rel, cy_rel, r_outer, function_id,
2662 )
2663 }
2664 Background::Color(_) => {
2665 format!(
2669 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 0 0] /Function {} 0 R /Extend [true true] >>",
2670 function_id,
2671 )
2672 }
2673 };
2674 builder.objects.push(PdfObject {
2675 id: shading_id,
2676 data: shading_data.into_bytes(),
2677 });
2678 (shading_id, format!("Sh{}", ordinal))
2679 }
2680
2681 fn register_page_background_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2685 for (page_idx, page) in pages.iter().enumerate() {
2686 let Some(src) = &page.config.background_image else {
2687 continue;
2688 };
2689 if let Some(&entry) = builder.page_background_url_cache.get(src) {
2691 builder.page_background_image_map.insert(page_idx, entry);
2692 continue;
2693 }
2694 match crate::image_loader::load_image(src) {
2697 Ok(image_data) => {
2698 let img_idx = builder.image_objects.len();
2699 let dims = (img_idx, image_data.width_px, image_data.height_px);
2700 let xobj_id = Self::write_image_xobject(builder, &image_data);
2701 builder.image_objects.push(xobj_id);
2702 builder.page_background_image_map.insert(page_idx, dims);
2703 builder.page_background_url_cache.insert(src.clone(), dims);
2704 }
2705 Err(e) => {
2706 eprintln!("[forme] page background image failed to load: {}", e);
2707 }
2708 }
2709 }
2710 }
2711
2712 fn write_page_background(
2717 &self,
2718 stream: &mut String,
2719 page: &LayoutPage,
2720 page_bg: (usize, u32, u32),
2721 builder: &PdfBuilder,
2722 ) {
2723 use crate::model::{BackgroundPosition, BackgroundSize};
2724 let (img_idx, iw_px, ih_px) = page_bg;
2725 let page_w = page.width;
2726 let page_h = page.height;
2727 let iw = iw_px as f64;
2728 let ih = ih_px as f64;
2729
2730 let size = page.config.background_size.unwrap_or_default();
2731 let (dest_w, dest_h) = match size {
2732 BackgroundSize::Fill => (page_w, page_h),
2733 BackgroundSize::Cover => {
2734 let s = (page_w / iw).max(page_h / ih);
2735 (iw * s, ih * s)
2736 }
2737 BackgroundSize::Contain => {
2738 let s = (page_w / iw).min(page_h / ih);
2739 (iw * s, ih * s)
2740 }
2741 };
2742
2743 let position = page.config.background_position.unwrap_or_default();
2747 let (dest_x, dest_y) = match position {
2750 BackgroundPosition::TopLeft => (0.0, page_h - dest_h),
2751 BackgroundPosition::TopRight => (page_w - dest_w, page_h - dest_h),
2752 BackgroundPosition::BottomLeft => (0.0, 0.0),
2753 BackgroundPosition::BottomRight => (page_w - dest_w, 0.0),
2754 BackgroundPosition::Center => ((page_w - dest_w) / 2.0, (page_h - dest_h) / 2.0),
2755 };
2756
2757 let opacity = page.config.background_opacity.unwrap_or(1.0);
2759 let needs_opacity = opacity < 1.0;
2760 if needs_opacity {
2761 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
2762 let _ = writeln!(stream, "q\n/{} gs", gs_name);
2763 } else {
2764 let _ = writeln!(stream, "q");
2765 }
2766 } else {
2767 let _ = writeln!(stream, "q");
2768 }
2769 let _ = writeln!(
2772 stream,
2773 "{:.2} 0 0 {:.2} {:.2} {:.2} cm\n/Im{} Do\nQ",
2774 dest_w, dest_h, dest_x, dest_y, img_idx,
2775 );
2776 }
2777
2778 fn register_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2780 for (page_idx, page) in pages.iter().enumerate() {
2781 let mut element_counter = 0usize;
2782 Self::collect_images_recursive(&page.elements, page_idx, &mut element_counter, builder);
2783 }
2784 }
2785
2786 fn collect_images_recursive(
2787 elements: &[LayoutElement],
2788 page_idx: usize,
2789 element_counter: &mut usize,
2790 builder: &mut PdfBuilder,
2791 ) {
2792 for element in elements {
2793 match &element.draw {
2794 DrawCommand::Image { image_data } => {
2795 let elem_idx = *element_counter;
2796 *element_counter += 1;
2797
2798 let img_idx = builder.image_objects.len();
2799 let xobj_id = Self::write_image_xobject(builder, image_data);
2800 builder.image_objects.push(xobj_id);
2801 builder
2802 .image_index_map
2803 .insert((page_idx, elem_idx), img_idx);
2804 }
2805 DrawCommand::ImagePlaceholder => {
2806 *element_counter += 1;
2807 }
2808 _ => {
2809 Self::collect_images_recursive(
2810 &element.children,
2811 page_idx,
2812 element_counter,
2813 builder,
2814 );
2815 }
2816 }
2817 }
2818 }
2819
2820 fn register_ext_gstates(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2822 let mut unique_opacities: Vec<f64> = Vec::new();
2823 for page in pages {
2824 Self::collect_opacities_recursive(&page.elements, &mut unique_opacities);
2825 if let Some(o) = page.config.background_opacity {
2827 if o < 1.0 {
2828 unique_opacities.push(o);
2829 }
2830 }
2831 }
2832 unique_opacities.sort_by(|a, b| a.partial_cmp(b).unwrap());
2833 unique_opacities.dedup();
2834
2835 for (idx, &opacity) in unique_opacities.iter().enumerate() {
2836 let obj_id = builder.objects.len();
2837 let gs_name = format!("GS{}", idx);
2838 let obj_data = format!(
2839 "<< /Type /ExtGState /ca {:.4} /CA {:.4} >>",
2840 opacity, opacity
2841 );
2842 builder.objects.push(PdfObject {
2843 id: obj_id,
2844 data: obj_data.into_bytes(),
2845 });
2846 let key = opacity.to_bits();
2847 builder.ext_gstate_map.insert(key, (obj_id, gs_name));
2848 }
2849 }
2850
2851 fn collect_opacities_recursive(elements: &[LayoutElement], opacities: &mut Vec<f64>) {
2852 for element in elements {
2853 if element.opacity < 1.0 {
2860 opacities.push(element.opacity);
2861 }
2862 if let DrawCommand::Rect {
2866 box_shadow: Some(shadow),
2867 ..
2868 } = &element.draw
2869 {
2870 if shadow.color.a < 1.0 {
2871 opacities.push(shadow.color.a);
2872 }
2873 }
2874 match &element.draw {
2875 DrawCommand::Rect { opacity, .. }
2876 | DrawCommand::Text { opacity, .. }
2877 | DrawCommand::Watermark { opacity, .. }
2878 if *opacity < 1.0 =>
2879 {
2880 opacities.push(*opacity);
2881 }
2882 DrawCommand::Chart { primitives } => {
2883 for prim in primitives {
2884 if let crate::chart::ChartPrimitive::FilledPath { opacity, .. } = prim {
2885 if *opacity < 1.0 {
2886 opacities.push(*opacity);
2887 }
2888 }
2889 }
2890 }
2891 DrawCommand::Svg { commands, .. } => {
2892 for cmd in commands {
2893 if let crate::svg::SvgCommand::SetOpacity(opacity) = cmd {
2894 if *opacity < 1.0 {
2895 opacities.push(*opacity);
2896 }
2897 }
2898 }
2899 }
2900 _ => {}
2901 }
2902 Self::collect_opacities_recursive(&element.children, opacities);
2903 }
2904 }
2905
2906 fn build_ext_gstate_resource_dict(&self, builder: &PdfBuilder) -> String {
2908 if builder.ext_gstate_map.is_empty() {
2909 return String::new();
2910 }
2911 let mut entries: Vec<(&String, usize)> = builder
2912 .ext_gstate_map
2913 .values()
2914 .map(|(obj_id, name)| (name, *obj_id))
2915 .collect();
2916 entries.sort_by_key(|(name, _)| (*name).clone());
2917 entries
2918 .iter()
2919 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
2920 .collect::<Vec<_>>()
2921 .join(" ")
2922 }
2923
2924 fn write_image_xobject(
2927 builder: &mut PdfBuilder,
2928 image: &crate::image_loader::LoadedImage,
2929 ) -> usize {
2930 use crate::image_loader::{ImagePixelData, JpegColorSpace};
2931
2932 match &image.pixel_data {
2933 ImagePixelData::Jpeg { data, color_space } => {
2934 let color_space_str = match color_space {
2935 JpegColorSpace::DeviceRGB => "/DeviceRGB",
2936 JpegColorSpace::DeviceGray => "/DeviceGray",
2937 };
2938
2939 let obj_id = builder.objects.len();
2940 let mut obj_data: Vec<u8> = Vec::new();
2941 let _ = write!(
2942 obj_data,
2943 "<< /Type /XObject /Subtype /Image \
2944 /Width {} /Height {} \
2945 /ColorSpace {} \
2946 /BitsPerComponent 8 \
2947 /Filter /DCTDecode \
2948 /Length {} >>\nstream\n",
2949 image.width_px,
2950 image.height_px,
2951 color_space_str,
2952 data.len()
2953 );
2954 obj_data.extend_from_slice(data);
2955 obj_data.extend_from_slice(b"\nendstream");
2956 builder.objects.push(PdfObject {
2957 id: obj_id,
2958 data: obj_data,
2959 });
2960 obj_id
2961 }
2962
2963 ImagePixelData::Decoded { rgb, alpha } => {
2964 let smask_id = alpha.as_ref().map(|alpha_data| {
2966 let compressed_alpha = compress_to_vec_zlib(alpha_data, 6);
2967 let smask_obj_id = builder.objects.len();
2968 let mut smask_data: Vec<u8> = Vec::new();
2969 let _ = write!(
2970 smask_data,
2971 "<< /Type /XObject /Subtype /Image \
2972 /Width {} /Height {} \
2973 /ColorSpace /DeviceGray \
2974 /BitsPerComponent 8 \
2975 /Filter /FlateDecode \
2976 /Length {} >>\nstream\n",
2977 image.width_px,
2978 image.height_px,
2979 compressed_alpha.len()
2980 );
2981 smask_data.extend_from_slice(&compressed_alpha);
2982 smask_data.extend_from_slice(b"\nendstream");
2983 builder.objects.push(PdfObject {
2984 id: smask_obj_id,
2985 data: smask_data,
2986 });
2987 smask_obj_id
2988 });
2989
2990 let compressed_rgb = compress_to_vec_zlib(rgb, 6);
2992 let obj_id = builder.objects.len();
2993 let mut obj_data: Vec<u8> = Vec::new();
2994
2995 let smask_ref = smask_id
2996 .map(|id| format!(" /SMask {} 0 R", id))
2997 .unwrap_or_default();
2998
2999 let _ = write!(
3000 obj_data,
3001 "<< /Type /XObject /Subtype /Image \
3002 /Width {} /Height {} \
3003 /ColorSpace /DeviceRGB \
3004 /BitsPerComponent 8 \
3005 /Filter /FlateDecode \
3006 /Length {}{} >>\nstream\n",
3007 image.width_px,
3008 image.height_px,
3009 compressed_rgb.len(),
3010 smask_ref
3011 );
3012 obj_data.extend_from_slice(&compressed_rgb);
3013 obj_data.extend_from_slice(b"\nendstream");
3014 builder.objects.push(PdfObject {
3015 id: obj_id,
3016 data: obj_data,
3017 });
3018 obj_id
3019 }
3020 }
3021 }
3022
3023 fn build_shading_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3027 let mut entries: Vec<(String, usize)> = builder
3028 .shading_map
3029 .iter()
3030 .filter(|(&(p, _), _)| p == page_idx)
3031 .map(|(_, (obj_id, name))| (name.clone(), *obj_id))
3032 .collect();
3033 if entries.is_empty() {
3034 return String::new();
3035 }
3036 entries.sort_by(|a, b| a.0.cmp(&b.0));
3037 entries
3038 .iter()
3039 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3040 .collect::<Vec<_>>()
3041 .join(" ")
3042 }
3043
3044 fn build_xobject_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3045 let mut entries: Vec<(usize, usize)> = Vec::new();
3046 for (&(pidx, _), &img_idx) in &builder.image_index_map {
3047 if pidx == page_idx {
3048 let obj_id = builder.image_objects[img_idx];
3049 entries.push((img_idx, obj_id));
3050 }
3051 }
3052 if let Some(&(img_idx, _, _)) = builder.page_background_image_map.get(&page_idx) {
3055 let obj_id = builder.image_objects[img_idx];
3056 entries.push((img_idx, obj_id));
3057 }
3058 if entries.is_empty() {
3059 return String::new();
3060 }
3061 entries.sort_by_key(|(idx, _)| *idx);
3062 entries.dedup();
3063 entries
3064 .iter()
3065 .map(|(idx, obj_id)| format!("/Im{} {} 0 R", idx, obj_id))
3066 .collect::<Vec<_>>()
3067 .join(" ")
3068 }
3069
3070 fn write_custom_font_objects(
3077 builder: &mut PdfBuilder,
3078 key: &FontKey,
3079 ttf_data: &[u8],
3080 used_glyph_ids: HashSet<u16>,
3081 used_chars: HashSet<char>,
3082 glyph_to_char_map: HashMap<u16, char>,
3083 ) -> Result<usize, FormeError> {
3084 let face = ttf_parser::Face::parse(ttf_data, 0).map_err(|e| {
3085 FormeError::FontError(format!(
3086 "Failed to parse TTF data for font '{}': {}",
3087 key.family, e
3088 ))
3089 })?;
3090
3091 let units_per_em = face.units_per_em();
3092 let ascender = face.ascender();
3093 let descender = face.descender();
3094
3095 let mut char_to_orig_gid: HashMap<char, u16> = HashMap::new();
3097 for &ch in &used_chars {
3098 if let Some(gid) = face.glyph_index(ch) {
3099 char_to_orig_gid.insert(ch, gid.0);
3100 }
3101 }
3102
3103 let mut all_orig_gids: HashSet<u16> = used_glyph_ids.clone();
3107 for &gid in char_to_orig_gid.values() {
3108 all_orig_gids.insert(gid);
3109 }
3110
3111 let (embed_ttf, gid_remap) = match subset_ttf(ttf_data, &all_orig_gids) {
3113 Ok(subset_result) => (subset_result.ttf_data, subset_result.gid_remap),
3114 Err(_) => {
3115 let identity: HashMap<u16, u16> =
3117 all_orig_gids.iter().map(|&gid| (gid, gid)).collect();
3118 (ttf_data.to_vec(), identity)
3119 }
3120 };
3121
3122 let char_to_gid: HashMap<char, u16> = char_to_orig_gid
3124 .iter()
3125 .filter_map(|(&ch, &orig_gid)| gid_remap.get(&orig_gid).map(|&new_gid| (ch, new_gid)))
3126 .collect();
3127
3128 let gid_remap_for_embed = gid_remap.clone();
3130
3131 let mut new_gid_to_char: HashMap<u16, char> = HashMap::new();
3133 for (&orig_gid, &ch) in &glyph_to_char_map {
3135 if let Some(&new_gid) = gid_remap.get(&orig_gid) {
3136 new_gid_to_char.entry(new_gid).or_insert(ch);
3137 }
3138 }
3139 for (&ch, &new_gid) in &char_to_gid {
3141 new_gid_to_char.entry(new_gid).or_insert(ch);
3142 }
3143
3144 let pdf_font_name = Self::sanitize_font_name(&key.family, key.weight, key.italic);
3145
3146 let compressed_ttf = compress_to_vec_zlib(&embed_ttf, 6);
3148 let fontfile2_id = builder.objects.len();
3149 let mut fontfile2_data: Vec<u8> = Vec::new();
3150 let _ = write!(
3151 fontfile2_data,
3152 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
3153 compressed_ttf.len(),
3154 embed_ttf.len()
3155 );
3156 fontfile2_data.extend_from_slice(&compressed_ttf);
3157 fontfile2_data.extend_from_slice(b"\nendstream");
3158 builder.objects.push(PdfObject {
3159 id: fontfile2_id,
3160 data: fontfile2_data,
3161 });
3162
3163 let subset_face = ttf_parser::Face::parse(&embed_ttf, 0).unwrap_or_else(|_| face.clone());
3165 let subset_upem = subset_face.units_per_em();
3166
3167 let font_descriptor_id = builder.objects.len();
3169 let bbox = face.global_bounding_box();
3170 let scale = 1000.0 / units_per_em as f64;
3171 let bbox_str = format!(
3172 "[{} {} {} {}]",
3173 (bbox.x_min as f64 * scale) as i32,
3174 (bbox.y_min as f64 * scale) as i32,
3175 (bbox.x_max as f64 * scale) as i32,
3176 (bbox.y_max as f64 * scale) as i32,
3177 );
3178
3179 let flags = 4u32;
3180 let cap_height = face.capital_height().unwrap_or(ascender) as f64 * scale;
3181 let stem_v = if key.weight >= 700 { 120 } else { 80 };
3182
3183 let font_descriptor_dict = format!(
3184 "<< /Type /FontDescriptor /FontName /{} /Flags {} \
3185 /FontBBox {} /ItalicAngle {} \
3186 /Ascent {} /Descent {} /CapHeight {} /StemV {} \
3187 /FontFile2 {} 0 R >>",
3188 pdf_font_name,
3189 flags,
3190 bbox_str,
3191 if key.italic { -12 } else { 0 },
3192 (ascender as f64 * scale) as i32,
3193 (descender as f64 * scale) as i32,
3194 cap_height as i32,
3195 stem_v,
3196 fontfile2_id,
3197 );
3198 builder.objects.push(PdfObject {
3199 id: font_descriptor_id,
3200 data: font_descriptor_dict.into_bytes(),
3201 });
3202
3203 let cidfont_id = builder.objects.len();
3205 let w_array = Self::build_w_array_from_gids(&gid_remap, &subset_face, subset_upem);
3207 let default_width = subset_face
3208 .glyph_hor_advance(ttf_parser::GlyphId(0))
3209 .map(|adv| (adv as f64 * 1000.0 / subset_upem as f64) as u32)
3210 .unwrap_or(1000);
3211 let cidfont_dict = format!(
3212 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{} \
3213 /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> \
3214 /FontDescriptor {} 0 R /DW {} /W {} \
3215 /CIDToGIDMap /Identity >>",
3216 pdf_font_name, font_descriptor_id, default_width, w_array,
3217 );
3218 builder.objects.push(PdfObject {
3219 id: cidfont_id,
3220 data: cidfont_dict.into_bytes(),
3221 });
3222
3223 let tounicode_id = builder.objects.len();
3225 let cmap_content = Self::build_tounicode_cmap_from_gids(&new_gid_to_char, &pdf_font_name);
3226 let compressed_cmap = compress_to_vec_zlib(cmap_content.as_bytes(), 6);
3227 let mut tounicode_data: Vec<u8> = Vec::new();
3228 let _ = write!(
3229 tounicode_data,
3230 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
3231 compressed_cmap.len()
3232 );
3233 tounicode_data.extend_from_slice(&compressed_cmap);
3234 tounicode_data.extend_from_slice(b"\nendstream");
3235 builder.objects.push(PdfObject {
3236 id: tounicode_id,
3237 data: tounicode_data,
3238 });
3239
3240 let type0_id = builder.objects.len();
3242 let type0_dict = format!(
3243 "<< /Type /Font /Subtype /Type0 /BaseFont /{} \
3244 /Encoding /Identity-H \
3245 /DescendantFonts [{} 0 R] \
3246 /ToUnicode {} 0 R >>",
3247 pdf_font_name, cidfont_id, tounicode_id,
3248 );
3249 builder.objects.push(PdfObject {
3250 id: type0_id,
3251 data: type0_dict.into_bytes(),
3252 });
3253
3254 builder.custom_font_data.insert(
3256 key.clone(),
3257 CustomFontEmbedData {
3258 ttf_data: embed_ttf,
3259 gid_remap: gid_remap_for_embed,
3260 glyph_to_char: glyph_to_char_map,
3261 char_to_gid,
3262 units_per_em,
3263 ascender,
3264 descender,
3265 },
3266 );
3267
3268 Ok(type0_id)
3269 }
3270
3271 fn build_w_array_from_gids(
3273 gid_remap: &HashMap<u16, u16>,
3274 face: &ttf_parser::Face,
3275 units_per_em: u16,
3276 ) -> String {
3277 let scale = 1000.0 / units_per_em as f64;
3278
3279 let mut entries: Vec<(u16, u32)> = Vec::new();
3280 let mut seen_gids: HashSet<u16> = HashSet::new();
3281
3282 for &new_gid in gid_remap.values() {
3283 if seen_gids.contains(&new_gid) {
3284 continue;
3285 }
3286 seen_gids.insert(new_gid);
3287 let advance = face
3288 .glyph_hor_advance(ttf_parser::GlyphId(new_gid))
3289 .unwrap_or(0);
3290 let width = (advance as f64 * scale) as u32;
3291 entries.push((new_gid, width));
3292 }
3293
3294 entries.sort_by_key(|(gid, _)| *gid);
3295
3296 let mut result = String::from("[");
3298 for (gid, width) in &entries {
3299 let _ = write!(result, " {} [{}]", gid, width);
3300 }
3301 result.push_str(" ]");
3302 result
3303 }
3304
3305 fn build_tounicode_cmap_from_gids(gid_to_char: &HashMap<u16, char>, font_name: &str) -> String {
3307 let mut gid_to_unicode: Vec<(u16, u32)> = gid_to_char
3308 .iter()
3309 .map(|(&gid, &ch)| (gid, ch as u32))
3310 .collect();
3311 gid_to_unicode.sort_by_key(|(gid, _)| *gid);
3312
3313 let mut cmap = String::new();
3314 let _ = writeln!(cmap, "/CIDInit /ProcSet findresource begin");
3315 let _ = writeln!(cmap, "12 dict begin");
3316 let _ = writeln!(cmap, "begincmap");
3317 let _ = writeln!(cmap, "/CIDSystemInfo");
3318 let _ = writeln!(
3319 cmap,
3320 "<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def"
3321 );
3322 let _ = writeln!(cmap, "/CMapName /{}-UTF16 def", font_name);
3323 let _ = writeln!(cmap, "/CMapType 2 def");
3324 let _ = writeln!(cmap, "1 begincodespacerange");
3325 let _ = writeln!(cmap, "<0000> <FFFF>");
3326 let _ = writeln!(cmap, "endcodespacerange");
3327
3328 for chunk in gid_to_unicode.chunks(100) {
3330 let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
3331 for &(gid, unicode) in chunk {
3332 let _ = writeln!(cmap, "<{:04X}> <{:04X}>", gid, unicode);
3333 }
3334 let _ = writeln!(cmap, "endbfchar");
3335 }
3336
3337 let _ = writeln!(cmap, "endcmap");
3338 let _ = writeln!(cmap, "CMapName currentdict /CMap defineresource pop");
3339 let _ = writeln!(cmap, "end");
3340 let _ = writeln!(cmap, "end");
3341
3342 cmap
3343 }
3344
3345 fn sanitize_font_name(family: &str, weight: u32, italic: bool) -> String {
3348 let mut name: String = family
3349 .chars()
3350 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
3351 .collect();
3352
3353 if weight >= 700 {
3354 name.push_str("-Bold");
3355 }
3356 if italic {
3357 name.push_str("-Italic");
3358 }
3359
3360 if name.is_empty() {
3362 name = "CustomFont".to_string();
3363 }
3364
3365 name
3366 }
3367
3368 fn build_font_resource_dict(&self, font_objects: &[(FontKey, usize)]) -> String {
3369 font_objects
3370 .iter()
3371 .enumerate()
3372 .map(|(i, (_, obj_id))| format!("/F{} {} 0 R", i, obj_id))
3373 .collect::<Vec<_>>()
3374 .join(" ")
3375 }
3376
3377 fn font_index(
3379 &self,
3380 family: &str,
3381 weight: u32,
3382 font_style: FontStyle,
3383 font_objects: &[(FontKey, usize)],
3384 ) -> usize {
3385 let italic = matches!(font_style, FontStyle::Italic | FontStyle::Oblique);
3386
3387 for (i, (key, _)) in font_objects.iter().enumerate() {
3389 if key.family == family && key.weight == weight && key.italic == italic {
3390 return i;
3391 }
3392 }
3393
3394 let snapped = if weight >= 600 { 700 } else { 400 };
3396 for (i, (key, _)) in font_objects.iter().enumerate() {
3397 if key.family == family && key.weight == snapped && key.italic == italic {
3398 return i;
3399 }
3400 }
3401
3402 for (i, (key, _)) in font_objects.iter().enumerate() {
3404 if key.family == "Helvetica" && key.weight == snapped && key.italic == italic {
3405 return i;
3406 }
3407 }
3408
3409 0
3411 }
3412
3413 fn group_glyphs_by_style(glyphs: &[PositionedGlyph]) -> Vec<Vec<&PositionedGlyph>> {
3416 if glyphs.is_empty() {
3417 return vec![];
3418 }
3419
3420 let mut groups: Vec<Vec<&PositionedGlyph>> = Vec::new();
3421 let mut current_group: Vec<&PositionedGlyph> = vec![&glyphs[0]];
3422
3423 for glyph in &glyphs[1..] {
3424 let prev = current_group.last().unwrap();
3425 let same_style = glyph.font_family == prev.font_family
3426 && glyph.font_weight == prev.font_weight
3427 && std::mem::discriminant(&glyph.font_style)
3428 == std::mem::discriminant(&prev.font_style)
3429 && (glyph.font_size - prev.font_size).abs() < 0.01
3430 && Self::colors_equal(&glyph.color, &prev.color)
3431 && std::mem::discriminant(&glyph.text_decoration)
3432 == std::mem::discriminant(&prev.text_decoration);
3433
3434 if same_style {
3435 current_group.push(glyph);
3436 } else {
3437 groups.push(current_group);
3438 current_group = vec![glyph];
3439 }
3440 }
3441 groups.push(current_group);
3442 groups
3443 }
3444
3445 fn colors_equal(a: &Option<Color>, b: &Option<Color>) -> bool {
3446 match (a, b) {
3447 (None, None) => true,
3448 (Some(ca), Some(cb)) => {
3449 (ca.r - cb.r).abs() < 0.001
3450 && (ca.g - cb.g).abs() < 0.001
3451 && (ca.b - cb.b).abs() < 0.001
3452 && (ca.a - cb.a).abs() < 0.001
3453 }
3454 _ => false,
3455 }
3456 }
3457
3458 fn collect_link_annotations(
3462 elements: &[LayoutElement],
3463 page_height: f64,
3464 annotations: &mut Vec<LinkAnnotation>,
3465 ) {
3466 for element in elements {
3467 if let Some(ref href) = element.href {
3468 if !href.is_empty() {
3469 let pdf_y = page_height - element.y - element.height;
3470 annotations.push(LinkAnnotation {
3471 x: element.x,
3472 y: pdf_y,
3473 width: element.width,
3474 height: element.height,
3475 href: href.clone(),
3476 });
3477 continue;
3479 }
3480 }
3481 Self::collect_link_annotations(&element.children, page_height, annotations);
3482 }
3483 }
3484
3485 fn collect_form_fields(
3487 elements: &[LayoutElement],
3488 page_height: f64,
3489 page_idx: usize,
3490 fields: &mut Vec<FormFieldData>,
3491 ) {
3492 for element in elements {
3493 if let DrawCommand::FormField {
3494 ref field_type,
3495 ref name,
3496 } = element.draw
3497 {
3498 let pdf_y = page_height - element.y - element.height;
3499 fields.push(FormFieldData {
3500 field_type: field_type.clone(),
3501 name: name.clone(),
3502 x: element.x,
3503 y: pdf_y,
3504 width: element.width,
3505 height: element.height,
3506 page_idx,
3507 });
3508 }
3509 Self::collect_form_fields(&element.children, page_height, page_idx, fields);
3510 }
3511 }
3512
3513 fn collect_bookmarks(
3515 elements: &[LayoutElement],
3516 page_height: f64,
3517 page_obj_id: usize,
3518 bookmarks: &mut Vec<PdfBookmark>,
3519 ) {
3520 for element in elements {
3521 if let Some(ref title) = element.bookmark {
3522 let y_pdf = page_height - element.y;
3523 bookmarks.push(PdfBookmark {
3524 title: title.clone(),
3525 page_obj_id,
3526 y_pdf,
3527 });
3528 }
3529 Self::collect_bookmarks(&element.children, page_height, page_obj_id, bookmarks);
3530 }
3531 }
3532
3533 fn write_outline_tree(&self, builder: &mut PdfBuilder, bookmarks: &[PdfBookmark]) -> usize {
3536 let outlines_id = builder.objects.len();
3538 builder.objects.push(PdfObject {
3539 id: outlines_id,
3540 data: vec![],
3541 });
3542
3543 let mut item_ids: Vec<usize> = Vec::new();
3545 for _bm in bookmarks {
3546 let item_id = builder.objects.len();
3547 builder.objects.push(PdfObject {
3548 id: item_id,
3549 data: vec![],
3550 });
3551 item_ids.push(item_id);
3552 }
3553
3554 for (i, (bm, &item_id)) in bookmarks.iter().zip(item_ids.iter()).enumerate() {
3556 let mut dict = format!(
3557 "<< /Title ({}) /Parent {} 0 R /Dest [{} 0 R /XYZ 0 {:.2} null]",
3558 Self::escape_pdf_string(&bm.title),
3559 outlines_id,
3560 bm.page_obj_id,
3561 bm.y_pdf,
3562 );
3563 if i > 0 {
3564 let _ = write!(dict, " /Prev {} 0 R", item_ids[i - 1]);
3565 }
3566 if i + 1 < item_ids.len() {
3567 let _ = write!(dict, " /Next {} 0 R", item_ids[i + 1]);
3568 }
3569 dict.push_str(" >>");
3570 builder.objects[item_id].data = dict.into_bytes();
3571 }
3572
3573 let first_id = item_ids.first().copied().unwrap_or(0);
3575 let last_id = item_ids.last().copied().unwrap_or(0);
3576 let outlines_dict = format!(
3577 "<< /Type /Outlines /First {} 0 R /Last {} 0 R /Count {} >>",
3578 first_id,
3579 last_id,
3580 bookmarks.len()
3581 );
3582 builder.objects[outlines_id].data = outlines_dict.into_bytes();
3583
3584 outlines_id
3585 }
3586
3587 fn write_svg_commands(
3589 stream: &mut String,
3590 commands: &[SvgCommand],
3591 ext_gstate_map: &HashMap<u64, (usize, String)>,
3592 ) {
3593 for cmd in commands {
3594 match cmd {
3595 SvgCommand::MoveTo(x, y) => {
3596 let _ = writeln!(stream, "{:.2} {:.2} m", x, y);
3597 }
3598 SvgCommand::LineTo(x, y) => {
3599 let _ = writeln!(stream, "{:.2} {:.2} l", x, y);
3600 }
3601 SvgCommand::CurveTo(x1, y1, x2, y2, x3, y3) => {
3602 let _ = writeln!(
3603 stream,
3604 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3605 x1, y1, x2, y2, x3, y3
3606 );
3607 }
3608 SvgCommand::ClosePath => {
3609 let _ = writeln!(stream, "h");
3610 }
3611 SvgCommand::SetFill(r, g, b) => {
3612 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", r, g, b);
3613 }
3614 SvgCommand::SetFillNone => {
3615 }
3617 SvgCommand::SetStroke(r, g, b) => {
3618 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", r, g, b);
3619 }
3620 SvgCommand::SetStrokeNone => {
3621 }
3623 SvgCommand::SetStrokeWidth(w) => {
3624 let _ = writeln!(stream, "{:.2} w", w);
3625 }
3626 SvgCommand::Fill => {
3627 let _ = writeln!(stream, "f");
3628 }
3629 SvgCommand::Stroke => {
3630 let _ = writeln!(stream, "S");
3631 }
3632 SvgCommand::FillAndStroke => {
3633 let _ = writeln!(stream, "B");
3634 }
3635 SvgCommand::SetLineCap(cap) => {
3636 let _ = writeln!(stream, "{} J", cap);
3637 }
3638 SvgCommand::SetLineJoin(join) => {
3639 let _ = writeln!(stream, "{} j", join);
3640 }
3641 SvgCommand::SaveState => {
3642 let _ = writeln!(stream, "q");
3643 }
3644 SvgCommand::RestoreState => {
3645 let _ = writeln!(stream, "Q");
3646 }
3647 SvgCommand::SetOpacity(opacity) => {
3648 if let Some((_, gs_name)) = ext_gstate_map.get(&opacity.to_bits()) {
3649 let _ = writeln!(stream, "/{} gs", gs_name);
3650 }
3651 }
3652 }
3653 }
3654 }
3655
3656 pub(crate) fn escape_pdf_string(s: &str) -> String {
3658 s.replace('\\', "\\\\")
3659 .replace('(', "\\(")
3660 .replace(')', "\\)")
3661 }
3662
3663 fn encode_winansi_text(s: &str) -> String {
3666 let mut result = String::with_capacity(s.len());
3667 for ch in s.chars() {
3668 let b = Self::unicode_to_winansi(ch).unwrap_or(b'?');
3669 match b {
3670 b'\\' => result.push_str("\\\\"),
3671 b'(' => result.push_str("\\("),
3672 b')' => result.push_str("\\)"),
3673 0x20..=0x7E => result.push(b as char),
3674 _ => {
3675 let _ = write!(result, "\\{:03o}", b);
3676 }
3677 }
3678 }
3679 result
3680 }
3681
3682 fn unicode_to_winansi(ch: char) -> Option<u8> {
3684 crate::font::unicode_to_winansi(ch)
3685 }
3686
3687 fn serialize(&self, builder: &PdfBuilder, info_obj_id: Option<usize>) -> Vec<u8> {
3689 let mut output: Vec<u8> = Vec::new();
3690 let mut offsets: Vec<usize> = vec![0; builder.objects.len()];
3691
3692 output.extend_from_slice(b"%PDF-1.7\n");
3694 output.extend_from_slice(b"%\xe2\xe3\xcf\xd3\n");
3695
3696 for (i, obj) in builder.objects.iter().enumerate().skip(1) {
3697 offsets[i] = output.len();
3698 let header = format!("{} 0 obj\n", i);
3699 output.extend_from_slice(header.as_bytes());
3700 output.extend_from_slice(&obj.data);
3701 output.extend_from_slice(b"\nendobj\n\n");
3702 }
3703
3704 let xref_offset = output.len();
3705 let _ = writeln!(output, "xref\n0 {}", builder.objects.len());
3706 let _ = writeln!(output, "0000000000 65535 f ");
3707 for offset in offsets.iter().skip(1) {
3708 let _ = writeln!(output, "{:010} 00000 n ", offset);
3709 }
3710
3711 let _ = write!(
3712 output,
3713 "trailer\n<< /Size {} /Root 1 0 R",
3714 builder.objects.len()
3715 );
3716 if let Some(info_id) = info_obj_id {
3717 let _ = write!(output, " /Info {} 0 R", info_id);
3718 }
3719 let _ = writeln!(output, " >>\nstartxref\n{}\n%%EOF", xref_offset);
3720
3721 output
3722 }
3723}
3724
3725fn write_chart_primitive(
3730 stream: &mut String,
3731 prim: &crate::chart::ChartPrimitive,
3732 _chart_height: f64,
3733 builder: &PdfBuilder,
3734) {
3735 use crate::chart::{ChartPrimitive, TextAnchor};
3736 use crate::font::metrics::unicode_to_winansi;
3737
3738 match prim {
3739 ChartPrimitive::Rect { x, y, w, h, fill } => {
3740 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3741 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re f", x, y, w, h);
3742 }
3743
3744 ChartPrimitive::Line {
3745 x1,
3746 y1,
3747 x2,
3748 y2,
3749 stroke,
3750 width,
3751 } => {
3752 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
3753 let _ = writeln!(stream, "{:.2} w", width);
3754 let _ = writeln!(stream, "{:.2} {:.2} m {:.2} {:.2} l S", x1, y1, x2, y2);
3755 }
3756
3757 ChartPrimitive::Polyline {
3758 points,
3759 stroke,
3760 width,
3761 } => {
3762 if points.len() < 2 {
3763 return;
3764 }
3765 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
3766 let _ = writeln!(stream, "{:.2} w", width);
3767 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
3768 for &(px, py) in &points[1..] {
3769 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
3770 }
3771 let _ = writeln!(stream, "S");
3772 }
3773
3774 ChartPrimitive::FilledPath {
3775 points,
3776 fill,
3777 opacity,
3778 } => {
3779 if points.len() < 3 {
3780 return;
3781 }
3782 let _ = writeln!(stream, "q");
3783 if *opacity < 1.0 {
3785 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
3786 let _ = writeln!(stream, "/{} gs", gs_name);
3787 }
3788 }
3789 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3790 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
3791 for &(px, py) in &points[1..] {
3792 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
3793 }
3794 let _ = writeln!(stream, "h f");
3795 let _ = writeln!(stream, "Q");
3796 }
3797
3798 ChartPrimitive::Circle { cx, cy, r, fill } => {
3799 let kappa: f64 = 0.5523;
3801 let kr = kappa * r;
3802 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3803 let _ = writeln!(stream, "{:.2} {:.2} m", cx + r, cy);
3804 let _ = writeln!(
3805 stream,
3806 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3807 cx + r,
3808 cy + kr,
3809 cx + kr,
3810 cy + r,
3811 cx,
3812 cy + r
3813 );
3814 let _ = writeln!(
3815 stream,
3816 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3817 cx - kr,
3818 cy + r,
3819 cx - r,
3820 cy + kr,
3821 cx - r,
3822 cy
3823 );
3824 let _ = writeln!(
3825 stream,
3826 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3827 cx - r,
3828 cy - kr,
3829 cx - kr,
3830 cy - r,
3831 cx,
3832 cy - r
3833 );
3834 let _ = writeln!(
3835 stream,
3836 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3837 cx + kr,
3838 cy - r,
3839 cx + r,
3840 cy - kr,
3841 cx + r,
3842 cy
3843 );
3844 let _ = writeln!(stream, "f");
3845 }
3846
3847 ChartPrimitive::ArcSector {
3848 cx,
3849 cy,
3850 r,
3851 start_angle,
3852 end_angle,
3853 fill,
3854 } => {
3855 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3856 let _ = writeln!(stream, "{:.2} {:.2} m", cx, cy);
3858 let sx = cx + r * start_angle.cos();
3860 let sy = cy + r * start_angle.sin();
3861 let _ = writeln!(stream, "{:.2} {:.2} l", sx, sy);
3862
3863 let mut angle = *start_angle;
3865 let total = end_angle - start_angle;
3866 let segments = ((total.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
3867 let step = total / segments as f64;
3868
3869 for _ in 0..segments {
3870 let a1 = angle;
3871 let a2 = angle + step;
3872 let alpha = 4.0 / 3.0 * ((a2 - a1) / 4.0).tan();
3873
3874 let p1x = cx + r * a1.cos();
3875 let p1y = cy + r * a1.sin();
3876 let p2x = cx + r * a2.cos();
3877 let p2y = cy + r * a2.sin();
3878
3879 let cp1x = p1x - alpha * r * a1.sin();
3880 let cp1y = p1y + alpha * r * a1.cos();
3881 let cp2x = p2x + alpha * r * a2.sin();
3882 let cp2y = p2y - alpha * r * a2.cos();
3883
3884 let _ = writeln!(
3885 stream,
3886 "{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c",
3887 cp1x, cp1y, cp2x, cp2y, p2x, p2y
3888 );
3889 angle = a2;
3890 }
3891
3892 let _ = writeln!(stream, "h f");
3894 }
3895
3896 ChartPrimitive::Label {
3897 text,
3898 x,
3899 y,
3900 font_size,
3901 color,
3902 anchor,
3903 } => {
3904 let metrics = crate::font::StandardFont::Helvetica.metrics();
3906 let text_width = metrics.measure_string(text, *font_size, 0.0);
3907 let x_offset = match anchor {
3908 TextAnchor::Left => 0.0,
3909 TextAnchor::Center => -text_width / 2.0,
3910 TextAnchor::Right => -text_width,
3911 };
3912
3913 let font_idx = builder
3915 .font_objects
3916 .iter()
3917 .enumerate()
3918 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
3919 .map(|(i, _)| i)
3920 .unwrap_or(0);
3921
3922 let encoded: String = text
3924 .chars()
3925 .map(|ch| {
3926 if let Some(code) = unicode_to_winansi(ch) {
3927 code as char
3928 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
3929 ch
3930 } else {
3931 '?'
3932 }
3933 })
3934 .collect();
3935 let escaped = pdf_escape_string(&encoded);
3936
3937 let _ = writeln!(stream, "q");
3939 let _ = writeln!(stream, "1 0 0 -1 {:.4} {:.4} cm", x + x_offset, *y);
3940 let _ = writeln!(
3941 stream,
3942 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
3943 font_idx, font_size, color.r, color.g, color.b, escaped
3944 );
3945 let _ = writeln!(stream, "Q");
3946 }
3947 }
3948}
3949
3950fn normalize_gradient_stops(
3957 stops: &[crate::style::GradientStop],
3958 fallback: Color,
3959) -> Vec<crate::style::GradientStop> {
3960 use crate::style::GradientStop;
3961 if stops.is_empty() {
3962 return vec![
3963 GradientStop {
3964 position: 0.0,
3965 color: fallback,
3966 },
3967 GradientStop {
3968 position: 1.0,
3969 color: fallback,
3970 },
3971 ];
3972 }
3973 let mut sorted: Vec<GradientStop> = stops
3974 .iter()
3975 .map(|s| GradientStop {
3976 position: s.position.clamp(0.0, 1.0),
3977 color: s.color,
3978 })
3979 .collect();
3980 sorted.sort_by(|a, b| {
3981 a.position
3982 .partial_cmp(&b.position)
3983 .unwrap_or(std::cmp::Ordering::Equal)
3984 });
3985 if sorted[0].position > 0.0 {
3986 sorted.insert(
3987 0,
3988 GradientStop {
3989 position: 0.0,
3990 color: sorted[0].color,
3991 },
3992 );
3993 }
3994 if sorted[sorted.len() - 1].position < 1.0 {
3995 let last = sorted[sorted.len() - 1].color;
3996 sorted.push(GradientStop {
3997 position: 1.0,
3998 color: last,
3999 });
4000 }
4001 sorted
4002}
4003
4004fn pdf_escape_string(s: &str) -> String {
4005 let mut out = String::with_capacity(s.len());
4006 for ch in s.chars() {
4007 match ch {
4008 '(' => out.push_str("\\("),
4009 ')' => out.push_str("\\)"),
4010 '\\' => out.push_str("\\\\"),
4011 _ => out.push(ch),
4012 }
4013 }
4014 out
4015}
4016
4017#[cfg(test)]
4018mod tests {
4019 use super::*;
4020 use crate::font::FontContext;
4021
4022 #[test]
4023 fn test_escape_pdf_string() {
4024 assert_eq!(
4025 PdfWriter::escape_pdf_string("Hello (World)"),
4026 "Hello \\(World\\)"
4027 );
4028 assert_eq!(PdfWriter::escape_pdf_string("back\\slash"), "back\\\\slash");
4029 }
4030
4031 #[test]
4032 fn test_empty_document_produces_valid_pdf() {
4033 let writer = PdfWriter::new();
4034 let font_context = FontContext::new();
4035 let pages = vec![LayoutPage {
4036 width: 595.28,
4037 height: 841.89,
4038 elements: vec![],
4039 fixed_header: vec![],
4040 fixed_footer: vec![],
4041 watermarks: vec![],
4042 config: PageConfig::default(),
4043 }];
4044 let metadata = Metadata::default();
4045 let bytes = writer
4046 .write(
4047 &pages,
4048 &metadata,
4049 &font_context,
4050 false,
4051 None,
4052 false,
4053 None,
4054 false,
4055 )
4056 .unwrap();
4057
4058 assert!(bytes.starts_with(b"%PDF-1.7"));
4059 assert!(bytes.windows(5).any(|w| w == b"%%EOF"));
4060 assert!(bytes.windows(4).any(|w| w == b"xref"));
4061 assert!(bytes.windows(7).any(|w| w == b"trailer"));
4062 }
4063
4064 #[test]
4065 fn test_metadata_in_pdf() {
4066 let writer = PdfWriter::new();
4067 let font_context = FontContext::new();
4068 let pages = vec![LayoutPage {
4069 width: 595.28,
4070 height: 841.89,
4071 elements: vec![],
4072 fixed_header: vec![],
4073 fixed_footer: vec![],
4074 watermarks: vec![],
4075 config: PageConfig::default(),
4076 }];
4077 let metadata = Metadata {
4078 title: Some("Test Document".to_string()),
4079 author: Some("Forme".to_string()),
4080 subject: None,
4081 creator: None,
4082 lang: None,
4083 };
4084 let bytes = writer
4085 .write(
4086 &pages,
4087 &metadata,
4088 &font_context,
4089 false,
4090 None,
4091 false,
4092 None,
4093 false,
4094 )
4095 .unwrap();
4096 let text = String::from_utf8_lossy(&bytes);
4097
4098 assert!(text.contains("/Title (Test Document)"));
4099 assert!(text.contains("/Author (Forme)"));
4100 }
4101
4102 #[test]
4103 fn test_bold_font_registered_separately() {
4104 let writer = PdfWriter::new();
4105 let font_context = FontContext::new();
4106
4107 let pages = vec![LayoutPage {
4109 width: 595.28,
4110 height: 841.89,
4111 elements: vec![
4112 LayoutElement {
4113 x: 54.0,
4114 y: 54.0,
4115 width: 100.0,
4116 height: 16.8,
4117 draw: DrawCommand::Text {
4118 lines: vec![TextLine {
4119 x: 54.0,
4120 y: 66.0,
4121 width: 50.0,
4122 height: 16.8,
4123 glyphs: vec![PositionedGlyph {
4124 glyph_id: 65,
4125 x_offset: 0.0,
4126 y_offset: 0.0,
4127 x_advance: 8.0,
4128 font_size: 12.0,
4129 font_family: "Helvetica".to_string(),
4130 font_weight: 400,
4131 font_style: FontStyle::Normal,
4132 char_value: 'A',
4133 color: None,
4134 href: None,
4135 text_decoration: TextDecoration::None,
4136 letter_spacing: 0.0,
4137 cluster_text: None,
4138 }],
4139 word_spacing: 0.0,
4140 }],
4141 color: Color::BLACK,
4142 text_decoration: TextDecoration::None,
4143 opacity: 1.0,
4144 },
4145 children: vec![],
4146 node_type: None,
4147 resolved_style: None,
4148 source_location: None,
4149 href: None,
4150 bookmark: None,
4151 alt: None,
4152 is_header_row: false,
4153 overflow: Overflow::default(),
4154 opacity: 1.0,
4155 },
4156 LayoutElement {
4157 x: 54.0,
4158 y: 74.0,
4159 width: 100.0,
4160 height: 16.8,
4161 draw: DrawCommand::Text {
4162 lines: vec![TextLine {
4163 x: 54.0,
4164 y: 86.0,
4165 width: 50.0,
4166 height: 16.8,
4167 glyphs: vec![PositionedGlyph {
4168 glyph_id: 65,
4169 x_offset: 0.0,
4170 y_offset: 0.0,
4171 x_advance: 8.0,
4172 font_size: 12.0,
4173 font_family: "Helvetica".to_string(),
4174 font_weight: 700,
4175 font_style: FontStyle::Normal,
4176 char_value: 'A',
4177 color: None,
4178 href: None,
4179 text_decoration: TextDecoration::None,
4180 letter_spacing: 0.0,
4181 cluster_text: None,
4182 }],
4183 word_spacing: 0.0,
4184 }],
4185 color: Color::BLACK,
4186 text_decoration: TextDecoration::None,
4187 opacity: 1.0,
4188 },
4189 children: vec![],
4190 node_type: None,
4191 resolved_style: None,
4192 source_location: None,
4193 href: None,
4194 bookmark: None,
4195 alt: None,
4196 is_header_row: false,
4197 overflow: Overflow::default(),
4198 opacity: 1.0,
4199 },
4200 ],
4201 fixed_header: vec![],
4202 fixed_footer: vec![],
4203 watermarks: vec![],
4204 config: PageConfig::default(),
4205 }];
4206
4207 let metadata = Metadata::default();
4208 let bytes = writer
4209 .write(
4210 &pages,
4211 &metadata,
4212 &font_context,
4213 false,
4214 None,
4215 false,
4216 None,
4217 false,
4218 )
4219 .unwrap();
4220 let text = String::from_utf8_lossy(&bytes);
4221
4222 assert!(
4224 text.contains("Helvetica"),
4225 "Should contain regular Helvetica"
4226 );
4227 assert!(
4228 text.contains("Helvetica-Bold"),
4229 "Should contain Helvetica-Bold"
4230 );
4231 }
4232
4233 #[test]
4234 fn test_sanitize_font_name() {
4235 assert_eq!(PdfWriter::sanitize_font_name("Inter", 400, false), "Inter");
4236 assert_eq!(
4237 PdfWriter::sanitize_font_name("Inter", 700, false),
4238 "Inter-Bold"
4239 );
4240 assert_eq!(
4241 PdfWriter::sanitize_font_name("Inter", 400, true),
4242 "Inter-Italic"
4243 );
4244 assert_eq!(
4245 PdfWriter::sanitize_font_name("Inter", 700, true),
4246 "Inter-Bold-Italic"
4247 );
4248 assert_eq!(
4249 PdfWriter::sanitize_font_name("Noto Sans", 400, false),
4250 "NotoSans"
4251 );
4252 assert_eq!(
4253 PdfWriter::sanitize_font_name("Font (Display)", 400, false),
4254 "FontDisplay"
4255 );
4256 }
4257
4258 #[test]
4259 fn test_tounicode_cmap_format() {
4260 let mut glyph_to_char = HashMap::new();
4262 glyph_to_char.insert(36u16, 'A');
4263 glyph_to_char.insert(37u16, 'B');
4264
4265 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&glyph_to_char, "TestFont");
4266
4267 assert!(cmap.contains("begincmap"), "CMap should contain begincmap");
4268 assert!(cmap.contains("endcmap"), "CMap should contain endcmap");
4269 assert!(
4270 cmap.contains("beginbfchar"),
4271 "CMap should contain beginbfchar"
4272 );
4273 assert!(cmap.contains("endbfchar"), "CMap should contain endbfchar");
4274 assert!(
4275 cmap.contains("<0024> <0041>"),
4276 "Should map gid 0x0024 to Unicode 'A' 0x0041"
4277 );
4278 assert!(
4279 cmap.contains("<0025> <0042>"),
4280 "Should map gid 0x0025 to Unicode 'B' 0x0042"
4281 );
4282 assert!(
4283 cmap.contains("begincodespacerange"),
4284 "Should define codespace range"
4285 );
4286 assert!(
4287 cmap.contains("<0000> <FFFF>"),
4288 "Codespace should be 0000-FFFF"
4289 );
4290 }
4291
4292 #[test]
4293 fn test_w_array_format() {
4294 let mut char_to_gid = HashMap::new();
4295 char_to_gid.insert('A', 36u16);
4296
4297 let w_array_str = "[ 36 [600] ]";
4300 assert!(w_array_str.starts_with('['));
4301 assert!(w_array_str.ends_with(']'));
4302 }
4303
4304 #[test]
4305 fn test_hex_glyph_encoding() {
4306 let gid: u16 = 0x0041;
4308 let hex = format!("{:04X}", gid);
4309 assert_eq!(hex, "0041");
4310
4311 let gids = [0x0041u16, 0x0042, 0x0043];
4312 let hex_str: String = gids.iter().map(|g| format!("{:04X}", g)).collect();
4313 assert_eq!(hex_str, "004100420043");
4314 }
4315
4316 #[test]
4317 fn test_standard_font_still_uses_text_string() {
4318 let writer = PdfWriter::new();
4319 let font_context = FontContext::new();
4320
4321 let pages = vec![LayoutPage {
4322 width: 595.28,
4323 height: 841.89,
4324 elements: vec![LayoutElement {
4325 x: 54.0,
4326 y: 54.0,
4327 width: 100.0,
4328 height: 16.8,
4329 draw: DrawCommand::Text {
4330 lines: vec![TextLine {
4331 x: 54.0,
4332 y: 66.0,
4333 width: 50.0,
4334 height: 16.8,
4335 glyphs: vec![PositionedGlyph {
4336 glyph_id: 65,
4337 x_offset: 0.0,
4338 y_offset: 0.0,
4339 x_advance: 8.0,
4340 font_size: 12.0,
4341 font_family: "Helvetica".to_string(),
4342 font_weight: 400,
4343 font_style: FontStyle::Normal,
4344 char_value: 'H',
4345 color: None,
4346 href: None,
4347 text_decoration: TextDecoration::None,
4348 letter_spacing: 0.0,
4349 cluster_text: None,
4350 }],
4351 word_spacing: 0.0,
4352 }],
4353 color: Color::BLACK,
4354 text_decoration: TextDecoration::None,
4355 opacity: 1.0,
4356 },
4357 children: vec![],
4358 node_type: None,
4359 resolved_style: None,
4360 source_location: None,
4361 href: None,
4362 bookmark: None,
4363 alt: None,
4364 is_header_row: false,
4365 overflow: Overflow::default(),
4366 opacity: 1.0,
4367 }],
4368 fixed_header: vec![],
4369 fixed_footer: vec![],
4370 watermarks: vec![],
4371 config: PageConfig::default(),
4372 }];
4373
4374 let metadata = Metadata::default();
4375 let bytes = writer
4376 .write(
4377 &pages,
4378 &metadata,
4379 &font_context,
4380 false,
4381 None,
4382 false,
4383 None,
4384 false,
4385 )
4386 .unwrap();
4387 let text = String::from_utf8_lossy(&bytes);
4388
4389 assert!(
4391 text.contains("/Type1"),
4392 "Standard font should use Type1 subtype"
4393 );
4394 assert!(
4395 !text.contains("CIDFontType2"),
4396 "Standard font should not use CIDFontType2"
4397 );
4398 }
4399}