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 OutlineFont {
124    /// Whether `self` and `other` are the same font, not merely equal ones.
125    pub(crate) fn is_same(&self, other: &Self) -> bool {
126        match (self, other) {
127            (Self::Type1(a), Self::Type1(b)) => Rc::ptr_eq(a, b),
128            (Self::TrueType(a), Self::TrueType(b)) => Rc::ptr_eq(a, b),
129            (Self::Type0(a), Self::Type0(b)) => Rc::ptr_eq(a, b),
130            _ => false,
131        }
132    }
133}
134
135impl CacheKey for OutlineFont {
136    fn cache_key(&self) -> u128 {
137        match self {
138            Self::Type1(f) => f.cache_key(),
139            Self::TrueType(t) => t.cache_key(),
140            Self::Type0(t0) => t0.cache_key(),
141        }
142    }
143}
144
145impl OutlineFont {
146    pub(crate) fn outline_glyph(&self, glyph: GlyphId, code: u32) -> BezPath {
147        match self {
148            Self::Type1(t) => t.outline_glyph(glyph),
149            Self::TrueType(t) => t.outline_glyph(glyph),
150            Self::Type0(t) => t.outline_glyph(glyph, code),
151        }
152    }
153
154    pub(crate) fn char_code_to_unicode(&self, char_code: u32) -> Option<BfString> {
155        match self {
156            Self::Type1(t) => t.char_code_to_unicode(char_code),
157            Self::TrueType(t) => t.char_code_to_unicode(char_code),
158            Self::Type0(t) => t.char_code_to_unicode(char_code),
159        }
160    }
161
162    /// Get the advance width for a glyph by character code.
163    pub(crate) fn glyph_advance_width(&self, char_code: u32) -> Option<f32> {
164        match self {
165            Self::Type1(t) => t.glyph_width(char_code as u8),
166            Self::TrueType(t) => Some(t.glyph_width(char_code as u8)),
167            Self::Type0(t) => Some(t.code_advance(char_code).x as f32),
168        }
169    }
170
171    /// PostScript name, if determinable.
172    ///
173    /// Unlike [`font_data`](Self::font_data) — which only returns
174    /// metadata for fonts whose program bytes are embedded —
175    /// `postscript_name` works for every variant where a name is
176    /// known, including non-embedded Type1 standard-14 fonts.
177    ///
178    /// This is the source for the WASM `getTextPositions()`
179    /// `fontName` field; it lets the editor render a stable font
180    /// label for standard-14 runs that don't carry an embedded
181    /// program.
182    pub(crate) fn postscript_name(&self) -> Option<String> {
183        match self {
184            Self::Type1(t) => t.postscript_name().map(|s| s.to_string()),
185            Self::TrueType(t) => t.postscript_name().map(|s| s.to_string()),
186            Self::Type0(t) => t.postscript_name().map(|s| s.to_string()),
187        }
188    }
189
190    /// The name the page gave the font, where it differs from
191    /// [`Self::postscript_name`] (#623): a Type1 or CFF font that stands on
192    /// a standard face. TrueType and Type0 report their /BaseFont already.
193    pub(crate) fn declared_name(&self) -> Option<String> {
194        match self {
195            Self::Type1(t) => t.declared_name().map(|s| s.to_string()),
196            Self::TrueType(_) | Self::Type0(_) => self.postscript_name(),
197        }
198    }
199
200    /// The name text extraction reports for a span: the document's name for
201    /// an embedded font, the substitute face otherwise. TrueType and Type0
202    /// already report the document's name through
203    /// [`Self::postscript_name`]; only Type1 answers differently here.
204    pub(crate) fn document_name(&self) -> Option<String> {
205        match self {
206            Self::Type1(t) => t.document_name().map(|s| s.to_string()),
207            Self::TrueType(_) | Self::Type0(_) => self.postscript_name(),
208        }
209    }
210
211    /// Get raw font bytes and metadata.
212    ///
213    /// Returns None for Type1 fonts and non-embedded TrueType fonts.
214    pub(crate) fn font_data(&self) -> Option<OutlineFontData> {
215        match self {
216            Self::Type1(_) => None,
217            Self::TrueType(t) => {
218                let m = t.font_metrics();
219                Some(OutlineFontData {
220                    data: t.font_data()?,
221                    cache_key: t.cache_key(),
222                    postscript_name: t.postscript_name().map(|s| s.to_string()),
223                    weight: t.weight(),
224                    is_italic: t.is_italic(),
225                    is_serif: t.is_serif(),
226                    is_monospace: t.is_monospace(),
227                    ascent: m.map(|x| x.0),
228                    descent: m.map(|x| x.1),
229                    cap_height: m.and_then(|x| x.2),
230                    x_height: m.and_then(|x| x.3),
231                })
232            }
233            Self::Type0(t) => {
234                let m = t.font_metrics();
235                Some(OutlineFontData {
236                    data: t.font_data()?,
237                    cache_key: t.cache_key(),
238                    postscript_name: t.postscript_name().map(|s| s.to_string()),
239                    weight: t.weight(),
240                    is_italic: t.is_italic(),
241                    is_serif: t.is_serif(),
242                    is_monospace: t.is_monospace(),
243                    ascent: m.map(|x| x.0),
244                    descent: m.map(|x| x.1),
245                    cap_height: m.and_then(|x| x.2),
246                    x_height: m.and_then(|x| x.3),
247                })
248            }
249        }
250    }
251
252    /// Vertical font metrics in /1000 em, from either embedded font data
253    /// (skrifa) or the Standard-14 AFM fallback table.
254    pub(crate) fn font_metrics(&self) -> Option<(f64, f64, Option<f64>, Option<f64>)> {
255        match self {
256            Self::Type1(t) => t.font_metrics(),
257            Self::TrueType(t) => t.font_metrics(),
258            Self::Type0(t) => t.font_metrics(),
259        }
260    }
261
262    /// The substitute face's AFM pair for Type1/CFF, ignoring the
263    /// descriptor: the pre-E4 answer, kept for the default extraction path
264    /// (#643 follow-up). TrueType and Type0 never changed, so they answer
265    /// as [`Self::font_metrics`] does.
266    pub(crate) fn substitute_metrics(&self) -> Option<(f64, f64, Option<f64>, Option<f64>)> {
267        match self {
268            Self::Type1(t) => t.substitute_metrics(),
269            Self::TrueType(t) => t.font_metrics(),
270            Self::Type0(t) => t.font_metrics(),
271        }
272    }
273}