F3: one documented substitution chain, and report what it chose and why

A font the document does not embed was replaced by whatever the caller
happened to pick, and nothing told the user. `doc_info` said only
`embedded: false`, which is why "my Chinese PDF shows nothing" has been
a mystery rather than a sentence.

`crates/fonts/src/fallback.rs` applies one chain, in a documented order,
and returns what it chose with a reason:

1. embedded -> nothing substituted;
2. /BaseFont names a standard-14 face or a known alias (exact metrics);
3. craft-fonts holds that exact family;
4. /CIDSystemInfo /Ordering names a script we have a face for;
5. the ordering names a script we have no licensed face for -- report
   none, so nothing is drawn rather than the wrong glyphs;
6. otherwise the nearest of the fourteen by descriptor flags.

It delegates to the existing per-script selectors instead of restating
their order, so each keeps one source of truth. It answers the
dictionary question only; "which face has a glyph for this character?"
depends on the string being drawn and stays where the drawing happens.

`doc_info` gains `substitute` and `substitution` per font. Both null
means the document's own glyphs are shown; a null `substitute` with a
reason means no face was available at all. That is Acrobat's Actual
Font, minus the interface, which `parity/` now records as still missing.

Two findings on the way:

`Std14::from_base_font` matches loosely, so "SomeHouseSerif" resolved to
Times by its name containing "serif". The face is a fair guess but
reporting it as "names one of the standard 14" would have been untrue of
a font nobody has heard of. `Std14::recognise` now says whether a name
matched an alias or only a family hint, and the two report differently.
`from_base_font` is unchanged.

The generated table moved to `crates/fonts/src/std14_metrics.rs`, data
alone, and the ATTRIBUTION entry and AGENTS.md 1.1 line follow it. F1
put the hash on a file holding both the table and the code reading it,
so this commit's ordinary edits tripped the gate -- and a hash that
churns on unrelated changes gets re-attested reflexively, which is the
protection gone. Verified both directions after the split: editing
std14.rs passes the gate, changing one width in std14_metrics.rs fails
it.

Every new test was watched failing first: removing the script step
breaks the two unit tests and the doc_info integration test.

