mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 11:12:43 +00:00
fix(render): an embedded CJK font without a usable ToUnicode reads its text through its collection's UCS2 CMap (hayro-interpret patch 84)
ISO 32000-2 §9.10.2: a Type 0 font of the Adobe-GB1, -CNS1, -Japan1 or -Korea1 collection maps code to CID to Unicode through the collection's UCS2 CMap. Only fonts that are not embedded took that route; an embedded one without /ToUnicode, or with one that maps nothing for a code, gave no text. /ToUnicode still comes first; the UCS2 CMap answers the codes it gives nothing for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
250c2623d1
commit
fc28e85fb8
@@ -1965,3 +1965,61 @@ fn text_outside_the_crop_box_is_clipped() {
|
||||
assert!(text.is_empty(), "expected no glyphs on {crop:?}, got {:?}", text.plain_text());
|
||||
}
|
||||
}
|
||||
|
||||
/// A one-page document showing `codes` in a Type 0 font with `encoding` over an embedded
|
||||
/// `descendant` (`CIDFontType0` takes the bundled CFF program, `CIDFontType2` a TrueType one)
|
||||
/// of the Adobe-Japan1 collection, with `to_unicode` as its `/ToUnicode` CMap when given.
|
||||
fn japan1_page(encoding: &str, descendant: &str, to_unicode: Option<&[u8]>, codes: &str) -> String {
|
||||
use lopdf::{Document, Object, Stream, dictionary};
|
||||
let mut doc = Document::with_version("1.7");
|
||||
let (key, program): (&str, &[u8]) = if descendant == "CIDFontType0" {
|
||||
("FontFile3", include_bytes!("../../../../vendor/hayro-interpret/assets/FoxitFixed.pfb"))
|
||||
} else {
|
||||
("FontFile2", include_bytes!("../../../../assets/fonts/Inter-Regular.ttf"))
|
||||
};
|
||||
let mut file = dictionary! {};
|
||||
if descendant == "CIDFontType0" {
|
||||
file.set("Subtype", "CIDFontType0C");
|
||||
}
|
||||
let program = doc.add_object(Stream::new(file, program.to_vec()));
|
||||
let descriptor = doc.add_object(dictionary! { "Type" => "FontDescriptor", "FontName" => "TestJapan1", "Flags" => 4, key => program });
|
||||
let cid = doc.add_object(dictionary! {
|
||||
"Type" => "Font", "Subtype" => descendant, "BaseFont" => "TestJapan1",
|
||||
"CIDSystemInfo" => dictionary! { "Registry" => Object::string_literal("Adobe"), "Ordering" => Object::string_literal("Japan1"), "Supplement" => 6 },
|
||||
"FontDescriptor" => descriptor, "DW" => 1000, "CIDToGIDMap" => "Identity"
|
||||
});
|
||||
let mut font = dictionary! { "Type" => "Font", "Subtype" => "Type0", "BaseFont" => "TestJapan1", "Encoding" => encoding, "DescendantFonts" => vec![Object::Reference(cid)] };
|
||||
if let Some(cmap) = to_unicode {
|
||||
font.set("ToUnicode", doc.add_object(Stream::new(dictionary! {}, cmap.to_vec())));
|
||||
}
|
||||
let font = doc.add_object(font);
|
||||
let content = doc.add_object(Stream::new(dictionary! {}, format!("BT /F1 20 Tf 1 0 0 1 20 250 Tm <{codes}> Tj ET").into_bytes()));
|
||||
let pages = doc.new_object_id();
|
||||
let page = doc.add_object(dictionary! { "Type" => "Page", "Parent" => pages, "MediaBox" => vec![Object::Integer(0), Object::Integer(0), Object::Integer(300), Object::Integer(300)], "Resources" => dictionary! {"Font" => dictionary! {"F1" => font}}, "Contents" => content });
|
||||
doc.objects.insert(pages, dictionary! {"Type" => "Pages", "Kids" => vec![Object::Reference(page)], "Count" => 1}.into());
|
||||
let catalog = doc.add_object(dictionary! {"Type" => "Catalog", "Pages" => pages});
|
||||
doc.trailer.set("Root", catalog);
|
||||
let mut bytes = Vec::new();
|
||||
doc.save_to(&mut bytes).unwrap();
|
||||
let pdf = Pdf::new(std::sync::Arc::new(bytes)).unwrap();
|
||||
extract_page(&pdf, 0, &InterpreterSettings::default()).unwrap().plain_text()
|
||||
}
|
||||
|
||||
/// An embedded font of an Adobe character collection reads as text through the collection's
|
||||
/// UCS2 CMap, code to CID to Unicode (ISO 32000-2 §9.10.2), where its `/ToUnicode` is missing or
|
||||
/// has nothing for a code: with Identity-H or a predefined CMap such as `/H`, CFF or TrueType.
|
||||
/// Only fonts that are not embedded took that route, so Japanese and Chinese pages whose fonts
|
||||
/// are embedded without a `/ToUnicode` had no text to select, find or copy. `/ToUnicode` still
|
||||
/// comes first where it maps a code.
|
||||
#[test]
|
||||
fn an_embedded_adobe_collection_font_reads_through_its_ucs2_cmap() {
|
||||
// Adobe-Japan1 CIDs 34, 1125 and 843: A, 亜 and あ.
|
||||
assert_eq!(japan1_page("Identity-H", "CIDFontType0", None, "00220465034B"), "A亜あ", "Identity-H, CFF, no /ToUnicode");
|
||||
assert_eq!(japan1_page("Identity-H", "CIDFontType2", None, "00220465034B"), "A亜あ", "Identity-H, TrueType, no /ToUnicode");
|
||||
// `/H` maps JIS X 0208 codes to CIDs: 3021 亜, 2422 あ, 2341 A.
|
||||
assert_eq!(japan1_page("H", "CIDFontType2", None, "302124222341"), "亜あA", "predefined /H");
|
||||
let empty = b"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Empty def /CMapType 2 def 1 begincodespacerange <0000> <FFFF> endcodespacerange endcmap CMapName currentdict /CMap defineresource pop end end";
|
||||
assert_eq!(japan1_page("Identity-H", "CIDFontType2", Some(empty), "00220465034B"), "A亜あ", "a /ToUnicode with no mappings");
|
||||
let one = b"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /One def /CMapType 2 def 1 begincodespacerange <0000> <FFFF> endcodespacerange 1 beginbfchar <0022> <005A> endbfchar endcmap CMapName currentdict /CMap defineresource pop end end";
|
||||
assert_eq!(japan1_page("Identity-H", "CIDFontType0", Some(one), "00220465034B"), "Z亜あ", "/ToUnicode first, the UCS2 CMap for the rest");
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+26
-12
@@ -42,6 +42,9 @@ pub(crate) struct Type0Font {
|
||||
/// Whether the `to_unicode` map is a UCS2 `CMap` (CID-indexed) rather than
|
||||
/// a `ToUnicode` `CMap` (code-indexed).
|
||||
to_unicode_is_cid_indexed: bool,
|
||||
/// PdfCraft patch (84): the character collection's UCS2 CMap (by CID), for the codes the
|
||||
/// font's own `/ToUnicode` gives nothing for, whether or not its program is embedded.
|
||||
ucs2: Option<CMap>,
|
||||
/// PdfCraft patch: whether the CIDs belong to an Adobe character collection (Japan1, GB1,
|
||||
/// CNS1, Korea1), whose glyph numbering no substitute other than a CID-keyed CFF font shares.
|
||||
adobe_collection: bool,
|
||||
@@ -131,18 +134,20 @@ impl Type0Font {
|
||||
.is_some_and(|n| matches!(n.as_ref(), b"Identity-H" | b"Identity-V"));
|
||||
let mut to_unicode_is_cid_indexed = false;
|
||||
|
||||
// PdfCraft patch (84): the character collection's UCS2 CMap (ISO 32000-2 §9.10.2), CID
|
||||
// to Unicode, for a font of an Adobe collection whether or not its program is embedded.
|
||||
let mut ucs2 = character_collection
|
||||
.as_ref()
|
||||
.and_then(|cc| cc.family.ucs2_cmap())
|
||||
.and_then(|name| (cmap_resolver)(name))
|
||||
.and_then(|data| {
|
||||
let resolver = cmap_resolver.clone();
|
||||
CMap::parse(data, move |n| (resolver)(n))
|
||||
});
|
||||
// If there is no ToUnicode map, try to get the UCS2 CMap.
|
||||
if fallback
|
||||
&& to_unicode.is_none()
|
||||
&& let Some(cc) = character_collection.as_ref()
|
||||
&& let Some(ucs2_name) = cc.family.ucs2_cmap()
|
||||
&& let Some(data) = (cmap_resolver)(ucs2_name)
|
||||
{
|
||||
let resolver = cmap_resolver.clone();
|
||||
if let Some(ucs2_cmap) = CMap::parse(data, move |n| (resolver)(n)) {
|
||||
to_unicode = Some(ucs2_cmap);
|
||||
to_unicode_is_cid_indexed = true;
|
||||
}
|
||||
if fallback && to_unicode.is_none() && ucs2.is_some() {
|
||||
to_unicode = ucs2.take();
|
||||
to_unicode_is_cid_indexed = true;
|
||||
}
|
||||
|
||||
let adobe_collection = character_collection
|
||||
@@ -173,6 +178,7 @@ impl Type0Font {
|
||||
font_flags,
|
||||
fallback,
|
||||
to_unicode_is_cid_indexed,
|
||||
ucs2,
|
||||
adobe_collection,
|
||||
identity_to_unicode,
|
||||
})
|
||||
@@ -466,7 +472,15 @@ impl Type0Font {
|
||||
} else {
|
||||
code
|
||||
};
|
||||
return to_unicode.lookup_bf_string(key);
|
||||
if let Some(s) = to_unicode.lookup_bf_string(key) {
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
// PdfCraft patch (84): then the collection's UCS2 CMap, by the code's CID.
|
||||
if let Some(ucs2) = &self.ucs2
|
||||
&& let Some(s) = self.code_to_cid(code).and_then(|cid| ucs2.lookup_bf_string(cid))
|
||||
{
|
||||
return Some(s);
|
||||
}
|
||||
|
||||
None
|
||||
|
||||
Reference in New Issue
Block a user