1pub mod certify;
30pub mod merge;
31pub mod redaction;
32pub(crate) mod tagged;
33pub(crate) mod xmp;
34
35use std::collections::{HashMap, HashSet};
36use std::fmt::Write as FmtWrite; use std::io::Write as IoWrite; use crate::error::FormeError;
40use crate::font::subset::subset_ttf;
41use crate::font::{FontContext, FontData, FontKey};
42use crate::layout::*;
43use crate::model::*;
44use crate::style::{Color, FontStyle, Overflow, TextDecoration, TransformOp};
45use crate::svg::SvgCommand;
46use miniz_oxide::deflate::compress_to_vec_zlib;
47
48struct LinkAnnotation {
50 x: f64,
51 y: f64,
52 width: f64,
53 height: f64,
54 href: String,
55}
56
57struct PdfBookmark {
59 title: String,
60 page_obj_id: usize,
61 y_pdf: f64,
62}
63
64struct FormFieldData {
66 field_type: FormFieldType,
67 name: String,
68 x: f64,
69 y: f64,
70 width: f64,
71 height: f64,
72 page_idx: usize,
73}
74
75pub struct PdfWriter;
76
77#[allow(dead_code)]
79struct CustomFontEmbedData {
80 ttf_data: Vec<u8>,
81 gid_remap: HashMap<u16, u16>,
83 glyph_to_char: HashMap<u16, char>,
85 char_to_gid: HashMap<char, u16>,
87 units_per_em: u16,
88 ascender: i16,
89 descender: i16,
90}
91
92struct FontUsage {
94 chars: HashSet<char>,
96 glyph_ids: HashSet<u16>,
98 glyph_to_char: HashMap<u16, char>,
100}
101
102struct PdfBuilder {
104 objects: Vec<PdfObject>,
105 font_objects: Vec<(FontKey, usize)>,
107 custom_font_data: HashMap<FontKey, CustomFontEmbedData>,
109 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 let transform_ops: &[TransformOp] = element
1213 .resolved_style
1214 .as_ref()
1215 .map(|s| s.transform.as_slice())
1216 .unwrap_or(&[]);
1217 let has_transform = !transform_ops.is_empty();
1218 if has_transform {
1219 let rs = element.resolved_style.as_ref().unwrap();
1220 let pdf_x = element.x;
1221 let pdf_y_bottom = page_height - element.y - element.height;
1222 let (ox_frac, oy_frac) = rs.transform_origin;
1223 let origin_x = pdf_x + element.width * ox_frac;
1224 let origin_y = pdf_y_bottom + (1.0 - oy_frac) * element.height;
1227
1228 let _ = writeln!(stream, "q");
1229 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", -origin_x, -origin_y);
1231 for op in transform_ops.iter().rev() {
1239 match op {
1240 TransformOp::Rotate { deg } => {
1241 let theta = (-deg).to_radians();
1246 let c = theta.cos();
1247 let s = theta.sin();
1248 let _ = writeln!(stream, "{:.6} {:.6} {:.6} {:.6} 0 0 cm", c, s, -s, c);
1249 }
1250 TransformOp::Scale { x, y } => {
1251 let _ = writeln!(stream, "{:.6} 0 0 {:.6} 0 0 cm", x, y);
1252 }
1253 TransformOp::Translate { x, y } => {
1254 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", x, -y);
1256 }
1257 }
1258 }
1259 let _ = writeln!(stream, "1 0 0 1 {:.4} {:.4} cm", origin_x, origin_y);
1261 }
1262
1263 match &element.draw {
1264 DrawCommand::None => {}
1265
1266 DrawCommand::Rect {
1267 background,
1268 border_width,
1269 border_color,
1270 border_radius,
1271 opacity,
1272 box_shadow,
1273 background_gradient,
1274 } => {
1275 let x = element.x;
1276 let y = page_height - element.y - element.height;
1277 let w = element.width;
1278 let h = element.height;
1279
1280 let needs_opacity = *opacity < 1.0;
1282 if needs_opacity {
1283 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1284 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1285 }
1286 }
1287
1288 if let Some(shadow) = box_shadow {
1294 if shadow.color.a > 0.0 {
1295 let sx = x + shadow.offset_x;
1300 let sy = y - shadow.offset_y;
1301 let needs_shadow_alpha = shadow.color.a < 1.0;
1302 if needs_shadow_alpha {
1303 if let Some((_, gs_name)) =
1304 builder.ext_gstate_map.get(&shadow.color.a.to_bits())
1305 {
1306 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1307 } else {
1308 let _ = writeln!(stream, "q");
1309 }
1310 } else {
1311 let _ = writeln!(stream, "q");
1312 }
1313 let _ = writeln!(
1314 stream,
1315 "{:.3} {:.3} {:.3} rg",
1316 shadow.color.r, shadow.color.g, shadow.color.b
1317 );
1318 if border_radius.top_left > 0.0
1319 || border_radius.top_right > 0.0
1320 || border_radius.bottom_right > 0.0
1321 || border_radius.bottom_left > 0.0
1322 {
1323 self.write_rounded_rect(stream, sx, sy, w, h, border_radius);
1324 } else {
1325 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", sx, sy, w, h);
1326 }
1327 let _ = writeln!(stream, "f\nQ");
1328 }
1329 }
1330
1331 if background_gradient.is_some() {
1337 let key = (page_idx, *gradient_counter);
1338 *gradient_counter += 1;
1339 if let Some((_, sh_name)) = builder.shading_map.get(&key) {
1340 let _ = writeln!(stream, "q");
1341 if border_radius.top_left > 0.0
1343 || border_radius.top_right > 0.0
1344 || border_radius.bottom_right > 0.0
1345 || border_radius.bottom_left > 0.0
1346 {
1347 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1348 let _ = writeln!(stream, "W n");
1349 } else {
1350 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re W n", x, y, w, h);
1351 }
1352 let _ =
1355 writeln!(stream, "1 0 0 1 {:.3} {:.3} cm\n/{} sh\nQ", x, y, sh_name);
1356 }
1357 } else if let Some(bg) = background {
1358 if bg.a > 0.0 {
1359 let _ = writeln!(stream, "q\n{:.3} {:.3} {:.3} rg", bg.r, bg.g, bg.b);
1360
1361 if border_radius.top_left > 0.0 {
1362 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1363 } else {
1364 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1365 }
1366
1367 let _ = writeln!(stream, "f\nQ");
1368 }
1369 }
1370
1371 let bw = border_width;
1372 if bw.top > 0.0 || bw.right > 0.0 || bw.bottom > 0.0 || bw.left > 0.0 {
1373 if (bw.top - bw.right).abs() < 0.001
1374 && (bw.right - bw.bottom).abs() < 0.001
1375 && (bw.bottom - bw.left).abs() < 0.001
1376 {
1377 let bc = &border_color.top;
1378 let _ = writeln!(
1379 stream,
1380 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w",
1381 bc.r, bc.g, bc.b, bw.top
1382 );
1383
1384 if border_radius.top_left > 0.0 {
1385 self.write_rounded_rect(stream, x, y, w, h, border_radius);
1386 } else {
1387 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
1388 }
1389
1390 let _ = writeln!(stream, "S\nQ");
1391 } else {
1392 self.write_border_sides(stream, x, y, w, h, bw, border_color);
1393 }
1394 }
1395
1396 if needs_opacity {
1397 let _ = writeln!(stream, "Q");
1398 }
1399 }
1400
1401 DrawCommand::Text {
1402 lines,
1403 color,
1404 text_decoration,
1405 opacity,
1406 } => {
1407 let needs_opacity = *opacity < 1.0;
1409 if needs_opacity {
1410 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1411 let _ = writeln!(stream, "q\n/{} gs", gs_name);
1412 }
1413 }
1414
1415 for line in lines {
1416 if line.glyphs.is_empty() {
1417 continue;
1418 }
1419
1420 let groups = Self::group_glyphs_by_style(&line.glyphs);
1423 let pdf_y = page_height - line.y;
1424
1425 let _ = writeln!(stream, "BT");
1426
1427 if line.word_spacing.abs() > 0.001 {
1429 let _ = writeln!(stream, "{:.4} Tw", line.word_spacing);
1430 }
1431
1432 let mut tm_x = 0.0_f64;
1434 let mut tm_y = 0.0_f64;
1435 let mut x_cursor = line.x;
1436
1437 let mut group_spans: Vec<(f64, f64, TextDecoration, Color)> = Vec::new();
1439
1440 for group in &groups {
1441 let first = &group[0];
1442 let glyph_color = first.color.unwrap_or(*color);
1443
1444 let idx = self.font_index(
1445 &first.font_family,
1446 first.font_weight,
1447 first.font_style,
1448 &builder.font_objects,
1449 );
1450 let italic =
1451 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
1452 let font_key = FontKey {
1453 family: first.font_family.clone(),
1454 weight: first.font_weight,
1455 italic,
1456 };
1457 let font_name = format!("F{}", idx);
1458
1459 let dx = x_cursor - tm_x;
1461 let dy = pdf_y - tm_y;
1462 let _ = writeln!(
1463 stream,
1464 "{:.3} {:.3} {:.3} rg\n/{} {:.1} Tf\n{:.2} Tc\n{:.2} {:.2} Td",
1465 glyph_color.r,
1466 glyph_color.g,
1467 glyph_color.b,
1468 font_name,
1469 first.font_size,
1470 first.letter_spacing,
1471 dx,
1472 dy
1473 );
1474 tm_x = x_cursor;
1475 tm_y = pdf_y;
1476
1477 let raw_text: String = group.iter().map(|g| g.char_value).collect();
1479 let has_placeholder = raw_text.contains(PAGE_NUMBER_SENTINEL)
1480 || raw_text.contains(TOTAL_PAGES_SENTINEL);
1481
1482 let is_custom = builder.custom_font_data.contains_key(&font_key);
1483
1484 if is_custom {
1485 if let Some(embed_data) = builder.custom_font_data.get(&font_key) {
1486 let mut hex = String::new();
1487 if has_placeholder {
1488 let pn = PAGE_NUMBER_SENTINEL.to_string();
1490 let tp = TOTAL_PAGES_SENTINEL.to_string();
1491 let text_after = raw_text
1492 .replace(&pn, &page_number.to_string())
1493 .replace(&tp, &total_pages.to_string());
1494 for ch in text_after.chars() {
1495 let gid =
1496 embed_data.char_to_gid.get(&ch).copied().unwrap_or(0);
1497 let _ = write!(hex, "{:04X}", gid);
1498 }
1499 } else {
1500 for g in group.iter() {
1502 let new_gid = embed_data
1503 .gid_remap
1504 .get(&g.glyph_id)
1505 .copied()
1506 .unwrap_or_else(|| {
1507 embed_data
1509 .char_to_gid
1510 .get(&g.char_value)
1511 .copied()
1512 .unwrap_or(0)
1513 });
1514 let _ = write!(hex, "{:04X}", new_gid);
1515 }
1516 }
1517 let _ = writeln!(stream, "<{}> Tj", hex);
1518 } else {
1519 let _ = writeln!(stream, "<> Tj");
1520 }
1521 } else {
1522 let pn = PAGE_NUMBER_SENTINEL.to_string();
1523 let tp = TOTAL_PAGES_SENTINEL.to_string();
1524 let text_after = raw_text
1525 .replace(&pn, &page_number.to_string())
1526 .replace(&tp, &total_pages.to_string());
1527 let mut text_str = String::new();
1528 for ch in text_after.chars() {
1529 let b = Self::unicode_to_winansi(ch).unwrap_or(b'?');
1530 match b {
1531 b'\\' => text_str.push_str("\\\\"),
1532 b'(' => text_str.push_str("\\("),
1533 b')' => text_str.push_str("\\)"),
1534 0x20..=0x7E => text_str.push(b as char),
1535 _ => {
1536 let _ = write!(text_str, "\\{:03o}", b);
1537 }
1538 }
1539 }
1540 let _ = writeln!(stream, "({}) Tj", text_str);
1541 }
1542
1543 let group_start_x = x_cursor;
1545
1546 if let Some(last) = group.last() {
1549 let space_count_in_group =
1550 group.iter().filter(|g| g.char_value == ' ').count();
1551 x_cursor = line.x
1552 + last.x_offset
1553 + last.x_advance
1554 + space_count_in_group as f64 * line.word_spacing;
1555 }
1556
1557 let group_dec = first.text_decoration;
1559 if !matches!(group_dec, TextDecoration::None) {
1560 group_spans.push((group_start_x, x_cursor, group_dec, glyph_color));
1561 }
1562 }
1563
1564 let _ = writeln!(stream, "ET");
1565
1566 for (span_x, span_end_x, dec, dec_color) in &group_spans {
1568 match dec {
1569 TextDecoration::Underline => {
1570 let underline_y = pdf_y - 1.5;
1571 let _ = write!(
1572 stream,
1573 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1574 dec_color.r, dec_color.g, dec_color.b,
1575 span_x, underline_y,
1576 span_end_x, underline_y
1577 );
1578 }
1579 TextDecoration::LineThrough => {
1580 let first_size =
1581 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
1582 let strikethrough_y = pdf_y + first_size * 0.3;
1583 let _ = write!(
1584 stream,
1585 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1586 dec_color.r, dec_color.g, dec_color.b,
1587 span_x, strikethrough_y,
1588 span_end_x, strikethrough_y
1589 );
1590 }
1591 TextDecoration::None => {}
1592 }
1593 }
1594
1595 if group_spans.is_empty() {
1597 if matches!(text_decoration, TextDecoration::Underline) {
1598 let underline_y = pdf_y - 1.5;
1599 let _ = write!(
1600 stream,
1601 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1602 color.r, color.g, color.b,
1603 line.x, underline_y,
1604 line.x + line.width, underline_y
1605 );
1606 }
1607 if matches!(text_decoration, TextDecoration::LineThrough) {
1608 let first_size =
1609 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
1610 let strikethrough_y = pdf_y + first_size * 0.3;
1611 let _ = write!(
1612 stream,
1613 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1614 color.r, color.g, color.b,
1615 line.x, strikethrough_y,
1616 line.x + line.width, strikethrough_y
1617 );
1618 }
1619 }
1620 }
1621
1622 if needs_opacity {
1623 let _ = writeln!(stream, "Q");
1624 }
1625 }
1626
1627 DrawCommand::Image { .. } => {
1628 let elem_idx = *element_counter;
1629 *element_counter += 1;
1630 if let Some(&img_idx) = builder.image_index_map.get(&(page_idx, elem_idx)) {
1631 let x = element.x;
1632 let y = page_height - element.y - element.height;
1633 let _ = write!(
1634 stream,
1635 "q\n{:.4} 0 0 {:.4} {:.2} {:.2} cm\n/Im{} Do\nQ\n",
1636 element.width, element.height, x, y, img_idx
1637 );
1638 } else {
1639 let x = element.x;
1641 let y = page_height - element.y - element.height;
1642 let _ = write!(
1643 stream,
1644 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
1645 x, y, element.width, element.height
1646 );
1647 }
1648 if tagged_mcid.is_some() {
1649 let _ = writeln!(stream, "EMC");
1650 if let Some(ref mut tb) = tag_builder {
1651 tb.end_element();
1652 }
1653 } else if is_artifact {
1654 let _ = writeln!(stream, "EMC");
1655 }
1656 return; }
1658
1659 DrawCommand::ImagePlaceholder => {
1660 *element_counter += 1;
1661 let x = element.x;
1662 let y = page_height - element.y - element.height;
1663 let _ = write!(
1664 stream,
1665 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
1666 x, y, element.width, element.height
1667 );
1668 if tagged_mcid.is_some() {
1669 let _ = writeln!(stream, "EMC");
1670 if let Some(ref mut tb) = tag_builder {
1671 tb.end_element();
1672 }
1673 } else if is_artifact {
1674 let _ = writeln!(stream, "EMC");
1675 }
1676 return;
1677 }
1678
1679 DrawCommand::Svg {
1680 commands,
1681 width: _svg_w,
1682 height: _svg_h,
1683 viewbox_min_x,
1684 viewbox_min_y,
1685 viewbox_width,
1686 viewbox_height,
1687 clip,
1688 } => {
1689 let x = element.x;
1690 let y = page_height - element.y - element.height;
1691
1692 let _ = writeln!(stream, "q");
1694 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", x, y);
1695
1696 if *viewbox_width > 0.0 && *viewbox_height > 0.0 {
1702 let raw_sx = element.width / *viewbox_width;
1703 let raw_sy = element.height / *viewbox_height;
1704 let s = raw_sx.min(raw_sy);
1705 let tx = (element.width - s * *viewbox_width) / 2.0;
1706 let ty = (element.height - s * *viewbox_height) / 2.0;
1707 let _ = writeln!(stream, "{:.4} 0 0 {:.4} {:.2} {:.2} cm", s, s, tx, ty);
1708 }
1709
1710 let _ = writeln!(stream, "1 0 0 -1 0 {:.2} cm", *viewbox_height);
1713
1714 if *viewbox_min_x != 0.0 || *viewbox_min_y != 0.0 {
1716 let _ = writeln!(
1717 stream,
1718 "1 0 0 1 {:.2} {:.2} cm",
1719 -*viewbox_min_x, -*viewbox_min_y
1720 );
1721 }
1722
1723 if *clip {
1725 let _ = writeln!(
1726 stream,
1727 "{:.2} {:.2} {:.2} {:.2} re W n",
1728 *viewbox_min_x, *viewbox_min_y, *viewbox_width, *viewbox_height
1729 );
1730 }
1731
1732 Self::write_svg_commands(stream, commands, &builder.ext_gstate_map);
1733
1734 let _ = writeln!(stream, "Q");
1735 if tagged_mcid.is_some() {
1736 let _ = writeln!(stream, "EMC");
1737 if let Some(ref mut tb) = tag_builder {
1738 tb.end_element();
1739 }
1740 } else if is_artifact {
1741 let _ = writeln!(stream, "EMC");
1742 }
1743 return;
1744 }
1745
1746 DrawCommand::Barcode {
1747 bars,
1748 bar_width,
1749 height,
1750 color,
1751 } => {
1752 *element_counter += 1;
1753 let _ = writeln!(stream, "q");
1754 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1755 for (i, &bar) in bars.iter().enumerate() {
1756 if bar == 1 {
1757 let bx = element.x + i as f64 * bar_width;
1758 let by = page_height - element.y - height;
1759 let _ = writeln!(
1760 stream,
1761 "{:.2} {:.2} {:.2} {:.2} re",
1762 bx, by, bar_width, height
1763 );
1764 }
1765 }
1766 let _ = writeln!(stream, "f\nQ");
1767 if tagged_mcid.is_some() {
1768 let _ = writeln!(stream, "EMC");
1769 if let Some(ref mut tb) = tag_builder {
1770 tb.end_element();
1771 }
1772 } else if is_artifact {
1773 let _ = writeln!(stream, "EMC");
1774 }
1775 return;
1776 }
1777
1778 DrawCommand::QrCode {
1779 modules,
1780 module_size,
1781 color,
1782 } => {
1783 *element_counter += 1;
1784 let _ = writeln!(stream, "q");
1785 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1786 for (row_idx, row) in modules.iter().enumerate() {
1787 for (col_idx, &dark) in row.iter().enumerate() {
1788 if dark {
1789 let mx = element.x + col_idx as f64 * module_size;
1790 let my = page_height - element.y - (row_idx as f64 + 1.0) * module_size;
1791 let _ = writeln!(
1792 stream,
1793 "{:.2} {:.2} {:.2} {:.2} re",
1794 mx, my, module_size, module_size
1795 );
1796 }
1797 }
1798 }
1799 let _ = writeln!(stream, "f\nQ");
1800 if tagged_mcid.is_some() {
1801 let _ = writeln!(stream, "EMC");
1802 if let Some(ref mut tb) = tag_builder {
1803 tb.end_element();
1804 }
1805 } else if is_artifact {
1806 let _ = writeln!(stream, "EMC");
1807 }
1808 return;
1809 }
1810
1811 DrawCommand::Chart { primitives } => {
1812 *element_counter += 1;
1813 let _ = writeln!(stream, "q");
1814 let _ = writeln!(
1816 stream,
1817 "1 0 0 -1 {:.4} {:.4} cm",
1818 element.x,
1819 page_height - element.y
1820 );
1821
1822 for prim in primitives {
1823 write_chart_primitive(stream, prim, element.height, builder);
1824 }
1825
1826 let _ = writeln!(stream, "Q");
1827 if tagged_mcid.is_some() {
1828 let _ = writeln!(stream, "EMC");
1829 if let Some(ref mut tb) = tag_builder {
1830 tb.end_element();
1831 }
1832 } else if is_artifact {
1833 let _ = writeln!(stream, "EMC");
1834 }
1835 return;
1836 }
1837
1838 DrawCommand::Watermark {
1839 lines,
1840 color,
1841 opacity,
1842 angle_rad,
1843 font_family: _,
1844 } => {
1845 let _ = writeln!(stream, "q");
1846 if *opacity < 1.0 {
1848 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1849 let _ = writeln!(stream, "/{} gs", gs_name);
1850 }
1851 }
1852 let pdf_cx = element.x;
1854 let pdf_cy = page_height - element.y;
1855 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", pdf_cx, pdf_cy);
1856 let cos_a = angle_rad.cos();
1858 let sin_a = angle_rad.sin();
1859 let _ = writeln!(
1860 stream,
1861 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
1862 cos_a, sin_a, -sin_a, cos_a
1863 );
1864 let _ = writeln!(stream, "BT");
1866 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1867 if let Some(line) = lines.first() {
1868 let groups = Self::group_glyphs_by_style(&line.glyphs);
1869 let text_width = line.width;
1870 let cap_height = line.height * 0.7;
1871 let _ = writeln!(
1872 stream,
1873 "{:.2} {:.2} Td",
1874 -text_width / 2.0,
1875 -cap_height / 2.0
1876 );
1877 for group in &groups {
1878 let first = &group[0];
1879 let italic =
1880 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
1881 let fk = FontKey {
1882 family: first.font_family.clone(),
1883 weight: first.font_weight,
1884 italic,
1885 };
1886 let idx = self.font_index(
1887 &first.font_family,
1888 first.font_weight,
1889 first.font_style,
1890 &builder.font_objects,
1891 );
1892 let font_name = format!("F{}", idx);
1893 let _ = writeln!(stream, "/{} {:.1} Tf", font_name, first.font_size);
1894 let is_custom = builder.custom_font_data.contains_key(&fk);
1895 if is_custom {
1896 if let Some(embed_data) = builder.custom_font_data.get(&fk) {
1897 let mut hex = String::new();
1898 for g in group.iter() {
1899 let gid =
1900 embed_data.gid_remap.get(&g.glyph_id).copied().unwrap_or(0);
1901 let _ = write!(hex, "{:04X}", gid);
1902 }
1903 let _ = writeln!(stream, "<{}> Tj", hex);
1904 }
1905 } else {
1906 let hex_str: String = group
1907 .iter()
1908 .map(|g| format!("{:02X}", g.glyph_id as u8))
1909 .collect();
1910 let _ = writeln!(stream, "<{}> Tj", hex_str);
1911 }
1912 }
1913 }
1914 let _ = writeln!(stream, "ET");
1915 let _ = writeln!(stream, "Q");
1916 if tagged_mcid.is_some() {
1917 let _ = writeln!(stream, "EMC");
1918 if let Some(ref mut tb) = tag_builder {
1919 tb.end_element();
1920 }
1921 } else if is_artifact {
1922 let _ = writeln!(stream, "EMC");
1923 }
1924 return;
1925 }
1926
1927 DrawCommand::FormField { field_type, .. } => {
1928 let pdf_x = element.x;
1932 let pdf_y = page_height - element.y - element.height;
1933 let w = element.width;
1934 let h = element.height;
1935 let _ = writeln!(stream, "q");
1936 match field_type {
1937 FormFieldType::Checkbox { checked, .. } => {
1938 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
1941 let _ =
1942 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
1943 if *checked {
1944 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
1946 let sx = w / 14.0;
1947 let sy = h / 14.0;
1948 let _ = writeln!(
1949 stream,
1950 "{:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l {:.2} {:.2} l f",
1951 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
1952 pdf_x + 5.5 * sx, pdf_y + 2.0 * sy,
1953 pdf_x + 12.0 * sx, pdf_y + 11.0 * sy,
1954 pdf_x + 11.0 * sx, pdf_y + 12.0 * sy,
1955 pdf_x + 5.5 * sx, pdf_y + 4.5 * sy,
1956 pdf_x + 3.0 * sx, pdf_y + 7.0 * sy,
1957 pdf_x + 2.0 * sx, pdf_y + 6.0 * sy,
1958 );
1959 }
1960 }
1961 FormFieldType::RadioButton { checked, .. } => {
1962 let _ = writeln!(stream, "0.6 0.6 0.6 RG"); let _ = writeln!(stream, "0.5 w");
1965 let _ =
1966 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re S", pdf_x, pdf_y, w, h);
1967 if *checked {
1968 let cx = pdf_x + w / 2.0;
1970 let cy = pdf_y + h / 2.0;
1971 let r = (w.min(h) / 2.0) * 0.6;
1972 let k = r * 0.5523;
1973 let _ = writeln!(stream, "0.2 0.2 0.2 rg");
1974 let _ = writeln!(
1975 stream,
1976 "{:.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",
1977 cx, cy + r,
1978 cx + k, cy + r, cx + r, cy + k, cx + r, cy,
1979 cx + r, cy - k, cx + k, cy - r, cx, cy - r,
1980 cx - k, cy - r, cx - r, cy - k, cx - r, cy,
1981 cx - r, cy + k, cx - k, cy + r, cx, cy + r,
1982 );
1983 }
1984 }
1985 FormFieldType::TextField {
1986 value,
1987 placeholder,
1988 font_size,
1989 multiline,
1990 password,
1991 ..
1992 } => {
1993 let _ = writeln!(stream, "1 1 1 rg");
1995 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
1996 let _ = writeln!(stream, "0.5 w");
1997 let _ =
1998 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
1999 if flatten_forms {
2001 let has_value = value.as_ref().is_some_and(|v| !v.is_empty());
2002 if has_value {
2003 let val = value.as_ref().unwrap();
2004 let display_text = if *password {
2005 "\u{2022}".repeat(val.len())
2006 } else {
2007 val.clone()
2008 };
2009 let font_idx = builder
2010 .font_objects
2011 .iter()
2012 .enumerate()
2013 .find(|(_, (key, _))| {
2014 key.family == "Helvetica"
2015 && key.weight == 400
2016 && !key.italic
2017 })
2018 .map(|(i, _)| i)
2019 .unwrap_or(0);
2020 if *multiline {
2021 let metrics = crate::font::StandardFont::Helvetica.metrics();
2023 let max_w = w - 4.0;
2024 let mut lines: Vec<String> = Vec::new();
2025 for paragraph in display_text.split('\n') {
2026 let mut line = String::new();
2027 let mut line_w = 0.0;
2028 for word in paragraph.split_whitespace() {
2029 let word_w =
2030 metrics.measure_string(word, *font_size, 0.0);
2031 let space_w = if line.is_empty() {
2032 0.0
2033 } else {
2034 metrics.measure_string(" ", *font_size, 0.0)
2035 };
2036 if word_w > max_w {
2038 let mut char_line = String::new();
2039 let mut char_w = 0.0;
2040 for ch in word.chars() {
2041 let cw = metrics.char_width(ch, *font_size);
2042 if !char_line.is_empty() && char_w + cw > max_w
2043 {
2044 if !line.is_empty() {
2045 lines.push(line.clone());
2046 line.clear();
2047 line_w = 0.0;
2048 }
2049 lines.push(char_line.clone());
2050 char_line.clear();
2051 char_w = 0.0;
2052 }
2053 char_line.push(ch);
2054 char_w += cw;
2055 }
2056 if !char_line.is_empty() {
2058 if !line.is_empty() {
2059 line.push(' ');
2060 line_w += metrics
2061 .measure_string(" ", *font_size, 0.0);
2062 }
2063 line.push_str(&char_line);
2064 line_w += char_w;
2065 }
2066 continue;
2067 }
2068 if !line.is_empty() && line_w + space_w + word_w > max_w
2069 {
2070 lines.push(line.clone());
2071 line.clear();
2072 line_w = 0.0;
2073 }
2074 if !line.is_empty() {
2075 line.push(' ');
2076 line_w += space_w;
2077 }
2078 line.push_str(word);
2079 line_w += word_w;
2080 }
2081 if !line.is_empty() {
2082 lines.push(line);
2083 }
2084 }
2085 let text_y = pdf_y + h - font_size - 2.0;
2086 for (i, line_text) in lines.iter().enumerate() {
2087 let ly = text_y - (i as f64) * (font_size * 1.2);
2088 if ly < pdf_y {
2089 break;
2090 }
2091 let esc = Self::encode_winansi_text(line_text);
2092 let _ = writeln!(
2093 stream,
2094 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2095 font_idx,
2096 font_size,
2097 pdf_x + 2.0,
2098 ly,
2099 esc
2100 );
2101 }
2102 } else {
2103 let escaped = Self::encode_winansi_text(&display_text);
2104 let text_y = pdf_y + (h - font_size) / 2.0;
2105 let _ = writeln!(
2106 stream,
2107 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2108 font_idx,
2109 font_size,
2110 pdf_x + 2.0,
2111 text_y,
2112 escaped
2113 );
2114 }
2115 } else if let Some(ref ph) = placeholder {
2116 if !ph.is_empty() {
2117 let font_idx = builder
2119 .font_objects
2120 .iter()
2121 .enumerate()
2122 .find(|(_, (key, _))| {
2123 key.family == "Helvetica"
2124 && key.weight == 400
2125 && !key.italic
2126 })
2127 .map(|(i, _)| i)
2128 .unwrap_or(0);
2129 let escaped = Self::encode_winansi_text(ph);
2130 let text_y = pdf_y + (h - font_size) / 2.0;
2131 let _ = writeln!(
2132 stream,
2133 "BT /F{} {:.1} Tf 0.6 g {:.2} {:.2} Td ({}) Tj ET",
2134 font_idx,
2135 font_size,
2136 pdf_x + 2.0,
2137 text_y,
2138 escaped
2139 );
2140 }
2141 }
2142 }
2143 }
2144 FormFieldType::Dropdown {
2145 value, font_size, ..
2146 } => {
2147 let _ = writeln!(stream, "1 1 1 rg");
2149 let _ = writeln!(stream, "0.6 0.6 0.6 RG");
2150 let _ = writeln!(stream, "0.5 w");
2151 let _ =
2152 writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re B", pdf_x, pdf_y, w, h);
2153 if flatten_forms {
2155 if let Some(ref val) = value {
2156 if !val.is_empty() {
2157 let font_idx = builder
2158 .font_objects
2159 .iter()
2160 .enumerate()
2161 .find(|(_, (key, _))| {
2162 key.family == "Helvetica"
2163 && key.weight == 400
2164 && !key.italic
2165 })
2166 .map(|(i, _)| i)
2167 .unwrap_or(0);
2168 let escaped = Self::encode_winansi_text(val);
2169 let text_y = pdf_y + (h - font_size) / 2.0;
2170 let _ = writeln!(
2171 stream,
2172 "BT /F{} {:.1} Tf 0 g {:.2} {:.2} Td ({}) Tj ET",
2173 font_idx,
2174 font_size,
2175 pdf_x + 2.0,
2176 text_y,
2177 escaped
2178 );
2179 }
2180 }
2181 }
2182 }
2183 }
2184 let _ = writeln!(stream, "Q");
2185 }
2186 }
2187
2188 let clip_overflow = matches!(element.overflow, Overflow::Hidden);
2193 if clip_overflow {
2194 let clip_x = element.x;
2195 let clip_y = page_height - element.y - element.height;
2196 let clip_w = element.width;
2197 let clip_h = element.height;
2198 let radius = if let DrawCommand::Rect { border_radius, .. } = &element.draw {
2202 Some(border_radius)
2203 } else {
2204 None
2205 };
2206 let has_rounded_corners = radius.is_some_and(|r| {
2207 r.top_left > 0.0 || r.top_right > 0.0 || r.bottom_right > 0.0 || r.bottom_left > 0.0
2208 });
2209 let _ = writeln!(stream, "q");
2210 if has_rounded_corners {
2211 self.write_rounded_rect(stream, clip_x, clip_y, clip_w, clip_h, radius.unwrap());
2212 let _ = writeln!(stream, "W n");
2213 } else {
2214 let _ = writeln!(
2215 stream,
2216 "{:.2} {:.2} {:.2} {:.2} re W n",
2217 clip_x, clip_y, clip_w, clip_h
2218 );
2219 }
2220 }
2221
2222 for child in &element.children {
2223 self.write_element(
2224 stream,
2225 child,
2226 page_height,
2227 builder,
2228 page_idx,
2229 element_counter,
2230 gradient_counter,
2231 page_number,
2232 total_pages,
2233 tag_builder.as_deref_mut(),
2234 flatten_forms,
2235 );
2236 }
2237
2238 if clip_overflow {
2239 let _ = writeln!(stream, "Q");
2240 }
2241
2242 if has_transform {
2244 let _ = writeln!(stream, "Q");
2245 }
2246
2247 if needs_element_opacity {
2250 let _ = writeln!(stream, "Q");
2251 }
2252
2253 if tagged_mcid.is_some() {
2255 let _ = writeln!(stream, "EMC");
2256 if let Some(ref mut tb) = tag_builder {
2257 tb.end_element();
2258 }
2259 } else if is_artifact {
2260 let _ = writeln!(stream, "EMC");
2261 }
2262 }
2263
2264 fn write_rounded_rect(
2265 &self,
2266 stream: &mut String,
2267 x: f64,
2268 y: f64,
2269 w: f64,
2270 h: f64,
2271 r: &crate::style::CornerValues,
2272 ) {
2273 let k = 0.5522847498;
2274
2275 let tl = r.top_left.min(w / 2.0).min(h / 2.0);
2276 let tr = r.top_right.min(w / 2.0).min(h / 2.0);
2277 let br = r.bottom_right.min(w / 2.0).min(h / 2.0);
2278 let bl = r.bottom_left.min(w / 2.0).min(h / 2.0);
2279
2280 let _ = writeln!(stream, "{:.2} {:.2} m", x + bl, y);
2281
2282 let _ = writeln!(stream, "{:.2} {:.2} l", x + w - br, y);
2283 if br > 0.0 {
2284 let _ = writeln!(
2285 stream,
2286 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2287 x + w - br + br * k,
2288 y,
2289 x + w,
2290 y + br - br * k,
2291 x + w,
2292 y + br
2293 );
2294 }
2295
2296 let _ = writeln!(stream, "{:.2} {:.2} l", x + w, y + h - tr);
2297 if tr > 0.0 {
2298 let _ = writeln!(
2299 stream,
2300 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2301 x + w,
2302 y + h - tr + tr * k,
2303 x + w - tr + tr * k,
2304 y + h,
2305 x + w - tr,
2306 y + h
2307 );
2308 }
2309
2310 let _ = writeln!(stream, "{:.2} {:.2} l", x + tl, y + h);
2311 if tl > 0.0 {
2312 let _ = writeln!(
2313 stream,
2314 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2315 x + tl - tl * k,
2316 y + h,
2317 x,
2318 y + h - tl + tl * k,
2319 x,
2320 y + h - tl
2321 );
2322 }
2323
2324 let _ = writeln!(stream, "{:.2} {:.2} l", x, y + bl);
2325 if bl > 0.0 {
2326 let _ = writeln!(
2327 stream,
2328 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2329 x,
2330 y + bl - bl * k,
2331 x + bl - bl * k,
2332 y,
2333 x + bl,
2334 y
2335 );
2336 }
2337
2338 let _ = writeln!(stream, "h");
2339 }
2340
2341 #[allow(clippy::too_many_arguments)]
2342 fn write_border_sides(
2343 &self,
2344 stream: &mut String,
2345 x: f64,
2346 y: f64,
2347 w: f64,
2348 h: f64,
2349 bw: &Edges,
2350 bc: &crate::style::EdgeValues<Color>,
2351 ) {
2352 if bw.top > 0.0 {
2353 let _ = write!(
2354 stream,
2355 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2356 bc.top.r,
2357 bc.top.g,
2358 bc.top.b,
2359 bw.top,
2360 x,
2361 y + h,
2362 x + w,
2363 y + h
2364 );
2365 }
2366 if bw.bottom > 0.0 {
2367 let _ = write!(
2368 stream,
2369 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2370 bc.bottom.r,
2371 bc.bottom.g,
2372 bc.bottom.b,
2373 bw.bottom,
2374 x,
2375 y,
2376 x + w,
2377 y
2378 );
2379 }
2380 if bw.left > 0.0 {
2381 let _ = write!(
2382 stream,
2383 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2384 bc.left.r,
2385 bc.left.g,
2386 bc.left.b,
2387 bw.left,
2388 x,
2389 y,
2390 x,
2391 y + h
2392 );
2393 }
2394 if bw.right > 0.0 {
2395 let _ = write!(
2396 stream,
2397 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
2398 bc.right.r,
2399 bc.right.g,
2400 bc.right.b,
2401 bw.right,
2402 x + w,
2403 y,
2404 x + w,
2405 y + h
2406 );
2407 }
2408 }
2409
2410 fn register_fonts(
2413 &self,
2414 builder: &mut PdfBuilder,
2415 pages: &[LayoutPage],
2416 font_context: &FontContext,
2417 ) -> Result<(), FormeError> {
2418 let mut font_usage_map: HashMap<FontKey, FontUsage> = HashMap::new();
2420
2421 for page in pages {
2422 Self::collect_font_usage(&page.elements, &mut font_usage_map);
2423 }
2424
2425 let mut keys: Vec<FontKey> = font_usage_map.keys().cloned().collect();
2426
2427 keys.sort_by(|a, b| {
2429 a.family
2430 .cmp(&b.family)
2431 .then(a.weight.cmp(&b.weight))
2432 .then(a.italic.cmp(&b.italic))
2433 });
2434 keys.dedup();
2435
2436 if keys.is_empty() {
2438 keys.push(FontKey {
2439 family: "Helvetica".to_string(),
2440 weight: 400,
2441 italic: false,
2442 });
2443 }
2444
2445 for key in &keys {
2446 let font_data = font_context.resolve(&key.family, key.weight, key.italic);
2447
2448 match font_data {
2449 FontData::Standard(std_font) => {
2450 let obj_id = builder.objects.len();
2451 let metrics = std_font.metrics();
2454 let widths_str: String = metrics
2455 .widths
2456 .iter()
2457 .map(|w| w.to_string())
2458 .collect::<Vec<_>>()
2459 .join(" ");
2460 let font_dict = format!(
2461 "<< /Type /Font /Subtype /Type1 /BaseFont /{} \
2462 /Encoding /WinAnsiEncoding \
2463 /FirstChar 32 /LastChar 255 /Widths [{}] >>",
2464 std_font.pdf_name(),
2465 widths_str,
2466 );
2467 builder.objects.push(PdfObject {
2468 id: obj_id,
2469 data: font_dict.into_bytes(),
2470 });
2471 builder.font_objects.push((key.clone(), obj_id));
2472 }
2473 FontData::Custom { data, .. } => {
2474 let usage = font_usage_map.get(key);
2475 let used_glyph_ids = usage.map(|u| &u.glyph_ids);
2476 let used_chars = usage.map(|u| &u.chars);
2477 let glyph_to_char = usage.map(|u| &u.glyph_to_char);
2478 let type0_obj_id = Self::write_custom_font_objects(
2479 builder,
2480 key,
2481 data,
2482 used_glyph_ids.cloned().unwrap_or_default(),
2483 used_chars.cloned().unwrap_or_default(),
2484 glyph_to_char.cloned().unwrap_or_default(),
2485 )?;
2486 builder.font_objects.push((key.clone(), type0_obj_id));
2487 }
2488 }
2489 }
2490
2491 Ok(())
2492 }
2493
2494 fn collect_font_usage(
2496 elements: &[LayoutElement],
2497 font_usage: &mut HashMap<FontKey, FontUsage>,
2498 ) {
2499 for element in elements {
2500 let lines_opt = match &element.draw {
2501 DrawCommand::Text { lines, .. } => Some(lines),
2502 DrawCommand::Watermark { lines, .. } => Some(lines),
2503 _ => None,
2504 };
2505 if let Some(lines) = lines_opt {
2506 for line in lines {
2507 for glyph in &line.glyphs {
2508 let italic =
2509 matches!(glyph.font_style, FontStyle::Italic | FontStyle::Oblique);
2510 let key = FontKey {
2511 family: glyph.font_family.clone(),
2512 weight: glyph.font_weight,
2513 italic,
2514 };
2515 let usage = font_usage.entry(key).or_insert_with(|| FontUsage {
2516 chars: HashSet::new(),
2517 glyph_ids: HashSet::new(),
2518 glyph_to_char: HashMap::new(),
2519 });
2520 usage.chars.insert(glyph.char_value);
2521 usage.glyph_ids.insert(glyph.glyph_id);
2522 usage
2524 .glyph_to_char
2525 .entry(glyph.glyph_id)
2526 .or_insert(glyph.char_value);
2527 if let Some(ref ct) = glyph.cluster_text {
2529 if let Some(first_char) = ct.chars().next() {
2531 usage
2532 .glyph_to_char
2533 .entry(glyph.glyph_id)
2534 .or_insert(first_char);
2535 }
2536 }
2537 }
2538 }
2539 }
2540 Self::collect_font_usage(&element.children, font_usage);
2541 }
2542 }
2543
2544 fn register_shadings(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2550 for (page_idx, page) in pages.iter().enumerate() {
2551 let mut counter = 0usize;
2552 Self::collect_shadings_recursive(&page.elements, page_idx, &mut counter, builder);
2553 }
2554 }
2555
2556 fn collect_shadings_recursive(
2557 elements: &[LayoutElement],
2558 page_idx: usize,
2559 counter: &mut usize,
2560 builder: &mut PdfBuilder,
2561 ) {
2562 for element in elements {
2563 if let DrawCommand::Rect {
2564 background_gradient: Some(gradient),
2565 ..
2566 } = &element.draw
2567 {
2568 let ordinal = *counter;
2569 *counter += 1;
2570 let (obj_id, name) =
2571 Self::write_shading_objects(builder, gradient, element, ordinal);
2572 builder
2573 .shading_map
2574 .insert((page_idx, ordinal), (obj_id, name));
2575 }
2576 Self::collect_shadings_recursive(&element.children, page_idx, counter, builder);
2577 }
2578 }
2579
2580 fn write_shading_objects(
2586 builder: &mut PdfBuilder,
2587 gradient: &crate::style::Background,
2588 element: &LayoutElement,
2589 ordinal: usize,
2590 ) -> (usize, String) {
2591 use crate::style::Background;
2592 use crate::style::GradientStop;
2593
2594 let black = Color {
2598 r: 0.0,
2599 g: 0.0,
2600 b: 0.0,
2601 a: 1.0,
2602 };
2603 let stops: Vec<GradientStop> = match gradient {
2604 Background::Color(c) => vec![
2605 GradientStop {
2606 position: 0.0,
2607 color: *c,
2608 },
2609 GradientStop {
2610 position: 1.0,
2611 color: *c,
2612 },
2613 ],
2614 Background::Linear(g) => normalize_gradient_stops(&g.stops, black),
2615 Background::Radial(g) => normalize_gradient_stops(&g.stops, black),
2616 };
2617
2618 let function_id = if stops.len() <= 2 {
2622 let c0 = stops.first().map(|s| s.color).unwrap_or(black);
2623 let c1 = stops.last().map(|s| s.color).unwrap_or(c0);
2624 let id = builder.objects.len();
2625 let data = format!(
2626 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
2627 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
2628 );
2629 builder.objects.push(PdfObject {
2630 id,
2631 data: data.into_bytes(),
2632 });
2633 id
2634 } else {
2635 let mut sub_ids: Vec<usize> = Vec::with_capacity(stops.len() - 1);
2637 for window in stops.windows(2) {
2638 let c0 = window[0].color;
2639 let c1 = window[1].color;
2640 let id = builder.objects.len();
2641 let data = format!(
2642 "<< /FunctionType 2 /Domain [0 1] /C0 [{:.4} {:.4} {:.4}] /C1 [{:.4} {:.4} {:.4}] /N 1 >>",
2643 c0.r, c0.g, c0.b, c1.r, c1.g, c1.b,
2644 );
2645 builder.objects.push(PdfObject {
2646 id,
2647 data: data.into_bytes(),
2648 });
2649 sub_ids.push(id);
2650 }
2651 let bounds: Vec<String> = stops[1..stops.len() - 1]
2655 .iter()
2656 .map(|s| format!("{:.4}", s.position))
2657 .collect();
2658 let encode: Vec<&str> = (0..sub_ids.len()).map(|_| "0 1").collect();
2659 let functions: Vec<String> = sub_ids.iter().map(|i| format!("{} 0 R", i)).collect();
2660 let id = builder.objects.len();
2661 let data = format!(
2662 "<< /FunctionType 3 /Domain [0 1] /Functions [{}] /Bounds [{}] /Encode [{}] >>",
2663 functions.join(" "),
2664 bounds.join(" "),
2665 encode.join(" "),
2666 );
2667 builder.objects.push(PdfObject {
2668 id,
2669 data: data.into_bytes(),
2670 });
2671 id
2672 };
2673
2674 let _ = element.x;
2678 let _ = element.y;
2679 let w = element.width;
2680 let h = element.height;
2681
2682 let shading_id = builder.objects.len();
2683 let shading_data = match gradient {
2684 Background::Linear(g) => {
2685 let theta = g.angle_deg.to_radians();
2695 let dx = theta.sin();
2696 let dy = theta.cos();
2697 let axis_len = w * dx.abs() + h * dy.abs();
2700 let cx_rel = w / 2.0;
2703 let cy_rel = h / 2.0;
2704 let half = axis_len / 2.0;
2705 let x0 = cx_rel - dx * half;
2706 let y0 = cy_rel - dy * half;
2707 let x1 = cx_rel + dx * half;
2708 let y1 = cy_rel + dy * half;
2709 format!(
2710 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
2711 x0, y0, x1, y1, function_id,
2712 )
2713 }
2714 Background::Radial(_) => {
2715 let cx_rel = w / 2.0;
2718 let cy_rel = h / 2.0;
2719 let r_outer = (w / 2.0).max(h / 2.0);
2720 format!(
2721 "<< /ShadingType 3 /ColorSpace /DeviceRGB /Coords [{:.3} {:.3} 0 {:.3} {:.3} {:.3}] /Function {} 0 R /Extend [true true] >>",
2722 cx_rel, cy_rel, cx_rel, cy_rel, r_outer, function_id,
2723 )
2724 }
2725 Background::Color(_) => {
2726 format!(
2730 "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 0 0] /Function {} 0 R /Extend [true true] >>",
2731 function_id,
2732 )
2733 }
2734 };
2735 builder.objects.push(PdfObject {
2736 id: shading_id,
2737 data: shading_data.into_bytes(),
2738 });
2739 (shading_id, format!("Sh{}", ordinal))
2740 }
2741
2742 fn register_page_background_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2746 for (page_idx, page) in pages.iter().enumerate() {
2747 let Some(src) = &page.config.background_image else {
2748 continue;
2749 };
2750 if let Some(&entry) = builder.page_background_url_cache.get(src) {
2752 builder.page_background_image_map.insert(page_idx, entry);
2753 continue;
2754 }
2755 match crate::image_loader::load_image(src) {
2758 Ok(image_data) => {
2759 let img_idx = builder.image_objects.len();
2760 let dims = (img_idx, image_data.width_px, image_data.height_px);
2761 let xobj_id = Self::write_image_xobject(builder, &image_data);
2762 builder.image_objects.push(xobj_id);
2763 builder.page_background_image_map.insert(page_idx, dims);
2764 builder.page_background_url_cache.insert(src.clone(), dims);
2765 }
2766 Err(e) => {
2767 eprintln!("[forme] page background image failed to load: {}", e);
2768 }
2769 }
2770 }
2771 }
2772
2773 fn write_page_background(
2778 &self,
2779 stream: &mut String,
2780 page: &LayoutPage,
2781 page_bg: (usize, u32, u32),
2782 builder: &PdfBuilder,
2783 ) {
2784 use crate::model::{BackgroundPosition, BackgroundSize};
2785 let (img_idx, iw_px, ih_px) = page_bg;
2786 let page_w = page.width;
2787 let page_h = page.height;
2788 let iw = iw_px as f64;
2789 let ih = ih_px as f64;
2790
2791 let size = page.config.background_size.unwrap_or_default();
2792 let (dest_w, dest_h) = match size {
2793 BackgroundSize::Fill => (page_w, page_h),
2794 BackgroundSize::Cover => {
2795 let s = (page_w / iw).max(page_h / ih);
2796 (iw * s, ih * s)
2797 }
2798 BackgroundSize::Contain => {
2799 let s = (page_w / iw).min(page_h / ih);
2800 (iw * s, ih * s)
2801 }
2802 };
2803
2804 let position = page.config.background_position.unwrap_or_default();
2808 let (dest_x, dest_y) = match position {
2811 BackgroundPosition::TopLeft => (0.0, page_h - dest_h),
2812 BackgroundPosition::TopRight => (page_w - dest_w, page_h - dest_h),
2813 BackgroundPosition::BottomLeft => (0.0, 0.0),
2814 BackgroundPosition::BottomRight => (page_w - dest_w, 0.0),
2815 BackgroundPosition::Center => ((page_w - dest_w) / 2.0, (page_h - dest_h) / 2.0),
2816 };
2817
2818 let opacity = page.config.background_opacity.unwrap_or(1.0);
2820 let needs_opacity = opacity < 1.0;
2821 if needs_opacity {
2822 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
2823 let _ = writeln!(stream, "q\n/{} gs", gs_name);
2824 } else {
2825 let _ = writeln!(stream, "q");
2826 }
2827 } else {
2828 let _ = writeln!(stream, "q");
2829 }
2830 let _ = writeln!(
2833 stream,
2834 "{:.2} 0 0 {:.2} {:.2} {:.2} cm\n/Im{} Do\nQ",
2835 dest_w, dest_h, dest_x, dest_y, img_idx,
2836 );
2837 }
2838
2839 fn register_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2841 for (page_idx, page) in pages.iter().enumerate() {
2842 let mut element_counter = 0usize;
2843 Self::collect_images_recursive(&page.elements, page_idx, &mut element_counter, builder);
2844 }
2845 }
2846
2847 fn collect_images_recursive(
2848 elements: &[LayoutElement],
2849 page_idx: usize,
2850 element_counter: &mut usize,
2851 builder: &mut PdfBuilder,
2852 ) {
2853 for element in elements {
2854 match &element.draw {
2855 DrawCommand::Image { image_data } => {
2856 let elem_idx = *element_counter;
2857 *element_counter += 1;
2858
2859 let img_idx = builder.image_objects.len();
2860 let xobj_id = Self::write_image_xobject(builder, image_data);
2861 builder.image_objects.push(xobj_id);
2862 builder
2863 .image_index_map
2864 .insert((page_idx, elem_idx), img_idx);
2865 }
2866 DrawCommand::ImagePlaceholder => {
2867 *element_counter += 1;
2868 }
2869 _ => {
2870 Self::collect_images_recursive(
2871 &element.children,
2872 page_idx,
2873 element_counter,
2874 builder,
2875 );
2876 }
2877 }
2878 }
2879 }
2880
2881 fn register_ext_gstates(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
2883 let mut unique_opacities: Vec<f64> = Vec::new();
2884 for page in pages {
2885 Self::collect_opacities_recursive(&page.elements, &mut unique_opacities);
2886 if let Some(o) = page.config.background_opacity {
2888 if o < 1.0 {
2889 unique_opacities.push(o);
2890 }
2891 }
2892 }
2893 unique_opacities.sort_by(|a, b| a.partial_cmp(b).unwrap());
2894 unique_opacities.dedup();
2895
2896 for (idx, &opacity) in unique_opacities.iter().enumerate() {
2897 let obj_id = builder.objects.len();
2898 let gs_name = format!("GS{}", idx);
2899 let obj_data = format!(
2900 "<< /Type /ExtGState /ca {:.4} /CA {:.4} >>",
2901 opacity, opacity
2902 );
2903 builder.objects.push(PdfObject {
2904 id: obj_id,
2905 data: obj_data.into_bytes(),
2906 });
2907 let key = opacity.to_bits();
2908 builder.ext_gstate_map.insert(key, (obj_id, gs_name));
2909 }
2910 }
2911
2912 fn collect_opacities_recursive(elements: &[LayoutElement], opacities: &mut Vec<f64>) {
2913 for element in elements {
2914 if element.opacity < 1.0 {
2921 opacities.push(element.opacity);
2922 }
2923 if let DrawCommand::Rect {
2927 box_shadow: Some(shadow),
2928 ..
2929 } = &element.draw
2930 {
2931 if shadow.color.a < 1.0 {
2932 opacities.push(shadow.color.a);
2933 }
2934 }
2935 match &element.draw {
2936 DrawCommand::Rect { opacity, .. }
2937 | DrawCommand::Text { opacity, .. }
2938 | DrawCommand::Watermark { opacity, .. }
2939 if *opacity < 1.0 =>
2940 {
2941 opacities.push(*opacity);
2942 }
2943 DrawCommand::Chart { primitives } => {
2944 for prim in primitives {
2945 if let crate::chart::ChartPrimitive::FilledPath { opacity, .. } = prim {
2946 if *opacity < 1.0 {
2947 opacities.push(*opacity);
2948 }
2949 }
2950 }
2951 }
2952 DrawCommand::Svg { commands, .. } => {
2953 for cmd in commands {
2954 if let crate::svg::SvgCommand::SetOpacity(opacity) = cmd {
2955 if *opacity < 1.0 {
2956 opacities.push(*opacity);
2957 }
2958 }
2959 }
2960 }
2961 _ => {}
2962 }
2963 Self::collect_opacities_recursive(&element.children, opacities);
2964 }
2965 }
2966
2967 fn build_ext_gstate_resource_dict(&self, builder: &PdfBuilder) -> String {
2969 if builder.ext_gstate_map.is_empty() {
2970 return String::new();
2971 }
2972 let mut entries: Vec<(&String, usize)> = builder
2973 .ext_gstate_map
2974 .values()
2975 .map(|(obj_id, name)| (name, *obj_id))
2976 .collect();
2977 entries.sort_by_key(|(name, _)| (*name).clone());
2978 entries
2979 .iter()
2980 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
2981 .collect::<Vec<_>>()
2982 .join(" ")
2983 }
2984
2985 fn write_image_xobject(
2988 builder: &mut PdfBuilder,
2989 image: &crate::image_loader::LoadedImage,
2990 ) -> usize {
2991 use crate::image_loader::{ImagePixelData, JpegColorSpace};
2992
2993 match &image.pixel_data {
2994 ImagePixelData::Jpeg { data, color_space } => {
2995 let color_space_str = match color_space {
2996 JpegColorSpace::DeviceRGB => "/DeviceRGB",
2997 JpegColorSpace::DeviceGray => "/DeviceGray",
2998 };
2999
3000 let obj_id = builder.objects.len();
3001 let mut obj_data: Vec<u8> = Vec::new();
3002 let _ = write!(
3003 obj_data,
3004 "<< /Type /XObject /Subtype /Image \
3005 /Width {} /Height {} \
3006 /ColorSpace {} \
3007 /BitsPerComponent 8 \
3008 /Filter /DCTDecode \
3009 /Length {} >>\nstream\n",
3010 image.width_px,
3011 image.height_px,
3012 color_space_str,
3013 data.len()
3014 );
3015 obj_data.extend_from_slice(data);
3016 obj_data.extend_from_slice(b"\nendstream");
3017 builder.objects.push(PdfObject {
3018 id: obj_id,
3019 data: obj_data,
3020 });
3021 obj_id
3022 }
3023
3024 ImagePixelData::Decoded { rgb, alpha } => {
3025 let smask_id = alpha.as_ref().map(|alpha_data| {
3027 let compressed_alpha = compress_to_vec_zlib(alpha_data, 6);
3028 let smask_obj_id = builder.objects.len();
3029 let mut smask_data: Vec<u8> = Vec::new();
3030 let _ = write!(
3031 smask_data,
3032 "<< /Type /XObject /Subtype /Image \
3033 /Width {} /Height {} \
3034 /ColorSpace /DeviceGray \
3035 /BitsPerComponent 8 \
3036 /Filter /FlateDecode \
3037 /Length {} >>\nstream\n",
3038 image.width_px,
3039 image.height_px,
3040 compressed_alpha.len()
3041 );
3042 smask_data.extend_from_slice(&compressed_alpha);
3043 smask_data.extend_from_slice(b"\nendstream");
3044 builder.objects.push(PdfObject {
3045 id: smask_obj_id,
3046 data: smask_data,
3047 });
3048 smask_obj_id
3049 });
3050
3051 let compressed_rgb = compress_to_vec_zlib(rgb, 6);
3053 let obj_id = builder.objects.len();
3054 let mut obj_data: Vec<u8> = Vec::new();
3055
3056 let smask_ref = smask_id
3057 .map(|id| format!(" /SMask {} 0 R", id))
3058 .unwrap_or_default();
3059
3060 let _ = write!(
3061 obj_data,
3062 "<< /Type /XObject /Subtype /Image \
3063 /Width {} /Height {} \
3064 /ColorSpace /DeviceRGB \
3065 /BitsPerComponent 8 \
3066 /Filter /FlateDecode \
3067 /Length {}{} >>\nstream\n",
3068 image.width_px,
3069 image.height_px,
3070 compressed_rgb.len(),
3071 smask_ref
3072 );
3073 obj_data.extend_from_slice(&compressed_rgb);
3074 obj_data.extend_from_slice(b"\nendstream");
3075 builder.objects.push(PdfObject {
3076 id: obj_id,
3077 data: obj_data,
3078 });
3079 obj_id
3080 }
3081 }
3082 }
3083
3084 fn build_shading_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3088 let mut entries: Vec<(String, usize)> = builder
3089 .shading_map
3090 .iter()
3091 .filter(|(&(p, _), _)| p == page_idx)
3092 .map(|(_, (obj_id, name))| (name.clone(), *obj_id))
3093 .collect();
3094 if entries.is_empty() {
3095 return String::new();
3096 }
3097 entries.sort_by(|a, b| a.0.cmp(&b.0));
3098 entries
3099 .iter()
3100 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
3101 .collect::<Vec<_>>()
3102 .join(" ")
3103 }
3104
3105 fn build_xobject_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
3106 let mut entries: Vec<(usize, usize)> = Vec::new();
3107 for (&(pidx, _), &img_idx) in &builder.image_index_map {
3108 if pidx == page_idx {
3109 let obj_id = builder.image_objects[img_idx];
3110 entries.push((img_idx, obj_id));
3111 }
3112 }
3113 if let Some(&(img_idx, _, _)) = builder.page_background_image_map.get(&page_idx) {
3116 let obj_id = builder.image_objects[img_idx];
3117 entries.push((img_idx, obj_id));
3118 }
3119 if entries.is_empty() {
3120 return String::new();
3121 }
3122 entries.sort_by_key(|(idx, _)| *idx);
3123 entries.dedup();
3124 entries
3125 .iter()
3126 .map(|(idx, obj_id)| format!("/Im{} {} 0 R", idx, obj_id))
3127 .collect::<Vec<_>>()
3128 .join(" ")
3129 }
3130
3131 fn write_custom_font_objects(
3138 builder: &mut PdfBuilder,
3139 key: &FontKey,
3140 ttf_data: &[u8],
3141 used_glyph_ids: HashSet<u16>,
3142 used_chars: HashSet<char>,
3143 glyph_to_char_map: HashMap<u16, char>,
3144 ) -> Result<usize, FormeError> {
3145 let face = ttf_parser::Face::parse(ttf_data, 0).map_err(|e| {
3146 FormeError::FontError(format!(
3147 "Failed to parse TTF data for font '{}': {}",
3148 key.family, e
3149 ))
3150 })?;
3151
3152 let units_per_em = face.units_per_em();
3153 let ascender = face.ascender();
3154 let descender = face.descender();
3155
3156 let mut char_to_orig_gid: HashMap<char, u16> = HashMap::new();
3158 for &ch in &used_chars {
3159 if let Some(gid) = face.glyph_index(ch) {
3160 char_to_orig_gid.insert(ch, gid.0);
3161 }
3162 }
3163
3164 let mut all_orig_gids: HashSet<u16> = used_glyph_ids.clone();
3168 for &gid in char_to_orig_gid.values() {
3169 all_orig_gids.insert(gid);
3170 }
3171
3172 let (embed_ttf, gid_remap) = match subset_ttf(ttf_data, &all_orig_gids) {
3174 Ok(subset_result) => (subset_result.ttf_data, subset_result.gid_remap),
3175 Err(_) => {
3176 let identity: HashMap<u16, u16> =
3178 all_orig_gids.iter().map(|&gid| (gid, gid)).collect();
3179 (ttf_data.to_vec(), identity)
3180 }
3181 };
3182
3183 let char_to_gid: HashMap<char, u16> = char_to_orig_gid
3185 .iter()
3186 .filter_map(|(&ch, &orig_gid)| gid_remap.get(&orig_gid).map(|&new_gid| (ch, new_gid)))
3187 .collect();
3188
3189 let gid_remap_for_embed = gid_remap.clone();
3191
3192 let mut new_gid_to_char: HashMap<u16, char> = HashMap::new();
3194 for (&orig_gid, &ch) in &glyph_to_char_map {
3196 if let Some(&new_gid) = gid_remap.get(&orig_gid) {
3197 new_gid_to_char.entry(new_gid).or_insert(ch);
3198 }
3199 }
3200 for (&ch, &new_gid) in &char_to_gid {
3202 new_gid_to_char.entry(new_gid).or_insert(ch);
3203 }
3204
3205 let pdf_font_name = Self::sanitize_font_name(&key.family, key.weight, key.italic);
3206
3207 let compressed_ttf = compress_to_vec_zlib(&embed_ttf, 6);
3209 let fontfile2_id = builder.objects.len();
3210 let mut fontfile2_data: Vec<u8> = Vec::new();
3211 let _ = write!(
3212 fontfile2_data,
3213 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
3214 compressed_ttf.len(),
3215 embed_ttf.len()
3216 );
3217 fontfile2_data.extend_from_slice(&compressed_ttf);
3218 fontfile2_data.extend_from_slice(b"\nendstream");
3219 builder.objects.push(PdfObject {
3220 id: fontfile2_id,
3221 data: fontfile2_data,
3222 });
3223
3224 let subset_face = ttf_parser::Face::parse(&embed_ttf, 0).unwrap_or_else(|_| face.clone());
3226 let subset_upem = subset_face.units_per_em();
3227
3228 let font_descriptor_id = builder.objects.len();
3230 let bbox = face.global_bounding_box();
3231 let scale = 1000.0 / units_per_em as f64;
3232 let bbox_str = format!(
3233 "[{} {} {} {}]",
3234 (bbox.x_min as f64 * scale) as i32,
3235 (bbox.y_min as f64 * scale) as i32,
3236 (bbox.x_max as f64 * scale) as i32,
3237 (bbox.y_max as f64 * scale) as i32,
3238 );
3239
3240 let flags = 4u32;
3241 let cap_height = face.capital_height().unwrap_or(ascender) as f64 * scale;
3242 let stem_v = if key.weight >= 700 { 120 } else { 80 };
3243
3244 let font_descriptor_dict = format!(
3245 "<< /Type /FontDescriptor /FontName /{} /Flags {} \
3246 /FontBBox {} /ItalicAngle {} \
3247 /Ascent {} /Descent {} /CapHeight {} /StemV {} \
3248 /FontFile2 {} 0 R >>",
3249 pdf_font_name,
3250 flags,
3251 bbox_str,
3252 if key.italic { -12 } else { 0 },
3253 (ascender as f64 * scale) as i32,
3254 (descender as f64 * scale) as i32,
3255 cap_height as i32,
3256 stem_v,
3257 fontfile2_id,
3258 );
3259 builder.objects.push(PdfObject {
3260 id: font_descriptor_id,
3261 data: font_descriptor_dict.into_bytes(),
3262 });
3263
3264 let cidfont_id = builder.objects.len();
3266 let w_array = Self::build_w_array_from_gids(&gid_remap, &subset_face, subset_upem);
3268 let default_width = subset_face
3269 .glyph_hor_advance(ttf_parser::GlyphId(0))
3270 .map(|adv| (adv as f64 * 1000.0 / subset_upem as f64) as u32)
3271 .unwrap_or(1000);
3272 let cidfont_dict = format!(
3273 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{} \
3274 /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> \
3275 /FontDescriptor {} 0 R /DW {} /W {} \
3276 /CIDToGIDMap /Identity >>",
3277 pdf_font_name, font_descriptor_id, default_width, w_array,
3278 );
3279 builder.objects.push(PdfObject {
3280 id: cidfont_id,
3281 data: cidfont_dict.into_bytes(),
3282 });
3283
3284 let tounicode_id = builder.objects.len();
3286 let cmap_content = Self::build_tounicode_cmap_from_gids(&new_gid_to_char, &pdf_font_name);
3287 let compressed_cmap = compress_to_vec_zlib(cmap_content.as_bytes(), 6);
3288 let mut tounicode_data: Vec<u8> = Vec::new();
3289 let _ = write!(
3290 tounicode_data,
3291 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
3292 compressed_cmap.len()
3293 );
3294 tounicode_data.extend_from_slice(&compressed_cmap);
3295 tounicode_data.extend_from_slice(b"\nendstream");
3296 builder.objects.push(PdfObject {
3297 id: tounicode_id,
3298 data: tounicode_data,
3299 });
3300
3301 let type0_id = builder.objects.len();
3303 let type0_dict = format!(
3304 "<< /Type /Font /Subtype /Type0 /BaseFont /{} \
3305 /Encoding /Identity-H \
3306 /DescendantFonts [{} 0 R] \
3307 /ToUnicode {} 0 R >>",
3308 pdf_font_name, cidfont_id, tounicode_id,
3309 );
3310 builder.objects.push(PdfObject {
3311 id: type0_id,
3312 data: type0_dict.into_bytes(),
3313 });
3314
3315 builder.custom_font_data.insert(
3317 key.clone(),
3318 CustomFontEmbedData {
3319 ttf_data: embed_ttf,
3320 gid_remap: gid_remap_for_embed,
3321 glyph_to_char: glyph_to_char_map,
3322 char_to_gid,
3323 units_per_em,
3324 ascender,
3325 descender,
3326 },
3327 );
3328
3329 Ok(type0_id)
3330 }
3331
3332 fn build_w_array_from_gids(
3334 gid_remap: &HashMap<u16, u16>,
3335 face: &ttf_parser::Face,
3336 units_per_em: u16,
3337 ) -> String {
3338 let scale = 1000.0 / units_per_em as f64;
3339
3340 let mut entries: Vec<(u16, u32)> = Vec::new();
3341 let mut seen_gids: HashSet<u16> = HashSet::new();
3342
3343 for &new_gid in gid_remap.values() {
3344 if seen_gids.contains(&new_gid) {
3345 continue;
3346 }
3347 seen_gids.insert(new_gid);
3348 let advance = face
3349 .glyph_hor_advance(ttf_parser::GlyphId(new_gid))
3350 .unwrap_or(0);
3351 let width = (advance as f64 * scale) as u32;
3352 entries.push((new_gid, width));
3353 }
3354
3355 entries.sort_by_key(|(gid, _)| *gid);
3356
3357 let mut result = String::from("[");
3359 for (gid, width) in &entries {
3360 let _ = write!(result, " {} [{}]", gid, width);
3361 }
3362 result.push_str(" ]");
3363 result
3364 }
3365
3366 fn build_tounicode_cmap_from_gids(gid_to_char: &HashMap<u16, char>, font_name: &str) -> String {
3368 let mut gid_to_unicode: Vec<(u16, u32)> = gid_to_char
3369 .iter()
3370 .map(|(&gid, &ch)| (gid, ch as u32))
3371 .collect();
3372 gid_to_unicode.sort_by_key(|(gid, _)| *gid);
3373
3374 let mut cmap = String::new();
3375 let _ = writeln!(cmap, "/CIDInit /ProcSet findresource begin");
3376 let _ = writeln!(cmap, "12 dict begin");
3377 let _ = writeln!(cmap, "begincmap");
3378 let _ = writeln!(cmap, "/CIDSystemInfo");
3379 let _ = writeln!(
3380 cmap,
3381 "<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def"
3382 );
3383 let _ = writeln!(cmap, "/CMapName /{}-UTF16 def", font_name);
3384 let _ = writeln!(cmap, "/CMapType 2 def");
3385 let _ = writeln!(cmap, "1 begincodespacerange");
3386 let _ = writeln!(cmap, "<0000> <FFFF>");
3387 let _ = writeln!(cmap, "endcodespacerange");
3388
3389 for chunk in gid_to_unicode.chunks(100) {
3391 let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
3392 for &(gid, unicode) in chunk {
3393 let _ = writeln!(cmap, "<{:04X}> <{:04X}>", gid, unicode);
3394 }
3395 let _ = writeln!(cmap, "endbfchar");
3396 }
3397
3398 let _ = writeln!(cmap, "endcmap");
3399 let _ = writeln!(cmap, "CMapName currentdict /CMap defineresource pop");
3400 let _ = writeln!(cmap, "end");
3401 let _ = writeln!(cmap, "end");
3402
3403 cmap
3404 }
3405
3406 fn sanitize_font_name(family: &str, weight: u32, italic: bool) -> String {
3409 let mut name: String = family
3410 .chars()
3411 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
3412 .collect();
3413
3414 if weight >= 700 {
3415 name.push_str("-Bold");
3416 }
3417 if italic {
3418 name.push_str("-Italic");
3419 }
3420
3421 if name.is_empty() {
3423 name = "CustomFont".to_string();
3424 }
3425
3426 name
3427 }
3428
3429 fn build_font_resource_dict(&self, font_objects: &[(FontKey, usize)]) -> String {
3430 font_objects
3431 .iter()
3432 .enumerate()
3433 .map(|(i, (_, obj_id))| format!("/F{} {} 0 R", i, obj_id))
3434 .collect::<Vec<_>>()
3435 .join(" ")
3436 }
3437
3438 fn font_index(
3440 &self,
3441 family: &str,
3442 weight: u32,
3443 font_style: FontStyle,
3444 font_objects: &[(FontKey, usize)],
3445 ) -> usize {
3446 let italic = matches!(font_style, FontStyle::Italic | FontStyle::Oblique);
3447
3448 for (i, (key, _)) in font_objects.iter().enumerate() {
3450 if key.family == family && key.weight == weight && key.italic == italic {
3451 return i;
3452 }
3453 }
3454
3455 let snapped = if weight >= 600 { 700 } else { 400 };
3457 for (i, (key, _)) in font_objects.iter().enumerate() {
3458 if key.family == family && key.weight == snapped && key.italic == italic {
3459 return i;
3460 }
3461 }
3462
3463 for (i, (key, _)) in font_objects.iter().enumerate() {
3465 if key.family == "Helvetica" && key.weight == snapped && key.italic == italic {
3466 return i;
3467 }
3468 }
3469
3470 0
3472 }
3473
3474 fn group_glyphs_by_style(glyphs: &[PositionedGlyph]) -> Vec<Vec<&PositionedGlyph>> {
3477 if glyphs.is_empty() {
3478 return vec![];
3479 }
3480
3481 let mut groups: Vec<Vec<&PositionedGlyph>> = Vec::new();
3482 let mut current_group: Vec<&PositionedGlyph> = vec![&glyphs[0]];
3483
3484 for glyph in &glyphs[1..] {
3485 let prev = current_group.last().unwrap();
3486 let same_style = glyph.font_family == prev.font_family
3487 && glyph.font_weight == prev.font_weight
3488 && std::mem::discriminant(&glyph.font_style)
3489 == std::mem::discriminant(&prev.font_style)
3490 && (glyph.font_size - prev.font_size).abs() < 0.01
3491 && Self::colors_equal(&glyph.color, &prev.color)
3492 && std::mem::discriminant(&glyph.text_decoration)
3493 == std::mem::discriminant(&prev.text_decoration);
3494
3495 if same_style {
3496 current_group.push(glyph);
3497 } else {
3498 groups.push(current_group);
3499 current_group = vec![glyph];
3500 }
3501 }
3502 groups.push(current_group);
3503 groups
3504 }
3505
3506 fn colors_equal(a: &Option<Color>, b: &Option<Color>) -> bool {
3507 match (a, b) {
3508 (None, None) => true,
3509 (Some(ca), Some(cb)) => {
3510 (ca.r - cb.r).abs() < 0.001
3511 && (ca.g - cb.g).abs() < 0.001
3512 && (ca.b - cb.b).abs() < 0.001
3513 && (ca.a - cb.a).abs() < 0.001
3514 }
3515 _ => false,
3516 }
3517 }
3518
3519 fn collect_link_annotations(
3523 elements: &[LayoutElement],
3524 page_height: f64,
3525 annotations: &mut Vec<LinkAnnotation>,
3526 ) {
3527 for element in elements {
3528 if let Some(ref href) = element.href {
3529 if !href.is_empty() {
3530 let pdf_y = page_height - element.y - element.height;
3531 annotations.push(LinkAnnotation {
3532 x: element.x,
3533 y: pdf_y,
3534 width: element.width,
3535 height: element.height,
3536 href: href.clone(),
3537 });
3538 continue;
3540 }
3541 }
3542 Self::collect_link_annotations(&element.children, page_height, annotations);
3543 }
3544 }
3545
3546 fn collect_form_fields(
3548 elements: &[LayoutElement],
3549 page_height: f64,
3550 page_idx: usize,
3551 fields: &mut Vec<FormFieldData>,
3552 ) {
3553 for element in elements {
3554 if let DrawCommand::FormField {
3555 ref field_type,
3556 ref name,
3557 } = element.draw
3558 {
3559 let pdf_y = page_height - element.y - element.height;
3560 fields.push(FormFieldData {
3561 field_type: field_type.clone(),
3562 name: name.clone(),
3563 x: element.x,
3564 y: pdf_y,
3565 width: element.width,
3566 height: element.height,
3567 page_idx,
3568 });
3569 }
3570 Self::collect_form_fields(&element.children, page_height, page_idx, fields);
3571 }
3572 }
3573
3574 fn collect_bookmarks(
3576 elements: &[LayoutElement],
3577 page_height: f64,
3578 page_obj_id: usize,
3579 bookmarks: &mut Vec<PdfBookmark>,
3580 ) {
3581 for element in elements {
3582 if let Some(ref title) = element.bookmark {
3583 let y_pdf = page_height - element.y;
3584 bookmarks.push(PdfBookmark {
3585 title: title.clone(),
3586 page_obj_id,
3587 y_pdf,
3588 });
3589 }
3590 Self::collect_bookmarks(&element.children, page_height, page_obj_id, bookmarks);
3591 }
3592 }
3593
3594 fn write_outline_tree(&self, builder: &mut PdfBuilder, bookmarks: &[PdfBookmark]) -> usize {
3597 let outlines_id = builder.objects.len();
3599 builder.objects.push(PdfObject {
3600 id: outlines_id,
3601 data: vec![],
3602 });
3603
3604 let mut item_ids: Vec<usize> = Vec::new();
3606 for _bm in bookmarks {
3607 let item_id = builder.objects.len();
3608 builder.objects.push(PdfObject {
3609 id: item_id,
3610 data: vec![],
3611 });
3612 item_ids.push(item_id);
3613 }
3614
3615 for (i, (bm, &item_id)) in bookmarks.iter().zip(item_ids.iter()).enumerate() {
3617 let mut dict = format!(
3618 "<< /Title ({}) /Parent {} 0 R /Dest [{} 0 R /XYZ 0 {:.2} null]",
3619 Self::escape_pdf_string(&bm.title),
3620 outlines_id,
3621 bm.page_obj_id,
3622 bm.y_pdf,
3623 );
3624 if i > 0 {
3625 let _ = write!(dict, " /Prev {} 0 R", item_ids[i - 1]);
3626 }
3627 if i + 1 < item_ids.len() {
3628 let _ = write!(dict, " /Next {} 0 R", item_ids[i + 1]);
3629 }
3630 dict.push_str(" >>");
3631 builder.objects[item_id].data = dict.into_bytes();
3632 }
3633
3634 let first_id = item_ids.first().copied().unwrap_or(0);
3636 let last_id = item_ids.last().copied().unwrap_or(0);
3637 let outlines_dict = format!(
3638 "<< /Type /Outlines /First {} 0 R /Last {} 0 R /Count {} >>",
3639 first_id,
3640 last_id,
3641 bookmarks.len()
3642 );
3643 builder.objects[outlines_id].data = outlines_dict.into_bytes();
3644
3645 outlines_id
3646 }
3647
3648 fn write_svg_commands(
3650 stream: &mut String,
3651 commands: &[SvgCommand],
3652 ext_gstate_map: &HashMap<u64, (usize, String)>,
3653 ) {
3654 for cmd in commands {
3655 match cmd {
3656 SvgCommand::MoveTo(x, y) => {
3657 let _ = writeln!(stream, "{:.2} {:.2} m", x, y);
3658 }
3659 SvgCommand::LineTo(x, y) => {
3660 let _ = writeln!(stream, "{:.2} {:.2} l", x, y);
3661 }
3662 SvgCommand::CurveTo(x1, y1, x2, y2, x3, y3) => {
3663 let _ = writeln!(
3664 stream,
3665 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3666 x1, y1, x2, y2, x3, y3
3667 );
3668 }
3669 SvgCommand::ClosePath => {
3670 let _ = writeln!(stream, "h");
3671 }
3672 SvgCommand::SetFill(r, g, b) => {
3673 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", r, g, b);
3674 }
3675 SvgCommand::SetFillNone => {
3676 }
3678 SvgCommand::SetStroke(r, g, b) => {
3679 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", r, g, b);
3680 }
3681 SvgCommand::SetStrokeNone => {
3682 }
3684 SvgCommand::SetStrokeWidth(w) => {
3685 let _ = writeln!(stream, "{:.2} w", w);
3686 }
3687 SvgCommand::Fill => {
3688 let _ = writeln!(stream, "f");
3689 }
3690 SvgCommand::Stroke => {
3691 let _ = writeln!(stream, "S");
3692 }
3693 SvgCommand::FillAndStroke => {
3694 let _ = writeln!(stream, "B");
3695 }
3696 SvgCommand::SetLineCap(cap) => {
3697 let _ = writeln!(stream, "{} J", cap);
3698 }
3699 SvgCommand::SetLineJoin(join) => {
3700 let _ = writeln!(stream, "{} j", join);
3701 }
3702 SvgCommand::SaveState => {
3703 let _ = writeln!(stream, "q");
3704 }
3705 SvgCommand::RestoreState => {
3706 let _ = writeln!(stream, "Q");
3707 }
3708 SvgCommand::SetOpacity(opacity) => {
3709 if let Some((_, gs_name)) = ext_gstate_map.get(&opacity.to_bits()) {
3710 let _ = writeln!(stream, "/{} gs", gs_name);
3711 }
3712 }
3713 }
3714 }
3715 }
3716
3717 pub(crate) fn escape_pdf_string(s: &str) -> String {
3719 s.replace('\\', "\\\\")
3720 .replace('(', "\\(")
3721 .replace(')', "\\)")
3722 }
3723
3724 fn encode_winansi_text(s: &str) -> String {
3727 let mut result = String::with_capacity(s.len());
3728 for ch in s.chars() {
3729 let b = Self::unicode_to_winansi(ch).unwrap_or(b'?');
3730 match b {
3731 b'\\' => result.push_str("\\\\"),
3732 b'(' => result.push_str("\\("),
3733 b')' => result.push_str("\\)"),
3734 0x20..=0x7E => result.push(b as char),
3735 _ => {
3736 let _ = write!(result, "\\{:03o}", b);
3737 }
3738 }
3739 }
3740 result
3741 }
3742
3743 fn unicode_to_winansi(ch: char) -> Option<u8> {
3745 crate::font::unicode_to_winansi(ch)
3746 }
3747
3748 fn serialize(&self, builder: &PdfBuilder, info_obj_id: Option<usize>) -> Vec<u8> {
3750 let mut output: Vec<u8> = Vec::new();
3751 let mut offsets: Vec<usize> = vec![0; builder.objects.len()];
3752
3753 output.extend_from_slice(b"%PDF-1.7\n");
3755 output.extend_from_slice(b"%\xe2\xe3\xcf\xd3\n");
3756
3757 for (i, obj) in builder.objects.iter().enumerate().skip(1) {
3758 offsets[i] = output.len();
3759 let header = format!("{} 0 obj\n", i);
3760 output.extend_from_slice(header.as_bytes());
3761 output.extend_from_slice(&obj.data);
3762 output.extend_from_slice(b"\nendobj\n\n");
3763 }
3764
3765 let xref_offset = output.len();
3766 let _ = writeln!(output, "xref\n0 {}", builder.objects.len());
3767 let _ = writeln!(output, "0000000000 65535 f ");
3768 for offset in offsets.iter().skip(1) {
3769 let _ = writeln!(output, "{:010} 00000 n ", offset);
3770 }
3771
3772 let _ = write!(
3773 output,
3774 "trailer\n<< /Size {} /Root 1 0 R",
3775 builder.objects.len()
3776 );
3777 if let Some(info_id) = info_obj_id {
3778 let _ = write!(output, " /Info {} 0 R", info_id);
3779 }
3780 let _ = writeln!(output, " >>\nstartxref\n{}\n%%EOF", xref_offset);
3781
3782 output
3783 }
3784}
3785
3786fn write_chart_primitive(
3791 stream: &mut String,
3792 prim: &crate::chart::ChartPrimitive,
3793 _chart_height: f64,
3794 builder: &PdfBuilder,
3795) {
3796 use crate::chart::{ChartPrimitive, TextAnchor};
3797 use crate::font::metrics::unicode_to_winansi;
3798
3799 match prim {
3800 ChartPrimitive::Rect { x, y, w, h, fill } => {
3801 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3802 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re f", x, y, w, h);
3803 }
3804
3805 ChartPrimitive::Line {
3806 x1,
3807 y1,
3808 x2,
3809 y2,
3810 stroke,
3811 width,
3812 } => {
3813 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
3814 let _ = writeln!(stream, "{:.2} w", width);
3815 let _ = writeln!(stream, "{:.2} {:.2} m {:.2} {:.2} l S", x1, y1, x2, y2);
3816 }
3817
3818 ChartPrimitive::Polyline {
3819 points,
3820 stroke,
3821 width,
3822 } => {
3823 if points.len() < 2 {
3824 return;
3825 }
3826 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
3827 let _ = writeln!(stream, "{:.2} w", width);
3828 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
3829 for &(px, py) in &points[1..] {
3830 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
3831 }
3832 let _ = writeln!(stream, "S");
3833 }
3834
3835 ChartPrimitive::FilledPath {
3836 points,
3837 fill,
3838 opacity,
3839 } => {
3840 if points.len() < 3 {
3841 return;
3842 }
3843 let _ = writeln!(stream, "q");
3844 if *opacity < 1.0 {
3846 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
3847 let _ = writeln!(stream, "/{} gs", gs_name);
3848 }
3849 }
3850 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3851 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
3852 for &(px, py) in &points[1..] {
3853 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
3854 }
3855 let _ = writeln!(stream, "h f");
3856 let _ = writeln!(stream, "Q");
3857 }
3858
3859 ChartPrimitive::Circle { cx, cy, r, fill } => {
3860 let kappa: f64 = 0.5523;
3862 let kr = kappa * r;
3863 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3864 let _ = writeln!(stream, "{:.2} {:.2} m", cx + r, cy);
3865 let _ = writeln!(
3866 stream,
3867 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3868 cx + r,
3869 cy + kr,
3870 cx + kr,
3871 cy + r,
3872 cx,
3873 cy + r
3874 );
3875 let _ = writeln!(
3876 stream,
3877 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3878 cx - kr,
3879 cy + r,
3880 cx - r,
3881 cy + kr,
3882 cx - r,
3883 cy
3884 );
3885 let _ = writeln!(
3886 stream,
3887 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3888 cx - r,
3889 cy - kr,
3890 cx - kr,
3891 cy - r,
3892 cx,
3893 cy - r
3894 );
3895 let _ = writeln!(
3896 stream,
3897 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
3898 cx + kr,
3899 cy - r,
3900 cx + r,
3901 cy - kr,
3902 cx + r,
3903 cy
3904 );
3905 let _ = writeln!(stream, "f");
3906 }
3907
3908 ChartPrimitive::ArcSector {
3909 cx,
3910 cy,
3911 r,
3912 start_angle,
3913 end_angle,
3914 fill,
3915 } => {
3916 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
3917 let _ = writeln!(stream, "{:.2} {:.2} m", cx, cy);
3919 let sx = cx + r * start_angle.cos();
3921 let sy = cy + r * start_angle.sin();
3922 let _ = writeln!(stream, "{:.2} {:.2} l", sx, sy);
3923
3924 let mut angle = *start_angle;
3926 let total = end_angle - start_angle;
3927 let segments = ((total.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
3928 let step = total / segments as f64;
3929
3930 for _ in 0..segments {
3931 let a1 = angle;
3932 let a2 = angle + step;
3933 let alpha = 4.0 / 3.0 * ((a2 - a1) / 4.0).tan();
3934
3935 let p1x = cx + r * a1.cos();
3936 let p1y = cy + r * a1.sin();
3937 let p2x = cx + r * a2.cos();
3938 let p2y = cy + r * a2.sin();
3939
3940 let cp1x = p1x - alpha * r * a1.sin();
3941 let cp1y = p1y + alpha * r * a1.cos();
3942 let cp2x = p2x + alpha * r * a2.sin();
3943 let cp2y = p2y - alpha * r * a2.cos();
3944
3945 let _ = writeln!(
3946 stream,
3947 "{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c",
3948 cp1x, cp1y, cp2x, cp2y, p2x, p2y
3949 );
3950 angle = a2;
3951 }
3952
3953 let _ = writeln!(stream, "h f");
3955 }
3956
3957 ChartPrimitive::Label {
3958 text,
3959 x,
3960 y,
3961 font_size,
3962 color,
3963 anchor,
3964 } => {
3965 let metrics = crate::font::StandardFont::Helvetica.metrics();
3967 let text_width = metrics.measure_string(text, *font_size, 0.0);
3968 let x_offset = match anchor {
3969 TextAnchor::Left => 0.0,
3970 TextAnchor::Center => -text_width / 2.0,
3971 TextAnchor::Right => -text_width,
3972 };
3973
3974 let font_idx = builder
3976 .font_objects
3977 .iter()
3978 .enumerate()
3979 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
3980 .map(|(i, _)| i)
3981 .unwrap_or(0);
3982
3983 let encoded: String = text
3985 .chars()
3986 .map(|ch| {
3987 if let Some(code) = unicode_to_winansi(ch) {
3988 code as char
3989 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
3990 ch
3991 } else {
3992 '?'
3993 }
3994 })
3995 .collect();
3996 let escaped = pdf_escape_string(&encoded);
3997
3998 let _ = writeln!(stream, "q");
4000 let _ = writeln!(stream, "1 0 0 -1 {:.4} {:.4} cm", x + x_offset, *y);
4001 let _ = writeln!(
4002 stream,
4003 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
4004 font_idx, font_size, color.r, color.g, color.b, escaped
4005 );
4006 let _ = writeln!(stream, "Q");
4007 }
4008 }
4009}
4010
4011fn normalize_gradient_stops(
4018 stops: &[crate::style::GradientStop],
4019 fallback: Color,
4020) -> Vec<crate::style::GradientStop> {
4021 use crate::style::GradientStop;
4022 if stops.is_empty() {
4023 return vec![
4024 GradientStop {
4025 position: 0.0,
4026 color: fallback,
4027 },
4028 GradientStop {
4029 position: 1.0,
4030 color: fallback,
4031 },
4032 ];
4033 }
4034 let mut sorted: Vec<GradientStop> = stops
4035 .iter()
4036 .map(|s| GradientStop {
4037 position: s.position.clamp(0.0, 1.0),
4038 color: s.color,
4039 })
4040 .collect();
4041 sorted.sort_by(|a, b| {
4042 a.position
4043 .partial_cmp(&b.position)
4044 .unwrap_or(std::cmp::Ordering::Equal)
4045 });
4046 if sorted[0].position > 0.0 {
4047 sorted.insert(
4048 0,
4049 GradientStop {
4050 position: 0.0,
4051 color: sorted[0].color,
4052 },
4053 );
4054 }
4055 if sorted[sorted.len() - 1].position < 1.0 {
4056 let last = sorted[sorted.len() - 1].color;
4057 sorted.push(GradientStop {
4058 position: 1.0,
4059 color: last,
4060 });
4061 }
4062 sorted
4063}
4064
4065fn pdf_escape_string(s: &str) -> String {
4066 let mut out = String::with_capacity(s.len());
4067 for ch in s.chars() {
4068 match ch {
4069 '(' => out.push_str("\\("),
4070 ')' => out.push_str("\\)"),
4071 '\\' => out.push_str("\\\\"),
4072 _ => out.push(ch),
4073 }
4074 }
4075 out
4076}
4077
4078#[cfg(test)]
4079mod tests {
4080 use super::*;
4081 use crate::font::FontContext;
4082
4083 #[test]
4084 fn test_escape_pdf_string() {
4085 assert_eq!(
4086 PdfWriter::escape_pdf_string("Hello (World)"),
4087 "Hello \\(World\\)"
4088 );
4089 assert_eq!(PdfWriter::escape_pdf_string("back\\slash"), "back\\\\slash");
4090 }
4091
4092 #[test]
4093 fn test_empty_document_produces_valid_pdf() {
4094 let writer = PdfWriter::new();
4095 let font_context = FontContext::new();
4096 let pages = vec![LayoutPage {
4097 width: 595.28,
4098 height: 841.89,
4099 elements: vec![],
4100 fixed_header: vec![],
4101 fixed_footer: vec![],
4102 watermarks: vec![],
4103 config: PageConfig::default(),
4104 }];
4105 let metadata = Metadata::default();
4106 let bytes = writer
4107 .write(
4108 &pages,
4109 &metadata,
4110 &font_context,
4111 false,
4112 None,
4113 false,
4114 None,
4115 false,
4116 )
4117 .unwrap();
4118
4119 assert!(bytes.starts_with(b"%PDF-1.7"));
4120 assert!(bytes.windows(5).any(|w| w == b"%%EOF"));
4121 assert!(bytes.windows(4).any(|w| w == b"xref"));
4122 assert!(bytes.windows(7).any(|w| w == b"trailer"));
4123 }
4124
4125 #[test]
4126 fn test_metadata_in_pdf() {
4127 let writer = PdfWriter::new();
4128 let font_context = FontContext::new();
4129 let pages = vec![LayoutPage {
4130 width: 595.28,
4131 height: 841.89,
4132 elements: vec![],
4133 fixed_header: vec![],
4134 fixed_footer: vec![],
4135 watermarks: vec![],
4136 config: PageConfig::default(),
4137 }];
4138 let metadata = Metadata {
4139 title: Some("Test Document".to_string()),
4140 author: Some("Forme".to_string()),
4141 subject: None,
4142 creator: None,
4143 lang: None,
4144 };
4145 let bytes = writer
4146 .write(
4147 &pages,
4148 &metadata,
4149 &font_context,
4150 false,
4151 None,
4152 false,
4153 None,
4154 false,
4155 )
4156 .unwrap();
4157 let text = String::from_utf8_lossy(&bytes);
4158
4159 assert!(text.contains("/Title (Test Document)"));
4160 assert!(text.contains("/Author (Forme)"));
4161 }
4162
4163 #[test]
4164 fn test_bold_font_registered_separately() {
4165 let writer = PdfWriter::new();
4166 let font_context = FontContext::new();
4167
4168 let pages = vec![LayoutPage {
4170 width: 595.28,
4171 height: 841.89,
4172 elements: vec![
4173 LayoutElement {
4174 x: 54.0,
4175 y: 54.0,
4176 width: 100.0,
4177 height: 16.8,
4178 draw: DrawCommand::Text {
4179 lines: vec![TextLine {
4180 x: 54.0,
4181 y: 66.0,
4182 width: 50.0,
4183 height: 16.8,
4184 glyphs: vec![PositionedGlyph {
4185 glyph_id: 65,
4186 x_offset: 0.0,
4187 y_offset: 0.0,
4188 x_advance: 8.0,
4189 font_size: 12.0,
4190 font_family: "Helvetica".to_string(),
4191 font_weight: 400,
4192 font_style: FontStyle::Normal,
4193 char_value: 'A',
4194 color: None,
4195 href: None,
4196 text_decoration: TextDecoration::None,
4197 letter_spacing: 0.0,
4198 cluster_text: None,
4199 }],
4200 word_spacing: 0.0,
4201 }],
4202 color: Color::BLACK,
4203 text_decoration: TextDecoration::None,
4204 opacity: 1.0,
4205 },
4206 children: vec![],
4207 node_type: None,
4208 resolved_style: None,
4209 source_location: None,
4210 href: None,
4211 bookmark: None,
4212 alt: None,
4213 is_header_row: false,
4214 overflow: Overflow::default(),
4215 opacity: 1.0,
4216 },
4217 LayoutElement {
4218 x: 54.0,
4219 y: 74.0,
4220 width: 100.0,
4221 height: 16.8,
4222 draw: DrawCommand::Text {
4223 lines: vec![TextLine {
4224 x: 54.0,
4225 y: 86.0,
4226 width: 50.0,
4227 height: 16.8,
4228 glyphs: vec![PositionedGlyph {
4229 glyph_id: 65,
4230 x_offset: 0.0,
4231 y_offset: 0.0,
4232 x_advance: 8.0,
4233 font_size: 12.0,
4234 font_family: "Helvetica".to_string(),
4235 font_weight: 700,
4236 font_style: FontStyle::Normal,
4237 char_value: 'A',
4238 color: None,
4239 href: None,
4240 text_decoration: TextDecoration::None,
4241 letter_spacing: 0.0,
4242 cluster_text: None,
4243 }],
4244 word_spacing: 0.0,
4245 }],
4246 color: Color::BLACK,
4247 text_decoration: TextDecoration::None,
4248 opacity: 1.0,
4249 },
4250 children: vec![],
4251 node_type: None,
4252 resolved_style: None,
4253 source_location: None,
4254 href: None,
4255 bookmark: None,
4256 alt: None,
4257 is_header_row: false,
4258 overflow: Overflow::default(),
4259 opacity: 1.0,
4260 },
4261 ],
4262 fixed_header: vec![],
4263 fixed_footer: vec![],
4264 watermarks: vec![],
4265 config: PageConfig::default(),
4266 }];
4267
4268 let metadata = Metadata::default();
4269 let bytes = writer
4270 .write(
4271 &pages,
4272 &metadata,
4273 &font_context,
4274 false,
4275 None,
4276 false,
4277 None,
4278 false,
4279 )
4280 .unwrap();
4281 let text = String::from_utf8_lossy(&bytes);
4282
4283 assert!(
4285 text.contains("Helvetica"),
4286 "Should contain regular Helvetica"
4287 );
4288 assert!(
4289 text.contains("Helvetica-Bold"),
4290 "Should contain Helvetica-Bold"
4291 );
4292 }
4293
4294 #[test]
4295 fn test_sanitize_font_name() {
4296 assert_eq!(PdfWriter::sanitize_font_name("Inter", 400, false), "Inter");
4297 assert_eq!(
4298 PdfWriter::sanitize_font_name("Inter", 700, false),
4299 "Inter-Bold"
4300 );
4301 assert_eq!(
4302 PdfWriter::sanitize_font_name("Inter", 400, true),
4303 "Inter-Italic"
4304 );
4305 assert_eq!(
4306 PdfWriter::sanitize_font_name("Inter", 700, true),
4307 "Inter-Bold-Italic"
4308 );
4309 assert_eq!(
4310 PdfWriter::sanitize_font_name("Noto Sans", 400, false),
4311 "NotoSans"
4312 );
4313 assert_eq!(
4314 PdfWriter::sanitize_font_name("Font (Display)", 400, false),
4315 "FontDisplay"
4316 );
4317 }
4318
4319 #[test]
4320 fn test_tounicode_cmap_format() {
4321 let mut glyph_to_char = HashMap::new();
4323 glyph_to_char.insert(36u16, 'A');
4324 glyph_to_char.insert(37u16, 'B');
4325
4326 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&glyph_to_char, "TestFont");
4327
4328 assert!(cmap.contains("begincmap"), "CMap should contain begincmap");
4329 assert!(cmap.contains("endcmap"), "CMap should contain endcmap");
4330 assert!(
4331 cmap.contains("beginbfchar"),
4332 "CMap should contain beginbfchar"
4333 );
4334 assert!(cmap.contains("endbfchar"), "CMap should contain endbfchar");
4335 assert!(
4336 cmap.contains("<0024> <0041>"),
4337 "Should map gid 0x0024 to Unicode 'A' 0x0041"
4338 );
4339 assert!(
4340 cmap.contains("<0025> <0042>"),
4341 "Should map gid 0x0025 to Unicode 'B' 0x0042"
4342 );
4343 assert!(
4344 cmap.contains("begincodespacerange"),
4345 "Should define codespace range"
4346 );
4347 assert!(
4348 cmap.contains("<0000> <FFFF>"),
4349 "Codespace should be 0000-FFFF"
4350 );
4351 }
4352
4353 #[test]
4354 fn test_w_array_format() {
4355 let mut char_to_gid = HashMap::new();
4356 char_to_gid.insert('A', 36u16);
4357
4358 let w_array_str = "[ 36 [600] ]";
4361 assert!(w_array_str.starts_with('['));
4362 assert!(w_array_str.ends_with(']'));
4363 }
4364
4365 #[test]
4366 fn test_hex_glyph_encoding() {
4367 let gid: u16 = 0x0041;
4369 let hex = format!("{:04X}", gid);
4370 assert_eq!(hex, "0041");
4371
4372 let gids = [0x0041u16, 0x0042, 0x0043];
4373 let hex_str: String = gids.iter().map(|g| format!("{:04X}", g)).collect();
4374 assert_eq!(hex_str, "004100420043");
4375 }
4376
4377 #[test]
4378 fn test_standard_font_still_uses_text_string() {
4379 let writer = PdfWriter::new();
4380 let font_context = FontContext::new();
4381
4382 let pages = vec![LayoutPage {
4383 width: 595.28,
4384 height: 841.89,
4385 elements: vec![LayoutElement {
4386 x: 54.0,
4387 y: 54.0,
4388 width: 100.0,
4389 height: 16.8,
4390 draw: DrawCommand::Text {
4391 lines: vec![TextLine {
4392 x: 54.0,
4393 y: 66.0,
4394 width: 50.0,
4395 height: 16.8,
4396 glyphs: vec![PositionedGlyph {
4397 glyph_id: 65,
4398 x_offset: 0.0,
4399 y_offset: 0.0,
4400 x_advance: 8.0,
4401 font_size: 12.0,
4402 font_family: "Helvetica".to_string(),
4403 font_weight: 400,
4404 font_style: FontStyle::Normal,
4405 char_value: 'H',
4406 color: None,
4407 href: None,
4408 text_decoration: TextDecoration::None,
4409 letter_spacing: 0.0,
4410 cluster_text: None,
4411 }],
4412 word_spacing: 0.0,
4413 }],
4414 color: Color::BLACK,
4415 text_decoration: TextDecoration::None,
4416 opacity: 1.0,
4417 },
4418 children: vec![],
4419 node_type: None,
4420 resolved_style: None,
4421 source_location: None,
4422 href: None,
4423 bookmark: None,
4424 alt: None,
4425 is_header_row: false,
4426 overflow: Overflow::default(),
4427 opacity: 1.0,
4428 }],
4429 fixed_header: vec![],
4430 fixed_footer: vec![],
4431 watermarks: vec![],
4432 config: PageConfig::default(),
4433 }];
4434
4435 let metadata = Metadata::default();
4436 let bytes = writer
4437 .write(
4438 &pages,
4439 &metadata,
4440 &font_context,
4441 false,
4442 None,
4443 false,
4444 None,
4445 false,
4446 )
4447 .unwrap();
4448 let text = String::from_utf8_lossy(&bytes);
4449
4450 assert!(
4452 text.contains("/Type1"),
4453 "Standard font should use Type1 subtype"
4454 );
4455 assert!(
4456 !text.contains("CIDFontType2"),
4457 "Standard font should not use CIDFontType2"
4458 );
4459 }
4460}