Co-Authored-By: Claude <[email protected]>
This commit is contained in:
Tanish MankuandClaude committed 2026-10-13 16:42:10 -07:00
1 parent 3f044b6263
commit d9607ec3be
12 files changed
+877 -333

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ An **asset** is any non-code material that the project includes, ships, embeds,
- Screenshots of Adobe software exist only as private, local observation notes under `plan/acrobat/`, which is gitignored. They must **never** be committed, published, linked, shown in docs, used as a design source to trace, or placed anywhere else in the repository or its outputs.
- The only Adobe-authored material allowed is **non-visual technical data** that is openly licensed and required for PDF compatibility. Each item needs its own entry in `ATTRIBUTION.toml` with `adobe_data = true`, and the list below is closed:
- The CMap resources (BSD-3-Clause), bundled inside the `hayro-cmap` dependency.
- The standard-14 font metrics and encoding tables in the PDF specification. They are data, not typefaces. They are embedded as code in hayro, and as our own generated table in `crates/fonts/src/std14.rs` (approved by the owner, 2026-10-08), which is what generated appearances measure with and what survives hayro being retired.
- The standard-14 font metrics and encoding tables in the PDF specification. They are data, not typefaces. They are embedded as code in hayro, and as our own generated table in `crates/fonts/src/std14_metrics.rs` (approved by the owner, 2026-10-08; that file holds the data alone, so the code reading it can change without re-attesting the table), which is what generated appearances measure with and what survives hayro being retired.
Adding anything to this list needs the user's explicit approval.
+1 -1
View File
@@ -222,7 +222,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
| `vendor/hayro-interpret/assets/FoxitSerifItalic.pfb` | FoxitSerifItalic (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) |
| `vendor/hayro-interpret/assets/FoxitSymbol.pfb` | FoxitSymbol (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) |
| `vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc` | CGATS001Compat v2 micro ICC profile | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | CC0-1.0 | https://github.com/saucecontrol/Compact-ICC-Profiles | Default CMYK colour conversion (vendored hayro-interpret) |
| `crates/fonts/src/std14.rs` | Standard-14 font advance widths and encodings (as Rust tables) | Adobe (Core14 AFM metrics, PDF specification encodings); tables generated by the PdfCraft contributors | MIT OR Apache-2.0 | ISO 32000-2 Annex D and the Adobe Core14 AFM metrics. Generated by joining the published WinAnsi, Symbol and ZapfDingbats encodings (code to glyph name) with the per-glyph advance widths, then spot-checked against the specification by hand in the tests of crates/fonts/src/std14.rs. | Advance widths for the fourteen standard fonts, which a PDF may name without embedding. Non-visual data, not a typeface: the item AGENTS.md 1.1 permits, held first-party so generated appearance streams and the renderer agree. |
| `crates/fonts/src/std14_metrics.rs` | Standard-14 font advance widths and encodings (as Rust tables) | Adobe (Core14 AFM metrics, PDF specification encodings); tables generated by the PdfCraft contributors | MIT OR Apache-2.0 | ISO 32000-2 Annex D and the Adobe Core14 AFM metrics. Generated by joining the published WinAnsi, Symbol and ZapfDingbats encodings (code to glyph name) with the per-glyph advance widths, then spot-checked against the specification by hand in the tests of crates/fonts/src/std14.rs. Data only: the code that reads these tables lives in std14.rs, so that ordinary edits do not churn this file's hash. | Advance widths for the fourteen standard fonts, which a PDF may name without embedding. Non-visual data, not a typeface: the item AGENTS.md 1.1 permits, held first-party so generated appearance streams and the renderer agree. |
| `docs/brand/artcraft-logo-white.png` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. |
| `docs/brand/artcraft-logo-white.svg` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. |
| `docs/brand/artcraft-logo.png` | ArtCraft brand: artcraft-logo | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. |
+3 -3
View File
@@ -2365,16 +2365,16 @@ usage = "Default CMYK colour conversion (vendored hayro-interpret)"
sha256 = "73e1ba37d2bad5bab2a964f40a9eed96209666efc067c3322626214bbef234a0"
[[asset]]
path = "crates/fonts/src/std14.rs"
path = "crates/fonts/src/std14_metrics.rs"
title = "Standard-14 font advance widths and encodings (as Rust tables)"
author = "Adobe (Core14 AFM metrics, PDF specification encodings); tables generated by the PdfCraft contributors"
source = "ISO 32000-2 Annex D and the Adobe Core14 AFM metrics. Generated by joining the published WinAnsi, Symbol and ZapfDingbats encodings (code to glyph name) with the per-glyph advance widths, then spot-checked against the specification by hand in the tests of crates/fonts/src/std14.rs."
source = "ISO 32000-2 Annex D and the Adobe Core14 AFM metrics. Generated by joining the published WinAnsi, Symbol and ZapfDingbats encodings (code to glyph name) with the per-glyph advance widths, then spot-checked against the specification by hand in the tests of crates/fonts/src/std14.rs. Data only: the code that reads these tables lives in std14.rs, so that ordinary edits do not churn this file's hash."
licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "data"
adobe_data = true
usage = "Advance widths for the fourteen standard fonts, which a PDF may name without embedding. Non-visual data, not a typeface: the item AGENTS.md 1.1 permits, held first-party so generated appearance streams and the renderer agree."
sha256 = "4ce47db7e65467d4ea1d1642ff76dfcaeb9669c1811a93819d7e00e63fa9fcf8"
sha256 = "f225e0b044875b8fa6375b6b5ad935f0d99fcc1ad03ecf9f0baae2cedbfca4d2"
[[bundled]]
crate = "epaint_default_fonts"
+5
View File
@@ -2205,8 +2205,13 @@ fn info(d: &Document) -> Value {
})).collect::<Vec<_>>(),
"layers": i.layers.iter().map(|l| json!({ "name": l.name, "visible": l.visible })).collect::<Vec<_>>(),
"attachments": i.attachments.iter().map(|a| json!({ "name": a.name, "description": a.description, "size": a.size })).collect::<Vec<_>>(),
// `substitute` names the face standing in for a program the document does not embed, and
// `substitution` says why. Both are null for an embedded font: nothing stands in. A null
// `substitute` with a non-null `substitution` means no face was available at all, so
// nothing is drawn for that font -- which is the answer to "why is my text missing?".
"fonts": i.fonts.iter().map(|f| json!({
"name": f.name, "kind": f.kind, "embedded": f.embedded, "subset": f.subset, "encoding": f.encoding,
"substitute": f.substitute, "substitution": f.substitution,
})).collect::<Vec<_>>(),
"warnings": i.warnings,
"repairs": d.repair_log(),
+7
View File
@@ -409,6 +409,13 @@ fn open_inspect_render_and_find() {
assert_eq!(info["pages"][0]["width"], 200.0);
assert_eq!(info["pages"][1]["label"], "2");
// The fixture names /Helvetica without embedding it, so an agent can see both that a face is
// standing in and why that face. `substitution` is null only when nothing stands in.
let font = &info["fonts"][0];
assert_eq!((&font["name"], &font["embedded"]), (&json!("Helvetica"), &json!(false)));
assert_eq!(font["substitute"], json!("Helvetica"));
assert!(font["substitution"].as_str().is_some_and(|w| w.contains("standard 14")), "{font}");
assert_eq!(page_text(&mut a, doc), ["Page 1", "Page 2", "Page 3"]);
let found = ok(&mut a, "text_find", json!({ "doc": doc, "query": "page 2" }));
+370
View File
@@ -0,0 +1,370 @@
//! 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<String> {
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 we have no openly licensed face for. AGENTS.md 1.1 rules out
// Source Han and Noto CJK (Source Han rebranded), which is why these have none.
"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:?}");
}
}
}
}
}
+2
View File
@@ -10,11 +10,13 @@
mod craft;
pub mod embedding;
mod encodings;
pub mod fallback;
pub mod pdf;
mod script;
mod shaping;
pub mod source_shaping;
mod std14;
mod std14_metrics;
pub use craft::{
CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_arabic_font, document_hebrew_font, document_japanese_font, document_japanese_font_for_style,
document_japanese_fonts_for_style, document_thai_font, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_hebrew_fonts, ui_japanese_fonts,
+40 -312
View File
@@ -32,44 +32,60 @@ pub enum Std14 {
ZapfDingbats,
}
/// How [`Std14::recognise`] matched a `/BaseFont` name.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Match {
/// A standard-14 face or a well-known alias of one (Arial for Helvetica, CourierNew for
/// Courier). The document is asking for this face.
Alias,
/// Only a family hint matched -- "Serif", "Roman" or "Mono" in an otherwise unknown name.
/// The face is a guess from the name, not the one the document asked for.
FamilyHint,
}
impl Std14 {
/// The font a `/BaseFont` name selects, by the spec's names and the common aliases viewers
/// accept (`Arial` for `Helvetica`, `TimesNewRoman` for `Times`, `CourierNew` for `Courier`).
/// A subset prefix such as `ABCDEF+Helvetica` is ignored.
pub fn from_base_font(base: &str) -> Option<Self> {
Self::recognise(base).map(|(face, _)| face)
}
/// [`Std14::from_base_font`], also saying *how* the name matched.
///
/// Reporting needs the difference. "Arial" really is a request for Helvetica, but
/// "SomeHouseSerif" is a house font whose name merely contains "Serif"; choosing Times for it
/// is a reasonable guess from the name, not the face the document asked for, and a substitution
/// report that called it a standard-14 name would be telling the reader something untrue.
pub fn recognise(base: &str) -> Option<(Self, Match)> {
let base = base.rsplit('+').next().unwrap_or(base);
let flat: String = base.chars().filter(|c| c.is_ascii_alphanumeric()).map(|c| c.to_ascii_lowercase()).collect();
let has = |w: &str| flat.contains(w);
let (bold, italic) = (has("bold"), has("italic") || has("oblique"));
// Two bools index a four-element array, so the subscript is provably 0..=3.
let styled = |faces: [Self; 4]| faces[usize::from(bold) | usize::from(italic) << 1];
let courier = || styled([Self::Courier, Self::CourierBold, Self::CourierOblique, Self::CourierBoldOblique]);
let times = || styled([Self::TimesRoman, Self::TimesBold, Self::TimesItalic, Self::TimesBoldItalic]);
if has("symbol") {
return Some(Self::Symbol);
return Some((Self::Symbol, Match::Alias));
}
if has("zapf") || has("dingbat") {
return Some(Self::ZapfDingbats);
return Some((Self::ZapfDingbats, Match::Alias));
}
if has("courier") || has("mono") {
return Some(match (bold, italic) {
(false, false) => Self::Courier,
(true, false) => Self::CourierBold,
(false, true) => Self::CourierOblique,
(true, true) => Self::CourierBoldOblique,
});
if has("courier") {
return Some((courier(), Match::Alias));
}
if has("times") || has("serif") || has("roman") {
return Some(match (bold, italic) {
(false, false) => Self::TimesRoman,
(true, false) => Self::TimesBold,
(false, true) => Self::TimesItalic,
(true, true) => Self::TimesBoldItalic,
});
if has("mono") {
return Some((courier(), Match::FamilyHint));
}
if has("times") {
return Some((times(), Match::Alias));
}
if has("serif") || has("roman") {
return Some((times(), Match::FamilyHint));
}
if has("helvetica") || has("arial") {
return Some(match (bold, italic) {
(false, false) => Self::Helvetica,
(true, false) => Self::HelveticaBold,
(false, true) => Self::HelveticaOblique,
(true, true) => Self::HelveticaBoldOblique,
});
return Some((styled([Self::Helvetica, Self::HelveticaBold, Self::HelveticaOblique, Self::HelveticaBoldOblique]), Match::Alias));
}
None
}
@@ -77,7 +93,7 @@ impl Std14 {
/// The advance of one character `code`, in thousandths of an em. Zero where the font's
/// encoding leaves the code undefined.
pub fn width(self, code: u8) -> f64 {
f64::from(WIDTHS[self as usize][code as usize])
f64::from(crate::std14_metrics::WIDTHS[self as usize][code as usize])
}
/// The advance of `code` at `size` points.
@@ -101,298 +117,10 @@ impl Std14 {
/// The `/BaseFont` name for this font, as it is written into a PDF.
pub fn base_font(self) -> &'static str {
BASE_FONTS[self as usize]
crate::std14_metrics::BASE_FONTS[self as usize]
}
}
static BASE_FONTS: [&str; 14] = [
"Helvetica",
"Helvetica-Bold",
"Helvetica-Oblique",
"Helvetica-BoldOblique",
"Times-Roman",
"Times-Bold",
"Times-Italic",
"Times-BoldItalic",
"Courier",
"Courier-Bold",
"Courier-Oblique",
"Courier-BoldOblique",
"Symbol",
"ZapfDingbats",
];
/// Advance widths by code, one row per [`Std14`] variant in declaration order.
#[rustfmt::skip]
static WIDTHS: [[u16; 256]; 14] = [
// Helvetica
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
0, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
],
// HelveticaBold
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
0, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
],
// HelveticaOblique
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
0, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
],
// HelveticaBoldOblique
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
0, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
],
// TimesRoman
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 333, 408, 500, 500, 833, 778, 180, 333, 333, 500, 564, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444,
921, 722, 667, 667, 722, 611, 556, 722, 722, 333, 389, 722, 611, 889, 722, 722,
556, 722, 667, 556, 611, 722, 722, 944, 722, 722, 611, 333, 278, 333, 469, 500,
333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500, 278, 778, 500, 500,
500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541, 0,
500, 0, 333, 500, 444, 1000, 500, 500, 333, 1000, 556, 333, 889, 0, 611, 0,
0, 333, 333, 444, 444, 350, 500, 1000, 333, 980, 389, 333, 722, 0, 444, 722,
250, 333, 500, 500, 500, 500, 200, 500, 333, 760, 276, 500, 564, 333, 760, 333,
400, 564, 300, 300, 333, 500, 453, 250, 333, 300, 310, 500, 750, 750, 750, 444,
722, 722, 722, 722, 722, 722, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333,
722, 722, 722, 722, 722, 722, 722, 564, 722, 722, 722, 722, 722, 722, 556, 500,
444, 444, 444, 444, 444, 444, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
500, 500, 500, 500, 500, 500, 500, 564, 500, 500, 500, 500, 500, 500, 500, 500,
],
// TimesBold
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 333, 555, 500, 500, 1000, 833, 278, 333, 333, 500, 570, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500,
930, 722, 667, 722, 722, 667, 611, 778, 778, 389, 500, 778, 667, 944, 722, 778,
611, 778, 722, 556, 667, 722, 722, 1000, 722, 722, 667, 333, 278, 333, 581, 500,
333, 500, 556, 444, 556, 444, 333, 500, 556, 278, 333, 556, 278, 833, 556, 500,
556, 556, 444, 389, 333, 556, 500, 722, 500, 500, 444, 394, 220, 394, 520, 0,
500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 1000, 0, 667, 0,
0, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 444, 722,
250, 333, 500, 500, 500, 500, 220, 500, 333, 747, 300, 500, 570, 333, 747, 333,
400, 570, 300, 300, 333, 556, 540, 250, 333, 300, 330, 500, 750, 750, 750, 500,
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 389, 389, 389, 389,
722, 722, 778, 778, 778, 778, 778, 570, 778, 722, 722, 722, 722, 722, 611, 556,
500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278,
500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 500, 556, 500,
],
// TimesItalic
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 333, 420, 500, 500, 833, 778, 214, 333, 333, 500, 675, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500,
920, 611, 611, 667, 722, 611, 611, 722, 722, 333, 444, 667, 556, 833, 667, 722,
611, 722, 611, 500, 556, 722, 611, 833, 611, 556, 556, 389, 278, 389, 422, 500,
333, 500, 500, 444, 500, 444, 278, 500, 500, 278, 278, 444, 278, 722, 500, 500,
500, 500, 389, 389, 278, 500, 444, 667, 444, 444, 389, 400, 275, 400, 541, 0,
500, 0, 333, 500, 556, 889, 500, 500, 333, 1000, 500, 333, 944, 0, 556, 0,
0, 333, 333, 556, 556, 350, 500, 889, 333, 980, 389, 333, 667, 0, 389, 556,
250, 389, 500, 500, 500, 500, 275, 500, 333, 760, 276, 500, 675, 333, 760, 333,
400, 675, 300, 300, 333, 500, 523, 250, 333, 300, 310, 500, 750, 750, 750, 500,
611, 611, 611, 611, 611, 611, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333,
722, 667, 722, 722, 722, 722, 722, 675, 722, 722, 722, 722, 722, 556, 611, 500,
500, 500, 500, 500, 500, 500, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
500, 500, 500, 500, 500, 500, 500, 675, 500, 500, 500, 500, 500, 444, 500, 444,
],
// TimesBoldItalic
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 389, 555, 500, 500, 833, 778, 278, 333, 333, 500, 570, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500,
832, 667, 667, 667, 722, 667, 667, 722, 778, 389, 500, 667, 611, 889, 722, 722,
611, 722, 667, 556, 611, 722, 667, 889, 667, 611, 611, 333, 278, 333, 570, 500,
333, 500, 500, 444, 500, 444, 333, 500, 556, 278, 278, 500, 278, 778, 556, 500,
500, 500, 389, 389, 278, 556, 444, 667, 500, 444, 389, 348, 220, 348, 570, 0,
500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 944, 0, 611, 0,
0, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 389, 611,
250, 389, 500, 500, 500, 500, 220, 500, 333, 747, 266, 500, 606, 333, 747, 333,
400, 570, 300, 300, 333, 576, 500, 250, 333, 300, 300, 500, 750, 750, 750, 500,
667, 667, 667, 667, 667, 667, 944, 667, 667, 667, 667, 667, 389, 389, 389, 389,
722, 722, 722, 722, 722, 722, 722, 570, 722, 722, 722, 722, 722, 611, 611, 500,
500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278,
500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 444, 500, 444,
],
// Courier
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
],
// CourierBold
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
],
// CourierOblique
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
],
// CourierBoldOblique
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
],
// Symbol
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 333, 713, 500, 549, 833, 778, 439, 333, 333, 500, 549, 250, 549, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 549, 549, 549, 444,
549, 722, 667, 722, 612, 611, 763, 603, 722, 333, 631, 722, 686, 889, 722, 722,
768, 741, 556, 592, 611, 690, 439, 768, 645, 795, 611, 333, 863, 333, 658, 500,
500, 631, 549, 549, 494, 439, 521, 411, 603, 329, 603, 549, 549, 576, 521, 549,
549, 521, 549, 603, 439, 576, 713, 686, 493, 686, 494, 480, 200, 480, 549, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
750, 620, 247, 549, 167, 713, 500, 753, 753, 753, 753, 1042, 987, 603, 987, 603,
400, 549, 411, 549, 549, 713, 494, 460, 549, 549, 549, 549, 1000, 603, 1000, 658,
823, 686, 795, 987, 768, 768, 823, 768, 768, 713, 713, 713, 713, 713, 713, 713,
768, 713, 790, 790, 890, 823, 549, 250, 713, 603, 603, 1042, 987, 603, 987, 603,
494, 329, 790, 790, 786, 713, 384, 384, 384, 384, 384, 384, 494, 494, 494, 494,
0, 329, 274, 686, 686, 686, 384, 384, 384, 384, 384, 384, 494, 494, 494, 0,
],
// ZapfDingbats
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 974, 961, 974, 980, 719, 789, 790, 791, 690, 960, 939, 549, 855, 911, 933,
911, 945, 974, 755, 846, 762, 761, 571, 677, 763, 760, 759, 754, 494, 552, 537,
577, 692, 786, 788, 788, 790, 793, 794, 816, 823, 789, 841, 823, 833, 816, 831,
923, 744, 723, 749, 790, 792, 695, 776, 768, 792, 759, 707, 708, 682, 701, 826,
815, 789, 789, 707, 687, 696, 689, 786, 787, 713, 791, 785, 791, 873, 761, 762,
762, 759, 759, 892, 892, 788, 784, 438, 138, 277, 415, 392, 392, 668, 668, 0,
390, 390, 317, 317, 276, 276, 509, 509, 410, 410, 234, 234, 334, 334, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 732, 544, 544, 910, 667, 760, 760, 776, 595, 694, 626, 788, 788, 788, 788,
788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
788, 788, 788, 788, 894, 838, 1016, 458, 748, 924, 748, 918, 927, 928, 928, 834,
873, 828, 924, 924, 917, 930, 931, 463, 883, 836, 836, 867, 867, 696, 696, 874,
0, 874, 760, 946, 771, 865, 771, 888, 967, 888, 831, 873, 927, 970, 918, 0,
],
];
#[cfg(test)]
mod tests {
use super::*;
+300
View File
@@ -0,0 +1,300 @@
//! The generated standard-14 tables: the fourteen base font names and their per-code advance
//! widths. **Data only, no logic** -- see `std14.rs` for everything that interprets it.
//!
//! This file is the closed-list item in AGENTS.md 1.1 and carries a SHA-256 in `ATTRIBUTION.toml`,
//! so `cargo xtask assets` fails until a change to it is re-attested. It is kept separate from the
//! code that reads it for exactly that reason: if ordinary edits churned the hash, re-attesting
//! would become reflexive and the check would stop meaning anything.
//!
//! Widths are in thousandths of an em, indexed by character code in each face's own encoding --
//! WinAnsi for the twelve text faces, and their built-in encodings for Symbol and ZapfDingbats.
//! A zero means the encoding leaves that code undefined. Verified against the published
//! specification by the tests in `std14.rs`, which type the expected values in by hand.
pub(crate) static BASE_FONTS: [&str; 14] = [
"Helvetica",
"Helvetica-Bold",
"Helvetica-Oblique",
"Helvetica-BoldOblique",
"Times-Roman",
"Times-Bold",
"Times-Italic",
"Times-BoldItalic",
"Courier",
"Courier-Bold",
"Courier-Oblique",
"Courier-BoldOblique",
"Symbol",
"ZapfDingbats",
];
/// Advance widths by code, one row per [`Std14`] variant in declaration order.
#[rustfmt::skip]
pub(crate) static WIDTHS: [[u16; 256]; 14] = [
// Helvetica
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
0, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
],
// HelveticaBold
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
0, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
],
// HelveticaOblique
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
0, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
],
// HelveticaBoldOblique
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
0, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
],
// TimesRoman
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 333, 408, 500, 500, 833, 778, 180, 333, 333, 500, 564, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444,
921, 722, 667, 667, 722, 611, 556, 722, 722, 333, 389, 722, 611, 889, 722, 722,
556, 722, 667, 556, 611, 722, 722, 944, 722, 722, 611, 333, 278, 333, 469, 500,
333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500, 278, 778, 500, 500,
500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541, 0,
500, 0, 333, 500, 444, 1000, 500, 500, 333, 1000, 556, 333, 889, 0, 611, 0,
0, 333, 333, 444, 444, 350, 500, 1000, 333, 980, 389, 333, 722, 0, 444, 722,
250, 333, 500, 500, 500, 500, 200, 500, 333, 760, 276, 500, 564, 333, 760, 333,
400, 564, 300, 300, 333, 500, 453, 250, 333, 300, 310, 500, 750, 750, 750, 444,
722, 722, 722, 722, 722, 722, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333,
722, 722, 722, 722, 722, 722, 722, 564, 722, 722, 722, 722, 722, 722, 556, 500,
444, 444, 444, 444, 444, 444, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
500, 500, 500, 500, 500, 500, 500, 564, 500, 500, 500, 500, 500, 500, 500, 500,
],
// TimesBold
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 333, 555, 500, 500, 1000, 833, 278, 333, 333, 500, 570, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500,
930, 722, 667, 722, 722, 667, 611, 778, 778, 389, 500, 778, 667, 944, 722, 778,
611, 778, 722, 556, 667, 722, 722, 1000, 722, 722, 667, 333, 278, 333, 581, 500,
333, 500, 556, 444, 556, 444, 333, 500, 556, 278, 333, 556, 278, 833, 556, 500,
556, 556, 444, 389, 333, 556, 500, 722, 500, 500, 444, 394, 220, 394, 520, 0,
500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 1000, 0, 667, 0,
0, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 444, 722,
250, 333, 500, 500, 500, 500, 220, 500, 333, 747, 300, 500, 570, 333, 747, 333,
400, 570, 300, 300, 333, 556, 540, 250, 333, 300, 330, 500, 750, 750, 750, 500,
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 389, 389, 389, 389,
722, 722, 778, 778, 778, 778, 778, 570, 778, 722, 722, 722, 722, 722, 611, 556,
500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278,
500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 500, 556, 500,
],
// TimesItalic
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 333, 420, 500, 500, 833, 778, 214, 333, 333, 500, 675, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500,
920, 611, 611, 667, 722, 611, 611, 722, 722, 333, 444, 667, 556, 833, 667, 722,
611, 722, 611, 500, 556, 722, 611, 833, 611, 556, 556, 389, 278, 389, 422, 500,
333, 500, 500, 444, 500, 444, 278, 500, 500, 278, 278, 444, 278, 722, 500, 500,
500, 500, 389, 389, 278, 500, 444, 667, 444, 444, 389, 400, 275, 400, 541, 0,
500, 0, 333, 500, 556, 889, 500, 500, 333, 1000, 500, 333, 944, 0, 556, 0,
0, 333, 333, 556, 556, 350, 500, 889, 333, 980, 389, 333, 667, 0, 389, 556,
250, 389, 500, 500, 500, 500, 275, 500, 333, 760, 276, 500, 675, 333, 760, 333,
400, 675, 300, 300, 333, 500, 523, 250, 333, 300, 310, 500, 750, 750, 750, 500,
611, 611, 611, 611, 611, 611, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333,
722, 667, 722, 722, 722, 722, 722, 675, 722, 722, 722, 722, 722, 556, 611, 500,
500, 500, 500, 500, 500, 500, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
500, 500, 500, 500, 500, 500, 500, 675, 500, 500, 500, 500, 500, 444, 500, 444,
],
// TimesBoldItalic
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 389, 555, 500, 500, 833, 778, 278, 333, 333, 500, 570, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500,
832, 667, 667, 667, 722, 667, 667, 722, 778, 389, 500, 667, 611, 889, 722, 722,
611, 722, 667, 556, 611, 722, 667, 889, 667, 611, 611, 333, 278, 333, 570, 500,
333, 500, 500, 444, 500, 444, 333, 500, 556, 278, 278, 500, 278, 778, 556, 500,
500, 500, 389, 389, 278, 556, 444, 667, 500, 444, 389, 348, 220, 348, 570, 0,
500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 944, 0, 611, 0,
0, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 389, 611,
250, 389, 500, 500, 500, 500, 220, 500, 333, 747, 266, 500, 606, 333, 747, 333,
400, 570, 300, 300, 333, 576, 500, 250, 333, 300, 300, 500, 750, 750, 750, 500,
667, 667, 667, 667, 667, 667, 944, 667, 667, 667, 667, 667, 389, 389, 389, 389,
722, 722, 722, 722, 722, 722, 722, 570, 722, 722, 722, 722, 722, 611, 611, 500,
500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278,
500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 444, 500, 444,
],
// Courier
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
],
// CourierBold
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
],
// CourierOblique
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
],
// CourierBoldOblique
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
],
// Symbol
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 333, 713, 500, 549, 833, 778, 439, 333, 333, 500, 549, 250, 549, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 549, 549, 549, 444,
549, 722, 667, 722, 612, 611, 763, 603, 722, 333, 631, 722, 686, 889, 722, 722,
768, 741, 556, 592, 611, 690, 439, 768, 645, 795, 611, 333, 863, 333, 658, 500,
500, 631, 549, 549, 494, 439, 521, 411, 603, 329, 603, 549, 549, 576, 521, 549,
549, 521, 549, 603, 439, 576, 713, 686, 493, 686, 494, 480, 200, 480, 549, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
750, 620, 247, 549, 167, 713, 500, 753, 753, 753, 753, 1042, 987, 603, 987, 603,
400, 549, 411, 549, 549, 713, 494, 460, 549, 549, 549, 549, 1000, 603, 1000, 658,
823, 686, 795, 987, 768, 768, 823, 768, 768, 713, 713, 713, 713, 713, 713, 713,
768, 713, 790, 790, 890, 823, 549, 250, 713, 603, 603, 1042, 987, 603, 987, 603,
494, 329, 790, 790, 786, 713, 384, 384, 384, 384, 384, 384, 494, 494, 494, 494,
0, 329, 274, 686, 686, 686, 384, 384, 384, 384, 384, 384, 494, 494, 494, 0,
],
// ZapfDingbats
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 974, 961, 974, 980, 719, 789, 790, 791, 690, 960, 939, 549, 855, 911, 933,
911, 945, 974, 755, 846, 762, 761, 571, 677, 763, 760, 759, 754, 494, 552, 537,
577, 692, 786, 788, 788, 790, 793, 794, 816, 823, 789, 841, 823, 833, 816, 831,
923, 744, 723, 749, 790, 792, 695, 776, 768, 792, 759, 707, 708, 682, 701, 826,
815, 789, 789, 707, 687, 696, 689, 786, 787, 713, 791, 785, 791, 873, 761, 762,
762, 759, 759, 892, 892, 788, 784, 438, 138, 277, 415, 392, 392, 668, 668, 0,
390, 390, 317, 317, 276, 276, 509, 509, 410, 410, 234, 234, 334, 334, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 732, 544, 544, 910, 667, 760, 760, 776, 595, 694, 626, 788, 788, 788, 788,
788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
788, 788, 788, 788, 894, 838, 1016, 458, 748, 924, 748, 918, 927, 928, 928, 834,
873, 828, 924, 924, 917, 930, 931, 463, 883, 836, 836, 867, 867, 696, 696, 874,
0, 874, 760, 946, 771, 865, 771, 888, 967, 888, 831, 873, 927, 970, 918, 0,
],
];
+143 -11
View File
@@ -365,6 +365,13 @@ pub struct FontInfo {
pub embedded: bool,
pub subset: bool,
pub encoding: Option<String>,
/// The face standing in for a program the document does not embed. `None` when the font is
/// embedded (nothing stands in) or when no face was available at all -- `substitution` then
/// says which of the two it is.
pub substitute: Option<String>,
/// Why a face was substituted, as a sentence for whoever has to act on it. `None` only when
/// the document's own glyphs are being shown.
pub substitution: Option<String>,
}
/// Check that `bytes` still open as the same document: the parser reads them and the page
@@ -1290,20 +1297,38 @@ impl<'a> Inspector<'a> {
_ => None,
};
// Embedded: FontFile/2/3 in the descriptor (for Type0, in the descendant font).
let descriptor_of = |d: &Dictionary| d.get(b"FontDescriptor").ok().and_then(|o| self.dict(o)).cloned();
let desc = descriptor_of(f).or_else(|| {
f.get(b"DescendantFonts")
.ok()
.and_then(|o| self.resolve(o).as_array().ok())
.and_then(|a| a.first())
.and_then(|o| self.dict(o))
.and_then(descriptor_of)
});
let descriptor_of = |d: &'a Dictionary| d.get(b"FontDescriptor").ok().and_then(|o| self.dict(o));
let descendant =
f.get(b"DescendantFonts").ok().and_then(|o| self.resolve(o).as_array().ok()).and_then(|a| a.first()).and_then(|o| self.dict(o));
let desc = descriptor_of(f).or_else(|| descendant.and_then(descriptor_of));
let embedded =
kind == "Type3" || desc.is_some_and(|d| d.get(b"FontFile").is_ok() || d.get(b"FontFile2").is_ok() || d.get(b"FontFile3").is_ok());
// What the substitution chain needs beyond the name: the descriptor flags, and for
// a composite font the script its descendant declares (ISO 32000-2 9.7.3).
let flags = desc
.and_then(|d| d.get(b"Flags").ok())
.and_then(|o| self.resolve(o).as_i64().ok())
.and_then(|n| u32::try_from(n).ok())
.unwrap_or(0);
let ordering =
descendant.and_then(|d| d.get(b"CIDSystemInfo").ok()).and_then(|o| self.dict(o)).and_then(|c| self.text(c, b"Ordering"));
let subset = base.len() > 7 && base.as_bytes()[6] == b'+' && base[..6].bytes().all(|b| b.is_ascii_uppercase());
let name = if subset { base[7..].to_string() } else { base };
let info = FontInfo { name, kind, embedded, subset, encoding };
let chosen = pdfcraft_fonts::fallback::substitute(&pdfcraft_fonts::fallback::Request {
base_font: &name,
embedded,
flags,
ordering: ordering.as_deref(),
});
let info = FontInfo {
name,
kind,
embedded,
subset,
encoding,
substitute: chosen.face_name(),
substitution: (!chosen.why.is_faithful()).then(|| chosen.why.note().to_owned()),
};
if seen.insert((info.name.clone(), info.kind.clone(), info.embedded)) {
out.push(info);
}
@@ -1981,7 +2006,20 @@ trailer << /Root 1 0 R >>
let expected: &[u8] = if a.name == "notes.txt" { b"hello notes" } else { b"a,b,c" };
assert_eq!(data, expected, "{}", a.name);
}
assert_eq!(info.fonts, vec![FontInfo { name: "Helvetica".into(), kind: "Type1".into(), embedded: false, subset: true, encoding: None }]);
// A non-embedded /Helvetica is reported as standing in for itself: the standard-14
// metrics are exact, so the substitution is named rather than left implicit.
assert_eq!(
info.fonts,
vec![FontInfo {
name: "Helvetica".into(),
kind: "Type1".into(),
embedded: false,
subset: true,
encoding: None,
substitute: Some("Helvetica".into()),
substitution: Some(pdfcraft_fonts::fallback::Why::NamedStandard14.note().into()),
}]
);
}
const LAYERS: &[u8] = b"%PDF-1.7
@@ -2520,3 +2558,97 @@ mod version_tests {
assert_eq!(super::version_label("Unknown"), "Unknown");
}
}
#[cfg(test)]
mod substitution_tests {
use std::sync::Arc;
use super::{FontInfo, inspect};
/// One page naming four fonts the document does not embed: a Chinese CID font (Adobe-GB1), a
/// Japanese one (Adobe-Japan1), an unknown serif-flagged simple font, and an aliased
/// standard-14 name.
const NOT_EMBEDDED: &[u8] = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Font << /F1 4 0 R /F2 6 0 R /F3 8 0 R /F4 9 0 R /F5 11 0 R >> >> >> endobj
4 0 obj << /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /UniGB-UCS2-H /DescendantFonts [5 0 R] >> endobj
5 0 obj << /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 2 >> >> endobj
6 0 obj << /Type /Font /Subtype /Type0 /BaseFont /KozMinPro-Regular /Encoding /UniJIS-UCS2-H /DescendantFonts [7 0 R] >> endobj
7 0 obj << /Type /Font /Subtype /CIDFontType0 /BaseFont /KozMinPro-Regular /CIDSystemInfo << /Registry (Adobe) /Ordering (Japan1) /Supplement 4 >> >> endobj
8 0 obj << /Type /Font /Subtype /TrueType /BaseFont /SomeHouseSerif /FontDescriptor 10 0 R >> endobj
9 0 obj << /Type /Font /Subtype /TrueType /BaseFont /ArialMT >> endobj
10 0 obj << /Type /FontDescriptor /FontName /SomeHouseSerif /Flags 34 >> endobj
11 0 obj << /Type /Font /Subtype /TrueType /BaseFont /HouseGrotesk /FontDescriptor 12 0 R >> endobj
12 0 obj << /Type /FontDescriptor /FontName /HouseGrotesk /Flags 33 >> endobj
trailer << /Size 13 /Root 1 0 R >>
%%EOF";
fn font<'a>(fonts: &'a [FontInfo], name: &str) -> &'a FontInfo {
fonts.iter().find(|f| f.name == name).unwrap_or_else(|| panic!("{name} listed; got {:?}", fonts.iter().map(|f| &f.name).collect::<Vec<_>>()))
}
#[test]
fn doc_info_says_which_face_stands_in_and_why() {
let info = inspect(Arc::new(NOT_EMBEDDED.to_vec()), None).expect("opens");
// The one that matters most to a user: a Chinese PDF that draws nothing. Before this,
// doc_info said only `embedded: false` and left them to guess.
let cjk = font(&info.fonts, "STSong-Light");
assert!(!cjk.embedded);
assert_eq!(cjk.substitute, None, "no licensed Chinese face, so nothing stands in");
let why = cjk.substitution.as_deref().unwrap_or_default();
assert!(why.contains("no licensed face") && why.contains("rather than the wrong glyphs"), "{why}");
// An aliased standard-14 name resolves to the real face, whose metrics are exact.
let arial = font(&info.fonts, "ArialMT");
assert_eq!(arial.substitute.as_deref(), Some("Helvetica"));
assert!(arial.substitution.as_deref().unwrap_or_default().contains("standard 14"));
// A house font whose name merely contains "Serif" gets Times -- reported as the guess it
// is, not as a standard-14 name, which would be untrue of a font nobody has heard of.
let serif = font(&info.fonts, "SomeHouseSerif");
assert_eq!(serif.substitute.as_deref(), Some("Times-Roman"));
assert!(serif.substitution.as_deref().unwrap_or_default().contains("hints at one"), "{serif:?}");
// No hint in the name at all, so the descriptor flags decide: 33 = fixed pitch + nonsymbolic.
let grotesk = font(&info.fonts, "HouseGrotesk");
assert_eq!(grotesk.substitute.as_deref(), Some("Courier"));
assert!(grotesk.substitution.as_deref().unwrap_or_default().contains("descriptor flags"), "{grotesk:?}");
// Japanese depends on the optional craft-fonts build input; either way it is reported,
// and never as a Latin face.
let jpan = font(&info.fonts, "KozMinPro-Regular");
assert!(jpan.substitution.is_some(), "a substitution is always explained");
match &jpan.substitute {
Some(face) => {
assert!(!face.starts_with("Helvetica") && !face.starts_with("Times"), "a Latin face is never a Japanese substitute: {face}")
}
None => assert!(jpan.substitution.as_deref().unwrap_or_default().contains("no licensed face")),
}
// Nothing claims to be embedded, so every font here carries a reason.
assert!(info.fonts.iter().all(|f| !f.embedded && f.substitution.is_some()), "{:?}", info.fonts);
}
/// An embedded font is reported as faithful: nothing stands in, and there is nothing to explain.
const EMBEDDED: &[u8] = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Font << /F1 4 0 R >> >> >> endobj
4 0 obj << /Type /Font /Subtype /TrueType /BaseFont /ABCDEF+SomeHouseSans /FontDescriptor 5 0 R >> endobj
5 0 obj << /Type /FontDescriptor /FontName /ABCDEF+SomeHouseSans /Flags 32 /FontFile2 6 0 R >> endobj
6 0 obj << /Length 4 >> stream
test
endstream endobj
trailer << /Size 7 /Root 1 0 R >>
%%EOF";
#[test]
fn an_embedded_font_reports_no_substitution_at_all() {
let info = inspect(Arc::new(EMBEDDED.to_vec()), None).expect("opens");
let f = font(&info.fonts, "SomeHouseSans");
assert!(f.embedded && f.subset, "{f:?}");
assert_eq!((f.substitute.as_deref(), f.substitution.as_deref()), (None, None), "the document's own glyphs are shown");
}
}
+2 -2
View File
@@ -3436,8 +3436,8 @@ milestone = "M3"
status = "shipped"
commands = ["file.properties"]
tools = ["doc_info"]
evidence = ["crates/ui-egui/tests/view.rs::fonts_tab_lists_fonts_and_embedding"]
notes = ""
evidence = ["crates/ui-egui/tests/view.rs::fonts_tab_lists_fonts_and_embedding", "crates/render/src/inspect.rs::doc_info_says_which_face_stands_in_and_why", "crates/render/src/inspect.rs::an_embedded_font_reports_no_substitution_at_all"]
notes = "name, type, embedding, subset and encoding; doc_info also reports the face standing in for a program the document does not embed and why it was chosen (Acrobat's Actual Font), including the case where no licensed face exists so nothing is drawn. Still missing: the Fonts tab in the interface does not show the substituted face yet, only the data layer does"
[[feature]]
id = "organize.initial-view"
+3 -3
View File
@@ -47,7 +47,7 @@ pub const ADOBE_DATA_ALLOWED: &[(&str, &str)] = &[("hayro-cmap", "assets/cmaps.b
/// technical data we hold ourselves rather than through a dependency. Each still needs an
/// `[[asset]]` entry with `adobe_data = true`, so its SHA-256 is checked and any change to the
/// table has to be re-attested deliberately.
pub const ADOBE_DATA_FILES: &[&str] = &["crates/fonts/src/std14.rs"];
pub const ADOBE_DATA_FILES: &[&str] = &["crates/fonts/src/std14_metrics.rs"];
/// File extensions that count as assets wherever they appear in the repository.
const ASSET_EXTENSIONS: &[&str] = &[
@@ -580,7 +580,7 @@ mod tests {
fn first_party_adobe_data_only_on_the_closed_list() {
let l = lock(&[]);
// The generated standard-14 table is on the list, as `kind = "data"`.
let mut ok = asset("crates/fonts/src/std14.rs", "Adobe (Core14 AFM metrics); tables by the PdfCraft contributors", "data", "MIT");
let mut ok = asset("crates/fonts/src/std14_metrics.rs", "Adobe (Core14 AFM metrics); tables by the PdfCraft contributors", "data", "MIT");
ok.adobe_data = true;
let m = Manifest { asset: vec![ok], ..Default::default() };
assert!(check(&root(), &m, &[], &l).is_empty(), "{:?}", check(&root(), &m, &[], &l));
@@ -592,7 +592,7 @@ mod tests {
assert!(check(&root(), &m, &[], &l).iter().any(|p| p.contains("closed list")));
// Even on the list, it may only be data — never a typeface.
let mut wrong_kind = asset("crates/fonts/src/std14.rs", "Adobe", "font", "MIT");
let mut wrong_kind = asset("crates/fonts/src/std14_metrics.rs", "Adobe", "font", "MIT");
wrong_kind.adobe_data = true;
let m = Manifest { asset: vec![wrong_kind], ..Default::default() };
assert!(!check(&root(), &m, &[], &l).is_empty());