1pub(crate) mod tagged;
30pub(crate) mod xmp;
31
32use std::collections::{HashMap, HashSet};
33use std::fmt::Write as FmtWrite; use std::io::Write as IoWrite; use crate::error::FormeError;
37use crate::font::subset::subset_ttf;
38use crate::font::{FontContext, FontData, FontKey};
39use crate::layout::*;
40use crate::model::*;
41use crate::style::{Color, FontStyle, Overflow, TextDecoration};
42use crate::svg::SvgCommand;
43use miniz_oxide::deflate::compress_to_vec_zlib;
44
45struct LinkAnnotation {
47 x: f64,
48 y: f64,
49 width: f64,
50 height: f64,
51 href: String,
52}
53
54struct PdfBookmark {
56 title: String,
57 page_obj_id: usize,
58 y_pdf: f64,
59}
60
61pub struct PdfWriter;
62
63#[allow(dead_code)]
65struct CustomFontEmbedData {
66 ttf_data: Vec<u8>,
67 gid_remap: HashMap<u16, u16>,
69 glyph_to_char: HashMap<u16, char>,
71 char_to_gid: HashMap<char, u16>,
73 units_per_em: u16,
74 ascender: i16,
75 descender: i16,
76}
77
78struct FontUsage {
80 chars: HashSet<char>,
82 glyph_ids: HashSet<u16>,
84 glyph_to_char: HashMap<u16, char>,
86}
87
88struct PdfBuilder {
90 objects: Vec<PdfObject>,
91 font_objects: Vec<(FontKey, usize)>,
93 custom_font_data: HashMap<FontKey, CustomFontEmbedData>,
95 image_objects: Vec<usize>,
98 image_index_map: HashMap<(usize, usize), usize>,
101 ext_gstate_map: HashMap<u64, (usize, String)>,
104}
105
106pub(crate) struct PdfObject {
107 #[allow(dead_code)]
108 pub(crate) id: usize,
109 pub(crate) data: Vec<u8>,
110}
111
112impl Default for PdfWriter {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118impl PdfWriter {
119 pub fn new() -> Self {
120 Self
121 }
122
123 pub fn write(
125 &self,
126 pages: &[LayoutPage],
127 metadata: &Metadata,
128 font_context: &FontContext,
129 tagged: bool,
130 pdfa: Option<&PdfAConformance>,
131 embedded_data: Option<&str>,
132 ) -> Result<Vec<u8>, FormeError> {
133 let mut builder = PdfBuilder {
134 objects: Vec::new(),
135 font_objects: Vec::new(),
136 custom_font_data: HashMap::new(),
137 image_objects: Vec::new(),
138 image_index_map: HashMap::new(),
139 ext_gstate_map: HashMap::new(),
140 };
141
142 builder.objects.push(PdfObject {
148 id: 0,
149 data: vec![],
150 });
151 builder.objects.push(PdfObject {
152 id: 1,
153 data: vec![],
154 });
155 builder.objects.push(PdfObject {
156 id: 2,
157 data: vec![],
158 });
159
160 self.register_fonts(&mut builder, pages, font_context)?;
162
163 if pdfa.is_some() {
165 for (key, _) in &builder.font_objects {
166 if !builder.custom_font_data.contains_key(key) {
167 return Err(FormeError::RenderError(format!(
168 "PDF/A requires all fonts to be embedded. Register a custom font for \
169 family '{}' using Font.register().",
170 key.family
171 )));
172 }
173 }
174 }
175
176 self.register_images(&mut builder, pages);
178
179 self.register_ext_gstates(&mut builder, pages);
181
182 let mut tag_builder = if tagged {
184 Some(tagged::TagBuilder::new(pages.len()))
185 } else {
186 None
187 };
188
189 let mut page_obj_ids: Vec<usize> = Vec::new();
193 let mut all_bookmarks: Vec<PdfBookmark> = Vec::new();
194 let mut per_page_content_obj_ids: Vec<usize> = Vec::new();
195 let mut per_page_annotations: Vec<Vec<LinkAnnotation>> = Vec::new();
196 let mut per_page_resources: Vec<String> = Vec::new();
197
198 for (page_idx, page) in pages.iter().enumerate() {
200 let content = self.build_content_stream_for_page(
201 page,
202 page_idx,
203 &builder,
204 page_idx + 1,
205 pages.len(),
206 tag_builder.as_mut(),
207 );
208 let compressed = compress_to_vec_zlib(content.as_bytes(), 6);
209
210 let content_obj_id = builder.objects.len();
211 let mut content_data: Vec<u8> = Vec::new();
212 let _ = write!(
213 content_data,
214 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
215 compressed.len()
216 );
217 content_data.extend_from_slice(&compressed);
218 content_data.extend_from_slice(b"\nendstream");
219 builder.objects.push(PdfObject {
220 id: content_obj_id,
221 data: content_data,
222 });
223 per_page_content_obj_ids.push(content_obj_id);
224
225 let mut annotations: Vec<LinkAnnotation> = Vec::new();
227 Self::collect_link_annotations(&page.elements, page.height, &mut annotations);
228 per_page_annotations.push(annotations);
229
230 let page_obj_id = builder.objects.len();
232 builder.objects.push(PdfObject {
233 id: page_obj_id,
234 data: vec![],
235 });
236
237 let font_resources = self.build_font_resource_dict(&builder.font_objects);
239 let xobject_resources = self.build_xobject_resource_dict(page_idx, &builder);
240 let ext_gstate_resources = self.build_ext_gstate_resource_dict(&builder);
241 let mut resources = format!("/Font << {} >>", font_resources);
242 if !xobject_resources.is_empty() {
243 let _ = write!(resources, " /XObject << {} >>", xobject_resources);
244 }
245 if !ext_gstate_resources.is_empty() {
246 let _ = write!(resources, " /ExtGState << {} >>", ext_gstate_resources);
247 }
248 per_page_resources.push(resources);
249
250 Self::collect_bookmarks(&page.elements, page.height, page_obj_id, &mut all_bookmarks);
252
253 page_obj_ids.push(page_obj_id);
254 }
255
256 for (page_idx, annotations) in per_page_annotations.iter().enumerate() {
258 let mut annot_obj_ids: Vec<usize> = Vec::new();
259 for annot in annotations {
260 let rect = format!(
261 "[{:.2} {:.2} {:.2} {:.2}]",
262 annot.x,
263 annot.y,
264 annot.x + annot.width,
265 annot.y + annot.height
266 );
267
268 if let Some(anchor) = annot.href.strip_prefix('#') {
269 if let Some(bm) = all_bookmarks.iter().find(|b| b.title == anchor) {
271 let annot_obj_id = builder.objects.len();
272 let annot_dict = format!(
273 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
274 /A << /S /GoTo /D [{} 0 R /XYZ 0 {:.2} null] >> >>",
275 rect, bm.page_obj_id, bm.y_pdf
276 );
277 builder.objects.push(PdfObject {
278 id: annot_obj_id,
279 data: annot_dict.into_bytes(),
280 });
281 annot_obj_ids.push(annot_obj_id);
282 }
283 } else {
285 let annot_obj_id = builder.objects.len();
287 let annot_dict = format!(
288 "<< /Type /Annot /Subtype /Link /Rect {} /Border [0 0 0] \
289 /A << /Type /Action /S /URI /URI ({}) >> >>",
290 rect,
291 Self::escape_pdf_string(&annot.href)
292 );
293 builder.objects.push(PdfObject {
294 id: annot_obj_id,
295 data: annot_dict.into_bytes(),
296 });
297 annot_obj_ids.push(annot_obj_id);
298 }
299 }
300
301 let annots_str = if annot_obj_ids.is_empty() {
302 String::new()
303 } else {
304 let refs: String = annot_obj_ids
305 .iter()
306 .map(|id| format!("{} 0 R", id))
307 .collect::<Vec<_>>()
308 .join(" ");
309 format!(" /Annots [{}]", refs)
310 };
311
312 let page_obj_id = page_obj_ids[page_idx];
313 let content_obj_id = per_page_content_obj_ids[page_idx];
314 let struct_parents_str = if tagged {
315 format!(" /StructParents {}", page_idx)
316 } else {
317 String::new()
318 };
319 let page_dict = format!(
320 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {:.2} {:.2}] \
321 /Contents {} 0 R /Resources << {} >>{}{} >>",
322 pages[page_idx].width,
323 pages[page_idx].height,
324 content_obj_id,
325 per_page_resources[page_idx],
326 annots_str,
327 struct_parents_str
328 );
329 builder.objects[page_obj_id].data = page_dict.into_bytes();
330 }
331
332 let outlines_obj_id = if !all_bookmarks.is_empty() {
334 Some(self.write_outline_tree(&mut builder, &all_bookmarks))
335 } else {
336 None
337 };
338
339 let struct_tree_root_id = if let Some(ref tb) = tag_builder {
341 let (root_id, _parent_tree_id) = tb.write_objects(&mut builder.objects, &page_obj_ids);
342 Some(root_id)
343 } else {
344 None
345 };
346
347 let xmp_metadata_id = if let Some(conf) = pdfa {
349 let xmp_xml = xmp::generate_xmp(metadata, conf);
350 let xmp_bytes = xmp_xml.as_bytes();
351 let xmp_obj_id = builder.objects.len();
352 let xmp_data = format!(
354 "<< /Type /Metadata /Subtype /XML /Length {} >>\nstream\n",
355 xmp_bytes.len()
356 );
357 let mut xmp_obj_data: Vec<u8> = xmp_data.into_bytes();
358 xmp_obj_data.extend_from_slice(xmp_bytes);
359 xmp_obj_data.extend_from_slice(b"\nendstream");
360 builder.objects.push(PdfObject {
361 id: xmp_obj_id,
362 data: xmp_obj_data,
363 });
364 Some(xmp_obj_id)
365 } else {
366 None
367 };
368
369 let output_intent_id = if pdfa.is_some() {
370 static SRGB_ICC: &[u8] = include_bytes!("srgb2014.icc");
372 let compressed_icc = compress_to_vec_zlib(SRGB_ICC, 6);
373
374 let icc_obj_id = builder.objects.len();
375 let mut icc_data: Vec<u8> = Vec::new();
376 let _ = write!(
377 icc_data,
378 "<< /N 3 /Length {} /Filter /FlateDecode >>\nstream\n",
379 compressed_icc.len()
380 );
381 icc_data.extend_from_slice(&compressed_icc);
382 icc_data.extend_from_slice(b"\nendstream");
383 builder.objects.push(PdfObject {
384 id: icc_obj_id,
385 data: icc_data,
386 });
387
388 let oi_obj_id = builder.objects.len();
390 let oi_data = format!(
391 "<< /Type /OutputIntent /S /GTS_PDFA1 \
392 /OutputConditionIdentifier (sRGB IEC61966-2.1) \
393 /RegistryName (http://www.color.org) \
394 /DestOutputProfile {} 0 R >>",
395 icc_obj_id
396 );
397 builder.objects.push(PdfObject {
398 id: oi_obj_id,
399 data: oi_data.into_bytes(),
400 });
401 Some(oi_obj_id)
402 } else {
403 None
404 };
405
406 let embedded_names_id = if let Some(data) = embedded_data {
408 let compressed = compress_to_vec_zlib(data.as_bytes(), 6);
409
410 let ef_obj_id = builder.objects.len();
412 let ef_data = format!(
413 "<< /Type /EmbeddedFile /Subtype /application#2Fjson /Length {} /Filter /FlateDecode >>\nstream\n",
414 compressed.len()
415 );
416 let mut ef_bytes = ef_data.into_bytes();
417 ef_bytes.extend_from_slice(&compressed);
418 ef_bytes.extend_from_slice(b"\nendstream");
419 builder.objects.push(PdfObject {
420 id: ef_obj_id,
421 data: ef_bytes,
422 });
423
424 let fs_obj_id = builder.objects.len();
426 let fs_data = format!(
427 "<< /Type /Filespec /F (forme-data.json) /UF (forme-data.json) /EF << /F {} 0 R >> /AFRelationship /Data >>",
428 ef_obj_id
429 );
430 builder.objects.push(PdfObject {
431 id: fs_obj_id,
432 data: fs_data.into_bytes(),
433 });
434
435 let names_obj_id = builder.objects.len();
437 let names_data = format!("<< /Names [(forme-data.json) {} 0 R] >>", fs_obj_id);
438 builder.objects.push(PdfObject {
439 id: names_obj_id,
440 data: names_data.into_bytes(),
441 });
442
443 Some(names_obj_id)
444 } else {
445 None
446 };
447
448 let mut catalog = String::from("<< /Type /Catalog /Pages 2 0 R");
450 if let Some(outlines_id) = outlines_obj_id {
451 write!(
452 catalog,
453 " /Outlines {} 0 R /PageMode /UseOutlines",
454 outlines_id
455 )
456 .unwrap();
457 }
458 if let Some(ref lang) = metadata.lang {
459 write!(catalog, " /Lang ({})", Self::escape_pdf_string(lang)).unwrap();
460 }
461 if let Some(struct_root_id) = struct_tree_root_id {
462 write!(
463 catalog,
464 " /MarkInfo << /Marked true >> /StructTreeRoot {} 0 R",
465 struct_root_id
466 )
467 .unwrap();
468 }
469 if let Some(xmp_id) = xmp_metadata_id {
470 write!(catalog, " /Metadata {} 0 R", xmp_id).unwrap();
471 }
472 if let Some(oi_id) = output_intent_id {
473 write!(catalog, " /OutputIntents [{} 0 R]", oi_id).unwrap();
474 }
475 if let Some(names_id) = embedded_names_id {
476 write!(catalog, " /Names << /EmbeddedFiles {} 0 R >>", names_id).unwrap();
477 }
478 catalog.push_str(" >>");
479 builder.objects[1].data = catalog.into_bytes();
480
481 let kids: String = page_obj_ids
483 .iter()
484 .map(|id| format!("{} 0 R", id))
485 .collect::<Vec<_>>()
486 .join(" ");
487 builder.objects[2].data = format!(
488 "<< /Type /Pages /Kids [{}] /Count {} >>",
489 kids,
490 page_obj_ids.len()
491 )
492 .into_bytes();
493
494 let info_obj_id = if metadata.title.is_some() || metadata.author.is_some() {
496 let id = builder.objects.len();
497 let mut info = String::from("<< ");
498 if let Some(ref title) = metadata.title {
499 let _ = write!(info, "/Title ({}) ", Self::escape_pdf_string(title));
500 }
501 if let Some(ref author) = metadata.author {
502 let _ = write!(info, "/Author ({}) ", Self::escape_pdf_string(author));
503 }
504 if let Some(ref subject) = metadata.subject {
505 let _ = write!(info, "/Subject ({}) ", Self::escape_pdf_string(subject));
506 }
507 let _ = write!(info, "/Producer (Forme 0.6) /Creator (Forme) >>");
508 builder.objects.push(PdfObject {
509 id,
510 data: info.into_bytes(),
511 });
512 Some(id)
513 } else {
514 None
515 };
516
517 Ok(self.serialize(&builder, info_obj_id))
518 }
519
520 fn build_content_stream_for_page(
522 &self,
523 page: &LayoutPage,
524 page_idx: usize,
525 builder: &PdfBuilder,
526 page_number: usize,
527 total_pages: usize,
528 mut tag_builder: Option<&mut tagged::TagBuilder>,
529 ) -> String {
530 let mut stream = String::new();
531 let page_height = page.height;
532 let mut element_counter = 0usize;
533
534 for element in &page.elements {
535 self.write_element(
536 &mut stream,
537 element,
538 page_height,
539 builder,
540 page_idx,
541 &mut element_counter,
542 page_number,
543 total_pages,
544 tag_builder.as_deref_mut(),
545 );
546 }
547
548 stream
549 }
550
551 #[allow(clippy::too_many_arguments)]
553 fn write_element(
554 &self,
555 stream: &mut String,
556 element: &LayoutElement,
557 page_height: f64,
558 builder: &PdfBuilder,
559 page_idx: usize,
560 element_counter: &mut usize,
561 page_number: usize,
562 total_pages: usize,
563 mut tag_builder: Option<&mut tagged::TagBuilder>,
564 ) {
565 let tagged_mcid = if let Some(ref mut tb) = tag_builder {
567 if let Some(ref nt) = element.node_type {
568 let is_header = element.is_header_row;
569 let mcid = tb.begin_element(nt, is_header, element.alt.as_deref(), page_idx);
571 let role = tb.map_role_public(nt, is_header);
572 let _ = writeln!(stream, "/{} <</MCID {}>> BDC", role, mcid);
573 Some(mcid)
574 } else {
575 None
576 }
577 } else {
578 None
579 };
580
581 match &element.draw {
582 DrawCommand::None => {}
583
584 DrawCommand::Rect {
585 background,
586 border_width,
587 border_color,
588 border_radius,
589 opacity,
590 } => {
591 let x = element.x;
592 let y = page_height - element.y - element.height;
593 let w = element.width;
594 let h = element.height;
595
596 let needs_opacity = *opacity < 1.0;
598 if needs_opacity {
599 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
600 let _ = writeln!(stream, "q\n/{} gs", gs_name);
601 }
602 }
603
604 if let Some(bg) = background {
605 if bg.a > 0.0 {
606 let _ = writeln!(stream, "q\n{:.3} {:.3} {:.3} rg", bg.r, bg.g, bg.b);
607
608 if border_radius.top_left > 0.0 {
609 self.write_rounded_rect(stream, x, y, w, h, border_radius);
610 } else {
611 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
612 }
613
614 let _ = writeln!(stream, "f\nQ");
615 }
616 }
617
618 let bw = border_width;
619 if bw.top > 0.0 || bw.right > 0.0 || bw.bottom > 0.0 || bw.left > 0.0 {
620 if (bw.top - bw.right).abs() < 0.001
621 && (bw.right - bw.bottom).abs() < 0.001
622 && (bw.bottom - bw.left).abs() < 0.001
623 {
624 let bc = &border_color.top;
625 let _ = writeln!(
626 stream,
627 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w",
628 bc.r, bc.g, bc.b, bw.top
629 );
630
631 if border_radius.top_left > 0.0 {
632 self.write_rounded_rect(stream, x, y, w, h, border_radius);
633 } else {
634 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re", x, y, w, h);
635 }
636
637 let _ = writeln!(stream, "S\nQ");
638 } else {
639 self.write_border_sides(stream, x, y, w, h, bw, border_color);
640 }
641 }
642
643 if needs_opacity {
644 let _ = writeln!(stream, "Q");
645 }
646 }
647
648 DrawCommand::Text {
649 lines,
650 color,
651 text_decoration,
652 opacity,
653 } => {
654 let needs_opacity = *opacity < 1.0;
656 if needs_opacity {
657 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
658 let _ = writeln!(stream, "q\n/{} gs", gs_name);
659 }
660 }
661
662 for line in lines {
663 if line.glyphs.is_empty() {
664 continue;
665 }
666
667 let groups = Self::group_glyphs_by_style(&line.glyphs);
670 let pdf_y = page_height - line.y;
671
672 let _ = writeln!(stream, "BT");
673
674 if line.word_spacing.abs() > 0.001 {
676 let _ = writeln!(stream, "{:.4} Tw", line.word_spacing);
677 }
678
679 let mut tm_x = 0.0_f64;
681 let mut tm_y = 0.0_f64;
682 let mut x_cursor = line.x;
683
684 let mut group_spans: Vec<(f64, f64, TextDecoration, Color)> = Vec::new();
686
687 for group in &groups {
688 let first = &group[0];
689 let glyph_color = first.color.unwrap_or(*color);
690
691 let idx = self.font_index(
692 &first.font_family,
693 first.font_weight,
694 first.font_style,
695 &builder.font_objects,
696 );
697 let italic =
698 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
699 let font_key = FontKey {
700 family: first.font_family.clone(),
701 weight: if first.font_weight >= 600 { 700 } else { 400 },
702 italic,
703 };
704 let font_name = format!("F{}", idx);
705
706 let dx = x_cursor - tm_x;
708 let dy = pdf_y - tm_y;
709 let _ = writeln!(
710 stream,
711 "{:.3} {:.3} {:.3} rg\n/{} {:.1} Tf\n{:.2} Tc\n{:.2} {:.2} Td",
712 glyph_color.r,
713 glyph_color.g,
714 glyph_color.b,
715 font_name,
716 first.font_size,
717 first.letter_spacing,
718 dx,
719 dy
720 );
721 tm_x = x_cursor;
722 tm_y = pdf_y;
723
724 let raw_text: String = group.iter().map(|g| g.char_value).collect();
726 let has_placeholder = raw_text.contains("{{pageNumber}}")
727 || raw_text.contains("{{totalPages}}");
728
729 let is_custom = builder.custom_font_data.contains_key(&font_key);
730
731 if is_custom {
732 if let Some(embed_data) = builder.custom_font_data.get(&font_key) {
733 let mut hex = String::new();
734 if has_placeholder {
735 let text_after = raw_text
737 .replace("{{pageNumber}}", &page_number.to_string())
738 .replace("{{totalPages}}", &total_pages.to_string());
739 for ch in text_after.chars() {
740 let gid =
741 embed_data.char_to_gid.get(&ch).copied().unwrap_or(0);
742 let _ = write!(hex, "{:04X}", gid);
743 }
744 } else {
745 for g in group.iter() {
747 let new_gid = embed_data
748 .gid_remap
749 .get(&g.glyph_id)
750 .copied()
751 .unwrap_or_else(|| {
752 embed_data
754 .char_to_gid
755 .get(&g.char_value)
756 .copied()
757 .unwrap_or(0)
758 });
759 let _ = write!(hex, "{:04X}", new_gid);
760 }
761 }
762 let _ = writeln!(stream, "<{}> Tj", hex);
763 } else {
764 let _ = writeln!(stream, "<> Tj");
765 }
766 } else {
767 let text_after = raw_text
768 .replace("{{pageNumber}}", &page_number.to_string())
769 .replace("{{totalPages}}", &total_pages.to_string());
770 let mut text_str = String::new();
771 for ch in text_after.chars() {
772 let b = Self::unicode_to_winansi(ch).unwrap_or(b'?');
773 match b {
774 b'\\' => text_str.push_str("\\\\"),
775 b'(' => text_str.push_str("\\("),
776 b')' => text_str.push_str("\\)"),
777 0x20..=0x7E => text_str.push(b as char),
778 _ => {
779 let _ = write!(text_str, "\\{:03o}", b);
780 }
781 }
782 }
783 let _ = writeln!(stream, "({}) Tj", text_str);
784 }
785
786 let group_start_x = x_cursor;
788
789 if let Some(last) = group.last() {
792 let space_count_in_group =
793 group.iter().filter(|g| g.char_value == ' ').count();
794 x_cursor = line.x
795 + last.x_offset
796 + last.x_advance
797 + space_count_in_group as f64 * line.word_spacing;
798 }
799
800 let group_dec = first.text_decoration;
802 if !matches!(group_dec, TextDecoration::None) {
803 group_spans.push((group_start_x, x_cursor, group_dec, glyph_color));
804 }
805 }
806
807 let _ = writeln!(stream, "ET");
808
809 for (span_x, span_end_x, dec, dec_color) in &group_spans {
811 match dec {
812 TextDecoration::Underline => {
813 let underline_y = pdf_y - 1.5;
814 let _ = write!(
815 stream,
816 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
817 dec_color.r, dec_color.g, dec_color.b,
818 span_x, underline_y,
819 span_end_x, underline_y
820 );
821 }
822 TextDecoration::LineThrough => {
823 let first_size =
824 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
825 let strikethrough_y = pdf_y + first_size * 0.3;
826 let _ = write!(
827 stream,
828 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
829 dec_color.r, dec_color.g, dec_color.b,
830 span_x, strikethrough_y,
831 span_end_x, strikethrough_y
832 );
833 }
834 TextDecoration::None => {}
835 }
836 }
837
838 if group_spans.is_empty() {
840 if matches!(text_decoration, TextDecoration::Underline) {
841 let underline_y = pdf_y - 1.5;
842 let _ = write!(
843 stream,
844 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
845 color.r, color.g, color.b,
846 line.x, underline_y,
847 line.x + line.width, underline_y
848 );
849 }
850 if matches!(text_decoration, TextDecoration::LineThrough) {
851 let first_size =
852 line.glyphs.first().map(|g| g.font_size).unwrap_or(12.0);
853 let strikethrough_y = pdf_y + first_size * 0.3;
854 let _ = write!(
855 stream,
856 "q\n{:.3} {:.3} {:.3} RG\n0.5 w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
857 color.r, color.g, color.b,
858 line.x, strikethrough_y,
859 line.x + line.width, strikethrough_y
860 );
861 }
862 }
863 }
864
865 if needs_opacity {
866 let _ = writeln!(stream, "Q");
867 }
868 }
869
870 DrawCommand::Image { .. } => {
871 let elem_idx = *element_counter;
872 *element_counter += 1;
873 if let Some(&img_idx) = builder.image_index_map.get(&(page_idx, elem_idx)) {
874 let x = element.x;
875 let y = page_height - element.y - element.height;
876 let _ = write!(
877 stream,
878 "q\n{:.4} 0 0 {:.4} {:.2} {:.2} cm\n/Im{} Do\nQ\n",
879 element.width, element.height, x, y, img_idx
880 );
881 } else {
882 let x = element.x;
884 let y = page_height - element.y - element.height;
885 let _ = write!(
886 stream,
887 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
888 x, y, element.width, element.height
889 );
890 }
891 if tagged_mcid.is_some() {
892 let _ = writeln!(stream, "EMC");
893 if let Some(ref mut tb) = tag_builder {
894 tb.end_element();
895 }
896 }
897 return; }
899
900 DrawCommand::ImagePlaceholder => {
901 *element_counter += 1;
902 let x = element.x;
903 let y = page_height - element.y - element.height;
904 let _ = write!(
905 stream,
906 "q\n0.9 0.9 0.9 rg\n{:.2} {:.2} {:.2} {:.2} re\nf\nQ\n",
907 x, y, element.width, element.height
908 );
909 if tagged_mcid.is_some() {
910 let _ = writeln!(stream, "EMC");
911 if let Some(ref mut tb) = tag_builder {
912 tb.end_element();
913 }
914 }
915 return;
916 }
917
918 DrawCommand::Svg {
919 commands,
920 width: svg_w,
921 height: svg_h,
922 clip,
923 } => {
924 let x = element.x;
925 let y = page_height - element.y - element.height;
926
927 let _ = writeln!(stream, "q");
929 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", x, y);
930
931 if *svg_w > 0.0 && *svg_h > 0.0 {
933 let sx = element.width / svg_w;
934 let sy = element.height / svg_h;
935 let _ = writeln!(stream, "{:.4} 0 0 {:.4} 0 0 cm", sx, sy);
936 }
937
938 let _ = writeln!(stream, "1 0 0 -1 0 {:.2} cm", svg_h);
940
941 if *clip {
943 let _ = writeln!(stream, "0 0 {:.2} {:.2} re W n", svg_w, svg_h);
944 }
945
946 Self::write_svg_commands(stream, commands);
947
948 let _ = writeln!(stream, "Q");
949 if tagged_mcid.is_some() {
950 let _ = writeln!(stream, "EMC");
951 if let Some(ref mut tb) = tag_builder {
952 tb.end_element();
953 }
954 }
955 return;
956 }
957
958 DrawCommand::Barcode {
959 bars,
960 bar_width,
961 height,
962 color,
963 } => {
964 *element_counter += 1;
965 let _ = writeln!(stream, "q");
966 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
967 for (i, &bar) in bars.iter().enumerate() {
968 if bar == 1 {
969 let bx = element.x + i as f64 * bar_width;
970 let by = page_height - element.y - height;
971 let _ = writeln!(
972 stream,
973 "{:.2} {:.2} {:.2} {:.2} re",
974 bx, by, bar_width, height
975 );
976 }
977 }
978 let _ = writeln!(stream, "f\nQ");
979 if tagged_mcid.is_some() {
980 let _ = writeln!(stream, "EMC");
981 if let Some(ref mut tb) = tag_builder {
982 tb.end_element();
983 }
984 }
985 return;
986 }
987
988 DrawCommand::QrCode {
989 modules,
990 module_size,
991 color,
992 } => {
993 *element_counter += 1;
994 let _ = writeln!(stream, "q");
995 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
996 for (row_idx, row) in modules.iter().enumerate() {
997 for (col_idx, &dark) in row.iter().enumerate() {
998 if dark {
999 let mx = element.x + col_idx as f64 * module_size;
1000 let my = page_height - element.y - (row_idx as f64 + 1.0) * module_size;
1001 let _ = writeln!(
1002 stream,
1003 "{:.2} {:.2} {:.2} {:.2} re",
1004 mx, my, module_size, module_size
1005 );
1006 }
1007 }
1008 }
1009 let _ = writeln!(stream, "f\nQ");
1010 if tagged_mcid.is_some() {
1011 let _ = writeln!(stream, "EMC");
1012 if let Some(ref mut tb) = tag_builder {
1013 tb.end_element();
1014 }
1015 }
1016 return;
1017 }
1018
1019 DrawCommand::Chart { primitives } => {
1020 *element_counter += 1;
1021 let _ = writeln!(stream, "q");
1022 let _ = writeln!(
1024 stream,
1025 "1 0 0 -1 {:.4} {:.4} cm",
1026 element.x,
1027 page_height - element.y
1028 );
1029
1030 for prim in primitives {
1031 write_chart_primitive(stream, prim, element.height, builder);
1032 }
1033
1034 let _ = writeln!(stream, "Q");
1035 if tagged_mcid.is_some() {
1036 let _ = writeln!(stream, "EMC");
1037 if let Some(ref mut tb) = tag_builder {
1038 tb.end_element();
1039 }
1040 }
1041 return;
1042 }
1043
1044 DrawCommand::Watermark {
1045 lines,
1046 color,
1047 opacity,
1048 angle_rad,
1049 font_family: _,
1050 } => {
1051 let _ = writeln!(stream, "q");
1052 if *opacity < 1.0 {
1054 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
1055 let _ = writeln!(stream, "/{} gs", gs_name);
1056 }
1057 }
1058 let pdf_cx = element.x;
1060 let pdf_cy = page_height - element.y;
1061 let _ = writeln!(stream, "1 0 0 1 {:.2} {:.2} cm", pdf_cx, pdf_cy);
1062 let cos_a = angle_rad.cos();
1064 let sin_a = angle_rad.sin();
1065 let _ = writeln!(
1066 stream,
1067 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
1068 cos_a, sin_a, -sin_a, cos_a
1069 );
1070 let _ = writeln!(stream, "BT");
1072 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", color.r, color.g, color.b);
1073 if let Some(line) = lines.first() {
1074 let groups = Self::group_glyphs_by_style(&line.glyphs);
1075 let text_width = line.width;
1076 let cap_height = line.height * 0.7;
1077 let _ = writeln!(
1078 stream,
1079 "{:.2} {:.2} Td",
1080 -text_width / 2.0,
1081 -cap_height / 2.0
1082 );
1083 for group in &groups {
1084 let first = &group[0];
1085 let italic =
1086 matches!(first.font_style, FontStyle::Italic | FontStyle::Oblique);
1087 let fk = FontKey {
1088 family: first.font_family.clone(),
1089 weight: if first.font_weight >= 600 { 700 } else { 400 },
1090 italic,
1091 };
1092 let idx = self.font_index(
1093 &first.font_family,
1094 first.font_weight,
1095 first.font_style,
1096 &builder.font_objects,
1097 );
1098 let font_name = format!("F{}", idx);
1099 let _ = writeln!(stream, "/{} {:.1} Tf", font_name, first.font_size);
1100 let is_custom = builder.custom_font_data.contains_key(&fk);
1101 if is_custom {
1102 if let Some(embed_data) = builder.custom_font_data.get(&fk) {
1103 let mut hex = String::new();
1104 for g in group.iter() {
1105 let gid =
1106 embed_data.gid_remap.get(&g.glyph_id).copied().unwrap_or(0);
1107 let _ = write!(hex, "{:04X}", gid);
1108 }
1109 let _ = writeln!(stream, "<{}> Tj", hex);
1110 }
1111 } else {
1112 let hex_str: String = group
1113 .iter()
1114 .map(|g| format!("{:02X}", g.glyph_id as u8))
1115 .collect();
1116 let _ = writeln!(stream, "<{}> Tj", hex_str);
1117 }
1118 }
1119 }
1120 let _ = writeln!(stream, "ET");
1121 let _ = writeln!(stream, "Q");
1122 if tagged_mcid.is_some() {
1123 let _ = writeln!(stream, "EMC");
1124 if let Some(ref mut tb) = tag_builder {
1125 tb.end_element();
1126 }
1127 }
1128 return;
1129 }
1130 }
1131
1132 let clip_overflow = matches!(element.overflow, Overflow::Hidden);
1134 if clip_overflow {
1135 let clip_x = element.x;
1136 let clip_y = page_height - element.y - element.height;
1137 let clip_w = element.width;
1138 let clip_h = element.height;
1139 let _ = writeln!(
1140 stream,
1141 "q\n{:.2} {:.2} {:.2} {:.2} re W n",
1142 clip_x, clip_y, clip_w, clip_h
1143 );
1144 }
1145
1146 for child in &element.children {
1147 self.write_element(
1148 stream,
1149 child,
1150 page_height,
1151 builder,
1152 page_idx,
1153 element_counter,
1154 page_number,
1155 total_pages,
1156 tag_builder.as_deref_mut(),
1157 );
1158 }
1159
1160 if clip_overflow {
1161 let _ = writeln!(stream, "Q");
1162 }
1163
1164 if tagged_mcid.is_some() {
1166 let _ = writeln!(stream, "EMC");
1167 if let Some(ref mut tb) = tag_builder {
1168 tb.end_element();
1169 }
1170 }
1171 }
1172
1173 fn write_rounded_rect(
1174 &self,
1175 stream: &mut String,
1176 x: f64,
1177 y: f64,
1178 w: f64,
1179 h: f64,
1180 r: &crate::style::CornerValues,
1181 ) {
1182 let k = 0.5522847498;
1183
1184 let tl = r.top_left.min(w / 2.0).min(h / 2.0);
1185 let tr = r.top_right.min(w / 2.0).min(h / 2.0);
1186 let br = r.bottom_right.min(w / 2.0).min(h / 2.0);
1187 let bl = r.bottom_left.min(w / 2.0).min(h / 2.0);
1188
1189 let _ = writeln!(stream, "{:.2} {:.2} m", x + bl, y);
1190
1191 let _ = writeln!(stream, "{:.2} {:.2} l", x + w - br, y);
1192 if br > 0.0 {
1193 let _ = writeln!(
1194 stream,
1195 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
1196 x + w - br + br * k,
1197 y,
1198 x + w,
1199 y + br - br * k,
1200 x + w,
1201 y + br
1202 );
1203 }
1204
1205 let _ = writeln!(stream, "{:.2} {:.2} l", x + w, y + h - tr);
1206 if tr > 0.0 {
1207 let _ = writeln!(
1208 stream,
1209 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
1210 x + w,
1211 y + h - tr + tr * k,
1212 x + w - tr + tr * k,
1213 y + h,
1214 x + w - tr,
1215 y + h
1216 );
1217 }
1218
1219 let _ = writeln!(stream, "{:.2} {:.2} l", x + tl, y + h);
1220 if tl > 0.0 {
1221 let _ = writeln!(
1222 stream,
1223 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
1224 x + tl - tl * k,
1225 y + h,
1226 x,
1227 y + h - tl + tl * k,
1228 x,
1229 y + h - tl
1230 );
1231 }
1232
1233 let _ = writeln!(stream, "{:.2} {:.2} l", x, y + bl);
1234 if bl > 0.0 {
1235 let _ = writeln!(
1236 stream,
1237 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
1238 x,
1239 y + bl - bl * k,
1240 x + bl - bl * k,
1241 y,
1242 x + bl,
1243 y
1244 );
1245 }
1246
1247 let _ = writeln!(stream, "h");
1248 }
1249
1250 #[allow(clippy::too_many_arguments)]
1251 fn write_border_sides(
1252 &self,
1253 stream: &mut String,
1254 x: f64,
1255 y: f64,
1256 w: f64,
1257 h: f64,
1258 bw: &Edges,
1259 bc: &crate::style::EdgeValues<Color>,
1260 ) {
1261 if bw.top > 0.0 {
1262 let _ = write!(
1263 stream,
1264 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1265 bc.top.r,
1266 bc.top.g,
1267 bc.top.b,
1268 bw.top,
1269 x,
1270 y + h,
1271 x + w,
1272 y + h
1273 );
1274 }
1275 if bw.bottom > 0.0 {
1276 let _ = write!(
1277 stream,
1278 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1279 bc.bottom.r,
1280 bc.bottom.g,
1281 bc.bottom.b,
1282 bw.bottom,
1283 x,
1284 y,
1285 x + w,
1286 y
1287 );
1288 }
1289 if bw.left > 0.0 {
1290 let _ = write!(
1291 stream,
1292 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1293 bc.left.r,
1294 bc.left.g,
1295 bc.left.b,
1296 bw.left,
1297 x,
1298 y,
1299 x,
1300 y + h
1301 );
1302 }
1303 if bw.right > 0.0 {
1304 let _ = write!(
1305 stream,
1306 "q\n{:.3} {:.3} {:.3} RG\n{:.2} w\n{:.2} {:.2} m\n{:.2} {:.2} l\nS\nQ\n",
1307 bc.right.r,
1308 bc.right.g,
1309 bc.right.b,
1310 bw.right,
1311 x + w,
1312 y,
1313 x + w,
1314 y + h
1315 );
1316 }
1317 }
1318
1319 fn register_fonts(
1322 &self,
1323 builder: &mut PdfBuilder,
1324 pages: &[LayoutPage],
1325 font_context: &FontContext,
1326 ) -> Result<(), FormeError> {
1327 let mut font_usage_map: HashMap<FontKey, FontUsage> = HashMap::new();
1329
1330 for page in pages {
1331 Self::collect_font_usage(&page.elements, &mut font_usage_map);
1332 }
1333
1334 let mut keys: Vec<FontKey> = font_usage_map.keys().cloned().collect();
1335
1336 keys.sort_by(|a, b| {
1338 a.family
1339 .cmp(&b.family)
1340 .then(a.weight.cmp(&b.weight))
1341 .then(a.italic.cmp(&b.italic))
1342 });
1343 keys.dedup();
1344
1345 if keys.is_empty() {
1347 keys.push(FontKey {
1348 family: "Helvetica".to_string(),
1349 weight: 400,
1350 italic: false,
1351 });
1352 }
1353
1354 for key in &keys {
1355 let font_data = font_context.resolve(&key.family, key.weight, key.italic);
1356
1357 match font_data {
1358 FontData::Standard(std_font) => {
1359 let obj_id = builder.objects.len();
1360 let metrics = std_font.metrics();
1363 let widths_str: String = metrics
1364 .widths
1365 .iter()
1366 .map(|w| w.to_string())
1367 .collect::<Vec<_>>()
1368 .join(" ");
1369 let font_dict = format!(
1370 "<< /Type /Font /Subtype /Type1 /BaseFont /{} \
1371 /Encoding /WinAnsiEncoding \
1372 /FirstChar 32 /LastChar 255 /Widths [{}] >>",
1373 std_font.pdf_name(),
1374 widths_str,
1375 );
1376 builder.objects.push(PdfObject {
1377 id: obj_id,
1378 data: font_dict.into_bytes(),
1379 });
1380 builder.font_objects.push((key.clone(), obj_id));
1381 }
1382 FontData::Custom { data, .. } => {
1383 let usage = font_usage_map.get(key);
1384 let used_glyph_ids = usage.map(|u| &u.glyph_ids);
1385 let used_chars = usage.map(|u| &u.chars);
1386 let glyph_to_char = usage.map(|u| &u.glyph_to_char);
1387 let type0_obj_id = Self::write_custom_font_objects(
1388 builder,
1389 key,
1390 data,
1391 used_glyph_ids.cloned().unwrap_or_default(),
1392 used_chars.cloned().unwrap_or_default(),
1393 glyph_to_char.cloned().unwrap_or_default(),
1394 )?;
1395 builder.font_objects.push((key.clone(), type0_obj_id));
1396 }
1397 }
1398 }
1399
1400 Ok(())
1401 }
1402
1403 fn collect_font_usage(
1405 elements: &[LayoutElement],
1406 font_usage: &mut HashMap<FontKey, FontUsage>,
1407 ) {
1408 for element in elements {
1409 let lines_opt = match &element.draw {
1410 DrawCommand::Text { lines, .. } => Some(lines),
1411 DrawCommand::Watermark { lines, .. } => Some(lines),
1412 _ => None,
1413 };
1414 if let Some(lines) = lines_opt {
1415 for line in lines {
1416 for glyph in &line.glyphs {
1417 let italic =
1418 matches!(glyph.font_style, FontStyle::Italic | FontStyle::Oblique);
1419 let key = FontKey {
1420 family: glyph.font_family.clone(),
1421 weight: if glyph.font_weight >= 600 { 700 } else { 400 },
1422 italic,
1423 };
1424 let usage = font_usage.entry(key).or_insert_with(|| FontUsage {
1425 chars: HashSet::new(),
1426 glyph_ids: HashSet::new(),
1427 glyph_to_char: HashMap::new(),
1428 });
1429 usage.chars.insert(glyph.char_value);
1430 usage.glyph_ids.insert(glyph.glyph_id);
1431 usage
1433 .glyph_to_char
1434 .entry(glyph.glyph_id)
1435 .or_insert(glyph.char_value);
1436 if let Some(ref ct) = glyph.cluster_text {
1438 if let Some(first_char) = ct.chars().next() {
1440 usage
1441 .glyph_to_char
1442 .entry(glyph.glyph_id)
1443 .or_insert(first_char);
1444 }
1445 }
1446 }
1447 }
1448 }
1449 Self::collect_font_usage(&element.children, font_usage);
1450 }
1451 }
1452
1453 fn register_images(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
1456 for (page_idx, page) in pages.iter().enumerate() {
1457 let mut element_counter = 0usize;
1458 Self::collect_images_recursive(&page.elements, page_idx, &mut element_counter, builder);
1459 }
1460 }
1461
1462 fn collect_images_recursive(
1463 elements: &[LayoutElement],
1464 page_idx: usize,
1465 element_counter: &mut usize,
1466 builder: &mut PdfBuilder,
1467 ) {
1468 for element in elements {
1469 match &element.draw {
1470 DrawCommand::Image { image_data } => {
1471 let elem_idx = *element_counter;
1472 *element_counter += 1;
1473
1474 let img_idx = builder.image_objects.len();
1475 let xobj_id = Self::write_image_xobject(builder, image_data);
1476 builder.image_objects.push(xobj_id);
1477 builder
1478 .image_index_map
1479 .insert((page_idx, elem_idx), img_idx);
1480 }
1481 DrawCommand::ImagePlaceholder => {
1482 *element_counter += 1;
1483 }
1484 _ => {
1485 Self::collect_images_recursive(
1486 &element.children,
1487 page_idx,
1488 element_counter,
1489 builder,
1490 );
1491 }
1492 }
1493 }
1494 }
1495
1496 fn register_ext_gstates(&self, builder: &mut PdfBuilder, pages: &[LayoutPage]) {
1498 let mut unique_opacities: Vec<f64> = Vec::new();
1499 for page in pages {
1500 Self::collect_opacities_recursive(&page.elements, &mut unique_opacities);
1501 }
1502 unique_opacities.sort_by(|a, b| a.partial_cmp(b).unwrap());
1503 unique_opacities.dedup();
1504
1505 for (idx, &opacity) in unique_opacities.iter().enumerate() {
1506 let obj_id = builder.objects.len();
1507 let gs_name = format!("GS{}", idx);
1508 let obj_data = format!(
1509 "<< /Type /ExtGState /ca {:.4} /CA {:.4} >>",
1510 opacity, opacity
1511 );
1512 builder.objects.push(PdfObject {
1513 id: obj_id,
1514 data: obj_data.into_bytes(),
1515 });
1516 let key = opacity.to_bits();
1517 builder.ext_gstate_map.insert(key, (obj_id, gs_name));
1518 }
1519 }
1520
1521 fn collect_opacities_recursive(elements: &[LayoutElement], opacities: &mut Vec<f64>) {
1522 for element in elements {
1523 match &element.draw {
1524 DrawCommand::Rect { opacity, .. }
1525 | DrawCommand::Text { opacity, .. }
1526 | DrawCommand::Watermark { opacity, .. }
1527 if *opacity < 1.0 =>
1528 {
1529 opacities.push(*opacity);
1530 }
1531 DrawCommand::Chart { primitives } => {
1532 for prim in primitives {
1533 if let crate::chart::ChartPrimitive::FilledPath { opacity, .. } = prim {
1534 if *opacity < 1.0 {
1535 opacities.push(*opacity);
1536 }
1537 }
1538 }
1539 }
1540 _ => {}
1541 }
1542 Self::collect_opacities_recursive(&element.children, opacities);
1543 }
1544 }
1545
1546 fn build_ext_gstate_resource_dict(&self, builder: &PdfBuilder) -> String {
1548 if builder.ext_gstate_map.is_empty() {
1549 return String::new();
1550 }
1551 let mut entries: Vec<(&String, usize)> = builder
1552 .ext_gstate_map
1553 .values()
1554 .map(|(obj_id, name)| (name, *obj_id))
1555 .collect();
1556 entries.sort_by_key(|(name, _)| (*name).clone());
1557 entries
1558 .iter()
1559 .map(|(name, obj_id)| format!("/{} {} 0 R", name, obj_id))
1560 .collect::<Vec<_>>()
1561 .join(" ")
1562 }
1563
1564 fn write_image_xobject(
1567 builder: &mut PdfBuilder,
1568 image: &crate::image_loader::LoadedImage,
1569 ) -> usize {
1570 use crate::image_loader::{ImagePixelData, JpegColorSpace};
1571
1572 match &image.pixel_data {
1573 ImagePixelData::Jpeg { data, color_space } => {
1574 let color_space_str = match color_space {
1575 JpegColorSpace::DeviceRGB => "/DeviceRGB",
1576 JpegColorSpace::DeviceGray => "/DeviceGray",
1577 };
1578
1579 let obj_id = builder.objects.len();
1580 let mut obj_data: Vec<u8> = Vec::new();
1581 let _ = write!(
1582 obj_data,
1583 "<< /Type /XObject /Subtype /Image \
1584 /Width {} /Height {} \
1585 /ColorSpace {} \
1586 /BitsPerComponent 8 \
1587 /Filter /DCTDecode \
1588 /Length {} >>\nstream\n",
1589 image.width_px,
1590 image.height_px,
1591 color_space_str,
1592 data.len()
1593 );
1594 obj_data.extend_from_slice(data);
1595 obj_data.extend_from_slice(b"\nendstream");
1596 builder.objects.push(PdfObject {
1597 id: obj_id,
1598 data: obj_data,
1599 });
1600 obj_id
1601 }
1602
1603 ImagePixelData::Decoded { rgb, alpha } => {
1604 let smask_id = alpha.as_ref().map(|alpha_data| {
1606 let compressed_alpha = compress_to_vec_zlib(alpha_data, 6);
1607 let smask_obj_id = builder.objects.len();
1608 let mut smask_data: Vec<u8> = Vec::new();
1609 let _ = write!(
1610 smask_data,
1611 "<< /Type /XObject /Subtype /Image \
1612 /Width {} /Height {} \
1613 /ColorSpace /DeviceGray \
1614 /BitsPerComponent 8 \
1615 /Filter /FlateDecode \
1616 /Length {} >>\nstream\n",
1617 image.width_px,
1618 image.height_px,
1619 compressed_alpha.len()
1620 );
1621 smask_data.extend_from_slice(&compressed_alpha);
1622 smask_data.extend_from_slice(b"\nendstream");
1623 builder.objects.push(PdfObject {
1624 id: smask_obj_id,
1625 data: smask_data,
1626 });
1627 smask_obj_id
1628 });
1629
1630 let compressed_rgb = compress_to_vec_zlib(rgb, 6);
1632 let obj_id = builder.objects.len();
1633 let mut obj_data: Vec<u8> = Vec::new();
1634
1635 let smask_ref = smask_id
1636 .map(|id| format!(" /SMask {} 0 R", id))
1637 .unwrap_or_default();
1638
1639 let _ = write!(
1640 obj_data,
1641 "<< /Type /XObject /Subtype /Image \
1642 /Width {} /Height {} \
1643 /ColorSpace /DeviceRGB \
1644 /BitsPerComponent 8 \
1645 /Filter /FlateDecode \
1646 /Length {}{} >>\nstream\n",
1647 image.width_px,
1648 image.height_px,
1649 compressed_rgb.len(),
1650 smask_ref
1651 );
1652 obj_data.extend_from_slice(&compressed_rgb);
1653 obj_data.extend_from_slice(b"\nendstream");
1654 builder.objects.push(PdfObject {
1655 id: obj_id,
1656 data: obj_data,
1657 });
1658 obj_id
1659 }
1660 }
1661 }
1662
1663 fn build_xobject_resource_dict(&self, page_idx: usize, builder: &PdfBuilder) -> String {
1665 let mut entries: Vec<(usize, usize)> = Vec::new();
1666 for (&(pidx, _), &img_idx) in &builder.image_index_map {
1667 if pidx == page_idx {
1668 let obj_id = builder.image_objects[img_idx];
1669 entries.push((img_idx, obj_id));
1670 }
1671 }
1672 if entries.is_empty() {
1673 return String::new();
1674 }
1675 entries.sort_by_key(|(idx, _)| *idx);
1676 entries.dedup();
1677 entries
1678 .iter()
1679 .map(|(idx, obj_id)| format!("/Im{} {} 0 R", idx, obj_id))
1680 .collect::<Vec<_>>()
1681 .join(" ")
1682 }
1683
1684 fn write_custom_font_objects(
1691 builder: &mut PdfBuilder,
1692 key: &FontKey,
1693 ttf_data: &[u8],
1694 used_glyph_ids: HashSet<u16>,
1695 used_chars: HashSet<char>,
1696 glyph_to_char_map: HashMap<u16, char>,
1697 ) -> Result<usize, FormeError> {
1698 let face = ttf_parser::Face::parse(ttf_data, 0).map_err(|e| {
1699 FormeError::FontError(format!(
1700 "Failed to parse TTF data for font '{}': {}",
1701 key.family, e
1702 ))
1703 })?;
1704
1705 let units_per_em = face.units_per_em();
1706 let ascender = face.ascender();
1707 let descender = face.descender();
1708
1709 let mut char_to_orig_gid: HashMap<char, u16> = HashMap::new();
1711 for &ch in &used_chars {
1712 if let Some(gid) = face.glyph_index(ch) {
1713 char_to_orig_gid.insert(ch, gid.0);
1714 }
1715 }
1716
1717 let mut all_orig_gids: HashSet<u16> = used_glyph_ids.clone();
1721 for &gid in char_to_orig_gid.values() {
1722 all_orig_gids.insert(gid);
1723 }
1724
1725 let (embed_ttf, gid_remap) = match subset_ttf(ttf_data, &all_orig_gids) {
1727 Ok(subset_result) => (subset_result.ttf_data, subset_result.gid_remap),
1728 Err(_) => {
1729 let identity: HashMap<u16, u16> =
1731 all_orig_gids.iter().map(|&gid| (gid, gid)).collect();
1732 (ttf_data.to_vec(), identity)
1733 }
1734 };
1735
1736 let char_to_gid: HashMap<char, u16> = char_to_orig_gid
1738 .iter()
1739 .filter_map(|(&ch, &orig_gid)| gid_remap.get(&orig_gid).map(|&new_gid| (ch, new_gid)))
1740 .collect();
1741
1742 let gid_remap_for_embed = gid_remap.clone();
1744
1745 let mut new_gid_to_char: HashMap<u16, char> = HashMap::new();
1747 for (&orig_gid, &ch) in &glyph_to_char_map {
1749 if let Some(&new_gid) = gid_remap.get(&orig_gid) {
1750 new_gid_to_char.entry(new_gid).or_insert(ch);
1751 }
1752 }
1753 for (&ch, &new_gid) in &char_to_gid {
1755 new_gid_to_char.entry(new_gid).or_insert(ch);
1756 }
1757
1758 let pdf_font_name = Self::sanitize_font_name(&key.family, key.weight, key.italic);
1759
1760 let compressed_ttf = compress_to_vec_zlib(&embed_ttf, 6);
1762 let fontfile2_id = builder.objects.len();
1763 let mut fontfile2_data: Vec<u8> = Vec::new();
1764 let _ = write!(
1765 fontfile2_data,
1766 "<< /Length {} /Length1 {} /Filter /FlateDecode >>\nstream\n",
1767 compressed_ttf.len(),
1768 embed_ttf.len()
1769 );
1770 fontfile2_data.extend_from_slice(&compressed_ttf);
1771 fontfile2_data.extend_from_slice(b"\nendstream");
1772 builder.objects.push(PdfObject {
1773 id: fontfile2_id,
1774 data: fontfile2_data,
1775 });
1776
1777 let subset_face = ttf_parser::Face::parse(&embed_ttf, 0).unwrap_or_else(|_| face.clone());
1779 let subset_upem = subset_face.units_per_em();
1780
1781 let font_descriptor_id = builder.objects.len();
1783 let bbox = face.global_bounding_box();
1784 let scale = 1000.0 / units_per_em as f64;
1785 let bbox_str = format!(
1786 "[{} {} {} {}]",
1787 (bbox.x_min as f64 * scale) as i32,
1788 (bbox.y_min as f64 * scale) as i32,
1789 (bbox.x_max as f64 * scale) as i32,
1790 (bbox.y_max as f64 * scale) as i32,
1791 );
1792
1793 let flags = 4u32;
1794 let cap_height = face.capital_height().unwrap_or(ascender) as f64 * scale;
1795 let stem_v = if key.weight >= 700 { 120 } else { 80 };
1796
1797 let font_descriptor_dict = format!(
1798 "<< /Type /FontDescriptor /FontName /{} /Flags {} \
1799 /FontBBox {} /ItalicAngle {} \
1800 /Ascent {} /Descent {} /CapHeight {} /StemV {} \
1801 /FontFile2 {} 0 R >>",
1802 pdf_font_name,
1803 flags,
1804 bbox_str,
1805 if key.italic { -12 } else { 0 },
1806 (ascender as f64 * scale) as i32,
1807 (descender as f64 * scale) as i32,
1808 cap_height as i32,
1809 stem_v,
1810 fontfile2_id,
1811 );
1812 builder.objects.push(PdfObject {
1813 id: font_descriptor_id,
1814 data: font_descriptor_dict.into_bytes(),
1815 });
1816
1817 let cidfont_id = builder.objects.len();
1819 let w_array = Self::build_w_array_from_gids(&gid_remap, &subset_face, subset_upem);
1821 let default_width = subset_face
1822 .glyph_hor_advance(ttf_parser::GlyphId(0))
1823 .map(|adv| (adv as f64 * 1000.0 / subset_upem as f64) as u32)
1824 .unwrap_or(1000);
1825 let cidfont_dict = format!(
1826 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{} \
1827 /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> \
1828 /FontDescriptor {} 0 R /DW {} /W {} \
1829 /CIDToGIDMap /Identity >>",
1830 pdf_font_name, font_descriptor_id, default_width, w_array,
1831 );
1832 builder.objects.push(PdfObject {
1833 id: cidfont_id,
1834 data: cidfont_dict.into_bytes(),
1835 });
1836
1837 let tounicode_id = builder.objects.len();
1839 let cmap_content = Self::build_tounicode_cmap_from_gids(&new_gid_to_char, &pdf_font_name);
1840 let compressed_cmap = compress_to_vec_zlib(cmap_content.as_bytes(), 6);
1841 let mut tounicode_data: Vec<u8> = Vec::new();
1842 let _ = write!(
1843 tounicode_data,
1844 "<< /Length {} /Filter /FlateDecode >>\nstream\n",
1845 compressed_cmap.len()
1846 );
1847 tounicode_data.extend_from_slice(&compressed_cmap);
1848 tounicode_data.extend_from_slice(b"\nendstream");
1849 builder.objects.push(PdfObject {
1850 id: tounicode_id,
1851 data: tounicode_data,
1852 });
1853
1854 let type0_id = builder.objects.len();
1856 let type0_dict = format!(
1857 "<< /Type /Font /Subtype /Type0 /BaseFont /{} \
1858 /Encoding /Identity-H \
1859 /DescendantFonts [{} 0 R] \
1860 /ToUnicode {} 0 R >>",
1861 pdf_font_name, cidfont_id, tounicode_id,
1862 );
1863 builder.objects.push(PdfObject {
1864 id: type0_id,
1865 data: type0_dict.into_bytes(),
1866 });
1867
1868 builder.custom_font_data.insert(
1870 key.clone(),
1871 CustomFontEmbedData {
1872 ttf_data: embed_ttf,
1873 gid_remap: gid_remap_for_embed,
1874 glyph_to_char: glyph_to_char_map,
1875 char_to_gid,
1876 units_per_em,
1877 ascender,
1878 descender,
1879 },
1880 );
1881
1882 Ok(type0_id)
1883 }
1884
1885 fn build_w_array_from_gids(
1887 gid_remap: &HashMap<u16, u16>,
1888 face: &ttf_parser::Face,
1889 units_per_em: u16,
1890 ) -> String {
1891 let scale = 1000.0 / units_per_em as f64;
1892
1893 let mut entries: Vec<(u16, u32)> = Vec::new();
1894 let mut seen_gids: HashSet<u16> = HashSet::new();
1895
1896 for &new_gid in gid_remap.values() {
1897 if seen_gids.contains(&new_gid) {
1898 continue;
1899 }
1900 seen_gids.insert(new_gid);
1901 let advance = face
1902 .glyph_hor_advance(ttf_parser::GlyphId(new_gid))
1903 .unwrap_or(0);
1904 let width = (advance as f64 * scale) as u32;
1905 entries.push((new_gid, width));
1906 }
1907
1908 entries.sort_by_key(|(gid, _)| *gid);
1909
1910 let mut result = String::from("[");
1912 for (gid, width) in &entries {
1913 let _ = write!(result, " {} [{}]", gid, width);
1914 }
1915 result.push_str(" ]");
1916 result
1917 }
1918
1919 fn build_tounicode_cmap_from_gids(gid_to_char: &HashMap<u16, char>, font_name: &str) -> String {
1921 let mut gid_to_unicode: Vec<(u16, u32)> = gid_to_char
1922 .iter()
1923 .map(|(&gid, &ch)| (gid, ch as u32))
1924 .collect();
1925 gid_to_unicode.sort_by_key(|(gid, _)| *gid);
1926
1927 let mut cmap = String::new();
1928 let _ = writeln!(cmap, "/CIDInit /ProcSet findresource begin");
1929 let _ = writeln!(cmap, "12 dict begin");
1930 let _ = writeln!(cmap, "begincmap");
1931 let _ = writeln!(cmap, "/CIDSystemInfo");
1932 let _ = writeln!(
1933 cmap,
1934 "<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def"
1935 );
1936 let _ = writeln!(cmap, "/CMapName /{}-UTF16 def", font_name);
1937 let _ = writeln!(cmap, "/CMapType 2 def");
1938 let _ = writeln!(cmap, "1 begincodespacerange");
1939 let _ = writeln!(cmap, "<0000> <FFFF>");
1940 let _ = writeln!(cmap, "endcodespacerange");
1941
1942 for chunk in gid_to_unicode.chunks(100) {
1944 let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
1945 for &(gid, unicode) in chunk {
1946 let _ = writeln!(cmap, "<{:04X}> <{:04X}>", gid, unicode);
1947 }
1948 let _ = writeln!(cmap, "endbfchar");
1949 }
1950
1951 let _ = writeln!(cmap, "endcmap");
1952 let _ = writeln!(cmap, "CMapName currentdict /CMap defineresource pop");
1953 let _ = writeln!(cmap, "end");
1954 let _ = writeln!(cmap, "end");
1955
1956 cmap
1957 }
1958
1959 fn sanitize_font_name(family: &str, weight: u32, italic: bool) -> String {
1962 let mut name: String = family
1963 .chars()
1964 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
1965 .collect();
1966
1967 if weight >= 700 {
1968 name.push_str("-Bold");
1969 }
1970 if italic {
1971 name.push_str("-Italic");
1972 }
1973
1974 if name.is_empty() {
1976 name = "CustomFont".to_string();
1977 }
1978
1979 name
1980 }
1981
1982 fn build_font_resource_dict(&self, font_objects: &[(FontKey, usize)]) -> String {
1983 font_objects
1984 .iter()
1985 .enumerate()
1986 .map(|(i, (_, obj_id))| format!("/F{} {} 0 R", i, obj_id))
1987 .collect::<Vec<_>>()
1988 .join(" ")
1989 }
1990
1991 fn font_index(
1993 &self,
1994 family: &str,
1995 weight: u32,
1996 font_style: FontStyle,
1997 font_objects: &[(FontKey, usize)],
1998 ) -> usize {
1999 let italic = matches!(font_style, FontStyle::Italic | FontStyle::Oblique);
2000 let snapped_weight = if weight >= 600 { 700 } else { 400 };
2001
2002 for (i, (key, _)) in font_objects.iter().enumerate() {
2004 if key.family == family && key.weight == snapped_weight && key.italic == italic {
2005 return i;
2006 }
2007 }
2008
2009 for (i, (key, _)) in font_objects.iter().enumerate() {
2011 if key.family == "Helvetica" && key.weight == snapped_weight && key.italic == italic {
2012 return i;
2013 }
2014 }
2015
2016 0
2018 }
2019
2020 fn group_glyphs_by_style(glyphs: &[PositionedGlyph]) -> Vec<Vec<&PositionedGlyph>> {
2023 if glyphs.is_empty() {
2024 return vec![];
2025 }
2026
2027 let mut groups: Vec<Vec<&PositionedGlyph>> = Vec::new();
2028 let mut current_group: Vec<&PositionedGlyph> = vec![&glyphs[0]];
2029
2030 for glyph in &glyphs[1..] {
2031 let prev = current_group.last().unwrap();
2032 let same_style = glyph.font_family == prev.font_family
2033 && glyph.font_weight == prev.font_weight
2034 && std::mem::discriminant(&glyph.font_style)
2035 == std::mem::discriminant(&prev.font_style)
2036 && (glyph.font_size - prev.font_size).abs() < 0.01
2037 && Self::colors_equal(&glyph.color, &prev.color);
2038
2039 if same_style {
2040 current_group.push(glyph);
2041 } else {
2042 groups.push(current_group);
2043 current_group = vec![glyph];
2044 }
2045 }
2046 groups.push(current_group);
2047 groups
2048 }
2049
2050 fn colors_equal(a: &Option<Color>, b: &Option<Color>) -> bool {
2051 match (a, b) {
2052 (None, None) => true,
2053 (Some(ca), Some(cb)) => {
2054 (ca.r - cb.r).abs() < 0.001
2055 && (ca.g - cb.g).abs() < 0.001
2056 && (ca.b - cb.b).abs() < 0.001
2057 && (ca.a - cb.a).abs() < 0.001
2058 }
2059 _ => false,
2060 }
2061 }
2062
2063 fn collect_link_annotations(
2067 elements: &[LayoutElement],
2068 page_height: f64,
2069 annotations: &mut Vec<LinkAnnotation>,
2070 ) {
2071 for element in elements {
2072 if let Some(ref href) = element.href {
2073 if !href.is_empty() {
2074 let pdf_y = page_height - element.y - element.height;
2075 annotations.push(LinkAnnotation {
2076 x: element.x,
2077 y: pdf_y,
2078 width: element.width,
2079 height: element.height,
2080 href: href.clone(),
2081 });
2082 continue;
2084 }
2085 }
2086 Self::collect_link_annotations(&element.children, page_height, annotations);
2087 }
2088 }
2089
2090 fn collect_bookmarks(
2092 elements: &[LayoutElement],
2093 page_height: f64,
2094 page_obj_id: usize,
2095 bookmarks: &mut Vec<PdfBookmark>,
2096 ) {
2097 for element in elements {
2098 if let Some(ref title) = element.bookmark {
2099 let y_pdf = page_height - element.y;
2100 bookmarks.push(PdfBookmark {
2101 title: title.clone(),
2102 page_obj_id,
2103 y_pdf,
2104 });
2105 }
2106 Self::collect_bookmarks(&element.children, page_height, page_obj_id, bookmarks);
2107 }
2108 }
2109
2110 fn write_outline_tree(&self, builder: &mut PdfBuilder, bookmarks: &[PdfBookmark]) -> usize {
2113 let outlines_id = builder.objects.len();
2115 builder.objects.push(PdfObject {
2116 id: outlines_id,
2117 data: vec![],
2118 });
2119
2120 let mut item_ids: Vec<usize> = Vec::new();
2122 for _bm in bookmarks {
2123 let item_id = builder.objects.len();
2124 builder.objects.push(PdfObject {
2125 id: item_id,
2126 data: vec![],
2127 });
2128 item_ids.push(item_id);
2129 }
2130
2131 for (i, (bm, &item_id)) in bookmarks.iter().zip(item_ids.iter()).enumerate() {
2133 let mut dict = format!(
2134 "<< /Title ({}) /Parent {} 0 R /Dest [{} 0 R /XYZ 0 {:.2} null]",
2135 Self::escape_pdf_string(&bm.title),
2136 outlines_id,
2137 bm.page_obj_id,
2138 bm.y_pdf,
2139 );
2140 if i > 0 {
2141 let _ = write!(dict, " /Prev {} 0 R", item_ids[i - 1]);
2142 }
2143 if i + 1 < item_ids.len() {
2144 let _ = write!(dict, " /Next {} 0 R", item_ids[i + 1]);
2145 }
2146 dict.push_str(" >>");
2147 builder.objects[item_id].data = dict.into_bytes();
2148 }
2149
2150 let first_id = item_ids.first().copied().unwrap_or(0);
2152 let last_id = item_ids.last().copied().unwrap_or(0);
2153 let outlines_dict = format!(
2154 "<< /Type /Outlines /First {} 0 R /Last {} 0 R /Count {} >>",
2155 first_id,
2156 last_id,
2157 bookmarks.len()
2158 );
2159 builder.objects[outlines_id].data = outlines_dict.into_bytes();
2160
2161 outlines_id
2162 }
2163
2164 fn write_svg_commands(stream: &mut String, commands: &[SvgCommand]) {
2166 for cmd in commands {
2167 match cmd {
2168 SvgCommand::MoveTo(x, y) => {
2169 let _ = writeln!(stream, "{:.2} {:.2} m", x, y);
2170 }
2171 SvgCommand::LineTo(x, y) => {
2172 let _ = writeln!(stream, "{:.2} {:.2} l", x, y);
2173 }
2174 SvgCommand::CurveTo(x1, y1, x2, y2, x3, y3) => {
2175 let _ = writeln!(
2176 stream,
2177 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2178 x1, y1, x2, y2, x3, y3
2179 );
2180 }
2181 SvgCommand::ClosePath => {
2182 let _ = writeln!(stream, "h");
2183 }
2184 SvgCommand::SetFill(r, g, b) => {
2185 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", r, g, b);
2186 }
2187 SvgCommand::SetFillNone => {
2188 }
2190 SvgCommand::SetStroke(r, g, b) => {
2191 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", r, g, b);
2192 }
2193 SvgCommand::SetStrokeNone => {
2194 }
2196 SvgCommand::SetStrokeWidth(w) => {
2197 let _ = writeln!(stream, "{:.2} w", w);
2198 }
2199 SvgCommand::Fill => {
2200 let _ = writeln!(stream, "f");
2201 }
2202 SvgCommand::Stroke => {
2203 let _ = writeln!(stream, "S");
2204 }
2205 SvgCommand::FillAndStroke => {
2206 let _ = writeln!(stream, "B");
2207 }
2208 SvgCommand::SetLineCap(cap) => {
2209 let _ = writeln!(stream, "{} J", cap);
2210 }
2211 SvgCommand::SetLineJoin(join) => {
2212 let _ = writeln!(stream, "{} j", join);
2213 }
2214 SvgCommand::SaveState => {
2215 let _ = writeln!(stream, "q");
2216 }
2217 SvgCommand::RestoreState => {
2218 let _ = writeln!(stream, "Q");
2219 }
2220 }
2221 }
2222 }
2223
2224 pub(crate) fn escape_pdf_string(s: &str) -> String {
2226 s.replace('\\', "\\\\")
2227 .replace('(', "\\(")
2228 .replace(')', "\\)")
2229 }
2230
2231 fn unicode_to_winansi(ch: char) -> Option<u8> {
2233 crate::font::unicode_to_winansi(ch)
2234 }
2235
2236 fn serialize(&self, builder: &PdfBuilder, info_obj_id: Option<usize>) -> Vec<u8> {
2238 let mut output: Vec<u8> = Vec::new();
2239 let mut offsets: Vec<usize> = vec![0; builder.objects.len()];
2240
2241 output.extend_from_slice(b"%PDF-1.7\n");
2243 output.extend_from_slice(b"%\xe2\xe3\xcf\xd3\n");
2244
2245 for (i, obj) in builder.objects.iter().enumerate().skip(1) {
2246 offsets[i] = output.len();
2247 let header = format!("{} 0 obj\n", i);
2248 output.extend_from_slice(header.as_bytes());
2249 output.extend_from_slice(&obj.data);
2250 output.extend_from_slice(b"\nendobj\n\n");
2251 }
2252
2253 let xref_offset = output.len();
2254 let _ = writeln!(output, "xref\n0 {}", builder.objects.len());
2255 let _ = writeln!(output, "0000000000 65535 f ");
2256 for offset in offsets.iter().skip(1) {
2257 let _ = writeln!(output, "{:010} 00000 n ", offset);
2258 }
2259
2260 let _ = write!(
2261 output,
2262 "trailer\n<< /Size {} /Root 1 0 R",
2263 builder.objects.len()
2264 );
2265 if let Some(info_id) = info_obj_id {
2266 let _ = write!(output, " /Info {} 0 R", info_id);
2267 }
2268 let _ = writeln!(output, " >>\nstartxref\n{}\n%%EOF", xref_offset);
2269
2270 output
2271 }
2272}
2273
2274fn write_chart_primitive(
2279 stream: &mut String,
2280 prim: &crate::chart::ChartPrimitive,
2281 _chart_height: f64,
2282 builder: &PdfBuilder,
2283) {
2284 use crate::chart::{ChartPrimitive, TextAnchor};
2285 use crate::font::metrics::unicode_to_winansi;
2286
2287 match prim {
2288 ChartPrimitive::Rect { x, y, w, h, fill } => {
2289 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
2290 let _ = writeln!(stream, "{:.2} {:.2} {:.2} {:.2} re f", x, y, w, h);
2291 }
2292
2293 ChartPrimitive::Line {
2294 x1,
2295 y1,
2296 x2,
2297 y2,
2298 stroke,
2299 width,
2300 } => {
2301 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
2302 let _ = writeln!(stream, "{:.2} w", width);
2303 let _ = writeln!(stream, "{:.2} {:.2} m {:.2} {:.2} l S", x1, y1, x2, y2);
2304 }
2305
2306 ChartPrimitive::Polyline {
2307 points,
2308 stroke,
2309 width,
2310 } => {
2311 if points.len() < 2 {
2312 return;
2313 }
2314 let _ = writeln!(stream, "{:.3} {:.3} {:.3} RG", stroke.r, stroke.g, stroke.b);
2315 let _ = writeln!(stream, "{:.2} w", width);
2316 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
2317 for &(px, py) in &points[1..] {
2318 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
2319 }
2320 let _ = writeln!(stream, "S");
2321 }
2322
2323 ChartPrimitive::FilledPath {
2324 points,
2325 fill,
2326 opacity,
2327 } => {
2328 if points.len() < 3 {
2329 return;
2330 }
2331 let _ = writeln!(stream, "q");
2332 if *opacity < 1.0 {
2334 if let Some((_, gs_name)) = builder.ext_gstate_map.get(&opacity.to_bits()) {
2335 let _ = writeln!(stream, "/{} gs", gs_name);
2336 }
2337 }
2338 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
2339 let _ = writeln!(stream, "{:.2} {:.2} m", points[0].0, points[0].1);
2340 for &(px, py) in &points[1..] {
2341 let _ = writeln!(stream, "{:.2} {:.2} l", px, py);
2342 }
2343 let _ = writeln!(stream, "h f");
2344 let _ = writeln!(stream, "Q");
2345 }
2346
2347 ChartPrimitive::Circle { cx, cy, r, fill } => {
2348 let kappa: f64 = 0.5523;
2350 let kr = kappa * r;
2351 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
2352 let _ = writeln!(stream, "{:.2} {:.2} m", cx + r, cy);
2353 let _ = writeln!(
2354 stream,
2355 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2356 cx + r,
2357 cy + kr,
2358 cx + kr,
2359 cy + r,
2360 cx,
2361 cy + r
2362 );
2363 let _ = writeln!(
2364 stream,
2365 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2366 cx - kr,
2367 cy + r,
2368 cx - r,
2369 cy + kr,
2370 cx - r,
2371 cy
2372 );
2373 let _ = writeln!(
2374 stream,
2375 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2376 cx - r,
2377 cy - kr,
2378 cx - kr,
2379 cy - r,
2380 cx,
2381 cy - r
2382 );
2383 let _ = writeln!(
2384 stream,
2385 "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
2386 cx + kr,
2387 cy - r,
2388 cx + r,
2389 cy - kr,
2390 cx + r,
2391 cy
2392 );
2393 let _ = writeln!(stream, "f");
2394 }
2395
2396 ChartPrimitive::ArcSector {
2397 cx,
2398 cy,
2399 r,
2400 start_angle,
2401 end_angle,
2402 fill,
2403 } => {
2404 let _ = writeln!(stream, "{:.3} {:.3} {:.3} rg", fill.r, fill.g, fill.b);
2405 let _ = writeln!(stream, "{:.2} {:.2} m", cx, cy);
2407 let sx = cx + r * start_angle.cos();
2409 let sy = cy + r * start_angle.sin();
2410 let _ = writeln!(stream, "{:.2} {:.2} l", sx, sy);
2411
2412 let mut angle = *start_angle;
2414 let total = end_angle - start_angle;
2415 let segments = ((total.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
2416 let step = total / segments as f64;
2417
2418 for _ in 0..segments {
2419 let a1 = angle;
2420 let a2 = angle + step;
2421 let alpha = 4.0 / 3.0 * ((a2 - a1) / 4.0).tan();
2422
2423 let p1x = cx + r * a1.cos();
2424 let p1y = cy + r * a1.sin();
2425 let p2x = cx + r * a2.cos();
2426 let p2y = cy + r * a2.sin();
2427
2428 let cp1x = p1x - alpha * r * a1.sin();
2429 let cp1y = p1y + alpha * r * a1.cos();
2430 let cp2x = p2x + alpha * r * a2.sin();
2431 let cp2y = p2y - alpha * r * a2.cos();
2432
2433 let _ = writeln!(
2434 stream,
2435 "{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c",
2436 cp1x, cp1y, cp2x, cp2y, p2x, p2y
2437 );
2438 angle = a2;
2439 }
2440
2441 let _ = writeln!(stream, "h f");
2443 }
2444
2445 ChartPrimitive::Label {
2446 text,
2447 x,
2448 y,
2449 font_size,
2450 color,
2451 anchor,
2452 } => {
2453 let metrics = crate::font::StandardFont::Helvetica.metrics();
2455 let text_width = metrics.measure_string(text, *font_size, 0.0);
2456 let x_offset = match anchor {
2457 TextAnchor::Left => 0.0,
2458 TextAnchor::Center => -text_width / 2.0,
2459 TextAnchor::Right => -text_width,
2460 };
2461
2462 let font_idx = builder
2464 .font_objects
2465 .iter()
2466 .enumerate()
2467 .find(|(_, (key, _))| key.family == "Helvetica" && key.weight == 400 && !key.italic)
2468 .map(|(i, _)| i)
2469 .unwrap_or(0);
2470
2471 let encoded: String = text
2473 .chars()
2474 .map(|ch| {
2475 if let Some(code) = unicode_to_winansi(ch) {
2476 code as char
2477 } else if (ch as u32) >= 32 && (ch as u32) <= 255 {
2478 ch
2479 } else {
2480 '?'
2481 }
2482 })
2483 .collect();
2484 let escaped = pdf_escape_string(&encoded);
2485
2486 let _ = writeln!(stream, "q");
2488 let _ = writeln!(stream, "1 0 0 -1 {:.4} {:.4} cm", x + x_offset, *y);
2489 let _ = writeln!(
2490 stream,
2491 "BT /F{} {:.1} Tf {:.3} {:.3} {:.3} rg 0 0 Td ({}) Tj ET",
2492 font_idx, font_size, color.r, color.g, color.b, escaped
2493 );
2494 let _ = writeln!(stream, "Q");
2495 }
2496 }
2497}
2498
2499fn pdf_escape_string(s: &str) -> String {
2501 let mut out = String::with_capacity(s.len());
2502 for ch in s.chars() {
2503 match ch {
2504 '(' => out.push_str("\\("),
2505 ')' => out.push_str("\\)"),
2506 '\\' => out.push_str("\\\\"),
2507 _ => out.push(ch),
2508 }
2509 }
2510 out
2511}
2512
2513#[cfg(test)]
2514mod tests {
2515 use super::*;
2516 use crate::font::FontContext;
2517
2518 #[test]
2519 fn test_escape_pdf_string() {
2520 assert_eq!(
2521 PdfWriter::escape_pdf_string("Hello (World)"),
2522 "Hello \\(World\\)"
2523 );
2524 assert_eq!(PdfWriter::escape_pdf_string("back\\slash"), "back\\\\slash");
2525 }
2526
2527 #[test]
2528 fn test_empty_document_produces_valid_pdf() {
2529 let writer = PdfWriter::new();
2530 let font_context = FontContext::new();
2531 let pages = vec![LayoutPage {
2532 width: 595.28,
2533 height: 841.89,
2534 elements: vec![],
2535 fixed_header: vec![],
2536 fixed_footer: vec![],
2537 watermarks: vec![],
2538 config: PageConfig::default(),
2539 }];
2540 let metadata = Metadata::default();
2541 let bytes = writer
2542 .write(&pages, &metadata, &font_context, false, None, None)
2543 .unwrap();
2544
2545 assert!(bytes.starts_with(b"%PDF-1.7"));
2546 assert!(bytes.windows(5).any(|w| w == b"%%EOF"));
2547 assert!(bytes.windows(4).any(|w| w == b"xref"));
2548 assert!(bytes.windows(7).any(|w| w == b"trailer"));
2549 }
2550
2551 #[test]
2552 fn test_metadata_in_pdf() {
2553 let writer = PdfWriter::new();
2554 let font_context = FontContext::new();
2555 let pages = vec![LayoutPage {
2556 width: 595.28,
2557 height: 841.89,
2558 elements: vec![],
2559 fixed_header: vec![],
2560 fixed_footer: vec![],
2561 watermarks: vec![],
2562 config: PageConfig::default(),
2563 }];
2564 let metadata = Metadata {
2565 title: Some("Test Document".to_string()),
2566 author: Some("Forme".to_string()),
2567 subject: None,
2568 creator: None,
2569 lang: None,
2570 };
2571 let bytes = writer
2572 .write(&pages, &metadata, &font_context, false, None, None)
2573 .unwrap();
2574 let text = String::from_utf8_lossy(&bytes);
2575
2576 assert!(text.contains("/Title (Test Document)"));
2577 assert!(text.contains("/Author (Forme)"));
2578 }
2579
2580 #[test]
2581 fn test_bold_font_registered_separately() {
2582 let writer = PdfWriter::new();
2583 let font_context = FontContext::new();
2584
2585 let pages = vec![LayoutPage {
2587 width: 595.28,
2588 height: 841.89,
2589 elements: vec![
2590 LayoutElement {
2591 x: 54.0,
2592 y: 54.0,
2593 width: 100.0,
2594 height: 16.8,
2595 draw: DrawCommand::Text {
2596 lines: vec![TextLine {
2597 x: 54.0,
2598 y: 66.0,
2599 width: 50.0,
2600 height: 16.8,
2601 glyphs: vec![PositionedGlyph {
2602 glyph_id: 65,
2603 x_offset: 0.0,
2604 y_offset: 0.0,
2605 x_advance: 8.0,
2606 font_size: 12.0,
2607 font_family: "Helvetica".to_string(),
2608 font_weight: 400,
2609 font_style: FontStyle::Normal,
2610 char_value: 'A',
2611 color: None,
2612 href: None,
2613 text_decoration: TextDecoration::None,
2614 letter_spacing: 0.0,
2615 cluster_text: None,
2616 }],
2617 word_spacing: 0.0,
2618 }],
2619 color: Color::BLACK,
2620 text_decoration: TextDecoration::None,
2621 opacity: 1.0,
2622 },
2623 children: vec![],
2624 node_type: None,
2625 resolved_style: None,
2626 source_location: None,
2627 href: None,
2628 bookmark: None,
2629 alt: None,
2630 is_header_row: false,
2631 overflow: Overflow::default(),
2632 },
2633 LayoutElement {
2634 x: 54.0,
2635 y: 74.0,
2636 width: 100.0,
2637 height: 16.8,
2638 draw: DrawCommand::Text {
2639 lines: vec![TextLine {
2640 x: 54.0,
2641 y: 86.0,
2642 width: 50.0,
2643 height: 16.8,
2644 glyphs: vec![PositionedGlyph {
2645 glyph_id: 65,
2646 x_offset: 0.0,
2647 y_offset: 0.0,
2648 x_advance: 8.0,
2649 font_size: 12.0,
2650 font_family: "Helvetica".to_string(),
2651 font_weight: 700,
2652 font_style: FontStyle::Normal,
2653 char_value: 'A',
2654 color: None,
2655 href: None,
2656 text_decoration: TextDecoration::None,
2657 letter_spacing: 0.0,
2658 cluster_text: None,
2659 }],
2660 word_spacing: 0.0,
2661 }],
2662 color: Color::BLACK,
2663 text_decoration: TextDecoration::None,
2664 opacity: 1.0,
2665 },
2666 children: vec![],
2667 node_type: None,
2668 resolved_style: None,
2669 source_location: None,
2670 href: None,
2671 bookmark: None,
2672 alt: None,
2673 is_header_row: false,
2674 overflow: Overflow::default(),
2675 },
2676 ],
2677 fixed_header: vec![],
2678 fixed_footer: vec![],
2679 watermarks: vec![],
2680 config: PageConfig::default(),
2681 }];
2682
2683 let metadata = Metadata::default();
2684 let bytes = writer
2685 .write(&pages, &metadata, &font_context, false, None, None)
2686 .unwrap();
2687 let text = String::from_utf8_lossy(&bytes);
2688
2689 assert!(
2691 text.contains("Helvetica"),
2692 "Should contain regular Helvetica"
2693 );
2694 assert!(
2695 text.contains("Helvetica-Bold"),
2696 "Should contain Helvetica-Bold"
2697 );
2698 }
2699
2700 #[test]
2701 fn test_sanitize_font_name() {
2702 assert_eq!(PdfWriter::sanitize_font_name("Inter", 400, false), "Inter");
2703 assert_eq!(
2704 PdfWriter::sanitize_font_name("Inter", 700, false),
2705 "Inter-Bold"
2706 );
2707 assert_eq!(
2708 PdfWriter::sanitize_font_name("Inter", 400, true),
2709 "Inter-Italic"
2710 );
2711 assert_eq!(
2712 PdfWriter::sanitize_font_name("Inter", 700, true),
2713 "Inter-Bold-Italic"
2714 );
2715 assert_eq!(
2716 PdfWriter::sanitize_font_name("Noto Sans", 400, false),
2717 "NotoSans"
2718 );
2719 assert_eq!(
2720 PdfWriter::sanitize_font_name("Font (Display)", 400, false),
2721 "FontDisplay"
2722 );
2723 }
2724
2725 #[test]
2726 fn test_tounicode_cmap_format() {
2727 let mut glyph_to_char = HashMap::new();
2729 glyph_to_char.insert(36u16, 'A');
2730 glyph_to_char.insert(37u16, 'B');
2731
2732 let cmap = PdfWriter::build_tounicode_cmap_from_gids(&glyph_to_char, "TestFont");
2733
2734 assert!(cmap.contains("begincmap"), "CMap should contain begincmap");
2735 assert!(cmap.contains("endcmap"), "CMap should contain endcmap");
2736 assert!(
2737 cmap.contains("beginbfchar"),
2738 "CMap should contain beginbfchar"
2739 );
2740 assert!(cmap.contains("endbfchar"), "CMap should contain endbfchar");
2741 assert!(
2742 cmap.contains("<0024> <0041>"),
2743 "Should map gid 0x0024 to Unicode 'A' 0x0041"
2744 );
2745 assert!(
2746 cmap.contains("<0025> <0042>"),
2747 "Should map gid 0x0025 to Unicode 'B' 0x0042"
2748 );
2749 assert!(
2750 cmap.contains("begincodespacerange"),
2751 "Should define codespace range"
2752 );
2753 assert!(
2754 cmap.contains("<0000> <FFFF>"),
2755 "Codespace should be 0000-FFFF"
2756 );
2757 }
2758
2759 #[test]
2760 fn test_w_array_format() {
2761 let mut char_to_gid = HashMap::new();
2762 char_to_gid.insert('A', 36u16);
2763
2764 let w_array_str = "[ 36 [600] ]";
2767 assert!(w_array_str.starts_with('['));
2768 assert!(w_array_str.ends_with(']'));
2769 }
2770
2771 #[test]
2772 fn test_hex_glyph_encoding() {
2773 let gid: u16 = 0x0041;
2775 let hex = format!("{:04X}", gid);
2776 assert_eq!(hex, "0041");
2777
2778 let gids = [0x0041u16, 0x0042, 0x0043];
2779 let hex_str: String = gids.iter().map(|g| format!("{:04X}", g)).collect();
2780 assert_eq!(hex_str, "004100420043");
2781 }
2782
2783 #[test]
2784 fn test_standard_font_still_uses_text_string() {
2785 let writer = PdfWriter::new();
2786 let font_context = FontContext::new();
2787
2788 let pages = vec![LayoutPage {
2789 width: 595.28,
2790 height: 841.89,
2791 elements: vec![LayoutElement {
2792 x: 54.0,
2793 y: 54.0,
2794 width: 100.0,
2795 height: 16.8,
2796 draw: DrawCommand::Text {
2797 lines: vec![TextLine {
2798 x: 54.0,
2799 y: 66.0,
2800 width: 50.0,
2801 height: 16.8,
2802 glyphs: vec![PositionedGlyph {
2803 glyph_id: 65,
2804 x_offset: 0.0,
2805 y_offset: 0.0,
2806 x_advance: 8.0,
2807 font_size: 12.0,
2808 font_family: "Helvetica".to_string(),
2809 font_weight: 400,
2810 font_style: FontStyle::Normal,
2811 char_value: 'H',
2812 color: None,
2813 href: None,
2814 text_decoration: TextDecoration::None,
2815 letter_spacing: 0.0,
2816 cluster_text: None,
2817 }],
2818 word_spacing: 0.0,
2819 }],
2820 color: Color::BLACK,
2821 text_decoration: TextDecoration::None,
2822 opacity: 1.0,
2823 },
2824 children: vec![],
2825 node_type: None,
2826 resolved_style: None,
2827 source_location: None,
2828 href: None,
2829 bookmark: None,
2830 alt: None,
2831 is_header_row: false,
2832 overflow: Overflow::default(),
2833 }],
2834 fixed_header: vec![],
2835 fixed_footer: vec![],
2836 watermarks: vec![],
2837 config: PageConfig::default(),
2838 }];
2839
2840 let metadata = Metadata::default();
2841 let bytes = writer
2842 .write(&pages, &metadata, &font_context, false, None, None)
2843 .unwrap();
2844 let text = String::from_utf8_lossy(&bytes);
2845
2846 assert!(
2848 text.contains("/Type1"),
2849 "Standard font should use Type1 subtype"
2850 );
2851 assert!(
2852 !text.contains("CIDFontType2"),
2853 "Standard font should not use CIDFontType2"
2854 );
2855 }
2856}