mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 02:40:09 +00:00
f2e222ebc5
Type0Font::new built its FallbackFontQuery from the Type 0 font dictionary, which has no /FontDescriptor (ISO 32000-2 §9.7.6.1, Table 119); the descriptor belongs to the descendant CIDFont (§9.7.4.1, Table 115). Every request for a non-embedded CID font therefore said not serif, not italic, weight 400, and no resolver could tell a Song or Mincho font from a Hei or Gothic one, or see /FontWeight 700 (§9.8, Tables 120, 121). FallbackFontQuery::with_descriptor takes the name from the font dictionary and the style from the given descriptor; new() calls it with the dictionary's own descriptor, so simple fonts are unchanged. The CID path passes the descendant's descriptor it already reads. Vendored hayro-interpret patch (58). Tests in crates/render/tests/it/cid_fallback_query.rs record the request a font_resolver receives. The CID-fonts parity note now says the Japanese face also follows the Serif flag and /FontWeight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
95 lines
4.5 KiB
Rust
95 lines
4.5 KiB
Rust
//! 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())
|
|
}
|