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    /// Ascent above baseline in /1000 em, from font binary metrics.
30    pub ascent: Option<f64>,
31    /// Descent below baseline (negative) in /1000 em, from font binary metrics.
32    pub descent: Option<f64>,
33    /// Cap height in /1000 em, when present in the font.
34    pub cap_height: Option<f64>,
35    /// x-height in /1000 em, when present in the font.
36    pub x_height: Option<f64>,
37}
38
39pub(crate) struct OutlinePath(BezPath);
40
41impl OutlinePath {
42    pub(crate) fn new() -> Self {
43        Self(BezPath::new())
44    }
45
46    pub(crate) fn take(self) -> BezPath {
47        self.0
48    }
49}
50
51impl OutlinePen for OutlinePath {
52    #[inline]
53    fn move_to(&mut self, x: f32, y: f32) {
54        self.0.move_to((x, y));
55    }
56
57    #[inline]
58    fn line_to(&mut self, x: f32, y: f32) {
59        if !self.0.elements().is_empty() {
60            self.0.line_to((x, y));
61        }
62    }
63
64    #[inline]
65    fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
66        if !self.0.elements().is_empty() {
67            self.0.quad_to((cx, cy), (x, y));
68        }
69    }
70
71    #[inline]
72    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
73        if !self.0.elements().is_empty() {
74            self.0.curve_to((cx0, cy0), (cx1, cy1), (x, y));
75        }
76    }
77
78    #[inline]
79    fn close(&mut self) {
80        if !self.0.elements().is_empty() {
81            self.0.close_path();
82        }
83    }
84}
85
86impl OutlineBuilder for OutlinePath {
87    fn move_to(&mut self, x: f32, y: f32) {
88        self.0.move_to((x, y));
89    }
90
91    fn line_to(&mut self, x: f32, y: f32) {
92        if !self.0.elements().is_empty() {
93            self.0.line_to((x, y));
94        }
95    }
96
97    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
98        if !self.0.elements().is_empty() {
99            self.0.quad_to((x1, y1), (x, y));
100        }
101    }
102
103    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
104        if !self.0.elements().is_empty() {
105            self.0.curve_to((x1, y1), (x2, y2), (x, y));
106        }
107    }
108
109    fn close(&mut self) {
110        if !self.0.elements().is_empty() {
111            self.0.close_path();
112        }
113    }
114}
115
116#[derive(Debug, Clone)]
117pub(crate) enum OutlineFont {
118    Type1(Rc<Type1Font>),
119    TrueType(Rc<TrueTypeFont>),
120    Type0(Rc<Type0Font>),
121}
122
123impl CacheKey for OutlineFont {
124    fn cache_key(&self) -> u128 {
125        match self {
126            Self::Type1(f) => f.cache_key(),
127            Self::TrueType(t) => t.cache_key(),
128            Self::Type0(t0) => t0.cache_key(),
129        }
130    }
131}
132
133impl OutlineFont {
134    pub(crate) fn outline_glyph(&self, glyph: GlyphId, code: u32) -> BezPath {
135        match self {
136            Self::Type1(t) => t.outline_glyph(glyph),
137            Self::TrueType(t) => t.outline_glyph(glyph),
138            Self::Type0(t) => t.outline_glyph(glyph, code),
139        }
140    }
141
142    pub(crate) fn char_code_to_unicode(&self, char_code: u32) -> Option<BfString> {
143        match self {
144            Self::Type1(t) => t.char_code_to_unicode(char_code),
145            Self::TrueType(t) => t.char_code_to_unicode(char_code),
146            Self::Type0(t) => t.char_code_to_unicode(char_code),
147        }
148    }
149
150    /// Get the advance width for a glyph by character code.
151    pub(crate) fn glyph_advance_width(&self, char_code: u32) -> Option<f32> {
152        match self {
153            Self::Type1(t) => t.glyph_width(char_code as u8),
154            Self::TrueType(t) => Some(t.glyph_width(char_code as u8)),
155            Self::Type0(t) => Some(t.code_advance(char_code).x as f32),
156        }
157    }
158
159    /// PostScript name, if determinable.
160    ///
161    /// Unlike [`font_data`](Self::font_data) — which only returns
162    /// metadata for fonts whose program bytes are embedded —
163    /// `postscript_name` works for every variant where a name is
164    /// known, including non-embedded Type1 standard-14 fonts.
165    ///
166    /// This is the source for the WASM `getTextPositions()`
167    /// `fontName` field; it lets the editor render a stable font
168    /// label for standard-14 runs that don't carry an embedded
169    /// program.
170    pub(crate) fn postscript_name(&self) -> Option<String> {
171        match self {
172            Self::Type1(t) => t.postscript_name().map(|s| s.to_string()),
173            Self::TrueType(t) => t.postscript_name().map(|s| s.to_string()),
174            Self::Type0(t) => t.postscript_name().map(|s| s.to_string()),
175        }
176    }
177
178    /// Get raw font bytes and metadata.
179    ///
180    /// Returns None for Type1 fonts and non-embedded TrueType fonts.
181    pub(crate) fn font_data(&self) -> Option<OutlineFontData> {
182        match self {
183            Self::Type1(_) => None,
184            Self::TrueType(t) => {
185                let m = t.font_metrics();
186                Some(OutlineFontData {
187                    data: t.font_data()?,
188                    cache_key: t.cache_key(),
189                    postscript_name: t.postscript_name().map(|s| s.to_string()),
190                    weight: t.weight(),
191                    is_italic: t.is_italic(),
192                    is_serif: t.is_serif(),
193                    is_monospace: t.is_monospace(),
194                    ascent: m.map(|x| x.0),
195                    descent: m.map(|x| x.1),
196                    cap_height: m.and_then(|x| x.2),
197                    x_height: m.and_then(|x| x.3),
198                })
199            }
200            Self::Type0(t) => {
201                let m = t.font_metrics();
202                Some(OutlineFontData {
203                    data: t.font_data()?,
204                    cache_key: t.cache_key(),
205                    postscript_name: t.postscript_name().map(|s| s.to_string()),
206                    weight: t.weight(),
207                    is_italic: t.is_italic(),
208                    is_serif: t.is_serif(),
209                    is_monospace: t.is_monospace(),
210                    ascent: m.map(|x| x.0),
211                    descent: m.map(|x| x.1),
212                    cap_height: m.and_then(|x| x.2),
213                    x_height: m.and_then(|x| x.3),
214                })
215            }
216        }
217    }
218
219    /// Vertical font metrics in /1000 em, from either embedded font data
220    /// (skrifa) or the Standard-14 AFM fallback table.
221    pub(crate) fn font_metrics(&self) -> Option<(f64, f64, Option<f64>, Option<f64>)> {
222        match self {
223            Self::Type1(t) => t.font_metrics(),
224            Self::TrueType(t) => t.font_metrics(),
225            Self::Type0(t) => t.font_metrics(),
226        }
227    }
228}