Merge PR #4380: M2: a non-embedded CIDFont's fallback request carries its descendant's font descriptor

Conflicts: vendor/README.md: hayro-interpret row keeps patches (55) to (57) and #4380's (58), with their test lists.
This commit is contained in:
aiongg
2026-10-24 06:25:24 +00:00
6 changed files with 108 additions and 4 deletions
@@ -0,0 +1,94 @@
//! The request a font resolver gets for a CIDFont the document doesn't embed carries the style
//! of the descendant's font descriptor (ISO 32000-2 §9.7.4.1, §9.7.6.1, §9.8). Contributor-original
//! synthetic fixtures.
use hayro::hayro_interpret::InterpreterSettings;
use hayro::hayro_interpret::font::{FallbackFontQuery, FontQuery};
use hayro::hayro_interpret::hayro_cmap::CidFamily;
use hayro::hayro_syntax::Pdf;
use hayro::{RenderCache, RenderSettings, render};
use std::sync::{Arc, Mutex};
/// A one-page document drawing two characters in a Type 0 font whose descendant is a
/// non-embedded CIDFontType0 with the given entries after `/CIDSystemInfo`. The name says
/// nothing about the style and matches no standard font.
fn fixture(descendant_extra: &str, descriptor: Option<&str>) -> Vec<u8> {
let mut objects = vec![
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".into(),
stream("", "BT /F1 24 Tf 10 40 Td <11b30ed4> Tj ET"),
"<< /Type /Font /Subtype /Type0 /BaseFont /CraftCJK-W3 /Encoding /Identity-H /DescendantFonts [6 0 R] >>".into(),
format!(
"<< /Type /Font /Subtype /CIDFontType0 /BaseFont /CraftCJK-W3 \
/CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 2 >> {descendant_extra} >>"
),
];
if let Some(descriptor) = descriptor {
objects.push(descriptor.to_string());
}
pdf(&objects)
}
/// The fallback queries the resolver received while drawing the page.
fn fallback_queries(bytes: Vec<u8>) -> Vec<FallbackFontQuery> {
let seen = Arc::new(Mutex::new(Vec::new()));
let sink = seen.clone();
let settings = InterpreterSettings {
font_resolver: Arc::new(move |query| {
if let FontQuery::Fallback(f) = query {
sink.lock().unwrap().push(f.clone());
}
None
}),
..InterpreterSettings::default()
};
let pdf = Pdf::new(bytes).unwrap();
let pages = pdf.pages();
let page = pages.first().unwrap();
let cache = RenderCache::new();
render(page, &cache, &settings, &RenderSettings::default());
seen.lock().unwrap().clone()
}
#[test]
fn a_cid_font_fallback_request_carries_its_descendants_style() {
// Flags: Serif (bit 2), Nonsymbolic (bit 6), Italic (bit 7) = 2 + 32 + 64.
let descriptor = "<< /Type /FontDescriptor /FontName /CraftCJK-W3 /Flags 98 /FontWeight 700 \
/FontBBox [0 -120 1000 880] /ItalicAngle -12 /Ascent 880 /Descent -120 /CapHeight 700 /StemV 120 >>";
let queries = fallback_queries(fixture("/FontDescriptor 7 0 R", Some(descriptor)));
let query = queries.first().expect("the resolver is asked for a fallback");
assert_eq!(query.post_script_name.as_deref(), Some("CraftCJK-W3"));
assert!(query.character_collection.as_ref().is_some_and(|cc| cc.family == CidFamily::AdobeGB1), "{query:?}");
assert!(query.is_serif, "Serif flag of the descendant's descriptor: {query:?}");
assert!(query.is_italic, "Italic flag of the descendant's descriptor: {query:?}");
assert_eq!(query.font_weight, 700, "/FontWeight of the descendant's descriptor: {query:?}");
}
#[test]
fn a_cid_font_without_a_descriptor_asks_for_the_default_style() {
let queries = fallback_queries(fixture("", None));
let query = queries.first().expect("the resolver is asked for a fallback");
assert_eq!(query.post_script_name.as_deref(), Some("CraftCJK-W3"));
assert!(!query.is_serif && !query.is_italic && !query.is_bold, "{query:?}");
assert_eq!(query.font_weight, 400);
}
fn pdf(objects: &[String]) -> Vec<u8> {
let mut bytes = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, object) in objects.iter().enumerate() {
offsets.push(bytes.len());
bytes.extend_from_slice(format!("{} 0 obj\n{object}\nendobj\n", i + 1).as_bytes());
}
let xref = bytes.len();
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes());
for offset in offsets {
bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
bytes.extend_from_slice(format!("trailer << /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes());
bytes
}
fn stream(dict: &str, body: &str) -> String {
format!("<< {dict} /Length {} >>\nstream\n{body}\nendstream", body.len())
}
+1
View File
@@ -1,5 +1,6 @@
//! The crate's integration tests, linked into one test binary.
mod cid_fallback_query;
mod inline_image_ei;
mod jbig2_invariants;
mod named_form_properties;
+1 -1
View File
@@ -783,7 +783,7 @@ status = "partial"
commands = []
tools = []
evidence = ["crates/render/src/raster.rs::huge_cid_width_ranges_terminate", "crates/render/src/raster.rs::non_embedded_japanese_cid_fonts_draw_with_a_craft_fonts_face", "crates/render/src/raster.rs::mincho_substitute_covers_half_width_katakana", "crates/render/src/text.rs::identity_to_unicode_names_give_text", "crates/fonts/src/pdf.rs::identity_to_unicode_names_read_codes_as_unicode"]
notes = "CID-to-Unicode tables are M2.4; a non-embedded Adobe-Japan1 font is drawn in a craft-fonts Japanese face (Mincho or Gothic by name; BIZ UDMincho for Mincho, which covers half-width katakana) when built with craft-fonts; non-embedded GB1, CNS1 and Korea1 fonts still draw nothing (no allowed face); a `/ToUnicode` given as the name `/Identity-H` or `/Identity-V` (WSCAD) reads codes as Unicode for selection, search and editing, without control characters"
notes = "CID-to-Unicode tables are M2.4; a non-embedded Adobe-Japan1 font is drawn in a craft-fonts Japanese face (Mincho or Gothic by name, else by the descendant font descriptor's Serif flag; bold Gothic for a Bold name or a `/FontWeight` of 600 or more; BIZ UDMincho for Mincho, which covers half-width katakana) when built with craft-fonts; non-embedded GB1, CNS1 and Korea1 fonts still draw nothing (no allowed face); a `/ToUnicode` given as the name `/Identity-H` or `/Identity-V` (WSCAD) reads codes as Unicode for selection, search and editing, without control characters"
[[feature]]
id = "view.render-image-codecs"
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -86,7 +86,9 @@ impl Type0Font {
if let Some((standard, _)) = select_standard_font(dict, &font_descriptor) {
(FontQuery::Standard(standard), true)
} else {
let mut query = FallbackFontQuery::new(dict);
// PdfCraft patch: the style comes from the descendant's descriptor.
let mut query =
FallbackFontQuery::with_descriptor(dict, Some(font_descriptor.clone()));
query.character_collection = character_collection.clone();
warn!(
+8 -1
View File
@@ -564,6 +564,13 @@ pub struct FallbackFontQuery {
impl FallbackFontQuery {
pub(crate) fn new(dict: &Dict<'_>) -> Self {
Self::with_descriptor(dict, dict.get::<Dict<'_>>(FONT_DESC))
}
/// PdfCraft patch: the query for the font `dict` names, with its style read from
/// `descriptor`. A Type 0 font has no font descriptor of its own; its descendant CIDFont
/// has it (ISO 32000-2 §9.7.4.1, §9.7.6.1).
pub(crate) fn with_descriptor(dict: &Dict<'_>, descriptor: Option<Dict<'_>>) -> Self {
let post_script_name = dict
.get::<Name<'_>>(BASE_FONT)
.map(|n| strip_subset_prefix(n.as_str()).to_string());
@@ -573,7 +580,7 @@ impl FallbackFontQuery {
..Default::default()
};
if let Some(descriptor) = dict.get::<Dict<'_>>(FONT_DESC) {
if let Some(descriptor) = descriptor {
data.font_name = dict
.get::<Name<'_>>(FONT_NAME)
.map(|n| strip_subset_prefix(n.as_str()).to_string());