Refs #347: encode the full WinAnsi 0x80-0x9F range (Š š Ž ž Œ œ Ÿ ...) (#546)

pdfcraft_fonts::win_ansi, edit's is_win_ansi_char and the xtask demo
encoder each carried a hand-written subset of WinAnsiEncoding's 0x80-0x9F
row (quotes, dashes, euro, trademark) and left out Š š Ž ž Œ œ Ÿ ƒ † ‡ ˆ
˜ ‰ ‹ ›. Text editing therefore treated š/ž as non-WinAnsi and sent them
to the Type3 fallback font (or refused them without craft-fonts), and
every other generator (forms, annotations, OCR layer, redaction overlay,
signatures, create) wrote them as '?'.

win_ansi now goes through a new win_ansi_byte, which reads the 0x80-0x9F
codes from the same ISO 32000-2 Annex D table the text extractor decodes
with, and is_win_ansi_char reuses it. xtask doesn't depend on
pdfcraft-fonts, so its demo encoder gets the full row with a pointer to
the shared table. č/ć are not in WinAnsi and still use the fallback.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-09 10:45:28 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent bd57377435
commit a593512700
4 changed files with 87 additions and 27 deletions
+35 -5
View File
@@ -351,20 +351,41 @@ pub fn pdf_string(s: &str) -> String {
}
/// Map text to WinAnsiEncoding bytes (unmappable characters become `?`).
///
/// Mirrors `pdfcraft_fonts::win_ansi` (ISO 32000-2 Annex D), which xtask doesn't depend on;
/// keep the 0x80–0x9F rows in step with `crates/fonts/src/encodings.rs`.
pub fn winansi(s: &str) -> Vec<u8> {
s.chars()
.map(|c| match c {
'\u{20}'..='\u{7e}' => c as u8,
'\u{a0}'..='\u{ff}' => c as u32 as u8,
'\u{2022}' => 0x95,
'\u{2013}' => 0x96,
'\u{2014}' => 0x97,
'\u{20ac}' => 0x80,
'\u{201a}' => 0x82,
'\u{0192}' => 0x83,
'\u{201e}' => 0x84,
'\u{2026}' => 0x85,
'\u{2020}' => 0x86,
'\u{2021}' => 0x87,
'\u{02c6}' => 0x88,
'\u{2030}' => 0x89,
'\u{0160}' => 0x8a,
'\u{2039}' => 0x8b,
'\u{0152}' => 0x8c,
'\u{017d}' => 0x8e,
'\u{2018}' => 0x91,
'\u{2019}' => 0x92,
'\u{201c}' => 0x93,
'\u{201d}' => 0x94,
'\u{2026}' => 0x85,
'\u{20ac}' => 0x80,
'\u{2022}' => 0x95,
'\u{2013}' => 0x96,
'\u{2014}' => 0x97,
'\u{02dc}' => 0x98,
'\u{2122}' => 0x99,
'\u{0161}' => 0x9a,
'\u{203a}' => 0x9b,
'\u{0153}' => 0x9c,
'\u{017e}' => 0x9e,
'\u{0178}' => 0x9f,
_ => b'?',
})
.collect()
@@ -415,4 +436,13 @@ mod tests {
assert_eq!(pdf_string("a(b)\\"), "(a\\(b\\)\\\\)");
assert_eq!(pdf_string("—"), "(\\227)");
}
#[test]
fn winansi_covers_the_0x80_to_0x9f_glyphs() {
// The 27 defined codes of ISO 32000-2 Annex D WinAnsiEncoding between 0x80 and 0x9F.
let high = "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ";
let want: Vec<u8> = (0x80..=0x9f).filter(|b| ![0x81, 0x8d, 0x8f, 0x90, 0x9d].contains(b)).collect();
assert_eq!(winansi(high), want);
assert_eq!(winansi("č"), b"?");
}
}