Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ version and date. Earlier releases are described in their
advances reads as a space. Word justifies lines with Arial's en and em
spaces, which came out as U+FFFD between every word
(`Proposer�Legal�Name`) and sent the pages to OCR as garbled text.
- A simple font's ToUnicode CMap of fewer than ten entries keeps its say
for the codes the embedded program does not read. Such a CMap yields to
the program's own reading, and its entries were then dropped, so a code
whose glyph the program does not name read as its byte value: a
heading set by macOS Quartz in a small subset of Times New Roman Bold
read `CÔNG TY` as `CÔ!G TY` and `phẩm` as `ph'm`. The CMap's entry is
now read before the base encoding, an unreadable `/Differences` name
and that guess, and a code it maps to a control character
reads as U+FFFD.

## [1.25.2] - 2026-09-28

Expand Down
125 changes: 115 additions & 10 deletions src/extractor/fonts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2622,17 +2622,35 @@ pub(crate) fn extract_text_from_operand(
return Some(fb);
}
}
// 4. Differences mapped it? Use Differences result. A
// code the Differences name but could not map is
// 4. Differences mapped it? Use Differences result.
if let Some(NamedReading::Text(text)) = by_name {
return Some(text);
}
// 5. A simple font's sparse ToUnicode CMap that
// yielded the primary role to the program's reading
// (the one alternative such a font has, see
// `cmap_entry`): it says what the code is, so it
// is read before a code the Differences name but
// could not map, and before the base encoding;
// a code it maps to no text is not guessed at below.
if is_simple_font {
match entry.remapped.as_ref().map(|c| c.lookup_code(code)) {
Some(CodeMapping::Text(text)) if !text.contains('\u{FFFD}') => {
return Some(text);
}
Some(CodeMapping::ControlDestination) => control_destination = true,
_ => {}
}
}
// A code the Differences name but could not map is
// that glyph and no other: neither the base encoding
// nor the fallback below has a say, and it reads as
// nothing.
match by_name {
Some(NamedReading::Text(text)) => return Some(text),
Some(NamedReading::Unread) => return None,
None => {}
// nor the printable guess below has a say, and it
// reads as nothing unless the sparse CMap, read
// above, names it.
if let Some(NamedReading::Unread) = by_name {
return None;
}
// 5. The font's base encoding, for printable bytes
// 6. The font's base encoding, for printable bytes
// (the predefined tables spell out the control
// codes too, and those are dropped like they are
// by the fallback below)
Expand All @@ -2657,7 +2675,7 @@ pub(crate) fn extract_text_from_operand(
}
}
}
// 6. Printable single-byte fallback — a guess at a
// 7. Printable single-byte fallback — a guess at a
// code the CMap says nothing about, not at one it
// maps to no text
if b >= 0x20 && !control_destination {
Expand Down Expand Up @@ -6024,6 +6042,93 @@ mod tests {
);
}

/// A simple font's sparse ToUnicode CMap, once the program's own reading
/// has taken the primary role, still reads the codes that reading has
/// no entry for: the program names the glyph of `o` alone, and `c` and
/// `e` read as the CMap has them, not as the bytes `!` and `$`. Where
/// both read a code, the program's reading stays first.
#[test]
fn a_simple_fonts_sparse_cmap_reads_the_codes_the_program_does_not() {
const SPARSE_BFCHAR: &str = "<21> <0063>\n<22> <006F>\n<24> <0065>";
let mut names = vec![None; 0x25];
names[0x22] = Some("o");
assert_eq!(
decode_simple_font_string(
SPARSE_BFCHAR,
Some(sfnt_with_glyph_names(&names)),
None,
&[0x21, 0x22, 0x24, 0x24]
),
"coee"
);
names[0x21] = Some("x");
assert_eq!(
decode_simple_font_string(
SPARSE_BFCHAR,
Some(sfnt_with_glyph_names(&names)),
None,
&[0x21, 0x22, 0x24, 0x24]
),
"xoee"
);
}

/// A control destination left in that sparse CMap — the program does
/// not name the glyph, so nothing repaired it — marks the code like any
/// other CMap's would: it is not guessed to be `#` from its byte value.
#[test]
fn a_simple_fonts_sparse_cmap_marks_its_control_destination() {
let mut names = vec![None; 0x25];
names[0x22] = Some("o");
assert_eq!(
decode_simple_font_string(
"<21> <0063>\n<22> <006F>\n<23> <0003>\n<24> <0065>",
Some(sfnt_with_glyph_names(&names)),
None,
&[0x21, 0x22, 0x23, 0x24, 0x24]
),
"co\u{FFFD}ee"
);
}

/// The sparse CMap says what a code is, so it is read before a declared
/// base encoding and before a `/Differences` name that reads as nothing,
/// for the codes it maps: `<21>` is `c` under `/WinAnsiEncoding` (which
/// has `!` there) and under a Differences name no table can read. A
/// Differences name that does read keeps the code.
#[test]
fn a_simple_fonts_sparse_cmap_beats_a_base_encoding_and_unreadable_differences() {
const SPARSE_BFCHAR: &str = "<21> <0063>\n<22> <006F>";
let mut names = vec![None; 0x25];
names[0x22] = Some("o");
let read = |encoding: Object| {
decode_simple_font_string(
SPARSE_BFCHAR,
Some(sfnt_with_glyph_names(&names)),
Some(encoding),
&[0x21, 0x22],
)
};
assert_eq!(read(Object::Name(b"WinAnsiEncoding".to_vec())), "co");
let base_only = Object::Dictionary(lopdf::dictionary! {
"Type" => "Encoding",
"BaseEncoding" => "WinAnsiEncoding"
});
assert_eq!(read(base_only), "co");
let differences = |name: &[u8]| {
Object::Dictionary(lopdf::dictionary! {
"Type" => "Encoding",
"BaseEncoding" => "WinAnsiEncoding",
"Differences" => Object::Array(vec![
Object::Integer(0x21), Object::Name(name.to_vec()),
])
})
};
assert_eq!(read(differences(b"glyph9999")), "co");
// A name that reads stays the code's reading.
assert_eq!(read(differences(b"x")), "xo");
}

/// Codes 0x21..=0x2C are glyph indices; the ligature at 0x23 is mapped
/// to its own index, `<0003>`, in place of a character. The CMap lists
/// eight more codes than the strings below show, as a subset's CMap
Expand Down