Skip to main content

pdfrum_font/subst/
substfont.rs

1//! What a substitution decided, and the synthetic adjustments that follow
2//! from it.
3//!
4//! When a document's font is replaced by a different face, the replacement is
5//! rarely the right weight or slant. PDFium compensates by shearing the
6//! outline and dilating it, by amounts read from three hand-tuned tables. The
7//! tables are ported verbatim — including three entries in the middle of one
8//! of them that look like transcription errors and are part of the observable
9//! output.
10
11use super::charset::Charset;
12#[cfg(test)]
13use super::tables::{ANGLE_SKEW, WEIGHT_POW, WEIGHT_POW_11, WEIGHT_POW_SHIFT_JIS};
14
15/// The record a substitution produces.
16///
17/// `weight` and `weight_cjk` are `Option` where PDFium overloads **0** to mean
18/// "the face's natural weight" (D12). The sentinel is real behavior — it makes
19/// the embolden level 0 and the Multiple-Master axis take its default — so the
20/// mapping back to 0 happens at the two places the arithmetic needs it, not
21/// silently at construction.
22#[derive(Debug, Clone, PartialEq, Eq, Default)]
23pub struct SubstFont {
24    /// The family name the substitution settled on.
25    pub(crate) family: String,
26    /// The charset the face was chosen for.
27    pub charset: Charset,
28    /// The requested weight, or `None` for the face's own.
29    pub(crate) weight: Option<i32>,
30    /// The CJK weight, tracked separately because it has its own default.
31    pub(crate) weight_cjk: Option<i32>,
32    /// The synthetic italic angle, in degrees. Negative slants right.
33    pub italic_angle: i32,
34    /// Whether a CJK substitution happened, which switches both the weight and
35    /// the skew to their CJK variants for a CID font.
36    pub(crate) subst_cjk: bool,
37    /// Whether the CJK substitution asked for italic.
38    pub(crate) italic_cjk: bool,
39    /// Whether this is one of the two built-in Multiple-Master generics, which
40    /// suppresses artificial emboldening entirely — the design space handles
41    /// weight properly, so dilating on top would double-count it.
42    pub is_builtin_generic: bool,
43}
44
45impl SubstFont {
46    /// The weight the embolden and axis arithmetic reads, mapping `None` back
47    /// to PDFium's 0 sentinel.
48    #[must_use]
49    pub fn raw_weight(&self) -> i32 {
50        self.weight.unwrap_or(0)
51    }
52
53    /// The weight in effect, which for a CID font in a CJK substitution is the
54    /// separately-tracked CJK weight (`GetEffectiveWeight`).
55    #[cfg(test)]
56    #[must_use]
57    pub(crate) fn effective_weight(&self, is_cid_font: bool) -> i32 {
58        if self.subst_cjk && is_cid_font {
59            self.weight_cjk.unwrap_or(0)
60        } else {
61            self.raw_weight()
62        }
63    }
64
65    /// The synthetic shear, as hundredths of a unit of x per unit of y.
66    ///
67    /// A table lookup by `-italic_angle`, saturating at **-58** for a positive
68    /// angle or one past the table's 30 entries.
69    #[cfg(test)]
70    #[must_use]
71    pub(crate) fn skew(&self) -> i32 {
72        skew_from_angle(self.italic_angle)
73    }
74
75    /// The CJK shear: a fixed -15° when the CJK substitution asked for italic,
76    /// and none otherwise.
77    #[cfg(test)]
78    #[must_use]
79    pub(crate) fn skew_cjk(&self) -> i32 {
80        skew_from_angle(if self.italic_cjk { -15 } else { 0 })
81    }
82
83    /// The shear actually applied, which for a CID font in a CJK substitution
84    /// is the CJK one.
85    #[cfg(test)]
86    #[must_use]
87    pub(crate) fn effective_skew(&self, is_cid_font: bool) -> i32 {
88        if self.subst_cjk && is_cid_font {
89            self.skew_cjk()
90        } else {
91            self.skew()
92        }
93    }
94
95    /// How much to dilate an outline when *rendering*, given the text matrix's
96    /// two horizontal components.
97    ///
98    /// Returns `None` where the C++ returns -1 and its caller abandons the
99    /// glyph: a weight index at or past 100, i.e. a weight of 1400 or more.
100    /// The intermediate is 64-bit deliberately — a large matrix overflows
101    /// 32 bits and the oracle's own unittest pins the wide result.
102    // `xx` and `xy` are the matrix components' own names; renaming either to
103    // please the lint would make the pair harder to read, not easier.
104    #[allow(clippy::similar_names)]
105    #[must_use]
106    #[cfg(test)]
107    pub(crate) fn embolden_level_for_render(
108        &self,
109        is_cid_font: bool,
110        matrix_xx: i32,
111        matrix_xy: i32,
112    ) -> Option<i32> {
113        if self.is_builtin_generic {
114            return Some(0);
115        }
116        let w = self.effective_weight(is_cid_font);
117        if w <= 400 {
118            return Some(0);
119        }
120        let index = usize::try_from((w - 400) / 10).ok()?;
121        let level = weight_level(index, self.charset == Charset::ShiftJis)?;
122        let scaled =
123            i64::from(level) * (i64::from(matrix_xx).abs() + i64::from(matrix_xy).abs()) / 36655;
124        Some(i32::try_from(scaled).unwrap_or(0))
125    }
126
127    /// How much to dilate when *loading* a glyph, which reads a different
128    /// table and — unlike the render path — **clamps** the index rather than
129    /// failing past 99.
130    ///
131    /// Note it also reads the plain weight, not the effective one.
132    #[cfg(test)]
133    #[must_use]
134    pub(crate) fn embolden_level_for_load(&self) -> i32 {
135        if self.is_builtin_generic {
136            return 0;
137        }
138        let w = self.raw_weight();
139        if w <= 400 {
140            return 0;
141        }
142        let Ok(index) = usize::try_from((w - 400) / 10) else {
143            return 0;
144        };
145        weight_level_for_load(index.min(99), self.charset == Charset::ShiftJis)
146    }
147
148    /// The stem thickness implied by the weight.
149    #[cfg(test)]
150    #[must_use]
151    pub(crate) fn estimated_stem_v(&self) -> i32 {
152        self.raw_weight() / 5
153    }
154
155    /// Whether a base font name names *this* face.
156    ///
157    /// A **prefix** test over the lowercased family with all spaces removed,
158    /// which is loose enough to be wrong — the C++'s own comment notes that a
159    /// family called `Book` would match `Bookman`. Ported as-is because the
160    /// glyph-spacing heuristic of the former working note turns on it.
161    ///
162    /// `base_name` is a `/BaseFont` name, so it arrives as bytes and **the
163    /// caller lowercases it** — the two sides are lowered separately upstream
164    /// and a caller that has already lowered it for other tests should not
165    /// pay for it twice. An empty family never matches: a substitution that
166    /// named no family did not load the document's font.
167    #[must_use]
168    pub(crate) fn is_actual_font_loaded(&self, base_name: &[u8]) -> bool {
169        let normalized: String = self
170            .family
171            .chars()
172            .filter(|c| *c != ' ')
173            .flat_map(char::to_lowercase)
174            .collect();
175        if normalized.is_empty() {
176            return false;
177        }
178        base_name.starts_with(normalized.as_bytes())
179    }
180
181    /// Apply the adjustments `ConfigureExternalSubst` makes when a *system*
182    /// face was chosen.
183    ///
184    /// Two sentinels live here. The weight is left at `None` — PDFium's 0 —
185    /// when the request already matches the face's own weight, which is what
186    /// makes the embolden level 0 for a face that needs no help. And the
187    /// italic angle is nudged: an unslanted request against an upright face
188    /// becomes -12°, while an angle already within 5° of upright is zeroed as
189    /// not worth synthesizing.
190    // The parameter list is the ported one: each argument is read by a distinct
191    // rung of the adjustment above, and grouping them into a struct would only
192    // move the same eight values behind a name that means nothing on its own.
193    #[allow(clippy::too_many_arguments)]
194    pub(crate) fn configure_external(
195        &mut self,
196        face_name: String,
197        charset: Charset,
198        weight: i32,
199        is_italic: bool,
200        mut italic_angle: i32,
201        face_is_bold: bool,
202        face_is_italic: bool,
203    ) {
204        self.family = face_name;
205        self.charset = charset;
206        let face_weight = if face_is_bold { 700 } else { 400 };
207        if weight != face_weight {
208            self.weight = Some(weight);
209        }
210        if is_italic && !face_is_italic {
211            if italic_angle == 0 {
212                italic_angle = -12;
213            } else if italic_angle.abs() < 5 {
214                italic_angle = 0;
215            }
216            self.italic_angle = italic_angle;
217        }
218    }
219
220    /// Mark this as the built-in serif generic, which also scales the weight
221    /// down by a fifth (`UseChromeSerif`).
222    pub(crate) fn use_chrome_serif(&mut self) {
223        "Chrome Serif".clone_into(&mut self.family);
224        if let Some(w) = self.weight {
225            self.weight = Some(w * 4 / 5);
226        }
227    }
228}
229
230/// The shear for an italic angle (`GetSkewFromAngle`).
231#[cfg(test)]
232#[must_use]
233pub fn skew_from_angle(angle: i32) -> i32 {
234    // A positive angle, the `i32::MIN` whose negation overflows, and anything
235    // past the table all take the terminal value.
236    if angle > 0 || angle == i32::MIN {
237        return -58;
238    }
239    let index = angle.unsigned_abs() as usize;
240    ANGLE_SKEW.get(index).map_or(-58, |&s| i32::from(s))
241}
242
243/// The render-path dilation table lookup. `None` past the table, where the
244/// C++ returns -1 and its caller abandons the glyph.
245#[must_use]
246#[cfg(test)]
247fn weight_level(index: usize, shift_jis: bool) -> Option<i32> {
248    if index >= 100 {
249        return None;
250    }
251    let table = if shift_jis {
252        &WEIGHT_POW_SHIFT_JIS
253    } else {
254        &WEIGHT_POW_11
255    };
256    table.get(index).map(|&v| i32::from(v))
257}
258
259/// The load-path dilation table lookup, whose Shift-JIS arm is additionally
260/// rescaled by `65536 / 36655`.
261#[must_use]
262#[cfg(test)]
263fn weight_level_for_load(index: usize, shift_jis: bool) -> i32 {
264    if shift_jis {
265        WEIGHT_POW_SHIFT_JIS
266            .get(index)
267            .map_or(0, |&v| i32::from(v) * 65536 / 36655)
268    } else {
269        WEIGHT_POW.get(index).map_or(0, |&v| i32::from(v))
270    }
271}
272
273/// The facts a font offers the glyph-spacing gate of the former working note.
274///
275/// A borrowed view rather than owned state: the answer is a property of a
276/// loaded font, and pulling the five inputs out makes each of the gate's
277/// refusals sayable on its own.
278#[derive(Debug, Clone, Copy)]
279pub struct GlyphSpacingGate<'a> {
280    /// Whether the font writes vertically — a `-V` CMap.
281    pub vertical: bool,
282    /// Whether the document shipped a usable font program.
283    pub embedded: bool,
284    /// Whether the PDF declared its own advance widths (`HasFontWidths`).
285    pub declared_widths: bool,
286    /// The `/BaseFont` name, subset prefix already stripped, in any case.
287    pub base_font_name: &'a [u8],
288    /// What substitution settled on, or `None` when none ran.
289    pub subst: Option<&'a SubstFont>,
290}
291
292/// Whether a font's glyphs take the glyph-spacing correction of the former working note.
293///
294/// The correction exists for one situation: a PDF that declares its own
295/// advance widths, does **not** ship the font program, and got substituted
296/// onto a face that draws its glyphs at some other width. The document's
297/// widths are then the only truth about how wide the text should look, and the
298/// face disagrees with it. Five conditions each say instead "the widths and
299/// the outlines already agree, leave the glyph alone":
300///
301/// - **vertical writing** — the correction is horizontal, while a `-V` CMap's
302///   advances run down the page;
303/// - **an embedded font** — the program in the file *is* the font, so its
304///   glyph widths are the document's own and cannot disagree with `/Widths`;
305/// - **no declared widths** — a simple font with no `/Widths` reads its
306///   advances off the face, so the comparison is a number against itself;
307/// - **a standard-14 `/BaseFont` name** — Helvetica landing on Arial is a
308///   sanctioned alias rather than a failed match, and the two families were
309///   designed to share metrics;
310/// - **a built-in generic** — the Multiple-Master fallbacks solve their own
311///   width axis to the declared width, so their outlines already come out at
312///   it and correcting again would double-count.
313///
314/// What survives is a substitution onto some *named* face, and the last
315/// question is whether that face is the one the document asked for:
316/// [`SubstFont::is_actual_font_loaded`] answers no, and only then does the
317/// correction run. A font with no substitution record at all has no
318/// substituted face to disagree with, and declines.
319#[must_use]
320pub fn applies_glyph_spacing(gate: &GlyphSpacingGate<'_>) -> bool {
321    if gate.vertical || gate.embedded || !gate.declared_widths {
322        return false;
323    }
324    // Both remaining tests are asked in lower case, so the name is lowered
325    // once for the two of them.
326    let lower = gate.base_font_name.to_ascii_lowercase();
327    if super::standard_font_index(&lower).is_some() {
328        return false;
329    }
330    let Some(subst) = gate.subst else {
331        return false;
332    };
333    !subst.is_builtin_generic && !subst.is_actual_font_loaded(&lower)
334}
335
336#[cfg(test)]
337mod tests {
338    // Test fixtures are fixed-size arrays with known contents.
339    #![allow(clippy::indexing_slicing)]
340    use super::*;
341
342    /// A gate that passes, which each refusal test then breaks one way.
343    ///
344    /// A document asking for `Verdana`, substituted onto a `Nimbus Sans`
345    /// that is plainly a different family.
346    fn passing_gate(subst: &SubstFont) -> GlyphSpacingGate<'_> {
347        GlyphSpacingGate {
348            vertical: false,
349            embedded: false,
350            declared_widths: true,
351            base_font_name: b"Verdana",
352            subst: Some(subst),
353        }
354    }
355
356    fn nimbus() -> SubstFont {
357        SubstFont {
358            family: "Nimbus Sans".to_owned(),
359            ..SubstFont::default()
360        }
361    }
362
363    #[test]
364    fn a_substitution_onto_a_different_family_takes_the_correction() {
365        let subst = nimbus();
366        assert!(applies_glyph_spacing(&passing_gate(&subst)));
367    }
368
369    #[test]
370    fn vertical_writing_declines_the_correction() {
371        let subst = nimbus();
372        let gate = GlyphSpacingGate {
373            vertical: true,
374            ..passing_gate(&subst)
375        };
376        assert!(!applies_glyph_spacing(&gate));
377    }
378
379    #[test]
380    fn an_embedded_program_declines_the_correction() {
381        let subst = nimbus();
382        let gate = GlyphSpacingGate {
383            embedded: true,
384            ..passing_gate(&subst)
385        };
386        assert!(!applies_glyph_spacing(&gate));
387    }
388
389    #[test]
390    fn a_font_without_declared_widths_declines_the_correction() {
391        let subst = nimbus();
392        let gate = GlyphSpacingGate {
393            declared_widths: false,
394            ..passing_gate(&subst)
395        };
396        assert!(!applies_glyph_spacing(&gate));
397    }
398
399    /// The standard-14 test goes through the **alias** table, so a name that
400    /// is not one of the fourteen canonical spellings still refuses.
401    #[test]
402    fn a_standard_fourteen_base_font_name_declines_the_correction() {
403        let subst = nimbus();
404        for name in [&b"Helvetica"[..], b"ArialMT", b"arial,bold", b"CourierNew"] {
405            let gate = GlyphSpacingGate {
406                base_font_name: name,
407                ..passing_gate(&subst)
408            };
409            assert!(
410                !applies_glyph_spacing(&gate),
411                "{}",
412                String::from_utf8_lossy(name)
413            );
414        }
415    }
416
417    #[test]
418    fn a_built_in_generic_declines_the_correction() {
419        let subst = SubstFont {
420            family: "Chrome Sans".to_owned(),
421            is_builtin_generic: true,
422            ..SubstFont::default()
423        };
424        assert!(!applies_glyph_spacing(&passing_gate(&subst)));
425    }
426
427    /// The sixth refusal, and the one the gate ends on: the face that was
428    /// loaded *is* the one the document named, so nothing needs correcting.
429    #[test]
430    fn loading_the_document_s_own_face_declines_the_correction() {
431        let subst = SubstFont {
432            family: "Verdana".to_owned(),
433            ..SubstFont::default()
434        };
435        assert!(!applies_glyph_spacing(&passing_gate(&subst)));
436        // And the prefix test is case-insensitive on the document's side,
437        // because the gate lowers the `/BaseFont` name before asking.
438        let bold = GlyphSpacingGate {
439            base_font_name: b"Verdana,Bold",
440            ..passing_gate(&subst)
441        };
442        assert!(!applies_glyph_spacing(&bold));
443    }
444
445    #[test]
446    fn a_font_that_was_never_substituted_declines_the_correction() {
447        let subst = nimbus();
448        let gate = GlyphSpacingGate {
449            subst: None,
450            ..passing_gate(&subst)
451        };
452        assert!(!applies_glyph_spacing(&gate));
453    }
454
455    /// `cfx_substfont_unittest.cpp`'s `EffectiveSkew`.
456    #[test]
457    fn effective_skew_matches_the_oracle() {
458        let mut s = SubstFont {
459            italic_angle: -12,
460            ..SubstFont::default()
461        };
462        assert_eq!(s.effective_skew(false), -21);
463        s.subst_cjk = true;
464        s.italic_cjk = true;
465        assert_eq!(s.effective_skew(true), -27);
466        // Not a CID font, so the CJK arm does not apply.
467        assert_eq!(s.effective_skew(false), -21);
468    }
469
470    #[test]
471    fn the_skew_table_saturates_outside_its_range() {
472        assert_eq!(skew_from_angle(0), 0);
473        assert_eq!(skew_from_angle(-1), -2);
474        assert_eq!(skew_from_angle(-29), -55);
475        // Past the table's 30 entries.
476        assert_eq!(skew_from_angle(-30), -58);
477        assert_eq!(skew_from_angle(-1000), -58);
478        // Any positive angle.
479        assert_eq!(skew_from_angle(1), -58);
480        assert_eq!(skew_from_angle(i32::MAX), -58);
481        // And the value whose negation overflows.
482        assert_eq!(skew_from_angle(i32::MIN), -58);
483    }
484
485    #[test]
486    fn the_cjk_skew_is_a_fixed_fifteen_degrees_or_none() {
487        let mut s = SubstFont::default();
488        assert_eq!(s.skew_cjk(), 0);
489        s.italic_cjk = true;
490        assert_eq!(s.skew_cjk(), -27);
491    }
492
493    /// `cfx_substfont_unittest.cpp`'s `EffectiveWeight`.
494    #[test]
495    fn effective_weight_switches_only_for_a_cid_font() {
496        let s = SubstFont {
497            weight: Some(700),
498            weight_cjk: Some(400),
499            subst_cjk: true,
500            ..SubstFont::default()
501        };
502        assert_eq!(s.effective_weight(true), 400);
503        assert_eq!(s.effective_weight(false), 700);
504        // Without a CJK substitution the CJK weight is never consulted.
505        let s = SubstFont {
506            weight: Some(700),
507            weight_cjk: Some(400),
508            ..SubstFont::default()
509        };
510        assert_eq!(s.effective_weight(true), 700);
511    }
512
513    /// `cfx_substfont_unittest.cpp`'s `EmboldenLevels`, all five assertions.
514    #[test]
515    fn embolden_levels_match_the_oracle() {
516        let mut s = SubstFont {
517            weight: Some(700),
518            ..SubstFont::default()
519        };
520        // Weight 700 is index 30, whose render value is 39.
521        assert_eq!(s.embolden_level_for_render(false, 1024, 0), Some(1));
522        // And whose load value is 70, from the *other* table.
523        assert_eq!(s.embolden_level_for_load(), 70);
524        // The i64 intermediate: 39 * 60_000_000 overflows an i32 before the
525        // division, so a 32-bit intermediate would give the wrong answer.
526        assert_eq!(
527            s.embolden_level_for_render(false, 30_000_000, 30_000_000),
528            Some(63838)
529        );
530        // A built-in generic zeroes both, because its design space already
531        // carries the weight.
532        s.is_builtin_generic = true;
533        assert_eq!(s.embolden_level_for_render(false, 1024, 0), Some(0));
534        assert_eq!(s.embolden_level_for_load(), 0);
535    }
536
537    #[test]
538    fn a_weight_at_or_below_four_hundred_needs_no_emboldening() {
539        for w in [0, 100, 400] {
540            let s = SubstFont {
541                weight: Some(w),
542                ..SubstFont::default()
543            };
544            assert_eq!(s.embolden_level_for_render(false, 1024, 0), Some(0));
545            assert_eq!(s.embolden_level_for_load(), 0);
546        }
547    }
548
549    #[test]
550    fn the_render_path_fails_past_the_table_while_the_load_path_clamps() {
551        // Index 100 is weight 1400.
552        let s = SubstFont {
553            weight: Some(1400),
554            ..SubstFont::default()
555        };
556        assert_eq!(
557            s.embolden_level_for_render(false, 1024, 0),
558            None,
559            "the render path abandons the glyph"
560        );
561        assert_eq!(
562            s.embolden_level_for_load(),
563            i32::from(WEIGHT_POW[99]),
564            "the load path clamps to the last entry"
565        );
566    }
567
568    /// `cfx_substfont_unittest.cpp`'s `EstimatedStemV`.
569    #[test]
570    fn the_stem_estimate_is_a_fifth_of_the_weight() {
571        let s = SubstFont {
572            weight: Some(700),
573            ..SubstFont::default()
574        };
575        assert_eq!(s.estimated_stem_v(), 140);
576    }
577
578    /// `cfx_substfont_unittest.cpp`'s `IsActualFontLoaded`.
579    #[test]
580    fn is_actual_font_loaded_is_a_loose_prefix_test() {
581        let s = SubstFont {
582            family: "Times New Roman".to_owned(),
583            ..SubstFont::default()
584        };
585        assert!(s.is_actual_font_loaded(b"timesnewroman,bold"));
586        assert!(s.is_actual_font_loaded(b"timesnewromanps-bold"));
587        assert!(!s.is_actual_font_loaded(b"arial,bold"));
588        // The looseness the C++ comment acknowledges.
589        let book = SubstFont {
590            family: "Book".to_owned(),
591            ..SubstFont::default()
592        };
593        assert!(book.is_actual_font_loaded(b"bookman"));
594        // An empty family matches nothing rather than everything.
595        assert!(!SubstFont::default().is_actual_font_loaded(b"anything"));
596    }
597
598    #[test]
599    fn the_weight_sentinel_survives_a_matching_face() {
600        let mut s = SubstFont::default();
601        // A 400-weight request against an upright face leaves the weight
602        // unset, which is what keeps the embolden level at 0.
603        s.configure_external(
604            "Arial".to_owned(),
605            Charset::Ansi,
606            400,
607            false,
608            0,
609            false,
610            false,
611        );
612        assert_eq!(s.weight, None);
613        assert_eq!(s.raw_weight(), 0);
614        assert_eq!(s.embolden_level_for_load(), 0);
615
616        // A 700-weight request against the same upright face does set it.
617        let mut s = SubstFont::default();
618        s.configure_external(
619            "Arial".to_owned(),
620            Charset::Ansi,
621            700,
622            false,
623            0,
624            false,
625            false,
626        );
627        assert_eq!(s.weight, Some(700));
628
629        // ...and a 700-weight request against a *bold* face does not.
630        let mut s = SubstFont::default();
631        s.configure_external(
632            "Arial".to_owned(),
633            Charset::Ansi,
634            700,
635            false,
636            0,
637            true,
638            false,
639        );
640        assert_eq!(s.weight, None);
641    }
642
643    #[test]
644    fn the_three_italic_angle_cases() {
645        // Zero against an upright face becomes -12.
646        let mut s = SubstFont::default();
647        s.configure_external("F".to_owned(), Charset::Ansi, 400, true, 0, false, false);
648        assert_eq!(s.italic_angle, -12);
649
650        // An angle within 5 degrees of upright is not worth synthesizing.
651        for angle in [-4, 4, 1] {
652            let mut s = SubstFont::default();
653            s.configure_external(
654                "F".to_owned(),
655                Charset::Ansi,
656                400,
657                true,
658                angle,
659                false,
660                false,
661            );
662            assert_eq!(s.italic_angle, 0, "angle {angle}");
663        }
664
665        // Anything larger is kept.
666        let mut s = SubstFont::default();
667        s.configure_external("F".to_owned(), Charset::Ansi, 400, true, -20, false, false);
668        assert_eq!(s.italic_angle, -20);
669
670        // And a face that is already italic needs no synthesis at all.
671        let mut s = SubstFont::default();
672        s.configure_external("F".to_owned(), Charset::Ansi, 400, true, 0, false, true);
673        assert_eq!(s.italic_angle, 0);
674    }
675
676    #[test]
677    fn chrome_serif_scales_the_weight_by_four_fifths() {
678        let mut s = SubstFont {
679            weight: Some(500),
680            ..SubstFont::default()
681        };
682        s.use_chrome_serif();
683        assert_eq!(s.family, "Chrome Serif");
684        assert_eq!(s.weight, Some(400));
685        // With no weight set, the sentinel survives.
686        let mut s = SubstFont::default();
687        s.use_chrome_serif();
688        assert_eq!(s.weight, None);
689    }
690
691    #[test]
692    fn the_weight_pow_11_table_has_three_non_monotonic_entries() {
693        // These look like transcription errors frozen into PDFium's output.
694        // They are part of the observable behavior and must not be "fixed".
695        assert_eq!(WEIGHT_POW_11[52], 43, "dips below the 47 before it");
696        assert_eq!(WEIGHT_POW_11[51], 46);
697        assert_eq!(WEIGHT_POW_11[59], 45, "dips below the 48 before it");
698        assert_eq!(WEIGHT_POW_11[58], 48);
699        assert_eq!(WEIGHT_POW_11[63], 46, "dips below the 50 before it");
700        assert_eq!(WEIGHT_POW_11[62], 50);
701
702        // ...and they are the *only* three descents in the whole ramp.
703        let descents = WEIGHT_POW_11.windows(2).filter(|w| w[1] < w[0]).count();
704        assert_eq!(descents, 3);
705    }
706
707    #[test]
708    fn the_other_two_weight_tables_are_monotonic() {
709        for (name, table) in [
710            ("kWeightPow", &WEIGHT_POW),
711            ("kWeightPowShiftJis", &WEIGHT_POW_SHIFT_JIS),
712        ] {
713            assert!(
714                table.windows(2).all(|w| w[1] >= w[0]),
715                "{name} should be non-decreasing"
716            );
717        }
718    }
719
720    #[test]
721    fn the_shift_jis_arm_reads_a_different_table() {
722        let s = SubstFont {
723            weight: Some(700),
724            charset: Charset::ShiftJis,
725            ..SubstFont::default()
726        };
727        // Index 30 in the Shift-JIS table is 96, not 39 or 70.
728        assert_eq!(s.embolden_level_for_render(false, 36655, 0), Some(96));
729        // And the load path rescales it.
730        assert_eq!(s.embolden_level_for_load(), 96 * 65536 / 36655);
731    }
732}