1use anyhow::{bail, Error as AnyError};
2use pdf_writer::{Content, Filter, Finish, Name, Pdf, Rect, Ref, Str, TextStr};
3
4use itertools::Itertools;
5use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap};
6use siphasher::sip128::{Hasher128, SipHasher13};
7use std::collections::{BTreeMap, HashMap, HashSet};
8use std::fs;
9use std::hash::Hash;
10use ttf_parser::{GlyphId, Tag};
11use unicode_bidi::BidiInfo;
12use usvg::fontdb::{Database, Family, Query, Source, Stretch, Style, Weight};
13use usvg::{
14 Font, FontStretch, FontStyle, Node, NodeExt, NodeKind, Opacity, Paint, Text, TextAnchor,
15 TextToPath, Tree,
16};
17
18const CFF: Tag = Tag::from_bytes(b"CFF ");
19const SYSTEM_INFO: SystemInfo = SystemInfo {
20 registry: Str(b"Adobe"),
21 ordering: Str(b"Identity"),
22 supplement: 0,
23};
24const CMAP_NAME: Name = Name(b"Custom");
25
26pub fn svg_to_pdf(tree: &Tree, font_db: &Database, scale: f32) -> Result<Vec<u8>, AnyError> {
30 let width = tree.size.width();
32 let height = tree.size.height();
33
34 let font_chars = collect_font_to_chars_mapping(tree)?;
35
36 let mut ctx = PdfContext::new(width, height, scale);
37
38 let mut font_metrics = HashMap::new();
42 for (font, chars) in font_chars.iter().sorted_by_key(|(f, _)| format!("{f:?}")) {
43 font_metrics.insert(
44 font.clone(),
45 compute_font_metrics(&mut ctx, font, chars, font_db)?,
46 );
47 }
48
49 ctx.svg_id = ctx.alloc.bump();
51 construct_page(&mut ctx, &font_metrics);
52 write_svg(&mut ctx, tree);
53 write_fonts(&mut ctx, &font_metrics)?;
54 write_content(&mut ctx, tree, &font_metrics, font_db)?;
55 Ok(ctx.writer.finish())
56}
57
58trait RefExt {
60 fn bump(&mut self) -> Self;
62}
63
64impl RefExt for Ref {
65 fn bump(&mut self) -> Self {
66 let prev = *self;
67 *self = Self::new(prev.get() + 1);
68 prev
69 }
70}
71
72struct PdfContext {
73 writer: Pdf,
74 width: f32,
75 height: f32,
76 scale: f32,
77 alloc: Ref,
78 info_id: Ref,
79 catalog_id: Ref,
80 page_tree_id: Ref,
81 page_id: Ref,
82 content_id: Ref,
83 svg_id: Ref,
84 svg_name: Vec<u8>,
85 next_font_name_index: usize,
86}
87
88impl PdfContext {
89 fn new(width: f32, height: f32, scale: f32) -> Self {
90 let mut alloc = Ref::new(1);
91 let info_id = alloc.bump();
92 let catalog_id = alloc.bump();
93 let page_tree_id = alloc.bump();
94 let page_id = alloc.bump();
95 let content_id = alloc.bump();
96
97 let svg_id = Ref::new(1);
99
100 Self {
101 writer: Pdf::new(),
102 width,
103 height,
104 scale,
105 alloc,
106 info_id,
107 catalog_id,
108 page_tree_id,
109 page_id,
110 content_id,
111 svg_id,
112 svg_name: Vec::from(b"S1".as_slice()),
113 next_font_name_index: 1,
114 }
115 }
116
117 fn next_font_name(&mut self) -> String {
118 let name = format!("F{}", self.next_font_name_index);
119 self.next_font_name_index += 1;
120 name
121 }
122}
123
124fn construct_page(ctx: &mut PdfContext, font_metrics: &HashMap<Font, FontMetrics>) {
126 let mut info = ctx.writer.document_info(ctx.info_id);
127 info.creator(TextStr("VlConvert"));
128 info.finish();
129
130 ctx.writer.catalog(ctx.catalog_id).pages(ctx.page_tree_id);
131 ctx.writer
132 .pages(ctx.page_tree_id)
133 .kids([ctx.page_id])
134 .count(1);
135
136 let mut page = ctx.writer.page(ctx.page_id);
138 page.media_box(Rect::new(
139 0.0,
140 0.0,
141 ctx.width * ctx.scale,
142 ctx.height * ctx.scale,
143 ));
144 page.parent(ctx.page_tree_id);
145 page.contents(ctx.content_id);
146
147 let mut resources = page.resources();
148 resources
150 .x_objects()
151 .pair(Name(ctx.svg_name.as_slice()), ctx.svg_id);
152
153 let mut resource_fonts = resources.fonts();
155 for mapped_font in font_metrics.values().sorted_by_key(|f| f.font_ref) {
156 resource_fonts.pair(
157 Name(mapped_font.font_ref_name.as_slice()),
158 mapped_font.font_ref,
159 );
160 }
161 resource_fonts.finish();
162 resources.finish();
163
164 page.finish();
166}
167
168fn write_svg(ctx: &mut PdfContext, tree: &Tree) {
172 ctx.alloc = svg2pdf::convert_tree_into(
173 tree,
174 svg2pdf::Options::default(),
175 &mut ctx.writer,
176 ctx.svg_id,
177 );
178}
179
180fn write_fonts(
182 ctx: &mut PdfContext,
183 font_metrics: &HashMap<Font, FontMetrics>,
184) -> Result<(), AnyError> {
185 for font_specs in font_metrics.values().sorted_by_key(|f| f.font_ref) {
186 let cid_ref = ctx.alloc.bump();
187 let descriptor_ref = ctx.alloc.bump();
188 let cmap_ref = ctx.alloc.bump();
189 let data_ref = ctx.alloc.bump();
190 let is_cff = font_specs.is_cff;
191
192 ctx.writer
193 .type0_font(font_specs.font_ref)
194 .base_font(Name(font_specs.base_font_type0.as_bytes()))
195 .encoding_predefined(Name(b"Identity-H"))
196 .descendant_font(cid_ref)
197 .to_unicode(cmap_ref);
198
199 let mut cid = ctx.writer.cid_font(cid_ref);
201 cid.subtype(CidFontType::Type2);
202 cid.subtype(if is_cff {
203 CidFontType::Type0
204 } else {
205 CidFontType::Type2
206 });
207 cid.base_font(Name(font_specs.base_font.as_bytes()));
208 cid.system_info(SYSTEM_INFO);
209 cid.font_descriptor(descriptor_ref);
210 cid.default_width(0.0);
211 if !is_cff {
212 cid.cid_to_gid_map_predefined(Name(b"Identity"));
213 }
214
215 let mut width_writer = cid.widths();
217 for (i, w) in font_specs.widths.iter().enumerate().skip(1) {
218 if *w != 0.0 {
219 width_writer.same(i as u16, i as u16, *w);
220 }
221 }
222
223 width_writer.finish();
224 cid.finish();
225
226 let mut font_descriptor = ctx.writer.font_descriptor(descriptor_ref);
228 font_descriptor
229 .name(Name(font_specs.base_font.as_bytes()))
230 .flags(font_specs.flags)
231 .bbox(font_specs.bbox)
232 .italic_angle(font_specs.italic_angle)
233 .ascent(font_specs.ascender)
234 .descent(font_specs.descender)
235 .cap_height(font_specs.cap_height)
236 .stem_v(font_specs.stem_v);
237
238 if is_cff {
239 font_descriptor.font_file3(data_ref);
240 } else {
241 font_descriptor.font_file2(data_ref);
242 }
243 font_descriptor.finish();
244
245 let cmap = create_cmap(&font_specs.cid_set);
248 ctx.writer.cmap(cmap_ref, &cmap.finish());
249
250 let glyphs: Vec<_> = font_specs.glyph_set.keys().copied().collect();
251 let profile = subsetter::Profile::pdf(&glyphs);
252 let subsetted = subsetter::subset(&font_specs.font_data, font_specs.face_index, profile);
253 let subset_font_data = deflate(subsetted.as_deref().unwrap_or(&font_specs.font_data));
254
255 let mut stream = ctx.writer.stream(data_ref, &subset_font_data);
256 stream.filter(Filter::FlateDecode);
257 if is_cff {
258 stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C"));
259 }
260 stream.finish();
261 }
262 Ok(())
263}
264
265fn write_content(
266 ctx: &mut PdfContext,
267 tree: &Tree,
268 font_mapping: &HashMap<Font, FontMetrics>,
269 font_db: &Database,
270) -> Result<(), AnyError> {
271 let mut content = Content::new();
273
274 content
278 .save_state()
279 .transform([
280 ctx.width * ctx.scale,
281 0.0,
282 0.0,
283 ctx.height * ctx.scale,
284 0.0,
285 0.0,
286 ])
287 .x_object(Name(ctx.svg_name.as_slice()))
288 .restore_state();
289
290 content.save_state();
292
293 for node in tree.root.children() {
294 write_text(ctx, node, &mut content, font_db, font_mapping)?;
295 }
296
297 content.restore_state();
298
299 ctx.writer.stream(ctx.content_id, &content.finish());
301 Ok(())
302}
303
304fn write_text(
305 ctx: &PdfContext,
306 node: Node,
307 content: &mut Content,
308 font_db: &Database,
309 font_metrics: &HashMap<Font, FontMetrics>,
310) -> Result<(), AnyError> {
311 match *node.borrow() {
312 NodeKind::Text(ref text) if text.chunks.len() == 1 => {
313 let Some(text_width) = get_text_width(text, font_db) else {
314 bail!("Failed to calculate text bounding box")
315 };
316
317 let chunk = &text.chunks[0];
318 let x_offset = match chunk.anchor {
319 TextAnchor::Start => 0.0,
320 TextAnchor::Middle => -text_width / 2.0,
321 TextAnchor::End => -text_width,
322 };
323
324 let chunk_x = chunk.x.unwrap_or(0.0) + x_offset as f32;
326 let chunk_y = -chunk.y.unwrap_or(0.0);
327
328 let tx = node.abs_transform();
329
330 content.save_state().transform([
331 tx.sx * ctx.scale,
332 tx.kx * ctx.scale,
333 tx.ky * ctx.scale,
334 tx.sy * ctx.scale,
335 tx.tx * ctx.scale,
336 (ctx.height - tx.ty) * ctx.scale,
337 ]);
338
339 content.begin_text().next_line(chunk_x, chunk_y);
341
342 for span in &chunk.spans {
343 let font_size = span.font_size.get();
344
345 let span_opacity = span.fill.clone().unwrap_or_default().opacity;
347 if span.fill.is_none()
348 || span_opacity == Opacity::ZERO
349 || node_has_zero_opacity(&node)
350 {
351 continue;
352 }
353
354 let Some(font_specs) = font_metrics.get(&span.font) else {
355 bail!("Font metrics not found")
356 };
357
358 let mut span_text = chunk.text[span.start..span.end].to_string();
360 let bidi_info = BidiInfo::new(&span_text, None);
361 if bidi_info.paragraphs.len() == 1 {
362 let para = &bidi_info.paragraphs[0];
363 let line = para.range.clone();
364 span_text = bidi_info.reorder_line(para, line).to_string();
365 }
366
367 let mut encoded_text = Vec::new();
369 for ch in span_text.chars() {
370 if let Some(g) = font_specs.char_set.get(&ch) {
371 encoded_text.push((*g >> 8) as u8);
372 encoded_text.push((*g & 0xff) as u8);
373 }
374 }
375
376 let (fill_r, fill_g, fill_b) = match &span.fill {
378 Some(fill) => {
379 if let Paint::Color(color) = fill.paint {
380 (
381 color.red as f32 / 255.0,
382 color.green as f32 / 255.0,
383 color.blue as f32 / 255.0,
384 )
385 } else {
386 (0.0, 0.0, 0.0)
388 }
389 }
390 None => (0.0, 0.0, 0.0),
391 };
392
393 content
394 .set_font(Name(font_specs.font_ref_name.as_slice()), font_size)
395 .set_fill_rgb(fill_r, fill_g, fill_b)
396 .show(Str(encoded_text.as_slice()));
397 }
398
399 content.end_text().restore_state();
400 }
401 NodeKind::Group(_) => {
402 for child in node.children() {
403 write_text(ctx, child, content, font_db, font_metrics)?;
404 }
405 }
406 _ => {}
407 }
408 Ok(())
409}
410
411fn node_has_zero_opacity(node: &Node) -> bool {
414 if let NodeKind::Group(ref group) = *node.borrow() {
415 if group.opacity == Opacity::ZERO {
416 return true;
417 }
418 }
419 if let Some(parent) = &node.parent() {
420 node_has_zero_opacity(parent)
421 } else {
422 false
423 }
424}
425
426fn get_text_width(text: &Text, font_db: &Database) -> Option<f64> {
427 get_text_width_from_path(text.convert(font_db, Default::default())?)
428}
429
430fn get_text_width_from_path(node: Node) -> Option<f64> {
431 match *node.borrow() {
432 NodeKind::Group(_) => {
433 for child in node.children() {
434 if let Some(res) = get_text_width_from_path(child) {
435 return Some(res);
436 }
437 }
438 None
439 }
440 NodeKind::Path(ref path) => {
441 path.text_bbox.map(|p| p.width() as f64)
443 }
444 _ => None,
445 }
446}
447
448fn collect_font_to_chars_mapping(
450 tree: &Tree,
451) -> Result<HashMap<Font, HashSet<char>>, anyhow::Error> {
452 let mut fonts: HashMap<Font, HashSet<char>> = HashMap::new();
453 for node in tree.root.descendants() {
454 if let NodeKind::Text(ref text) = *node.borrow() {
455 match text.chunks.len() {
456 0 => {}
458 1 => {
459 let chunk = &text.chunks[0];
460 let chunk_text = chunk.text.as_str();
461 for span in &chunk.spans {
462 let span_text = &chunk_text[span.start..span.end];
463 let font = &span.font;
464 fonts
465 .entry(font.clone())
466 .or_default()
467 .extend(span_text.chars());
468 }
469 }
470 _ => bail!("multi-chunk text not supported"),
471 }
472 }
473 }
474 Ok(fonts)
475}
476
477struct FontMetrics {
478 font_ref: Ref,
479 font_ref_name: Vec<u8>,
480 font_data: Vec<u8>,
481 face_index: u32,
482 glyph_set: BTreeMap<u16, String>,
483 char_set: BTreeMap<char, u16>,
484 flags: FontFlags,
485 bbox: Rect,
486 widths: Vec<f32>,
487 italic_angle: f32,
488 ascender: f32,
489 descender: f32,
490 cap_height: f32,
491 stem_v: f32,
492 base_font: String,
493 is_cff: bool,
494 base_font_type0: String,
495 cid_set: BTreeMap<u16, String>,
496}
497
498fn compute_font_metrics(
500 ctx: &mut PdfContext,
501 font: &Font,
502 chars: &HashSet<char>,
503 font_db: &Database,
504) -> Result<FontMetrics, anyhow::Error> {
505 let families = font
506 .families
507 .iter()
508 .map(|family| match family.as_str() {
509 "serif" => Family::Serif,
510 "sans-serif" | "sans serif" => Family::SansSerif,
511 "monospace" => Family::Monospace,
512 "cursive" => Family::Cursive,
513 name => Family::Name(name),
514 })
515 .collect::<Vec<_>>();
516
517 let stretch = match font.stretch {
518 FontStretch::UltraCondensed => Stretch::UltraCondensed,
519 FontStretch::ExtraCondensed => Stretch::ExtraCondensed,
520 FontStretch::Condensed => Stretch::Condensed,
521 FontStretch::SemiCondensed => Stretch::SemiCondensed,
522 FontStretch::Normal => Stretch::Normal,
523 FontStretch::SemiExpanded => Stretch::SemiExpanded,
524 FontStretch::Expanded => Stretch::Expanded,
525 FontStretch::ExtraExpanded => Stretch::ExtraExpanded,
526 FontStretch::UltraExpanded => Stretch::UltraExpanded,
527 };
528
529 let style = match font.style {
530 FontStyle::Normal => Style::Normal,
531 FontStyle::Italic => Style::Italic,
532 FontStyle::Oblique => Style::Oblique,
533 };
534
535 let Some(font_id) = font_db.query(&Query {
536 families: &families,
537 weight: Weight(font.weight),
538 stretch,
539 style,
540 }) else {
541 bail!("Unable to find installed font matching {font:?}")
542 };
543
544 let Some(face) = font_db.face(font_id) else {
545 bail!("Unable to find installed font matching {font:?}")
546 };
547
548 let postscript_name = face.post_script_name.clone();
549
550 let font_data = match &face.source {
551 Source::Binary(d) => Vec::from(d.as_ref().as_ref()),
552 Source::File(f) => fs::read(f)?,
553 Source::SharedFile(_, d) => Vec::from(d.as_ref().as_ref()),
554 };
555
556 let ttf = ttf_parser::Face::parse(&font_data, face.index)?;
557
558 let is_cff = ttf.raw_face().table(CFF).is_some();
559
560 let to_font_units = |v: f32| (v / ttf.units_per_em() as f32) * 1000.0;
562
563 let mut flags = FontFlags::empty();
565 flags.set(FontFlags::SERIF, postscript_name.contains("Serif"));
566 flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced());
567 flags.set(FontFlags::ITALIC, ttf.is_italic());
568 flags.insert(FontFlags::SYMBOLIC);
569 flags.insert(FontFlags::SMALL_CAP);
570
571 let global_bbox = ttf.global_bounding_box();
573 let bbox = Rect::new(
574 to_font_units(global_bbox.x_min.into()),
575 to_font_units(global_bbox.y_min.into()),
576 to_font_units(global_bbox.x_max.into()),
577 to_font_units(global_bbox.y_max.into()),
578 );
579
580 let mut glyph_set: BTreeMap<u16, String> = BTreeMap::new();
582 let mut cid_set: BTreeMap<u16, String> = BTreeMap::new();
583 let mut char_set: BTreeMap<char, u16> = BTreeMap::new();
584 for ch in chars {
585 if let Some(g) = ttf.glyph_index(*ch) {
586 let cid = glyph_cid(&ttf, g.0);
587 glyph_set.entry(g.0).or_default().push(*ch);
588 cid_set.entry(cid).or_default().push(*ch);
589 char_set.insert(*ch, cid);
590 }
591 }
592
593 let mut widths = vec![];
595 for gid in std::iter::once(0).chain(glyph_set.keys().copied()) {
596 let width = ttf.glyph_hor_advance(GlyphId(gid)).unwrap_or(0);
597 let units = to_font_units(width as f32);
598 let cid = glyph_cid(&ttf, gid);
599 if usize::from(cid) >= widths.len() {
600 widths.resize(usize::from(cid) + 1, 0.0);
601 widths[usize::from(cid)] = units;
602 }
603 }
604
605 let italic_angle = ttf.italic_angle().unwrap_or(0.0);
607 let ascender = to_font_units(ttf.typographic_ascender().unwrap_or(ttf.ascender()).into());
608 let descender = to_font_units(
609 ttf.typographic_descender()
610 .unwrap_or(ttf.descender())
611 .into(),
612 );
613 let cap_height = to_font_units(ttf.capital_height().unwrap_or(ttf.ascender()).into());
614 let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0);
615
616 let subset_tag = subset_tag(&glyph_set);
618 let base_font = format!("{subset_tag}+{postscript_name}");
619 let base_font_type0 = if is_cff {
620 format!("{base_font}-Identity-H")
621 } else {
622 base_font.clone()
623 };
624
625 Ok(FontMetrics {
626 base_font,
627 base_font_type0,
628 is_cff,
629 font_ref: ctx.alloc.bump(),
630 font_ref_name: Vec::from(ctx.next_font_name().as_bytes()),
631 font_data,
632 face_index: face.index,
633 glyph_set,
634 cid_set,
635 char_set,
636 flags,
637 bbox,
638 widths,
639 italic_angle,
640 ascender,
641 descender,
642 cap_height,
643 stem_v,
644 })
645}
646
647fn subset_tag(glyphs: &BTreeMap<u16, String>) -> String {
649 const LEN: usize = 6;
650 const BASE: u128 = 26;
651 let mut hash = hash128(glyphs);
652 let mut letter = [b'A'; LEN];
653 for l in letter.iter_mut() {
654 *l = b'A' + (hash % BASE) as u8;
655 hash /= BASE;
656 }
657 std::str::from_utf8(&letter).unwrap().to_string()
658}
659
660fn hash128<T: Hash + ?Sized>(value: &T) -> u128 {
662 let mut state = SipHasher13::new();
663 value.hash(&mut state);
664 state.finish128().as_u128()
665}
666
667fn create_cmap(cid_set: &BTreeMap<u16, String>) -> UnicodeCmap {
669 let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO);
671 for (&g, text) in cid_set.iter() {
672 if !text.is_empty() {
673 cmap.pair_with_multiple(g, text.chars());
674 }
675 }
676
677 cmap
678}
679
680fn deflate(data: &[u8]) -> Vec<u8> {
681 const COMPRESSION_LEVEL: u8 = 6;
682 miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL)
683}
684
685fn glyph_cid(ttf: &ttf_parser::Face, glyph_id: u16) -> u16 {
712 ttf.tables()
713 .cff
714 .and_then(|cff| cff.glyph_cid(ttf_parser::GlyphId(glyph_id)))
715 .unwrap_or(glyph_id)
716}