mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 09:20:58 -06:00
M2: a CMap's bfchar mappings read codes at their codespace length (view.render-cid-fonts)
A CMap that writes its CID mappings as `bfchar` was looked up without the code's byte length, so the first byte of a two-byte code matched a two-byte entry of the same value and drew as its own glyph: `<fe96>` drew as "þ–" (PDFBOX-1282, Characters_Arial.pdf page 3). The bfchar fallback of `lookup_cid_code` now applies only when the codespace ranges give the code the length being tried (ISO 32000-2 §9.7.6.2), byte by byte; a CMap without codespace ranges keeps the old fallback. An unmapped code still advances one byte. Vendored hayro-cmap patch 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f42fda7342
commit
37e5708ed2
@@ -5503,6 +5503,40 @@ fn a_font_whose_post_version_has_swapped_halves_is_still_used() {
|
||||
assert_eq!(alternate([0, 0, 0, 2]), alternate([0, 2, 0, 0]), "and also with swapped halves");
|
||||
}
|
||||
|
||||
/// An embedded CMap whose codes are all two bytes long (codespace `<0000> <FFFF>`) and whose
|
||||
/// mappings are written as `bfchar` reads `<2041>` as the one code 0x2041 (ISO 32000-2
|
||||
/// §9.7.6.2: the codespace ranges decide how many bytes form a code). It drew as two
|
||||
/// glyphs: the lone first byte 0x20 matched the two-byte entry `<0020>` (PDFBOX-1282,
|
||||
/// `Characters_Arial.pdf` page 3, codes 0x20AC to 0xFFFC). Vendored hayro-cmap patch 4.
|
||||
#[test]
|
||||
fn a_bfchar_cmap_reads_codes_of_its_codespace_length() {
|
||||
let font = include_bytes!("../../../../assets/fonts/JetBrainsMono-Regular.ttf");
|
||||
let render = |encoding: &str, text: &str| -> Vec<u8> {
|
||||
let cmap = b"/CIDInit /ProcSet findresource begin 12 dict begin begincmap
|
||||
/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> def
|
||||
/CMapName /Test-H def /CMapType 1 def
|
||||
1 begincodespacerange <0000> <FFFF> endcodespacerange
|
||||
3 beginbfchar <0020> <0005> <0041> <0006> <2041> <0024> endbfchar
|
||||
endcmap CMapName currentdict /CMap defineresource pop end end";
|
||||
let resources = format!(
|
||||
"<< /Font << /F1 << /Type /Font /Subtype /Type0 /BaseFont /JetBrainsMono-Regular /Encoding {encoding} /DescendantFonts [<< /Type /Font /Subtype /CIDFontType2 /BaseFont /JetBrainsMono-Regular /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /DW 600 /CIDToGIDMap /Identity /FontDescriptor << /Type /FontDescriptor /FontName /JetBrainsMono-Regular /Flags 4 /FontBBox [-1000 -300 1000 1000] /ItalicAngle 0 /Ascent 1020 /Descent -300 /CapHeight 730 /StemV 80 /FontFile2 5 0 R >> >>] >> >> >>"
|
||||
);
|
||||
let content = format!("BT /F1 40 Tf 10 20 Td {text} Tj ET");
|
||||
let font_dict = format!("<< /Length {} >>", font.len());
|
||||
let cmap_dict = format!(
|
||||
"<< /Type /CMap /CMapName /Test-H /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /Length {} >>",
|
||||
cmap.len()
|
||||
);
|
||||
let page = render_first_page(image_cache_pdf(&content, &resources, &[(&font_dict, font), (&cmap_dict, cmap)]));
|
||||
page.rgba.to_vec()
|
||||
};
|
||||
let blank = render("/Identity-H", "<>");
|
||||
let reference = render("/Identity-H", "<0024>");
|
||||
assert_ne!(reference, blank, "the reference glyph draws");
|
||||
assert_eq!(render("6 0 R", "<2041>"), reference, "<2041> is one code, CID 0x24");
|
||||
assert_ne!(render("/Identity-H", "<00050006>"), reference, "the two-glyph misreading looks different");
|
||||
}
|
||||
|
||||
/// A page whose content expands far beyond its compressed stream stops at the expansion budget,
|
||||
/// which is 64 times the compressed size and at least `MAX_DECODED_STREAM`, and the page says so.
|
||||
#[test]
|
||||
|
||||
@@ -794,8 +794,8 @@ milestone = "M2"
|
||||
status = "partial"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = ["crates/render/src/raster/tests.rs::huge_cid_width_ranges_terminate", "crates/render/src/raster/tests.rs::non_embedded_japanese_cid_fonts_draw_with_a_craft_fonts_face", "crates/render/src/raster/tests.rs::mincho_substitute_covers_half_width_katakana", "crates/render/src/raster/tests.rs::non_embedded_chinese_and_korean_cid_fonts_draw_with_an_installed_face", "crates/engine/src/tests.rs::installed_cjk_fonts_draw_in_the_viewer_but_not_in_an_export", "crates/render/src/text/tests.rs::identity_to_unicode_names_give_text", "crates/fonts/src/pdf/tests.rs::identity_to_unicode_names_read_codes_as_unicode", "crates/render/src/raster/tests.rs::a_soft_hyphen_a_substitute_lacks_draws_its_hyphen"]
|
||||
notes = "CID-to-Unicode tables are M2.4; a non-embedded Adobe-Japan1 font is drawn in a craft-fonts Japanese face (Mincho or Gothic by name, else by the descendant font descriptor's Serif flag; bold Gothic for a Bold name or a `/FontWeight` of 600 or more; BIZ UDMincho for Mincho, which covers half-width katakana) when built with craft-fonts; the desktop viewer can fall back to an installed CJK face for GB1, CNS1, Korea1 and Japan1, including GBK-EUC-H TrueType CID fonts without ToUnicode. Windows preserves installed SimSun, SimHei, KaiTi and FangSong families, including subset-prefixed Founder aliases. Combine thumbnails and accessibility figure previews use the same on-screen fallback. System faces are display-only, disabled by PDFCRAFT_SYSTEM_FONTS=0, and not used by export, printing, OCR or automation; machines without a candidate and web builds remain unsupported. A `/ToUnicode` given as the name `/Identity-H` or `/Identity-V` (WSCAD) reads codes as Unicode for selection, search and editing, without control characters. A non-embedded Identity font whose `/ToUnicode` gives a soft hyphen (U+00AD, as Windows exporters write Arial's hyphen) draws the standard substitute's hyphen (#5888). Any other character the substitute lacks still falls back to the CID taken as one of the substitute's glyph indexes, an unrelated glyph (#3529, #5726)"
|
||||
evidence = ["crates/render/src/raster/tests.rs::huge_cid_width_ranges_terminate", "crates/render/src/raster/tests.rs::non_embedded_japanese_cid_fonts_draw_with_a_craft_fonts_face", "crates/render/src/raster/tests.rs::mincho_substitute_covers_half_width_katakana", "crates/render/src/raster/tests.rs::non_embedded_chinese_and_korean_cid_fonts_draw_with_an_installed_face", "crates/engine/src/tests.rs::installed_cjk_fonts_draw_in_the_viewer_but_not_in_an_export", "crates/render/src/text/tests.rs::identity_to_unicode_names_give_text", "crates/fonts/src/pdf/tests.rs::identity_to_unicode_names_read_codes_as_unicode", "crates/render/src/raster/tests.rs::a_soft_hyphen_a_substitute_lacks_draws_its_hyphen", "crates/render/src/raster/tests.rs::a_bfchar_cmap_reads_codes_of_its_codespace_length"]
|
||||
notes = "CID-to-Unicode tables are M2.4; a non-embedded Adobe-Japan1 font is drawn in a craft-fonts Japanese face (Mincho or Gothic by name, else by the descendant font descriptor's Serif flag; bold Gothic for a Bold name or a `/FontWeight` of 600 or more; BIZ UDMincho for Mincho, which covers half-width katakana) when built with craft-fonts; the desktop viewer can fall back to an installed CJK face for GB1, CNS1, Korea1 and Japan1, including GBK-EUC-H TrueType CID fonts without ToUnicode. Windows preserves installed SimSun, SimHei, KaiTi and FangSong families, including subset-prefixed Founder aliases. Combine thumbnails and accessibility figure previews use the same on-screen fallback. System faces are display-only, disabled by PDFCRAFT_SYSTEM_FONTS=0, and not used by export, printing, OCR or automation; machines without a candidate and web builds remain unsupported. A `/ToUnicode` given as the name `/Identity-H` or `/Identity-V` (WSCAD) reads codes as Unicode for selection, search and editing, without control characters. A non-embedded Identity font whose `/ToUnicode` gives a soft hyphen (U+00AD, as Windows exporters write Arial's hyphen) draws the standard substitute's hyphen (#5888). Any other character the substitute lacks still falls back to the CID taken as one of the substitute's glyph indexes, an unrelated glyph (#3529, #5726). An embedded CMap that writes its CID mappings as `bfchar` reads each code at the length its codespace ranges give it (ISO 32000-2 §9.7.6.2), so a two-byte code is one glyph, not two (hayro-cmap patch 4)"
|
||||
|
||||
[[feature]]
|
||||
id = "view.render-image-codecs"
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+106
-4
@@ -359,8 +359,9 @@ const MAX_NESTING_DEPTH: u32 = 16;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CMap {
|
||||
metadata: Metadata,
|
||||
// Note that we don't actually use this, because Acrobat _seems_ to ignore
|
||||
// it, too.
|
||||
// Note that the CID lookup doesn't check this, because Acrobat _seems_ to
|
||||
// ignore it, too. PdfCraft patch (4): the `bfchar` fallback of
|
||||
// `lookup_cid_code` does check it (see there).
|
||||
_codespace_ranges: Vec<CodespaceRange>,
|
||||
cid_ranges: PartitionedRanges,
|
||||
notdef_ranges: PartitionedRanges,
|
||||
@@ -449,7 +450,16 @@ impl CMap {
|
||||
// See pdfjs_bug920426, it uses bf chars for encoding the characters
|
||||
// of a text-showing operator, so try that as well. We don't want to
|
||||
// recurse though, only check the bf chars of this level.
|
||||
if let Some(BfString::Char(lookup)) = self.lookup_bf_string_inner(code, false) {
|
||||
//
|
||||
// PdfCraft patch (4): bf entries don't record how many bytes their code
|
||||
// has, so the fallback only applies when the codespace ranges give
|
||||
// the code `byte_len` bytes (ISO 32000-2 §9.7.6.2). Otherwise the
|
||||
// first byte of `<fe96>` matched a `<00fe>` entry and a two-byte code
|
||||
// drew as two glyphs. A CMap with no codespace ranges keeps the old
|
||||
// fallback.
|
||||
if self.codespace_allows(code, byte_len, 0)
|
||||
&& let Some(BfString::Char(lookup)) = self.lookup_bf_string_inner(code, false)
|
||||
{
|
||||
return Some(lookup as u32);
|
||||
}
|
||||
|
||||
@@ -459,6 +469,32 @@ impl CMap {
|
||||
.and_then(|b| b.lookup_cid_code(code, byte_len))
|
||||
}
|
||||
|
||||
/// PdfCraft patch (4): whether a code of `byte_len` bytes lies in a codespace range of that
|
||||
/// length (each byte within the range's bounds for its position, ISO
|
||||
/// 32000-2 §9.7.6.2). A CMap without codespace ranges defers to its base,
|
||||
/// and one with none anywhere allows every code.
|
||||
fn codespace_allows(&self, code: u32, byte_len: u8, depth: u32) -> bool {
|
||||
if self._codespace_ranges.is_empty() {
|
||||
return match &self.base {
|
||||
Some(base) if depth < MAX_NESTING_DEPTH => {
|
||||
base.codespace_allows(code, byte_len, depth.saturating_add(1))
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
self._codespace_ranges.iter().any(|range| {
|
||||
range.number_bytes == byte_len
|
||||
&& (0..u32::from(byte_len)).all(|i| {
|
||||
// `byte_len` is at most 4 here (the range's own length),
|
||||
// so `shift` is at most 24.
|
||||
let shift = (u32::from(byte_len) - 1 - i) * 8;
|
||||
let byte = |v: u32| v.checked_shr(shift).map_or(0, |b| b & 0xFF);
|
||||
byte(range.low) <= byte(code) && byte(code) <= byte(range.high)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Look up a bf string in the cmap. This is usually
|
||||
/// used for mapping character codes to Unicode codepoints in a
|
||||
/// `ToUnicode` cmap.
|
||||
@@ -594,7 +630,6 @@ impl HasRange for BfRange {
|
||||
|
||||
/// A codespace range defining valid character code byte sequences.
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct CodespaceRange {
|
||||
pub(crate) number_bytes: u8,
|
||||
pub(crate) low: u32,
|
||||
@@ -1128,6 +1163,73 @@ endbfchar
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse `body` after the metadata of [`PREAMBLE`] but without its
|
||||
/// codespace ranges.
|
||||
fn parse_without_codespace(body: &[u8]) -> CMap {
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(
|
||||
br#"/CIDSystemInfo 3 dict dup begin
|
||||
/Registry (Adobe) def
|
||||
/Ordering (Identity) def
|
||||
/Supplement 0 def
|
||||
end def
|
||||
/CMapName /Test def
|
||||
"#,
|
||||
);
|
||||
data.extend_from_slice(body);
|
||||
CMap::parse(&data, |_| None).unwrap()
|
||||
}
|
||||
|
||||
/// PdfCraft patch (4) test: a `bfchar` entry used as a CID mapping matches only
|
||||
/// codes of the length the codespace ranges give it (ISO 32000-2 §9.7.6.2).
|
||||
#[test]
|
||||
fn bfchar_cid_fallback_takes_the_codespace_length() {
|
||||
let cmap = parse_without_codespace(
|
||||
br#"
|
||||
1 begincodespacerange
|
||||
<0000> <FFFF>
|
||||
endcodespacerange
|
||||
2 beginbfchar
|
||||
<0020> <0020>
|
||||
<2041> <0042>
|
||||
endbfchar
|
||||
"#,
|
||||
);
|
||||
// The first byte of <2041> alone is not a code of this CMap.
|
||||
assert_eq!(cmap.lookup_cid_code(0x20, 1), None);
|
||||
assert_eq!(cmap.lookup_cid_code(0x2041, 2), Some(0x42));
|
||||
assert_eq!(cmap.lookup_cid_code(0x0020, 2), Some(0x20));
|
||||
// ToUnicode-style lookups are unchanged.
|
||||
assert_eq!(cmap.lookup_bf_string(0x20), Some(BfString::Char(' ')));
|
||||
}
|
||||
|
||||
/// PdfCraft patch (4) test: mixed codespaces give each code its own length,
|
||||
/// byte by byte, and a CMap without codespace ranges keeps the fallback.
|
||||
#[test]
|
||||
fn bfchar_cid_fallback_checks_each_byte_of_the_codespace() {
|
||||
let cmap = parse_without_codespace(
|
||||
br#"
|
||||
2 begincodespacerange
|
||||
<00> <80>
|
||||
<8140> <9FFC>
|
||||
endcodespacerange
|
||||
3 beginbfchar
|
||||
<41> <0041>
|
||||
<8141> <0100>
|
||||
<8220> <0101>
|
||||
endbfchar
|
||||
"#,
|
||||
);
|
||||
assert_eq!(cmap.lookup_cid_code(0x41, 1), Some(0x41));
|
||||
assert_eq!(cmap.lookup_cid_code(0x8141, 2), Some(0x100));
|
||||
// 0x8220 lies numerically between 0x8140 and 0x9FFC, but its second
|
||||
// byte 0x20 is below 0x40.
|
||||
assert_eq!(cmap.lookup_cid_code(0x8220, 2), None);
|
||||
|
||||
let no_codespace = parse_without_codespace(b"1 beginbfchar <0020> <0020> endbfchar\n");
|
||||
assert_eq!(no_codespace.lookup_cid_code(0x20, 1), Some(0x20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stream_cut_off_inside_a_block_keeps_the_entries_before_it() {
|
||||
let cmap = parse_with_preamble(b"1 beginbfchar <41> <0058>\n<42> <00");
|
||||
|
||||
Reference in New Issue
Block a user