//! The one chain PdfCraft follows when a document does not embed a font program, and a record of //! what it chose and why. //! //! Substitution used to be decided per caller and never written down: `edit` picked a craft face //! by the script of the text it was drawing, `Std14::from_base_font` matched Latin names, and the //! renderer did its own thing. Nothing told the *user* that the glyphs they are looking at are not //! the ones the document asked for, which is the difference between "PdfCraft shows nothing for my //! Chinese PDF" being a mystery and being a sentence. //! //! This module answers the *dictionary* question — "the document does not carry this program; what //! will stand in for it, and why?" — which is what inspection and reporting need. It deliberately //! does not answer the *text* question ("which face has a glyph for こ?"), because that depends on //! the string being drawn and is already handled where the drawing happens. It delegates to the //! existing per-script selectors rather than restating their order, so there is one source of //! truth for each. //! //! The chain is tried in order, and the first step that yields a face wins: //! //! 1. The document embeds the program. Nothing is substituted. //! 2. `/BaseFont` names one of the standard 14 (or a well-known alias such as Arial). Exact //! published metrics apply, so this is as close to right as a substitution gets. //! 3. craft-fonts holds that exact family. Never another family — see [`crate::embedding`]. //! 4. The descendant font's `/CIDSystemInfo /Ordering` names a script, and craft-fonts has a face //! for it. Today that means Adobe-Japan1. //! 5. The ordering names a script we have **no** licensed face for (GB1, CNS1, Korea1). There is //! no honest substitute, so the chain reports none: the caller draws nothing and can say why. //! 6. Otherwise, the nearest of the standard 14 by `/FontDescriptor /Flags` — serif, fixed pitch, //! bold and italic. This step always succeeds, so a simple font is never left without a face. use crate::{CraftFont, Std14}; /// What a font dictionary asks for, as far as the dictionary alone can say. #[derive(Clone, Copy, Debug, Default)] pub struct Request<'a> { /// `/BaseFont` with any `ABCDEF+` subset tag already stripped. pub base_font: &'a str, /// Whether the descriptor carries `/FontFile`, `/FontFile2` or `/FontFile3` (or it is Type 3). pub embedded: bool, /// `/FontDescriptor /Flags`, or 0 when there is no descriptor. pub flags: u32, /// The descendant font's `/CIDSystemInfo /Ordering`, for a composite font. pub ordering: Option<&'a str>, } /// The face the chain picked. /// /// `Debug`, `PartialEq` and `Eq` are written by hand rather than derived, because [`CraftFont`] /// carries a whole font program: deriving would print megabytes and compare font files. #[derive(Clone, Copy)] pub enum Face { /// The document's own program; nothing was substituted. Embedded, /// One of the standard 14, whose metrics are exact. Standard14(Std14), /// A face from the optional craft-fonts build input. Craft(&'static CraftFont), /// No face at all. The caller must draw nothing rather than draw the wrong glyphs. None, } impl PartialEq for Face { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Embedded, Self::Embedded) | (Self::None, Self::None) => true, (Self::Standard14(a), Self::Standard14(b)) => a == b, // craft-fonts entries are unique statics, so identity is the comparison that means // what we want: two styles of one family are different faces and must not be equal. (Self::Craft(a), Self::Craft(b)) => std::ptr::eq(*a, *b), _ => false, } } } impl Eq for Face {} impl std::fmt::Debug for Face { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Embedded => f.write_str("Embedded"), Self::Standard14(s) => write!(f, "Standard14({})", s.base_font()), // The name, never the bytes. Self::Craft(c) => write!(f, "Craft({})", c.name()), Self::None => f.write_str("None"), } } } /// Why the chain picked that face. The variants are in the order the chain tries them. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Why { /// The document carries the font program. Embedded, /// `/BaseFont` names a standard-14 face or a well-known alias of one. NamedStandard14, /// `/BaseFont` is otherwise unknown, but its name hints at a family ("Serif", "Roman", /// "Mono"). A guess from the name, and reported as one. NameHintedStandard14, /// craft-fonts holds this exact family. ExactCraftFamily, /// A craft-fonts face for the script named by `/CIDSystemInfo /Ordering`. ScriptFace, /// The ordering names a script with no licensed face available. NoFaceForScript, /// The nearest standard-14 face by descriptor flags. StyleMatched, } impl Why { /// A stable sentence for a report, written for whoever has to act on it. pub fn note(self) -> &'static str { match self { Self::Embedded => "embedded in the document; not substituted", Self::NamedStandard14 => "names one of the standard 14 fonts, or a well-known alias of one, whose metrics are exact", Self::NameHintedStandard14 => { "not embedded and not a known family, but its name hints at one (Serif, Roman or Mono), so the nearest standard-14 face was chosen from that hint" } Self::ExactCraftFamily => "this exact family is available, so no other family was chosen", Self::ScriptFace => "substituted by a face for the script the font declares", Self::NoFaceForScript => { "no licensed face is available for the script this font declares, so nothing is drawn for it \ rather than the wrong glyphs" } Self::StyleMatched => "not embedded and not a known family, so the nearest standard-14 face was chosen by its descriptor flags", } } /// Whether the document's own glyphs are being shown. Only [`Why::Embedded`] is faithful. pub fn is_faithful(self) -> bool { matches!(self, Self::Embedded) } } /// What the chain chose, and why. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Substitution { pub face: Face, pub why: Why, } impl Substitution { /// The chosen face's name, for a report. `None` when nothing was chosen. pub fn face_name(&self) -> Option { match self.face { Face::Embedded => None, Face::Standard14(f) => Some(f.base_font().to_owned()), Face::Craft(f) => Some(f.name()), Face::None => None, } } } /// CID orderings we recognise, and whether a craft-fonts face may exist for them. /// /// `Identity` is not a script: it says the codes are glyph ids, so it tells us nothing about /// which face would suit and the chain falls through to the flags. fn script_for(ordering: &str) -> Option<&'static str> { match ordering { "Japan1" => Some("Jpan"), // Registered orderings the chain maps to no craft-fonts face. "GB1" | "CNS1" | "Korea1" | "KR" => Some(""), _ => None, } } /// Apply the chain to `req`. Total: every request gets a [`Substitution`]. /// /// See the module documentation for the order and the reasoning behind each step. pub fn substitute(req: &Request<'_>) -> Substitution { let at = |face, why| Substitution { face, why }; if req.embedded { return at(Face::Embedded, Why::Embedded); } if let Some((f, how)) = Std14::recognise(req.base_font) { return match how { crate::std14::Match::Alias => at(Face::Standard14(f), Why::NamedStandard14), crate::std14::Match::FamilyHint => at(Face::Standard14(f), Why::NameHintedStandard14), }; } if let Some(f) = crate::embedding::matching_faces(req.base_font).first() { return at(Face::Craft(f), Why::ExactCraftFamily); } if let Some(script) = req.ordering.and_then(script_for) { // A script we hold a face for; otherwise there is no honest substitute to offer. return match crate::CRAFT_FONTS.iter().find(|f| f.covers(script)) { Some(f) => at(Face::Craft(f), Why::ScriptFace), None => at(Face::None, Why::NoFaceForScript), }; } at(Face::Standard14(style_matched(req.flags)), Why::StyleMatched) } /// The nearest standard-14 face for `/FontDescriptor /Flags` (ISO 32000-2 table 121): fixed pitch /// is bit 1, serif bit 2, italic bit 7 and ForceBold bit 19. fn style_matched(flags: u32) -> Std14 { let (bold, italic) = (flags & 0x4_0000 != 0, flags & 0x40 != 0); match (flags & 1 != 0, flags & 2 != 0, bold, italic) { (true, _, false, false) => Std14::Courier, (true, _, true, false) => Std14::CourierBold, (true, _, false, true) => Std14::CourierOblique, (true, _, true, true) => Std14::CourierBoldOblique, (false, true, false, false) => Std14::TimesRoman, (false, true, true, false) => Std14::TimesBold, (false, true, false, true) => Std14::TimesItalic, (false, true, true, true) => Std14::TimesBoldItalic, (false, false, false, false) => Std14::Helvetica, (false, false, true, false) => Std14::HelveticaBold, (false, false, false, true) => Std14::HelveticaOblique, (false, false, true, true) => Std14::HelveticaBoldOblique, } } #[cfg(test)] mod tests { use super::*; fn req<'a>(base: &'a str, embedded: bool, flags: u32, ordering: Option<&'a str>) -> Request<'a> { Request { base_font: base, embedded, flags, ordering } } #[test] fn an_embedded_font_is_never_substituted() { // True even when the name and flags would otherwise pick something: embedding wins first. for (base, ordering) in [("Helvetica", None), ("Whatever", Some("GB1")), ("", None)] { let s = substitute(&req(base, true, 0, ordering)); assert_eq!(s, Substitution { face: Face::Embedded, why: Why::Embedded }); assert!(s.why.is_faithful()); assert_eq!(s.face_name(), None, "nothing stands in, so there is no substitute to name"); } } #[test] fn a_standard_14_name_wins_over_the_flags() { // Flags claiming a fixed-pitch serif must not override a name we have exact metrics for. let s = substitute(&req("Times-BoldItalic", false, 0b11, None)); assert_eq!(s.face, Face::Standard14(Std14::TimesBoldItalic)); assert_eq!(s.why, Why::NamedStandard14); assert_eq!(s.face_name().as_deref(), Some("Times-BoldItalic")); // Aliases and subset tags resolve the same way. for name in ["Arial", "ArialMT", "Arial-Bold", "Symbol", "ZapfDingbats", "CourierNew"] { assert_eq!(substitute(&req(name, false, 0, None)).why, Why::NamedStandard14, "{name}"); } } #[test] fn a_name_hint_is_reported_as_a_guess_not_as_a_standard_14_name() { // A house font whose name merely contains "Serif" gets Times, which is a sensible guess -- // but calling that "names one of the standard 14" would tell the reader something untrue. let hinted = substitute(&req("SomeHouseSerif", false, 0, None)); assert_eq!(hinted.face, Face::Standard14(Std14::TimesRoman)); assert_eq!(hinted.why, Why::NameHintedStandard14); assert!(hinted.why.note().contains("hints at one"), "{}", hinted.why.note()); // Style words still apply to the guess. assert_eq!(substitute(&req("SomeHouseSerif-BoldItalic", false, 0, None)).face, Face::Standard14(Std14::TimesBoldItalic)); // "Mono" hints at Courier the same way. assert_eq!(substitute(&req("HouseMono", false, 0, None)).why, Why::NameHintedStandard14); // A real alias is still reported as the request it is. for alias in ["Arial", "CourierNew", "Times-Roman", "Symbol"] { assert_eq!(substitute(&req(alias, false, 0, None)).why, Why::NamedStandard14, "{alias}"); } // A name with no hint at all falls through to the flags instead. assert_eq!(substitute(&req("HouseGrotesk", false, 0b10, None)).why, Why::StyleMatched); } #[test] fn flags_pick_the_nearest_of_the_fourteen() { // Bit 1 fixed pitch, bit 2 serif, bit 7 italic, bit 19 ForceBold. let cases = [ (0, Std14::Helvetica), (0b10, Std14::TimesRoman), (0b1, Std14::Courier), (0x40, Std14::HelveticaOblique), (0x4_0000, Std14::HelveticaBold), (0x4_0040, Std14::HelveticaBoldOblique), (0b10 | 0x40, Std14::TimesItalic), (0b10 | 0x4_0000, Std14::TimesBold), (0b10 | 0x4_0040, Std14::TimesBoldItalic), (0b1 | 0x40, Std14::CourierOblique), (0b1 | 0x4_0000, Std14::CourierBold), (0b1 | 0x4_0040, Std14::CourierBoldOblique), // Fixed pitch and serif together: monospace is the stronger signal for layout. (0b11, Std14::Courier), ]; for (flags, want) in cases { let s = substitute(&req("SomethingUnknown", false, flags, None)); assert_eq!(s.face, Face::Standard14(want), "flags {flags:#x}"); assert_eq!(s.why, Why::StyleMatched); } } #[test] fn a_script_with_no_licensed_face_reports_none_rather_than_latin() { // The user's Chinese PDF draws nothing. The point of this is that we can now say so, // instead of quietly standing Helvetica in and showing the wrong glyphs. for ordering in ["GB1", "CNS1", "Korea1", "KR"] { let s = substitute(&req("SomeCJKFont", false, 0, Some(ordering))); assert_eq!(s.face, Face::None, "{ordering}"); assert_eq!(s.why, Why::NoFaceForScript, "{ordering}"); assert_eq!(s.face_name(), None); assert!(!s.why.is_faithful()); assert!(s.why.note().contains("no licensed face"), "{ordering}"); } } #[test] fn identity_ordering_is_not_a_script_and_falls_through_to_the_flags() { // /Ordering (Identity) says the codes are glyph ids. It implies nothing about the face, // so treating it as a script would invent a substitution the dictionary never asked for. let s = substitute(&req("SomeSubsetFont", false, 0b10, Some("Identity"))); assert_eq!(s.face, Face::Standard14(Std14::TimesRoman)); assert_eq!(s.why, Why::StyleMatched); // An unregistered ordering is treated the same way, not guessed at. assert_eq!(substitute(&req("X", false, 0, Some("MadeUp1"))).why, Why::StyleMatched); } #[test] fn japanese_depends_on_craft_fonts_and_says_which_it_got() { // craft-fonts is an optional build input, so both outcomes are correct -- but they must be // distinguishable, and neither may be a silent Latin stand-in. let s = substitute(&req("SomeJapaneseFont", false, 0, Some("Japan1"))); if crate::CRAFT_FONTS.iter().any(|f| f.covers("Jpan")) { assert_eq!(s.why, Why::ScriptFace); assert!(matches!(s.face, Face::Craft(_))); assert!(s.face_name().is_some_and(|n| !n.is_empty())); } else { assert_eq!(s.why, Why::NoFaceForScript); assert_eq!(s.face, Face::None); } assert!(!s.why.is_faithful(), "a substitution is never faithful"); } #[test] fn every_why_has_a_distinct_usable_note() { let all = [ Why::Embedded, Why::NamedStandard14, Why::NameHintedStandard14, Why::ExactCraftFamily, Why::ScriptFace, Why::NoFaceForScript, Why::StyleMatched, ]; let mut notes: Vec<&str> = all.iter().map(|w| w.note()).collect(); notes.sort_unstable(); let before = notes.len(); notes.dedup(); assert_eq!(notes.len(), before, "each reason reads differently"); assert!(all.iter().all(|w| !w.note().is_empty() && w.note().len() > 20), "notes are sentences, not labels"); assert_eq!(all.iter().filter(|w| w.is_faithful()).count(), 1, "only an embedded program is faithful"); } #[test] fn hostile_input_is_total_and_never_panics() { // Every field is attacker-controlled: a name is any bytes the document chose. let long = "a".repeat(10_000); let names = ["", " ", "+", "ABCDEF+", "\u{0}\u{1}", long.as_str(), "../../etc/passwd", "Ĥéļvéťïçà"]; for name in names { for flags in [0, u32::MAX, 0x4_0040, 1] { for ordering in [None, Some(""), Some("Japan1"), Some("\u{0}")] { let s = substitute(&req(name, false, flags, ordering)); // Always a decision, and the reason always reads. assert!(!s.why.note().is_empty()); assert_eq!(s.face == Face::None, s.why == Why::NoFaceForScript, "only one step declines: {name:?}"); } } } } }