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