1use std::collections::{BTreeSet, HashMap};
17use std::fmt;
18use std::rc::Rc;
19
20use miniz_oxide::deflate::compress_to_vec_zlib;
21use pdf_writer::types::{
22 ActionType, AnnotationType, CidFontType, FontFlags, PageMode, SystemInfo, UnicodeCmap,
23};
24use pdf_writer::writers::Resources;
25use pdf_writer::{Content, Filter, Finish, Name, Pdf, Rect as PdfRect, Ref, Str, TextStr};
26use ravenrs_core::{Color, Rect};
27use ravenrs_report::vector::{self, PathSegment};
28use ravenrs_report::{Document, DocumentImage, ImageFormat, Item, Page, TextBlock};
29use ravenrs_text::{TextAttrs, TextSystem};
30use skrifa::instance::{LocationRef, Size};
31use skrifa::{GlyphId, MetadataProvider};
32use subsetter::GlyphRemapper;
33
34#[derive(Clone, Debug, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum PdfError {
38 Font(String),
40}
41
42impl fmt::Display for PdfError {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::Font(reason) => write!(f, "could not embed a font: {reason}"),
46 }
47 }
48}
49
50impl std::error::Error for PdfError {}
51
52pub fn to_pdf(document: &Document) -> Result<Vec<u8>, PdfError> {
54 let mut writer = Writer::new()?;
55 let contents: Vec<(Vec<u8>, &Page)> =
56 document.pages.iter().map(|page| (writer.page_content(page), page)).collect();
57 writer.finish(document, contents)
58}
59
60const UNITS: f32 = 1000.0;
62
63struct Shaped {
65 glyphs: Vec<ShapedGlyph>,
66}
67
68struct ShapedGlyph {
69 cid: u16,
71 x: f32,
72}
73
74struct Font {
76 data: &'static [u8],
77 bold: bool,
78 remapper: GlyphRemapper,
79 originals: Vec<u16>,
81 widths: Vec<f32>,
83 unicode: Vec<Option<String>>,
85 advances: Vec<f32>,
87}
88
89impl Font {
90 fn new(bold: bool) -> Result<Self, PdfError> {
91 let data = ravenrs_text::inter_font(bold);
92 let font = skrifa::FontRef::new(data).map_err(|e| PdfError::Font(e.to_string()))?;
93 let metrics = font.glyph_metrics(Size::new(UNITS), LocationRef::default());
94 let advances: Vec<f32> = (0..metrics.glyph_count())
95 .map(|g| metrics.advance_width(GlyphId::new(g)).unwrap_or(0.0))
96 .collect();
97 let notdef = advance(&advances, 0);
98 Ok(Self {
99 data,
100 bold,
101 remapper: GlyphRemapper::new(),
102 originals: vec![0],
103 widths: vec![notdef],
104 unicode: vec![None],
105 advances,
106 })
107 }
108
109 fn cid(&mut self, glyph: u16, text: &str) -> u16 {
111 let cid = self.remapper.remap(glyph);
112 if usize::from(cid) == self.originals.len() {
113 self.originals.push(glyph);
114 self.widths.push(advance(&self.advances, glyph));
115 self.unicode.push(None);
116 }
117 let slot = &mut self.unicode[usize::from(cid)];
118 if slot.is_none() && !text.is_empty() {
119 *slot = Some(text.to_owned());
120 }
121 cid
122 }
123}
124
125fn advance(advances: &[f32], glyph: u16) -> f32 {
126 advances.get(usize::from(glyph)).copied().unwrap_or(0.0)
127}
128
129struct Writer {
130 text: TextSystem,
131 fonts: [Font; 2],
132 shaped: HashMap<(bool, String), Rc<Shaped>>,
133 alphas: BTreeSet<u8>,
135 images: Vec<DocumentImage>,
137 image_index: HashMap<u64, usize>,
138}
139
140impl Writer {
141 fn new() -> Result<Self, PdfError> {
142 Ok(Self {
143 text: TextSystem::new(),
144 fonts: [Font::new(false)?, Font::new(true)?],
145 shaped: HashMap::new(),
146 alphas: BTreeSet::new(),
147 images: Vec::new(),
148 image_index: HashMap::new(),
149 })
150 }
151
152 fn shape(&mut self, text: &str, bold: bool) -> Rc<Shaped> {
153 let key = (bold, text.to_owned());
154 if let Some(shaped) = self.shaped.get(&key) {
155 return shaped.clone();
156 }
157 let attrs = TextAttrs {
158 size: UNITS,
159 line_height: UNITS,
160 weight: if bold { 600.0 } else { 400.0 },
161 ..TextAttrs::default()
162 };
163 let mut layout = self.text.layout(text, &attrs);
164 layout.measure(None);
165 let font = &mut self.fonts[usize::from(bold)];
166 let glyphs = layout
167 .glyphs()
168 .into_iter()
169 .map(|g| {
170 let id = u16::try_from(g.id).unwrap_or(0);
171 ShapedGlyph { cid: font.cid(id, text.get(g.text).unwrap_or_default()), x: g.x }
172 })
173 .collect();
174 let shaped = Rc::new(Shaped { glyphs });
175 self.shaped.insert(key, shaped.clone());
176 shaped
177 }
178
179 fn page_content(&mut self, page: &Page) -> Vec<u8> {
181 let mut content = Content::new();
182 let h = page.height;
183 for item in &page.items {
184 match item {
185 Item::Rect { rect, fill, stroke, radius } => {
186 self.draw(&mut content, *fill, *stroke, |c| {
187 write_path(c, &vector::rectangle(*rect, *radius), h)
188 });
189 }
190 Item::Ellipse { rect, fill, stroke } => {
191 self.draw(&mut content, *fill, *stroke, |c| write_path(c, &vector::ellipse(*rect), h));
192 }
193 Item::Line { from, to, width, color } => {
194 let alpha = self.begin_alpha(&mut content, *color);
195 content.set_stroke_rgb(color.r, color.g, color.b);
196 content.set_line_width(*width);
197 content.move_to(from.x, h - from.y);
198 content.line_to(to.x, h - to.y);
199 content.stroke();
200 end_alpha(&mut content, alpha);
201 }
202 Item::Text(block) => self.draw_text(&mut content, block, h),
203 Item::Image { rect, clip, image } => {
204 let index = self.image_number(image);
205 content.save_state();
206 content.rect(clip.x(), h - clip.max_y(), clip.width(), clip.height());
207 content.clip_nonzero();
208 content.end_path();
209 content.transform([rect.width(), 0.0, 0.0, rect.height(), rect.x(), h - rect.max_y()]);
210 content.x_object(Name(image_name(index).as_bytes()));
211 content.restore_state();
212 }
213 Item::Vector { shapes, .. } => {
214 for shape in shapes {
215 self.draw(&mut content, shape.fill, shape.stroke, |c| {
216 write_path(c, &shape.path, h);
217 });
218 }
219 }
220 _ => {}
222 }
223 }
224 compress_to_vec_zlib(&content.finish(), 6)
225 }
226
227 fn image_number(&mut self, image: &DocumentImage) -> usize {
228 let images = &mut self.images;
229 *self.image_index.entry(image.pixels().id()).or_insert_with(|| {
230 images.push(image.clone());
231 images.len() - 1
232 })
233 }
234
235 fn draw(
236 &mut self,
237 content: &mut Content,
238 fill: Option<Color>,
239 stroke: Option<(f32, Color)>,
240 path: impl Fn(&mut Content),
241 ) {
242 if let Some(fill) = fill.filter(|c| c.a > 0.0) {
243 let alpha = self.begin_alpha(content, fill);
244 content.set_fill_rgb(fill.r, fill.g, fill.b);
245 path(content);
246 content.fill_nonzero();
247 end_alpha(content, alpha);
248 }
249 if let Some((width, color)) = stroke.filter(|(w, c)| *w > 0.0 && c.a > 0.0) {
250 let alpha = self.begin_alpha(content, color);
251 content.set_stroke_rgb(color.r, color.g, color.b);
252 content.set_line_width(width);
253 path(content);
254 content.stroke();
255 end_alpha(content, alpha);
256 }
257 }
258
259 fn begin_alpha(&mut self, content: &mut Content, color: Color) -> bool {
261 let alpha = (color.a.clamp(0.0, 1.0) * 255.0).round() as u8;
262 if alpha == 255 {
263 return false;
264 }
265 self.alphas.insert(alpha);
266 content.save_state();
267 content.set_parameters(Name(alpha_name(alpha).as_bytes()));
268 true
269 }
270
271 fn draw_text(&mut self, content: &mut Content, block: &TextBlock, h: f32) {
272 let c = block.clip;
274 let overflows = block.lines.iter().any(|l| {
275 l.x < c.x() - 0.5
276 || l.x + l.width > c.max_x() + 0.5
277 || l.top < c.y() - 0.5
278 || l.baseline + block.size * 0.3 > c.max_y() + 0.5
279 });
280 let alpha = self.begin_alpha(content, block.color);
281 if overflows {
282 content.save_state();
283 content.rect(c.x(), h - c.max_y(), c.width(), c.height());
284 content.clip_nonzero();
285 content.end_path();
286 }
287 content.set_fill_rgb(block.color.r, block.color.g, block.color.b);
288 content.begin_text();
289 content.set_font(Name(font_name(block.bold).as_bytes()), block.size);
290 let mut encoded = Vec::new();
291 for line in &block.lines {
292 let shaped = self.shape(&line.text, block.bold);
293 if shaped.glyphs.is_empty() {
294 continue;
295 }
296 content.set_text_matrix([1.0, 0.0, 0.0, 1.0, line.x, h - line.baseline]);
297 let widths = &self.fonts[usize::from(block.bold)].widths;
298 let mut show = content.show_positioned();
299 let mut items = show.items();
300 let mut pen = 0.0;
301 for glyph in &shaped.glyphs {
302 if (glyph.x - pen).abs() > 0.01 {
304 if !encoded.is_empty() {
305 items.show(Str(&encoded));
306 encoded.clear();
307 }
308 items.adjust(pen - glyph.x);
309 pen = glyph.x;
310 }
311 encoded.extend_from_slice(&glyph.cid.to_be_bytes());
312 pen += widths[usize::from(glyph.cid)];
313 }
314 if !encoded.is_empty() {
315 items.show(Str(&encoded));
316 encoded.clear();
317 }
318 items.finish();
319 show.finish();
320 }
321 content.end_text();
322 if overflows {
323 content.restore_state();
324 }
325 end_alpha(content, alpha);
326 }
327
328 fn finish(self, document: &Document, contents: Vec<(Vec<u8>, &Page)>) -> Result<Vec<u8>, PdfError> {
329 let mut next = 1;
330 let mut alloc = || {
331 let id = Ref::new(next);
332 next += 1;
333 id
334 };
335 let (catalog, tree, resources, info) = (alloc(), alloc(), alloc(), alloc());
336 let page_ids: Vec<(Ref, Ref)> = contents.iter().map(|_| (alloc(), alloc())).collect();
337 let mut pdf = Pdf::new();
338 let outline = write_outline(&mut pdf, &contents, &page_ids, &mut alloc);
339 let mut root = pdf.catalog(catalog);
340 root.pages(tree);
341 if let Some(outline) = outline {
342 root.outlines(outline);
343 root.page_mode(PageMode::UseOutlines);
344 }
345 root.finish();
346 let mut about = pdf.document_info(info);
347 about.title(TextStr(&document.title));
348 about.producer(TextStr("RavenRS Reporting"));
349 about.finish();
350
351 let font_ids: Vec<Ref> =
352 self.fonts.iter().map(|font| write_font(&mut pdf, font, &mut alloc)).collect::<Result<_, _>>()?;
353
354 let alpha_ids: Vec<(u8, Ref)> = self.alphas.iter().map(|a| (*a, alloc())).collect();
355 for (alpha, id) in &alpha_ids {
356 let value = f32::from(*alpha) / 255.0;
357 pdf.ext_graphics(*id).non_stroking_alpha(value).stroking_alpha(value);
358 }
359 let image_ids: Vec<Ref> =
360 self.images.iter().map(|image| write_image(&mut pdf, image, &mut alloc)).collect();
361 let mut res = pdf.indirect(resources).start::<Resources>();
362 let mut fonts = res.fonts();
363 for (bold, id) in [false, true].into_iter().zip(&font_ids) {
364 fonts.pair(Name(font_name(bold).as_bytes()), *id);
365 }
366 fonts.finish();
367 let mut states = res.ext_g_states();
368 for (alpha, id) in &alpha_ids {
369 states.pair(Name(alpha_name(*alpha).as_bytes()), *id);
370 }
371 states.finish();
372 let mut x_objects = res.x_objects();
373 for (index, id) in image_ids.iter().enumerate() {
374 x_objects.pair(Name(image_name(index).as_bytes()), *id);
375 }
376 x_objects.finish();
377 res.finish();
378
379 let mut pages = pdf.pages(tree);
380 pages.kids(page_ids.iter().map(|(page, _)| *page));
381 pages.count(i32::try_from(page_ids.len()).unwrap_or(i32::MAX));
382 pages.pair(Name(b"Resources"), resources);
383 pages.finish();
384 for ((data, page), (id, content_id)) in contents.iter().zip(&page_ids) {
385 let mut out = pdf.page(*id);
386 out.parent(tree);
387 out.media_box(PdfRect::new(0.0, 0.0, page.width, page.height));
388 out.contents(*content_id);
389 let links: Vec<(&Rect, &str)> = page
390 .items
391 .iter()
392 .filter_map(|item| match item {
393 Item::Link { rect, url } => Some((rect, url.as_str())),
394 _ => None,
395 })
396 .collect();
397 let annotation_ids: Vec<Ref> = links.iter().map(|_| alloc()).collect();
398 if !annotation_ids.is_empty() {
399 out.annotations(annotation_ids.iter().copied());
400 }
401 out.finish();
402 pdf.stream(*content_id, data).filter(Filter::FlateDecode);
403 for ((rect, url), annotation_id) in links.iter().zip(&annotation_ids) {
404 let mut annotation = pdf.annotation(*annotation_id);
405 annotation.subtype(AnnotationType::Link);
406 annotation.rect(PdfRect::new(
407 rect.x(),
408 page.height - rect.max_y(),
409 rect.max_x(),
410 page.height - rect.y(),
411 ));
412 annotation.border(0.0, 0.0, 0.0, None);
413 annotation.action().action_type(ActionType::Uri).uri(Str(url.as_bytes()));
414 }
415 }
416 Ok(pdf.finish())
417 }
418}
419
420fn end_alpha(content: &mut Content, opened: bool) {
421 if opened {
422 content.restore_state();
423 }
424}
425
426fn font_name(bold: bool) -> &'static str {
427 if bold { "F1" } else { "F0" }
428}
429
430fn alpha_name(alpha: u8) -> String {
431 format!("A{alpha}")
432}
433
434fn image_name(index: usize) -> String {
435 format!("I{index}")
436}
437
438fn write_image(pdf: &mut Pdf, image: &DocumentImage, alloc: &mut impl FnMut() -> Ref) -> Ref {
442 let id = alloc();
443 let pixels = image.pixels();
444 let (width, height) = (pixels.width() as i32, pixels.height() as i32);
445 if image.format() == ImageFormat::Jpeg && matches!(image.channels(), 1 | 3) {
446 let mut out = pdf.image_xobject(id, image.encoded());
447 out.filter(Filter::DctDecode);
448 out.width(width);
449 out.height(height);
450 if image.channels() == 1 {
451 out.color_space().device_gray();
452 } else {
453 out.color_space().device_rgb();
454 }
455 out.bits_per_component(8);
456 return id;
457 }
458 let (rgba, _) = pixels.pixels().as_chunks::<4>();
459 let rgb: Vec<u8> = rgba.iter().flat_map(|p| [p[0], p[1], p[2]]).collect();
460 let mask = rgba.iter().any(|p| p[3] != 255).then(|| {
461 let alpha: Vec<u8> = rgba.iter().map(|p| p[3]).collect();
462 let mask_id = alloc();
463 let compressed = compress_to_vec_zlib(&alpha, 6);
464 let mut out = pdf.image_xobject(mask_id, &compressed);
465 out.filter(Filter::FlateDecode);
466 out.width(width);
467 out.height(height);
468 out.color_space().device_gray();
469 out.bits_per_component(8);
470 out.finish();
471 mask_id
472 });
473 let compressed = compress_to_vec_zlib(&rgb, 6);
474 let mut out = pdf.image_xobject(id, &compressed);
475 out.filter(Filter::FlateDecode);
476 out.width(width);
477 out.height(height);
478 out.color_space().device_rgb();
479 out.bits_per_component(8);
480 if let Some(mask) = mask {
481 out.s_mask(mask);
482 }
483 id
484}
485
486struct OutlineNode<'d> {
488 id: Ref,
489 title: &'d str,
490 page: Ref,
491 top: f32,
492 parent: Option<usize>,
493 children: Vec<usize>,
494 descendants: i32,
495}
496
497fn write_outline(
499 pdf: &mut Pdf,
500 contents: &[(Vec<u8>, &Page)],
501 page_ids: &[(Ref, Ref)],
502 alloc: &mut impl FnMut() -> Ref,
503) -> Option<Ref> {
504 let mut nodes: Vec<OutlineNode<'_>> = Vec::new();
505 let mut roots = Vec::new();
506 let mut path: Vec<(u32, usize)> = Vec::new();
508 for ((_, page), (page_id, _)) in contents.iter().zip(page_ids) {
509 for item in &page.items {
510 let Item::Bookmark { title, level, y } = item else { continue };
511 while path.last().is_some_and(|(open, _)| open >= level) {
513 path.pop();
514 }
515 let index = nodes.len();
516 let parent = path.last().map(|(_, node)| *node);
517 nodes.push(OutlineNode {
518 id: alloc(),
519 title,
520 page: *page_id,
521 top: page.height - y,
522 parent,
523 children: Vec::new(),
524 descendants: 0,
525 });
526 match parent {
527 Some(parent) => nodes[parent].children.push(index),
528 None => roots.push(index),
529 }
530 path.push((*level, index));
531 }
532 }
533 if nodes.is_empty() {
534 return None;
535 }
536 for index in (0..nodes.len()).rev() {
538 if let Some(parent) = nodes[index].parent {
539 nodes[parent].descendants += nodes[index].descendants + 1;
540 }
541 }
542 let root = alloc();
543 let mut outline = pdf.outline(root);
544 outline.first(nodes[roots[0]].id);
545 outline.last(nodes[roots[roots.len() - 1]].id);
546 outline.count(nodes.len() as i32);
547 outline.finish();
548 for (index, node) in nodes.iter().enumerate() {
549 let siblings = node.parent.map_or(&roots, |parent| &nodes[parent].children);
550 let at = siblings.iter().position(|sibling| *sibling == index).unwrap_or(0);
551 let mut item = pdf.outline_item(node.id);
552 item.title(TextStr(node.title));
553 item.parent(node.parent.map_or(root, |parent| nodes[parent].id));
554 if at > 0 {
555 item.prev(nodes[siblings[at - 1]].id);
556 }
557 if let Some(next) = siblings.get(at + 1) {
558 item.next(nodes[*next].id);
559 }
560 if let (Some(first), Some(last)) = (node.children.first(), node.children.last()) {
561 item.first(nodes[*first].id);
562 item.last(nodes[*last].id);
563 item.count(node.descendants);
564 }
565 item.dest().page(node.page).xyz(0.0, node.top, None);
566 }
567 Some(root)
568}
569
570fn write_font(pdf: &mut Pdf, font: &Font, alloc: &mut impl FnMut() -> Ref) -> Result<Ref, PdfError> {
572 let (type0, cid, descriptor, file, cmap) = (alloc(), alloc(), alloc(), alloc(), alloc());
573 let name =
574 format!("{}+Inter-{}", subset_tag(&font.originals), if font.bold { "SemiBold" } else { "Regular" });
575 let name = Name(name.as_bytes());
576
577 let subset =
578 subsetter::subset(font.data, 0, &font.remapper).map_err(|e| PdfError::Font(e.to_string()))?;
579 let compressed = compress_to_vec_zlib(&subset, 6);
580 pdf.stream(file, &compressed).filter(Filter::FlateDecode).pair(Name(b"Length1"), subset.len() as i32);
581
582 let reader = skrifa::FontRef::new(font.data).map_err(|e| PdfError::Font(e.to_string()))?;
583 let metrics = reader.metrics(Size::new(UNITS), LocationRef::default());
584 let bbox = metrics.bounds.map_or(PdfRect::new(0.0, -300.0, 1000.0, 1000.0), |b| {
585 PdfRect::new(b.x_min, b.y_min, b.x_max, b.y_max)
586 });
587 let mut desc = pdf.font_descriptor(descriptor);
588 desc.name(name);
589 desc.flags(FontFlags::NON_SYMBOLIC);
590 desc.bbox(bbox);
591 desc.italic_angle(0.0);
592 desc.ascent(metrics.ascent);
593 desc.descent(metrics.descent);
594 desc.cap_height(metrics.cap_height.unwrap_or(metrics.ascent));
595 desc.stem_v(if font.bold { 120.0 } else { 80.0 });
596 desc.font_file2(file);
597 desc.finish();
598
599 let system = SystemInfo { registry: Str(b"Adobe"), ordering: Str(b"Identity"), supplement: 0 };
600 let mut cid_font = pdf.cid_font(cid);
601 cid_font.subtype(CidFontType::Type2);
602 cid_font.base_font(name);
603 cid_font.system_info(system);
604 cid_font.font_descriptor(descriptor);
605 cid_font.cid_to_gid_map_predefined(Name(b"Identity"));
606 let mut widths = cid_font.widths();
607 widths.consecutive(0, font.widths.iter().copied());
608 widths.finish();
609 cid_font.finish();
610
611 let mut to_unicode = UnicodeCmap::<u16>::new(Name(b"Custom"), system);
612 for (cid, text) in font.unicode.iter().enumerate() {
613 if let (Some(text), Ok(cid)) = (text, u16::try_from(cid)) {
614 to_unicode.pair_with_multiple(cid, text.chars());
615 }
616 }
617 let unicode = compress_to_vec_zlib(&to_unicode.finish(), 6);
618 pdf.cmap(cmap, &unicode).filter(Filter::FlateDecode);
619
620 let mut out = pdf.type0_font(type0);
621 out.base_font(name);
622 out.encoding_predefined(Name(b"Identity-H"));
623 out.descendant_font(cid);
624 out.to_unicode(cmap);
625 out.finish();
626 Ok(type0)
627}
628
629fn subset_tag(glyphs: &[u16]) -> String {
631 let mut hash = glyphs.iter().fold(2_166_136_261_u32, |h, g| (h ^ u32::from(*g)).wrapping_mul(16_777_619));
632 (0..6)
633 .map(|_| {
634 let letter = char::from(b'A' + (hash % 26) as u8);
635 hash /= 26;
636 letter
637 })
638 .collect()
639}
640
641fn write_path(content: &mut Content, path: &[PathSegment], h: f32) {
643 for segment in path {
644 match segment {
645 PathSegment::Move(p) => {
646 content.move_to(p.x, h - p.y);
647 }
648 PathSegment::Line(p) => {
649 content.line_to(p.x, h - p.y);
650 }
651 PathSegment::Cubic(a, b, c) => {
652 content.cubic_to(a.x, h - a.y, b.x, h - b.y, c.x, h - c.y);
653 }
654 PathSegment::Close => {
655 content.close_path();
656 }
657 _ => {}
658 }
659 }
660}