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}