fix(fonts): Fix decoding for custom encodings and CID Korean fonts
Merge CMap and Differences encoding at the byte level for single-byte fonts, fixing garbled output when partial ToUnicode CMaps caused Latin-1 fallback to block the Differences path. Add Adobe-Korea1 CID-to-Unicode predefined mapping for Identity-H CID fonts without ToUnicode streams, and support TrueType cmap extraction via ttf-parser for embedded fonts. Also handle suffixed glyph names (e.g. zero.tf, a.ss01) per Adobe spec. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
42c1d639f2
commit
421690c7c4
@@ -15,7 +15,7 @@ use std::collections::HashMap;
|
||||
|
||||
use super::fonts::{
|
||||
build_font_encodings, build_font_widths, compute_string_width_ts, extract_text_from_operand,
|
||||
get_operand_bytes,
|
||||
get_font_file2_obj_num, get_operand_bytes,
|
||||
};
|
||||
use super::xobjects::{extract_form_xobject_text, get_page_xobjects, XObjectType};
|
||||
use super::{get_number, multiply_matrices};
|
||||
@@ -53,11 +53,13 @@ pub(crate) fn extract_page_text_items(
|
||||
font_base_names.insert(resource_name.clone(), base_name);
|
||||
}
|
||||
}
|
||||
// Track ToUnicode object reference
|
||||
// Track ToUnicode object reference, with FontFile2 fallback for Identity-H/V
|
||||
if let Ok(tounicode) = font_dict.get(b"ToUnicode") {
|
||||
if let Ok(obj_ref) = tounicode.as_reference() {
|
||||
font_tounicode_refs.insert(resource_name, obj_ref.0);
|
||||
}
|
||||
} else if let Some(ff2_obj_num) = get_font_file2_obj_num(doc, font_dict) {
|
||||
font_tounicode_refs.insert(resource_name, ff2_obj_num);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+77
-3
@@ -530,6 +530,48 @@ pub(crate) fn parse_encoding_dictionary(
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the CMap lookup key for an Identity-H/V CID font without ToUnicode.
|
||||
/// Returns the object number used by `collect_cmaps_from_fonts` to store the CMap:
|
||||
/// - FontFile2 or FontFile3 obj_num (for embedded font cmap)
|
||||
/// - CIDFont dict obj_num (for predefined CIDSystemInfo-based mapping)
|
||||
pub(crate) fn get_font_file2_obj_num(doc: &Document, font_dict: &lopdf::Dictionary) -> Option<u32> {
|
||||
// Must be Identity-H or Identity-V encoding
|
||||
let encoding = font_dict.get(b"Encoding").ok()?.as_name().ok()?;
|
||||
if encoding != b"Identity-H" && encoding != b"Identity-V" {
|
||||
return None;
|
||||
}
|
||||
let desc_fonts_obj = font_dict.get(b"DescendantFonts").ok()?;
|
||||
let desc_fonts = resolve_array(doc, desc_fonts_obj)?;
|
||||
if desc_fonts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let cid_font_dict = resolve_dict(doc, &desc_fonts[0])?;
|
||||
let font_descriptor_obj = cid_font_dict.get(b"FontDescriptor").ok()?;
|
||||
let font_descriptor = resolve_dict(doc, font_descriptor_obj)?;
|
||||
|
||||
// Try FontFile2 (TrueType), then FontFile3 (OpenType/CFF)
|
||||
if let Some(ff_ref) = font_descriptor
|
||||
.get(b"FontFile2")
|
||||
.ok()
|
||||
.and_then(|o| o.as_reference().ok())
|
||||
.or_else(|| {
|
||||
font_descriptor
|
||||
.get(b"FontFile3")
|
||||
.ok()
|
||||
.and_then(|o| o.as_reference().ok())
|
||||
})
|
||||
{
|
||||
return Some(ff_ref.0);
|
||||
}
|
||||
|
||||
// Fallback: use DescendantFonts[0] obj_num (for predefined CIDSystemInfo mapping)
|
||||
if let Object::Reference(r) = &desc_fonts[0] {
|
||||
return Some(r.0);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Decode text from a PDF string operand using font CMaps, encodings, and fallbacks.
|
||||
pub(crate) fn extract_text_from_operand(
|
||||
obj: &Object,
|
||||
@@ -543,9 +585,41 @@ pub(crate) fn extract_text_from_operand(
|
||||
// Look up CMap by ToUnicode object reference
|
||||
if let Some(&obj_num) = font_tounicode_refs.get(current_font) {
|
||||
if let Some(cmap) = font_cmaps.get_by_obj(obj_num) {
|
||||
let decoded = cmap.decode_cids(bytes);
|
||||
if !decoded.is_empty() {
|
||||
return Some(decoded);
|
||||
// For single-byte CMaps, merge CMap + Differences at the byte level:
|
||||
// try CMap first, then Differences, then Latin-1 fallback per byte.
|
||||
// This prevents partial CMap results from blocking the Differences path.
|
||||
if cmap.code_byte_length == 1 {
|
||||
let encoding_map = font_encodings.get(current_font);
|
||||
let lookups = cmap.lookup_bytes(bytes);
|
||||
let decoded: String = lookups
|
||||
.iter()
|
||||
.filter_map(|&(b, ref cmap_result)| {
|
||||
// 1. CMap mapped it? Use CMap result
|
||||
if let Some(s) = cmap_result {
|
||||
return Some(s.clone());
|
||||
}
|
||||
// 2. Differences mapped it? Use Differences result
|
||||
if let Some(map) = encoding_map {
|
||||
if let Some(&ch) = map.get(&b) {
|
||||
return Some(ch.to_string());
|
||||
}
|
||||
}
|
||||
// 3. Printable ASCII/Latin-1 fallback
|
||||
if b >= 0x20 {
|
||||
return Some((b as char).to_string());
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
if !decoded.is_empty() {
|
||||
return Some(decoded);
|
||||
}
|
||||
} else {
|
||||
// 2-byte CMap: use standard decode_cids path
|
||||
let decoded = cmap.decode_cids(bytes);
|
||||
if !decoded.is_empty() {
|
||||
return Some(decoded);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::collections::HashMap;
|
||||
|
||||
use super::fonts::{
|
||||
build_font_encodings, build_font_widths, compute_string_width_ts, extract_text_from_operand,
|
||||
get_operand_bytes,
|
||||
get_font_file2_obj_num, get_operand_bytes,
|
||||
};
|
||||
use super::{get_number, multiply_matrices};
|
||||
|
||||
@@ -126,6 +126,8 @@ pub(crate) fn extract_form_xobject_text(
|
||||
if let Ok(obj_ref) = tounicode.as_reference() {
|
||||
font_tounicode_refs.insert(resource_name, obj_ref.0);
|
||||
}
|
||||
} else if let Some(ff2_obj_num) = get_font_file2_obj_num(doc, font_dict) {
|
||||
font_tounicode_refs.insert(resource_name, ff2_obj_num);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user