fix(render): draw Turkish Ğ İ Ş ğ ş in non-embedded fonts (vendored hayro-interpret patch 26) (#1409)

A PDF whose fonts aren't embedded is drawn with PDFium's Foxit stand-ins
for the standard 14 fonts. Those carry only the 230-odd glyphs of the
Standard, WinAnsi and Mac Roman encodings, so a letter a font names beyond
them in its /Differences mapped to .notdef and drew nothing. iText 2
writes Turkish exactly this way (Times New Roman left unembedded,
/Gbreve /Idotaccent /Scedilla /gbreve /scedilla over Ð Ý Þ ð þ), and such
letters showed blanks for Ğ, İ, Ş, ğ and ş while ı, ç, ö and ü drew.

StandardKind now builds a Latin Extended-A letter the stand-in lacks from
the base letter and accent it does have, as Type 1 seac composites and
the standard-14 AFM composites are built: the accent is centred on the
letter, raised over capitals and letters with ascenders (and moved along
the slant in italics), cedillas hang below and ogoneks from the right
edge. Composite glyph ids sit above any font's glyph count, one per
character code; their width comes from the standard metrics of the name.

Test: raster::tests::accented_letters_the_standard_stand_ins_lack_are_built_from_their_parts
(fails without the patch: Gbreve drew no pixel).

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
TheLastRoadRunnerandClaude Opus 5.5 authored and GitHub committed 2026-10-12 07:57:03 -04:00
1 parent a4c53f706f
commit a259af95c3
3 files changed
+267 -10

No files matched your search

+60
View File
@@ -4972,6 +4972,66 @@ trailer << /Root 1 0 R >>
assert!(px.rgba[((50 * px.width + 25) * 4) as usize] < 64, "the glyph is drawn");
}
/// Accented letters in non-embedded fonts (vendored hayro-interpret patch to
/// `StandardKind`): the stand-ins for the standard fonts hold only the glyphs of the
/// Standard, WinAnsi and Mac Roman encodings, so a letter a font names beyond those in its
/// /Differences drew nothing. iText 2 writes Turkish this way (`/Gbreve`, `/Idotaccent`,
/// `/Scedilla`, `/gbreve`, `/scedilla` over Ð, Ý, Þ, ð, þ), and a Turkish letter had blanks
/// for Ğ, İ, Ş, ğ and ş. Each is now its base letter with the accent above or below it.
#[test]
fn accented_letters_the_standard_stand_ins_lack_are_built_from_their_parts() {
let letters = ["G", "Gbreve", "S", "Scedilla", "I", "Idotaccent", "g", "gbreve", "s", "scedilla"];
let mut content = String::new();
for (i, _) in letters.iter().enumerate() {
content.push_str(&format!("BT /F1 40 Tf {} 20 Td <{:02X}> Tj ET\n", 10 + 40 * i, 65 + i));
}
let differences = letters.iter().map(|n| format!("/{n}")).collect::<Vec<_>>().join(" ");
let pdf = format!(
"%PDF-1.4
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 420 80] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj
4 0 obj << /Length {} >> stream
{content}endstream endobj
5 0 obj << /Type /Font /Subtype /TrueType /BaseFont /TimesNewRomanPSMT /FirstChar 65 /LastChar 74
/Widths [722 722 556 556 333 333 500 500 389 389] /FontDescriptor 6 0 R
/Encoding << /Type /Encoding /Differences [65 {differences}] >> >> endobj
6 0 obj << /Type /FontDescriptor /FontName /TimesNewRomanPSMT /Flags 32 /FontBBox [-568 -306 2045 1039]
/ItalicAngle 0 /Ascent 693 /Descent -215 /CapHeight 662 /StemV 80 >> endobj
trailer << /Root 1 0 R >>
%%EOF",
content.len()
);
let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default());
let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() });
assert!(p.error.is_none(), "{:?}", p.error);
// The dark pixels of the i-th letter: how many, and its top and bottom rows (y down).
let ink = |i: u32| {
let (mut count, mut top, mut bottom) = (0, u32::MAX, 0);
for y in 0..p.height {
for x in (8 + 40 * i)..(46 + 40 * i).min(p.width) {
if p.rgba[((y * p.width + x) * 4) as usize] < 128 {
count += 1;
top = top.min(y);
bottom = bottom.max(y);
}
}
}
(count, top, bottom)
};
for (pair, above) in [(0, true), (1, false), (2, true), (3, true), (4, false)] {
let (base, accented) = (ink(2 * pair), ink(2 * pair + 1));
let name = letters[2 * pair as usize + 1];
assert!(base.0 > 0, "{name}: the base letter draws");
assert!(accented.0 > base.0, "{name} draws its letter and accent: {accented:?} vs {base:?}");
if above {
assert!(accented.1 + 2 < base.1, "{name}: the accent sits above the letter: {accented:?} vs {base:?}");
} else {
assert!(accented.2 > base.2 + 2, "{name}: the accent hangs below the letter: {accented:?} vs {base:?}");
}
}
}
/// A page's `/UserUnit` (the size of its unit in points) scales the page and everything on
/// it, after the crop box and before `/Rotate`, as Acrobat shows such pages: with
/// `/UserUnit 2`, scale 1 renders pixel for pixel what scale 2 renders without it. As in