Skip to main content

incremental_font_transfer/
font_patch.rs

1//! Reads and applies font patches to font binaries.
2//!
3//! Font patch formats are defined as part of the incremental font transfer specification:
4//! <https://w3c.github.io/IFT/Overview.html#font-patch-formats>
5//!
6//! Two main types of font patches are implemented:
7//! 1. Table Keyed Patch - these patches contain a per table brotli binary patch to be applied
8//!    to the input font.
9//! 2. Glyph Keyed - these patches contain blobs of data associated with combinations of
10//!    glyph id + table. The patch inserts these blobs into the table at the location for
11//!    the corresponding glyph id.
12
13use std::collections::HashMap;
14
15use crate::patch_group::PatchInfo;
16
17use crate::glyph_keyed::apply_glyph_keyed_patches;
18
19use crate::table_keyed::apply_table_keyed_patch;
20use font_types::Tag;
21use read_fonts::tables::ift::{CompatibilityId, GlyphKeyedPatch, TableKeyedPatch};
22use skera::serialize::SerializeErrorFlags;
23
24use read_fonts::{FontData, FontRead, FontRef, ReadError};
25
26use shared_brotli_patch_decoder::decode_error::DecodeError;
27use shared_brotli_patch_decoder::SharedBrotliDecoder;
28
29/// A trait for types to which an incremental font transfer patch can be applied.
30///
31/// See: <https://w3c.github.io/IFT/Overview.html#font-patch-formats> for details on the format of patches.
32pub trait IncrementalFontPatchBase {
33    /// Apply a table keyed incremental font patches (<https://w3c.github.io/IFT/Overview.html#font-patch-formats>)
34    ///
35    /// Applies the patches to this base.
36    ///
37    /// Returns the byte data for the new font produced as a result of the patch applications.
38    fn apply_table_keyed_patch<D: SharedBrotliDecoder>(
39        &self,
40        patch: &PatchInfo,
41        patch_data: &[u8],
42        brotli_decoder: &D,
43    ) -> Result<Vec<u8>, PatchingError>;
44
45    /// Apply a set of glyph keyed incremental font patches (<https://w3c.github.io/IFT/Overview.html#font-patch-formats>)
46    ///
47    /// Applies the patches to this base.
48    ///
49    /// Returns the byte data for the new font produced as a result of the patch applications.
50    fn apply_glyph_keyed_patches<'a, D: SharedBrotliDecoder>(
51        &self,
52        patches: impl Iterator<Item = (&'a PatchInfo, &'a [u8])>,
53        brotli_decoder: &D,
54    ) -> Result<Vec<u8>, PatchingError>;
55}
56
57/// An error that occurs while trying to apply an IFT patch to a font file.
58#[derive(Debug, Clone, PartialEq)]
59pub enum PatchingError {
60    PatchParsingFailed(ReadError),
61    FontParsingFailed(ReadError),
62    SerializationError(SerializeErrorFlags),
63    IncompatiblePatch,
64    NonIncrementalFont,
65    InvalidPatch(&'static str),
66    EmptyPatchList,
67    InternalError,
68    MissingPatches,
69}
70
71impl From<SerializeErrorFlags> for PatchingError {
72    fn from(err: SerializeErrorFlags) -> Self {
73        PatchingError::SerializationError(err)
74    }
75}
76
77impl From<ReadError> for PatchingError {
78    fn from(err: ReadError) -> Self {
79        PatchingError::FontParsingFailed(err)
80    }
81}
82
83impl From<DecodeError> for PatchingError {
84    fn from(decoding_error: DecodeError) -> Self {
85        match decoding_error {
86            DecodeError::InitFailure => {
87                PatchingError::InvalidPatch("Failure to init brotli encoder.")
88            }
89            DecodeError::InvalidStream => PatchingError::InvalidPatch("Malformed brotli stream."),
90            DecodeError::InvalidDictionary => PatchingError::InvalidPatch("Malformed dictionary."),
91            DecodeError::MaxSizeExceeded => PatchingError::InvalidPatch("Max size exceeded."),
92            DecodeError::ExcessInputData => {
93                PatchingError::InvalidPatch("Input brotli stream has excess bytes.")
94            }
95            DecodeError::IoError(_) => {
96                PatchingError::InvalidPatch("IO error decoding input brotli stream.")
97            }
98        }
99    }
100}
101
102impl std::fmt::Display for PatchingError {
103    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104        match self {
105            PatchingError::PatchParsingFailed(err) => {
106                write!(f, "Failed to parse patch file: {}", err)
107            }
108            PatchingError::FontParsingFailed(err) => {
109                write!(f, "Failed to parse font file: {}", err)
110            }
111            PatchingError::SerializationError(err) => {
112                write!(f, "serialization failure constructing patched table: {err}")
113            }
114            PatchingError::IncompatiblePatch => {
115                write!(f, "Compatibility ID of the patch does not match the font.")
116            }
117
118            PatchingError::NonIncrementalFont => {
119                write!(
120                    f,
121                    "Can't patch font as it's not an incremental transfer font."
122                )
123            }
124            PatchingError::InvalidPatch(msg) => write!(f, "Invalid patch file: '{msg}'"),
125            PatchingError::EmptyPatchList => write!(f, "At least one patch file must be provided."),
126            PatchingError::InternalError => write!(
127                f,
128                "Internal constraint violated, typically should not happen."
129            ),
130            PatchingError::MissingPatches => write!(f, "Not all patch data has been supplied."),
131        }
132    }
133}
134
135impl std::error::Error for PatchingError {}
136
137impl IncrementalFontPatchBase for FontRef<'_> {
138    fn apply_table_keyed_patch<D: SharedBrotliDecoder>(
139        &self,
140        patch: &PatchInfo,
141        patch_data: &[u8],
142        brotli_decoder: &D,
143    ) -> Result<Vec<u8>, PatchingError> {
144        let font_compat_id = patch
145            .tag()
146            .font_compat_id(self)
147            .map_err(PatchingError::FontParsingFailed)?;
148        if font_compat_id != *patch.tag().expected_compat_id() {
149            return Err(PatchingError::IncompatiblePatch);
150        }
151
152        let patch = TableKeyedPatch::read(FontData::new(patch_data))
153            .map_err(PatchingError::PatchParsingFailed)?;
154
155        if patch.compatibility_id() != font_compat_id {
156            return Err(PatchingError::IncompatiblePatch);
157        }
158
159        apply_table_keyed_patch(&patch, self, brotli_decoder)
160    }
161
162    fn apply_glyph_keyed_patches<'a, D: SharedBrotliDecoder>(
163        &self,
164        patches: impl Iterator<Item = (&'a PatchInfo, &'a [u8])>,
165        brotli_decoder: &D,
166    ) -> Result<Vec<u8>, PatchingError> {
167        let mut cached_compat_ids: HashMap<Tag, Result<CompatibilityId, PatchingError>> =
168            Default::default();
169
170        let mut raw_patches: Vec<(&PatchInfo, GlyphKeyedPatch<'_>)> = vec![];
171        for (patch_info, patch_data) in patches {
172            let tag = patch_info.tag();
173            let font_compat_id = cached_compat_ids
174                .entry(tag.tag())
175                .or_insert_with(|| {
176                    tag.font_compat_id(self)
177                        .map_err(PatchingError::FontParsingFailed)
178                })
179                .as_ref()
180                .map_err(Clone::clone)?;
181            if font_compat_id != tag.expected_compat_id() {
182                return Err(PatchingError::IncompatiblePatch);
183            }
184
185            let patch = GlyphKeyedPatch::read(FontData::new(patch_data))
186                .map_err(PatchingError::PatchParsingFailed)?;
187
188            if *font_compat_id != patch.compatibility_id() {
189                return Err(PatchingError::IncompatiblePatch);
190            }
191
192            raw_patches.push((patch_info, patch));
193        }
194
195        apply_glyph_keyed_patches(&raw_patches, self, brotli_decoder)
196    }
197}
198
199impl IncrementalFontPatchBase for &[u8] {
200    fn apply_table_keyed_patch<D: SharedBrotliDecoder>(
201        &self,
202        patch: &PatchInfo,
203        patch_data: &[u8],
204        brotli_decoder: &D,
205    ) -> Result<Vec<u8>, PatchingError> {
206        FontRef::new(self)
207            .map_err(PatchingError::FontParsingFailed)?
208            .apply_table_keyed_patch(patch, patch_data, brotli_decoder)
209    }
210
211    fn apply_glyph_keyed_patches<'a, D: SharedBrotliDecoder>(
212        &self,
213        patches: impl Iterator<Item = (&'a PatchInfo, &'a [u8])>,
214        brotli_decoder: &D,
215    ) -> Result<Vec<u8>, PatchingError> {
216        FontRef::new(self)
217            .map_err(PatchingError::FontParsingFailed)?
218            .apply_glyph_keyed_patches(patches, brotli_decoder)
219    }
220}
221
222#[cfg(test)]
223mod tests {
224
225    use std::collections::HashMap;
226
227    use font_test_data::ift::{
228        codepoints_only_format2, glyf_u16_glyph_patches, glyph_keyed_patch_header,
229        table_keyed_patch,
230    };
231    use read_fonts::{
232        collections::IntSet,
233        tables::ift::{CompatibilityId, IFTX_TAG, IFT_TAG},
234    };
235    use shared_brotli_patch_decoder::BuiltInBrotliDecoder;
236
237    use crate::{
238        font_patch::PatchingError,
239        glyph_keyed::tests::assemble_glyph_keyed_patch,
240        patchmap::{IftTableTag, PatchId, PatchUrl},
241        testdata::test_font_for_patching_with_loca_mod,
242    };
243
244    use super::{IncrementalFontPatchBase, PatchInfo};
245
246    // Testing only exceptional situations here, actual applications are tested by "patch_group.rs".
247
248    #[test]
249    fn table_keyed_patch_and_font_compat_id_mismatch() {
250        let info = PatchInfo {
251            url: PatchUrl::expand_template(
252                &[8, b'f', b'o', b'o', b'.', b'b', b'a', b'r', b'/', 128],
253                &PatchId::Numeric(0),
254            )
255            .unwrap(),
256            source_table: IftTableTag::Ift(CompatibilityId::from_u32s([1, 2, 3, 4])),
257            application_flag_bit_indices: IntSet::<u32>::empty(),
258        };
259
260        let ift_table = codepoints_only_format2();
261        let mut iftx_table = codepoints_only_format2();
262        iftx_table.write_at("compat_id[0]", 2u32);
263
264        let font = test_font_for_patching_with_loca_mod(
265            true,
266            |_| {},
267            HashMap::from([
268                (IFT_TAG, ift_table.as_slice()),
269                (IFTX_TAG, iftx_table.as_slice()),
270            ]),
271        );
272
273        let mut patch = table_keyed_patch();
274        patch.write_at("compat_id", 2);
275        assert_eq!(
276            font.as_slice()
277                .apply_table_keyed_patch(&info, &patch, &BuiltInBrotliDecoder),
278            Err(PatchingError::IncompatiblePatch)
279        );
280    }
281
282    #[test]
283    fn table_keyed_patch_info_and_font_compat_id_mismatch() {
284        let info = PatchInfo {
285            url: PatchUrl::expand_template(
286                &[8, b'f', b'o', b'o', b'.', b'b', b'a', b'r', b'/', 128],
287                &PatchId::Numeric(0),
288            )
289            .unwrap(),
290            source_table: IftTableTag::Ift(CompatibilityId::from_u32s([2, 2, 3, 4])),
291            application_flag_bit_indices: IntSet::<u32>::empty(),
292        };
293
294        let ift_table = codepoints_only_format2();
295        let font = test_font_for_patching_with_loca_mod(
296            true,
297            |_| {},
298            HashMap::from([(IFT_TAG, ift_table.as_slice())]),
299        );
300
301        let patch = table_keyed_patch();
302        assert_eq!(
303            font.as_slice()
304                .apply_table_keyed_patch(&info, &patch, &BuiltInBrotliDecoder),
305            Err(PatchingError::IncompatiblePatch)
306        );
307    }
308
309    #[test]
310    fn glyph_keyed_patch_and_font_compat_id_mismatch() {
311        let info = PatchInfo {
312            url: PatchUrl::expand_template(
313                &[8, b'f', b'o', b'o', b'.', b'b', b'a', b'r', b'/', 128],
314                &PatchId::Numeric(0),
315            )
316            .unwrap(),
317            source_table: IftTableTag::Ift(CompatibilityId::from_u32s([1, 2, 3, 4])),
318            application_flag_bit_indices: IntSet::<u32>::empty(),
319        };
320
321        let ift_table = codepoints_only_format2();
322        let font = test_font_for_patching_with_loca_mod(
323            true,
324            |_| {},
325            HashMap::from([(IFT_TAG, ift_table.as_slice())]),
326        );
327
328        let patch =
329            assemble_glyph_keyed_patch(glyph_keyed_patch_header(), glyf_u16_glyph_patches());
330
331        let input = vec![(&info, patch.as_slice())];
332        assert_eq!(
333            font.as_slice()
334                .apply_glyph_keyed_patches(input.into_iter(), &BuiltInBrotliDecoder),
335            Err(PatchingError::IncompatiblePatch)
336        );
337    }
338
339    #[test]
340    fn glyph_keyed_patch_info_and_font_compat_id_mismatch() {
341        let info = PatchInfo {
342            url: PatchUrl::expand_template(
343                &[8, b'f', b'o', b'o', b'.', b'b', b'a', b'r', b'/', 128],
344                &PatchId::Numeric(0),
345            )
346            .unwrap(),
347            source_table: IftTableTag::Ift(CompatibilityId::from_u32s([6, 7, 9, 9])),
348            application_flag_bit_indices: IntSet::<u32>::empty(),
349        };
350
351        let mut ift_table = codepoints_only_format2();
352        ift_table.write_at("compat_id[0]", 6u32);
353        ift_table.write_at("compat_id[1]", 7u32);
354        ift_table.write_at("compat_id[2]", 8u32);
355        ift_table.write_at("compat_id[3]", 9u32);
356
357        let font = test_font_for_patching_with_loca_mod(
358            true,
359            |_| {},
360            HashMap::from([(IFT_TAG, ift_table.as_slice())]),
361        );
362
363        let patch =
364            assemble_glyph_keyed_patch(glyph_keyed_patch_header(), glyf_u16_glyph_patches());
365
366        let input = vec![(&info, patch.as_slice())];
367        assert_eq!(
368            font.as_slice()
369                .apply_glyph_keyed_patches(input.into_iter(), &BuiltInBrotliDecoder),
370            Err(PatchingError::IncompatiblePatch)
371        );
372    }
373}