fix(fonts): remap misnamed TeXCMMathsSymbols glyphs to their true symbols (#165)

IntechOpen-family academic PDFs embed Computer Modern math symbol
subsets whose glyphs are misnamed after Latin lookalikes (equal →
/onequarter, plus → /thorn, parens → /eth //Thorn) — and the generated
ToUnicode faithfully propagates the wrong names, so formulas decode as
'S ¼ kB þ 1' instead of 'S = kB + 1'.

Remap the observed misnames, gated strictly on the TeXCMMathsSymbols
base font (subset prefix stripped) so genuine fractions and thorns in
text fonts are untouched. Known limitation: a sibling subset misnames
the slash as /onequarter too, so an occasional '/' renders as '=' —
still strictly better than the previous mojibake.

Affects 4 bench PDFs (028/031 +0.001-0.004 NID) and zero pdf-evals
snapshots.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-07-15 13:19:34 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 4aa2c0c208
commit c908e33b39
+42
View File
@@ -1187,10 +1187,37 @@ pub(crate) fn extract_text_from_operand(
})(); })();
result.map(|text| { result.map(|text| {
let text = clean_symbol_pua(text); let text = clean_symbol_pua(text);
let text = remap_texcm_math_symbols(text, base_font_name);
normalize_cp1252_controls(text, use_cp1252_fallback) normalize_cp1252_controls(text, use_cp1252_fallback)
}) })
} }
/// Fix a known producer bug in "TeXCMMathsSymbols" subset fonts (IntechOpen
/// and sibling academic pipelines): the Computer Modern symbol glyphs are
/// misnamed after Latin lookalikes (equal → /onequarter, plus → /thorn, …)
/// and the generated ToUnicode faithfully propagates the wrong names. The
/// remap applies only to text decoded from that font, keyed on the glyphs'
/// observed misnames.
fn remap_texcm_math_symbols(text: String, base_font_name: Option<&str>) -> String {
let is_texcm = base_font_name.is_some_and(|n| {
let n = n.rsplit_once('+').map_or(n, |(_, s)| s);
n.eq_ignore_ascii_case("TeXCMMathsSymbols")
});
if !is_texcm {
return text;
}
text.chars()
.map(|c| match c {
'¼' => '=',
'½' => '-',
'þ' => '+',
'ð' => '(',
'Þ' => ')',
_ => c,
})
.collect()
}
fn decode_single_byte_fallback(bytes: &[u8], use_cp1252_fallback: bool) -> String { fn decode_single_byte_fallback(bytes: &[u8], use_cp1252_fallback: bool) -> String {
bytes bytes
.iter() .iter()
@@ -1409,6 +1436,21 @@ fn score_text(text: &str) -> i32 {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[test]
fn texcm_math_symbols_remap() {
assert_eq!(
super::remap_texcm_math_symbols("S ¼ kB þ 1".into(), Some("EEKVNO+TeXCMMathsSymbols")),
"S = kB + 1"
);
// Other fonts keep their genuine fractions/thorns.
assert_eq!(
super::remap_texcm_math_symbols("¼ cup þorn".into(), Some("Times-Roman")),
"¼ cup þorn"
);
assert_eq!(super::remap_texcm_math_symbols("¼".into(), None), "¼");
}
use super::*; use super::*;
use lopdf::dictionary; use lopdf::dictionary;