Skip to main content

skrifa/scaler/
truetype.rs

1//! TrueType hinting for a [`ScalerFont`](super::ScalerFont).
2//!
3//! read-fonts loads the outline, and this crate's interpreter hints it
4//! through the loader's [`Hinter`] hook.
5
6use super::{DrawnMetrics, HintOptions, ScaleError};
7use crate::{
8    font::Font,
9    outline::glyf::{
10        ClearType, ClearTypeState, DropoutControl, HintError, HintInstance, HintOutline,
11        HintTables, RasterPolicy,
12    },
13};
14use alloc::sync::Arc;
15use core::{
16    mem::{align_of, size_of},
17    ops::Range,
18};
19use raw::model::Error;
20use raw::{
21    mem::{take_slice, with_scratch},
22    model::{
23        _font_interop,
24        glyph::outline::{glyf::GlyfGlyph, PathContourStart, PathSink},
25        Tables,
26    },
27    tables::{
28        glyf::{
29            outline::{Buffers, GlyphZone, Hinter, Scale26Dot6},
30            PointFlags, PHANTOM_POINT_COUNT,
31        },
32        head::Flags as HeadFlags,
33    },
34    types::{F26Dot6, Fixed, GlyphId, Point},
35    TableProvider,
36};
37use yoke::Yoke;
38
39/// The interpreter, configured for one font at one size.
40#[derive(Default)]
41pub(super) struct TrueTypeHinter {
42    instance: HintInstance,
43    /// The tables the interpreter reads, parsed once per font and yoked to
44    /// the tables they borrow.
45    tables: Option<Yoke<HintTables<'static>, Arc<Tables>>>,
46    scale: Option<Scale26Dot6>,
47    /// The interpreter's size, rounded for a font that requests whole pixel
48    /// sizes.
49    ppem: f32,
50    pedantic: bool,
51    /// The byte range of the widths in the `hdmx` record for the size.
52    hdmx_widths: Option<Range<usize>>,
53    scratch_lengths: ScratchLengths,
54}
55
56/// The working memory a glyph program needs beyond the outline.
57#[derive(Copy, Clone, Default)]
58struct ScratchLengths {
59    stack: usize,
60    cvt: usize,
61    storage: usize,
62    twilight: usize,
63}
64
65impl ScratchLengths {
66    /// Returns the bytes a block needs for [`carve`](Self::carve) to
67    /// succeed with an outline of `points` points.
68    fn packed_len(&self, points: usize) -> usize {
69        let point = size_of::<Point<F26Dot6>>();
70        points * point
71            + (self.stack + self.cvt + self.storage) * size_of::<i32>()
72            + self.twilight * (2 * point + size_of::<PointFlags>())
73            // Seven slices are carved out, and each may need aligning.
74            + 7 * align_of::<Point<F26Dot6>>()
75    }
76
77    /// Splits `buf` into the slices a glyph program uses.
78    ///
79    /// Returns `None` if `buf` is shorter than
80    /// [`packed_len`](Self::packed_len).
81    fn carve<'a>(&self, buf: &'a mut [u8], points: usize) -> Option<HintScratch<'a>> {
82        let (original_scaled, rest) = take_slice::<Point<F26Dot6>>(buf, points)?;
83        let (twilight_scaled, rest) = take_slice::<Point<F26Dot6>>(rest, self.twilight)?;
84        let (twilight_original_scaled, rest) = take_slice::<Point<F26Dot6>>(rest, self.twilight)?;
85        let (stack, rest) = take_slice::<i32>(rest, self.stack)?;
86        let (cvt, rest) = take_slice::<i32>(rest, self.cvt)?;
87        let (storage, rest) = take_slice::<i32>(rest, self.storage)?;
88        let (twilight_flags, _) = take_slice::<PointFlags>(rest, self.twilight)?;
89        Some(HintScratch {
90            original_scaled,
91            stack,
92            cvt,
93            storage,
94            twilight_scaled,
95            twilight_original_scaled,
96            twilight_flags,
97        })
98    }
99}
100
101/// The working memory for a glyph program, carved from the caller's block.
102///
103/// Everything a glyph program may write lives here rather than in the
104/// hinter, so drawing leaves the hinter unchanged.
105struct HintScratch<'a> {
106    /// The scaled points before hinting moved them.
107    original_scaled: &'a mut [Point<F26Dot6>],
108    stack: &'a mut [i32],
109    cvt: &'a mut [i32],
110    storage: &'a mut [i32],
111    twilight_scaled: &'a mut [Point<F26Dot6>],
112    twilight_original_scaled: &'a mut [Point<F26Dot6>],
113    twilight_flags: &'a mut [PointFlags],
114}
115
116impl TrueTypeHinter {
117    /// Runs the font and control value programs for `font` at `ppem`.
118    pub(super) fn reconfigure(
119        &mut self,
120        font: &Font,
121        ppem: f32,
122        hinting: &HintOptions,
123    ) -> Result<(), ScaleError> {
124        // The font may be another one than last time. Finding its programs
125        // only borrows slices of tables the font already holds.
126        let host = _font_interop::_table_cache_host(font).ok_or(Error::MissingResource)?;
127        let hint_tables = self
128            .tables
129            .insert(Yoke::attach_to_cart(host, |tables| hint_tables(tables)))
130            .get();
131        let tables = font.tables();
132        let units_per_em = font.units_per_em();
133        // Bit 3 of head.flags asks for whole pixel sizes, in which case the
134        // size is rounded before hinting, as FreeType does.
135        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttobjs.c#L1424>
136        let fractional_sizes = !tables
137            .head()
138            .map(|head| head.flags().contains(HeadFlags::FORCE_INTEGER_PPEM))
139            .unwrap_or_default();
140        let ppem = if fractional_sizes {
141            ppem
142        } else {
143            F26Dot6::from_f64(ppem as f64).round().to_f32()
144        };
145        let scale = Scale26Dot6::new(Some(ppem), units_per_em);
146        let scale_bits = scale.to_bits();
147        // The size the MPPEM instruction reports is rounded in fixed point,
148        // as FreeType rounds it.
149        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftobjs.c#L3349>
150        let rounded_ppem =
151            ((Fixed::from_bits(scale_bits) * Fixed::from_bits(units_per_em as i32)).to_bits() + 32)
152                >> 6;
153        let policy = RasterPolicy::resolve(&tables, rounded_ppem.clamp(0, u16::MAX as i32) as u16);
154        let mode = if hinting.classic_cleartype() {
155            ClearType::Classic
156        } else {
157            ClearType::Enhanced
158        };
159        let cleartype = ClearTypeState::new(mode, font.is_hint_reliant(), policy);
160        self.instance.reconfigure(
161            hint_tables,
162            scale_bits,
163            rounded_ppem,
164            hinting.outline_target(),
165            font.normalized_coords(),
166            cleartype,
167        )?;
168        self.scale = Some(scale);
169        self.ppem = ppem;
170        self.pedantic = hinting.pedantic();
171        // FreeType never takes advances from `hdmx` for a fixed pitch font.
172        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/a5877f48fff8dd6cf8d53c505ce055499376aee2/src/truetype/ttgload.c#L2324>
173        let fixed_pitch = tables.post().is_ok_and(|post| post.is_fixed_pitch() != 0);
174        self.hdmx_widths = (!fixed_pitch)
175            .then(|| hdmx_widths(tables, rounded_ppem))
176            .flatten();
177        self.scratch_lengths = ScratchLengths {
178            stack: hint_tables.max_stack_elements as usize,
179            cvt: hint_tables.cvt.len(),
180            storage: hint_tables.max_storage as usize,
181            twilight: hint_tables.max_twilight_points as usize,
182        };
183        Ok(())
184    }
185
186    /// Returns true if glyphs are hinted at this size.
187    ///
188    /// The control value program or the `gasp` table can disable hinting
189    /// for a size.
190    pub(super) fn is_enabled(&self) -> bool {
191        self.scale.is_some() && self.instance.is_enabled()
192    }
193
194    /// Returns the size for an unhinted draw when the control value program
195    /// disabled hinting.
196    ///
197    /// This is the interpreter's size, because FreeType chooses its scale
198    /// before it reads the program's flag. Returns `None` when hinting is on
199    /// or when the `gasp` table disabled it. FreeType checks `gasp` before
200    /// choosing a scale, so it draws at the requested size.
201    pub(super) fn program_unhinted_ppem(&self) -> Option<f32> {
202        (self.scale.is_some() && self.instance.is_disabled_by_program()).then_some(self.ppem)
203    }
204
205    /// Returns whether glyph programs start in backward compatibility mode.
206    #[cfg(test)]
207    pub(super) fn backward_compatibility(&self) -> bool {
208        self.instance.backward_compatibility()
209    }
210
211    /// Makes the control value program disable hinting, as a font might.
212    #[cfg(test)]
213    pub(super) fn simulate_program_disabling_hinting(&mut self) {
214        self.instance.simulate_prep_flag_suppress_hinting();
215    }
216
217    /// Returns the size the interpreter hints at, once configured.
218    pub(super) fn ppem(&self) -> Option<f32> {
219        self.scale.is_some().then_some(self.ppem)
220    }
221
222    pub(super) fn raster_policy(&self) -> RasterPolicy {
223        self.instance.raster_policy()
224    }
225
226    /// Returns the bytes of working memory needed to draw `outline`.
227    ///
228    /// This covers the loader's buffers and the interpreter's.
229    pub(super) fn required_memory_size(&self, outline: &GlyfGlyph<'_>) -> usize {
230        let lengths = outline.plan().buffer_lengths::<Scale26Dot6>();
231        lengths.packed_len::<Scale26Dot6>()
232            + self.scratch_lengths.packed_len(lengths.points as usize)
233    }
234
235    pub(super) fn draw(
236        &self,
237        outline: &GlyfGlyph<'_>,
238        memory: Option<&mut [u8]>,
239        sink: &mut dyn PathSink,
240    ) -> Result<DrawnMetrics, ScaleError> {
241        match memory {
242            Some(buf) => self.draw_in(outline, buf, sink),
243            None => with_scratch(self.required_memory_size(outline), |buf| {
244                self.draw_in(outline, buf, sink)
245            }),
246        }
247    }
248
249    fn draw_in(
250        &self,
251        outline: &GlyfGlyph<'_>,
252        buf: &mut [u8],
253        mut sink: &mut dyn PathSink,
254    ) -> Result<DrawnMetrics, ScaleError> {
255        let (Some(tables), Some(scale)) = (&self.tables, &self.scale) else {
256            return Err(Error::MissingResource.into());
257        };
258        let insufficient = ScaleError::InsufficientMemory {
259            required: self.required_memory_size(outline),
260            supplied: buf.len(),
261        };
262        let tables = tables.get();
263        let glyph = outline.glyph();
264        let font = outline.font();
265        let plan = outline.plan();
266        let lengths = plan.buffer_lengths::<Scale26Dot6>();
267        let loader_len = lengths.packed_len::<Scale26Dot6>();
268        if buf.len() < loader_len {
269            return Err(insufficient);
270        }
271        let (loader_buf, rest) = buf.split_at_mut(loader_len);
272        let buffers = Buffers::from_bytes(loader_buf, &lengths).map_err(|error| match error {
273            Error::InsufficientMemory => insufficient.clone(),
274            error => error.into(),
275        })?;
276        let scratch = self
277            .scratch_lengths
278            .carve(rest, lengths.points as usize)
279            .ok_or_else(|| insufficient.clone())?;
280        let mut hinter = FontHinter {
281            instance: &self.instance,
282            tables,
283            scratch,
284            pedantic: self.pedantic,
285            // A glyph with no instructions of its own keeps what the control
286            // value program chose.
287            dropout_control: self.instance.dropout_control(),
288            error: None,
289        };
290        let loaded = plan
291            .load(&font, scale, buffers, Some(&mut hinter))
292            .map_err(|error| match hinter.error.take() {
293                Some(error) => ScaleError::Hinting(error),
294                None if error == Error::InsufficientMemory => insufficient.clone(),
295                None => error.into(),
296            })?;
297        // FreeType takes the advance from hdmx when hinting outside backward
298        // compatibility mode, as the control value program left it; a glyph
299        // program that changes the mode does so after the choice is made.
300        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/a5877f48fff8dd6cf8d53c505ce055499376aee2/src/truetype/ttgload.c#L2321>
301        let hdmx_width = if self.instance.backward_compatibility() {
302            None
303        } else {
304            self.hdmx_width(font.tables(), glyph)
305        };
306        let advance = hdmx_width
307            .map(F26Dot6::from_i32)
308            .unwrap_or_else(|| loaded.adjusted_advance_width());
309        if !loaded.to_path(PathContourStart::ScanBackward, &mut sink) {
310            return Err(Error::Malformed.into());
311        }
312        let mut metrics = DrawnMetrics::default();
313        // A hinted advance is rounded, as FreeType rounds it.
314        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftobjs.c#L889>
315        metrics.h_advance = Some(advance.round().to_f32());
316        metrics.x_bearing = Some(loaded.adjusted_lsb().to_f32());
317        metrics.has_overlaps = plan.has_overlaps;
318        metrics.dropout_control = hinter.dropout_control;
319        Ok(metrics)
320    }
321
322    /// Returns the `hdmx` advance for `glyph` at the size, if any.
323    fn hdmx_width(&self, tables: &Tables, glyph: GlyphId) -> Option<i32> {
324        let widths = tables.hdmx_data()?.get(self.hdmx_widths.clone()?)?;
325        Some(*widths.get(glyph.to_u32() as usize)? as i32)
326    }
327}
328
329/// The interpreter attached to the loader's hook for one draw.
330///
331/// Owns the per glyph working memory that the zone does not hold.
332struct FontHinter<'a> {
333    instance: &'a HintInstance,
334    tables: &'a HintTables<'a>,
335    scratch: HintScratch<'a>,
336    pedantic: bool,
337    dropout_control: DropoutControl,
338    /// The failure of a glyph program, kept because the load reports only
339    /// its kind.
340    error: Option<HintError>,
341}
342
343impl Hinter<F26Dot6> for FontHinter<'_> {
344    fn backward_compatibility(&self) -> bool {
345        self.instance.backward_compatibility()
346    }
347
348    fn round_composite_x_offset(&self) -> bool {
349        self.instance.round_composite_x_offset()
350    }
351
352    fn hint(&mut self, zone: &mut GlyphZone<'_, F26Dot6>) -> Result<(), HintError> {
353        // The zone hands over the unhinted scaled outline, so the originals
354        // are a copy taken on entry. Except for the phantom points: `points`
355        // carries them rounded for the interpreter to work on, while the
356        // original outline keeps them unrounded, which `phantom` still has.
357        let original_scaled = &mut self.scratch.original_scaled[..zone.points.len()];
358        original_scaled.copy_from_slice(zone.points);
359        let phantom_start = original_scaled.len() - PHANTOM_POINT_COUNT;
360        original_scaled[phantom_start..].copy_from_slice(&zone.phantom[..]);
361        let mut input = HintOutline {
362            glyph_id: zone.glyph,
363            unscaled: zone.unscaled,
364            scaled: zone.points,
365            original_scaled,
366            flags: zone.flags,
367            contours: zone.contours,
368            bytecode: zone.bytecode,
369            phantom: &mut zone.phantom[..],
370            stack: &mut *self.scratch.stack,
371            cvt: &mut *self.scratch.cvt,
372            storage: &mut *self.scratch.storage,
373            twilight_scaled: &mut *self.scratch.twilight_scaled,
374            twilight_original_scaled: &mut *self.scratch.twilight_original_scaled,
375            twilight_flags: &mut *self.scratch.twilight_flags,
376            is_composite: zone.is_composite,
377            coords: zone.coords,
378        };
379        match self.instance.hint(self.tables, &mut input, self.pedantic) {
380            Ok(dropout_control) => {
381                self.dropout_control = dropout_control;
382                Ok(())
383            }
384            // Only a pedantic hinter reports a failing program.
385            Err(error) => {
386                self.error = Some(error.clone());
387                Err(error)
388            }
389        }
390    }
391}
392
393/// Returns the tables the interpreter reads from `tables`.
394pub(super) fn hint_tables(tables: &Tables) -> HintTables<'_> {
395    let maxp = tables.selected_maxp().ok();
396    HintTables {
397        fpgm: tables.fpgm().unwrap_or_default(),
398        prep: tables.prep().unwrap_or_default(),
399        cvt: tables.cvt().unwrap_or_default(),
400        cvar: tables.cvar().ok(),
401        max_function_defs: maxp
402            .as_ref()
403            .and_then(|maxp| maxp.max_function_defs())
404            .unwrap_or_default(),
405        max_instruction_defs: maxp
406            .as_ref()
407            .and_then(|maxp| maxp.max_instruction_defs())
408            .unwrap_or_default(),
409        // Four more for the phantom points, as FreeType reserves.
410        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttobjs.c#L1188>
411        max_twilight_points: maxp
412            .as_ref()
413            .and_then(|maxp| maxp.max_twilight_points())
414            .unwrap_or_default()
415            .saturating_add(4),
416        // Thirty two more, FreeType's allowance for buggy fonts.
417        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttinterp.c#L356>
418        max_stack_elements: maxp
419            .as_ref()
420            .and_then(|maxp| maxp.max_stack_elements())
421            .unwrap_or_default()
422            .saturating_add(32),
423        max_storage: maxp
424            .as_ref()
425            .and_then(|maxp| maxp.max_storage())
426            .unwrap_or_default(),
427        axis_count: tables
428            .fvar()
429            .map(|fvar| fvar.axis_count())
430            .unwrap_or_default(),
431    }
432}
433
434/// Returns true if FreeType would run the interpreter for `tables` rather
435/// than the automatic hinter.
436///
437/// This is the case when the font has glyph instructions, or a font or
438/// control value program.
439/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftobjs.c#L1001>
440pub(super) fn prefer_interpreter(tables: &Tables) -> bool {
441    let max_instructions = tables
442        .selected_maxp()
443        .ok()
444        .and_then(|maxp| maxp.max_size_of_instructions())
445        .unwrap_or_default();
446    !(max_instructions == 0
447        && tables.fpgm().unwrap_or_default().is_empty()
448        && tables.prep().unwrap_or_default().is_empty())
449}
450
451/// Returns the byte range of the widths in the `hdmx` record for `ppem`, if
452/// any.
453///
454/// Only a whole size that fits in a byte has a record.
455/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttgload.c#L1996>
456fn hdmx_widths(tables: &Tables, ppem: i32) -> Option<Range<usize>> {
457    // FreeType looks the record up by the size rounded to a whole pixel,
458    // the one MPPEM reports, so a fractional size still finds its record.
459    let size = u8::try_from(ppem).ok()?;
460    let data = tables.hdmx_data()?;
461    let widths = tables.hdmx().ok()?.record_for_size(size)?.widths();
462    // The record borrows the same bytes, so its position is an offset.
463    let start = (widths.as_ptr() as usize).checked_sub(data.as_ptr() as usize)?;
464    let end = start.checked_add(widths.len())?;
465    (end <= data.len()).then_some(start..end)
466}