M2: kanji that Adobe-Japan1 maps to a variation sequence draw in non-embedded fonts (#662)

Adobe-Japan1-UCS2 maps the JIS X 0208 forms of the kanji that
JIS X 0213:2004 redrew to a variation sequence (CID 1247 is 噂 U+E0100).
hayro only looked up single characters in a substitute font, so a
non-embedded Japanese font drew 噂, 逢, 溢, 祇, 箸 and over 200 other
common kanji as unrelated glyphs. A character with a variation selector,
from Adobe-Japan1-UCS2 or a /ToUnicode, now draws the substitute's glyph
for that form when its cmap has one, else its glyph for the character.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
cyfung1031 2026-10-10 11:05:06 +09:00 committed by GitHub
parent d63436ed10
commit ef9b553f55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 87 additions and 5 deletions

View File

@ -3512,6 +3512,62 @@ trailer << /Root 1 0 R >>
}
}
/// Adobe-Japan1 maps the JIS X 0208 forms of the kanji that JIS X 0213:2004 redrew (噂, 逢,
/// 溢, …) to a variation sequence: CID 1247 is 噂 U+E0100. A non-embedded Japanese font drew
/// such a CID as an unrelated glyph, since the substitute has no glyph for a sequence. It
/// draws the substitute's glyph for that form now, and its 噂 for a form it doesn't have.
#[test]
fn variation_sequences_draw_their_kanji_in_a_substitute() {
if pdfcraft_fonts::document_japanese_font().is_none() {
eprintln!("built without craft-fonts (CRAFT_FONTS_DIR unset): no Japanese face to check");
return;
}
let render = |base_font: &str, encoding: &str, to_unicode: &str, code: &str| {
let content = format!("BT /F1 40 Tf 5 15 Td <{code}> Tj ET");
let cmap = format!(
"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Test def /CMapType 2 def 1 begincodespacerange <0000> <FFFF> endcodespacerange 1 beginbfchar <0001> <{to_unicode}> endbfchar endcmap CMapName currentdict /CMap defineresource pop end end"
);
// Code <0001> through `/ToUnicode` when the encoding is Identity-H; otherwise no
// `/ToUnicode`, so the CID goes through Adobe-Japan1-UCS2.
let to_unicode = if encoding == "Identity-H" { "/ToUnicode 8 0 R" } else { "" };
let pdf = format!(
"%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 60 60] /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 /Type0 /BaseFont /{base_font} /Encoding /{encoding} /DescendantFonts [6 0 R] {to_unicode} >> endobj
6 0 obj << /Type /Font /Subtype /CIDFontType0 /BaseFont /{base_font} /CIDSystemInfo << /Registry (Adobe) /Ordering (Japan1) /Supplement 6 >>
/FontDescriptor 7 0 R /DW 1000 >> endobj
7 0 obj << /Type /FontDescriptor /FontName /{base_font} /Flags 6 /FontBBox [0 -141 1000 859] /ItalicAngle 0 /Ascent 859 /Descent -141 /CapHeight 700 /StemV 80 >> endobj
8 0 obj << /Length {} >> stream
{cmap}
endstream endobj
trailer << /Root 1 0 R >>
%%EOF",
content.len(),
cmap.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 });
assert!(p.error.is_none(), "{base_font} <{code}>: {:?}", p.error);
p.rgba
};
let inked = |rgba: &[u8]| rgba.as_chunks::<4>().0.iter().filter(|c| c[0] < 128).count();
for base_font in ["HeiseiMin-W3", "HeiseiKakuGo-W5"] {
let collection = render(base_font, "UniJIS-UCS2-H", "", "5642");
let form = render(base_font, "Identity-H", "5642DB40DD00", "0001");
let character = render(base_font, "Identity-H", "5642", "0001");
let unknown_form = render(base_font, "Identity-H", "5642DB40DD05", "0001");
assert!(inked(&form) > 300, "{base_font}: 噂 U+E0100 is drawn ({} dark pixels)", inked(&form));
assert!(collection == form, "{base_font}: CID 1247 draws 噂 U+E0100");
assert!(form != character, "{base_font}: the form differs from the face's default 噂");
assert!(unknown_form == character, "{base_font}: a form the face lacks draws its 噂");
}
}
/// An alpha soft mask whose transparency group has no /CS (as Chrome writes gradient text)
/// must still mask. Regression test for the vendored hayro-interpret patch.
#[test]

2
vendor/README.md vendored

File diff suppressed because one or more lines are too long

View File

@ -14,6 +14,7 @@ use hayro_syntax::object::dict::keys::*;
use hayro_syntax::object::{Array, Object};
use kurbo::{BezPath, Vec2};
use skrifa::attribute::Style;
use skrifa::charmap::MapVariant;
use skrifa::raw::collections::int_set::Domain;
use skrifa::{FontRef, GlyphId, MetadataProvider};
use std::collections::HashMap;
@ -225,7 +226,7 @@ impl Type0Font {
fn map_via_unicode(&self, key: u32) -> Option<GlyphId> {
let to_unicode = self.to_unicode.as_ref()?;
let character = to_unicode
let (character, selector) = to_unicode
.lookup_bf_string(key)
.or_else(|| {
for len in 0..4 {
@ -239,12 +240,32 @@ impl Type0Font {
None
})
.and_then(|bf| match bf {
BfString::Char(c) => Some(c),
BfString::String(_) => None,
BfString::Char(c) => Some((c, None)),
// PdfCraft patch: a character and a variation selector is that character in a
// given form, as Adobe-Japan1 maps the JIS X 0208 forms of the kanji that
// JIS X 0213:2004 redrew (噂 is U+5642 U+E0100). Other strings (ligatures) still
// have no glyph.
BfString::String(s) => {
let mut chars = s.chars();
let character = chars.next()?;
let selector = chars.next().filter(|c| is_variation_selector(*c))?;
chars.next().is_none().then_some((character, Some(selector)))
}
})?;
match &self.font_type {
FontType::OpenType(t) => t.font_ref().charmap().map(character),
FontType::OpenType(t) => {
let charmap = t.font_ref().charmap();
// PdfCraft patch: the face's glyph for that form when its `cmap` has one, else its
// glyph for the character, as when a form isn't supported (Unicode FAQ on
// variation sequences).
if let Some(selector) = selector
&& let Some(MapVariant::Variant(glyph)) = charmap.map_variant(character, selector)
{
return Some(glyph);
}
charmap.map(character)
}
FontType::Cff(c) => {
// Map codepoint to glyph name via AFL, and then look it up.
if let Some(name) = glyph_names::get_reverse(character)
@ -527,6 +548,11 @@ impl CidToGIdMap {
}
}
/// PdfCraft patch: Variation Selectors (U+FE00–U+FE0F) and their supplement (U+E0100–U+E01EF).
fn is_variation_selector(c: char) -> bool {
matches!(c, '\u{FE00}'..='\u{FE0F}' | '\u{E0100}'..='\u{E01EF}')
}
/// PdfCraft patch: the largest CID (ISO 32000-2 §9.7.2; CIDs are at most two bytes).
const MAX_CID: u32 = 65_535;