use dxpdf::render::fonts::{FaceRequest, FontRegistry, Toggle};
use dxpdf::render::layout::draw_command::{DrawCommand, LayoutedPage};
use dxpdf::render::resolve_and_layout;
use skia_safe::FontMgr;
const CIRCLED: char = '\u{2460}'; const KATAKANA: char = '\u{30A2}'; const THAI: char = '\u{0E51}'; const HEBREW: char = '\u{05D0}';
fn fixture() -> dxpdf::model::Document {
let bytes = std::fs::read(concat!(
env!("CARGO_MANIFEST_DIR"),
"/test-files/issue-139-minimal.docx"
))
.expect(
"test-files/issue-139-minimal.docx — build it with scripts/make_font_fallback_fixture.py",
);
dxpdf::docx::parse(&bytes).expect("the fixture must parse")
}
fn text_commands(pages: &[LayoutedPage]) -> Vec<(String, String)> {
pages
.iter()
.flat_map(|p| &p.commands)
.filter_map(|c| match c {
DrawCommand::Text {
text, font_family, ..
} => Some((text.to_string(), font_family.to_string())),
_ => None,
})
.collect()
}
fn family_drawing(commands: &[(String, String)], ch: char) -> Option<String> {
commands
.iter()
.find(|(text, _)| text.contains(ch))
.map(|(_, family)| family.clone())
}
fn base_family(commands: &[(String, String)]) -> String {
family_drawing(commands, 'A').expect("the ASCII text must be drawn by something")
}
fn host_covers(ch: char) -> bool {
let mgr = FontMgr::new();
mgr.match_family_style_character(
"Times New Roman",
skia_safe::FontStyle::normal(),
&[],
ch as i32,
)
.is_some_and(|t| t.unichar_to_glyph(ch as i32) != 0)
}
fn resolved_family_covers(family: &str, ch: char) -> bool {
let registry = FontRegistry::new(FontMgr::new());
let entry = registry.resolve(&FaceRequest::new(family, Toggle::Absent, Toggle::Absent));
entry.typeface.unichar_to_glyph(ch as i32) != 0
}
#[test]
fn an_uncovered_codepoint_is_drawn_by_a_face_that_covers_it() {
let (_, pages) = resolve_and_layout(fixture());
let commands = text_commands(&pages);
let base = base_family(&commands);
for (label, ch) in [("circled", CIRCLED), ("katakana", KATAKANA), ("thai", THAI)] {
if !host_covers(ch) {
eprintln!(
"skipping {label}: no face on this host covers U+{:04X}",
ch as u32
);
continue;
}
let family = family_drawing(&commands, ch)
.unwrap_or_else(|| panic!("{label}: U+{:04X} reached no draw command", ch as u32));
assert_ne!(
family, base,
"{label}: U+{:04X} is still drawn by the base face, which cannot draw it",
ch as u32
);
assert!(
resolved_family_covers(&family, ch),
"{label}: the family reported for U+{:04X} ({family:?}) does not resolve to a face \
covering it — the painter and the subsetter would both draw nothing",
ch as u32
);
}
}
#[test]
fn a_codepoint_the_base_face_covers_is_left_where_it_was() {
let (_, pages) = resolve_and_layout(fixture());
let commands = text_commands(&pages);
let base = base_family(&commands);
if !resolved_family_covers(&base, HEBREW) {
eprintln!("skipping: this host's default face does not cover Hebrew either");
return;
}
assert_eq!(
family_drawing(&commands, HEBREW).as_deref(),
Some(base.as_str()),
"Hebrew rendered correctly before per-glyph fallback existed; moving it to another \
face means fallback fired on a codepoint that was never missing"
);
}
#[test]
#[cfg(feature = "subset-fonts")]
fn the_fallback_face_survives_subsetting() {
if !host_covers(KATAKANA) {
eprintln!("skipping: no face on this host covers katakana");
return;
}
let pdf = dxpdf::render::render_with_font_mgr(
fixture(),
&FontMgr::new(),
&dxpdf::RenderOptions::default(),
)
.expect("the fixture must render");
let parsed = lopdf::Document::load_mem(&pdf).expect("the PDF must parse");
let faces: Vec<String> = parsed
.objects
.values()
.filter_map(|o| o.as_dict().ok())
.filter(|d| {
d.get(b"Type")
.ok()
.and_then(|t| t.as_name().ok())
.is_some_and(|n| n == b"FontDescriptor")
})
.filter_map(|d| d.get(b"FontName").ok()?.as_name().ok())
.map(|n| String::from_utf8_lossy(n).into_owned())
.collect();
assert!(
faces.len() > 1,
"expected the fallback face to be embedded alongside the base face, found {faces:?} — \
the subsetter keys usage by re-resolved family name, so a face it cannot see is culled"
);
}