Skip to main content

pdf_interpret/font/
outline.rs

1use crate::CacheKey;
2use crate::font::cid::Type0Font;
3use crate::font::true_type::TrueTypeFont;
4use crate::font::type1::Type1Font;
5use kurbo::BezPath;
6use pdf_font::OutlineBuilder;
7use pdf_font::cmap::BfString;
8use skrifa::GlyphId;
9use skrifa::outline::OutlinePen;
10use std::rc::Rc;
11
12/// Font data and metadata for downstream use.
13#[derive(Clone)]
14pub struct OutlineFontData {
15    /// Raw font bytes (TrueType/OpenType/CFF data).
16    pub data: crate::font::FontData,
17    /// Cache key for font deduplication.
18    pub cache_key: u128,
19    /// PostScript name (e.g., "TimesNewRomanPS-BoldMT").
20    pub postscript_name: Option<String>,
21    /// Font weight (100-900, 400=normal, 700=bold).
22    pub weight: Option<u32>,
23    /// Whether the font is italic/oblique.
24    pub is_italic: bool,
25    /// Whether the font is serif (vs sans-serif).
26    pub is_serif: bool,
27    /// Whether the font is monospace.
28    pub is_monospace: bool,
29}
30
31pub(crate) struct OutlinePath(BezPath);
32
33impl OutlinePath {
34    pub(crate) fn new() -> Self {
35        Self(BezPath::new())
36    }
37
38    pub(crate) fn take(self) -> BezPath {
39        self.0
40    }
41}
42
43impl OutlinePen for OutlinePath {
44    #[inline]
45    fn move_to(&mut self, x: f32, y: f32) {
46        self.0.move_to((x, y));
47    }
48
49    #[inline]
50    fn line_to(&mut self, x: f32, y: f32) {
51        if !self.0.elements().is_empty() {
52            self.0.line_to((x, y));
53        }
54    }
55
56    #[inline]
57    fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
58        if !self.0.elements().is_empty() {
59            self.0.quad_to((cx, cy), (x, y));
60        }
61    }
62
63    #[inline]
64    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
65        if !self.0.elements().is_empty() {
66            self.0.curve_to((cx0, cy0), (cx1, cy1), (x, y));
67        }
68    }
69
70    #[inline]
71    fn close(&mut self) {
72        if !self.0.elements().is_empty() {
73            self.0.close_path();
74        }
75    }
76}
77
78impl OutlineBuilder for OutlinePath {
79    fn move_to(&mut self, x: f32, y: f32) {
80        self.0.move_to((x, y));
81    }
82
83    fn line_to(&mut self, x: f32, y: f32) {
84        if !self.0.elements().is_empty() {
85            self.0.line_to((x, y));
86        }
87    }
88
89    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
90        if !self.0.elements().is_empty() {
91            self.0.quad_to((x1, y1), (x, y));
92        }
93    }
94
95    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
96        if !self.0.elements().is_empty() {
97            self.0.curve_to((x1, y1), (x2, y2), (x, y));
98        }
99    }
100
101    fn close(&mut self) {
102        if !self.0.elements().is_empty() {
103            self.0.close_path();
104        }
105    }
106}
107
108#[derive(Debug, Clone)]
109pub(crate) enum OutlineFont {
110    Type1(Rc<Type1Font>),
111    TrueType(Rc<TrueTypeFont>),
112    Type0(Rc<Type0Font>),
113}
114
115impl CacheKey for OutlineFont {
116    fn cache_key(&self) -> u128 {
117        match self {
118            Self::Type1(f) => f.cache_key(),
119            Self::TrueType(t) => t.cache_key(),
120            Self::Type0(t0) => t0.cache_key(),
121        }
122    }
123}
124
125impl OutlineFont {
126    pub(crate) fn outline_glyph(&self, glyph: GlyphId, code: u32) -> BezPath {
127        match self {
128            Self::Type1(t) => t.outline_glyph(glyph),
129            Self::TrueType(t) => t.outline_glyph(glyph),
130            Self::Type0(t) => t.outline_glyph(glyph, code),
131        }
132    }
133
134    pub(crate) fn char_code_to_unicode(&self, char_code: u32) -> Option<BfString> {
135        match self {
136            Self::Type1(t) => t.char_code_to_unicode(char_code),
137            Self::TrueType(t) => t.char_code_to_unicode(char_code),
138            Self::Type0(t) => t.char_code_to_unicode(char_code),
139        }
140    }
141
142    /// Get the advance width for a glyph by character code.
143    pub(crate) fn glyph_advance_width(&self, char_code: u32) -> Option<f32> {
144        match self {
145            Self::Type1(t) => t.glyph_width(char_code as u8),
146            Self::TrueType(t) => Some(t.glyph_width(char_code as u8)),
147            Self::Type0(t) => Some(t.code_advance(char_code).x as f32),
148        }
149    }
150
151    /// PostScript name, if determinable.
152    ///
153    /// Unlike [`font_data`](Self::font_data) — which only returns
154    /// metadata for fonts whose program bytes are embedded —
155    /// `postscript_name` works for every variant where a name is
156    /// known, including non-embedded Type1 standard-14 fonts.
157    ///
158    /// This is the source for the WASM `getTextPositions()`
159    /// `fontName` field; it lets the editor render a stable font
160    /// label for standard-14 runs that don't carry an embedded
161    /// program.
162    pub(crate) fn postscript_name(&self) -> Option<String> {
163        match self {
164            Self::Type1(t) => t.postscript_name().map(|s| s.to_string()),
165            Self::TrueType(t) => t.postscript_name().map(|s| s.to_string()),
166            Self::Type0(t) => t.postscript_name().map(|s| s.to_string()),
167        }
168    }
169
170    /// Get raw font bytes and metadata.
171    ///
172    /// Returns None for Type1 fonts and non-embedded TrueType fonts.
173    pub(crate) fn font_data(&self) -> Option<OutlineFontData> {
174        match self {
175            Self::Type1(_) => None,
176            Self::TrueType(t) => Some(OutlineFontData {
177                data: t.font_data()?,
178                cache_key: t.cache_key(),
179                postscript_name: t.postscript_name().map(|s| s.to_string()),
180                weight: t.weight(),
181                is_italic: t.is_italic(),
182                is_serif: t.is_serif(),
183                is_monospace: t.is_monospace(),
184            }),
185            Self::Type0(t) => Some(OutlineFontData {
186                data: t.font_data()?,
187                cache_key: t.cache_key(),
188                postscript_name: t.postscript_name().map(|s| s.to_string()),
189                weight: t.weight(),
190                is_italic: t.is_italic(),
191                is_serif: t.is_serif(),
192                is_monospace: t.is_monospace(),
193            }),
194        }
195    }
196}