1use super::charset::Charset;
12#[cfg(test)]
13use super::tables::{ANGLE_SKEW, WEIGHT_POW, WEIGHT_POW_11, WEIGHT_POW_SHIFT_JIS};
14
15#[derive(Debug, Clone, PartialEq, Eq, Default)]
23pub struct SubstFont {
24 pub(crate) family: String,
26 pub charset: Charset,
28 pub(crate) weight: Option<i32>,
30 pub(crate) weight_cjk: Option<i32>,
32 pub italic_angle: i32,
34 pub(crate) subst_cjk: bool,
37 pub(crate) italic_cjk: bool,
39 pub is_builtin_generic: bool,
43}
44
45impl SubstFont {
46 #[must_use]
49 pub fn raw_weight(&self) -> i32 {
50 self.weight.unwrap_or(0)
51 }
52
53 #[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 #[cfg(test)]
70 #[must_use]
71 pub(crate) fn skew(&self) -> i32 {
72 skew_from_angle(self.italic_angle)
73 }
74
75 #[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 #[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 #[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 #[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 #[cfg(test)]
150 #[must_use]
151 pub(crate) fn estimated_stem_v(&self) -> i32 {
152 self.raw_weight() / 5
153 }
154
155 #[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 #[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 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#[cfg(test)]
232#[must_use]
233pub fn skew_from_angle(angle: i32) -> i32 {
234 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#[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#[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#[derive(Debug, Clone, Copy)]
279pub struct GlyphSpacingGate<'a> {
280 pub vertical: bool,
282 pub embedded: bool,
284 pub declared_widths: bool,
286 pub base_font_name: &'a [u8],
288 pub subst: Option<&'a SubstFont>,
290}
291
292#[must_use]
320pub fn applies_glyph_spacing(gate: &GlyphSpacingGate<'_>) -> bool {
321 if gate.vertical || gate.embedded || !gate.declared_widths {
322 return false;
323 }
324 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 #![allow(clippy::indexing_slicing)]
340 use super::*;
341
342 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 #[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 #[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 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 #[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 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 assert_eq!(skew_from_angle(-30), -58);
477 assert_eq!(skew_from_angle(-1000), -58);
478 assert_eq!(skew_from_angle(1), -58);
480 assert_eq!(skew_from_angle(i32::MAX), -58);
481 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 #[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 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 #[test]
515 fn embolden_levels_match_the_oracle() {
516 let mut s = SubstFont {
517 weight: Some(700),
518 ..SubstFont::default()
519 };
520 assert_eq!(s.embolden_level_for_render(false, 1024, 0), Some(1));
522 assert_eq!(s.embolden_level_for_load(), 70);
524 assert_eq!(
527 s.embolden_level_for_render(false, 30_000_000, 30_000_000),
528 Some(63838)
529 );
530 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 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 #[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 #[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 let book = SubstFont {
590 family: "Book".to_owned(),
591 ..SubstFont::default()
592 };
593 assert!(book.is_actual_font_loaded(b"bookman"));
594 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(s.embolden_level_for_render(false, 36655, 0), Some(96));
729 assert_eq!(s.embolden_level_for_load(), 96 * 65536 / 36655);
731 }
732}