PDF: title and author are written as UTF-16BE when they are not ASCII (#813)
Info strings were WinAnsi; PDF text strings are PDFDocEncoding or UTF-16BE. Printable ASCII stays a literal string; anything else is a <FEFF...> hex string. Info strings are capped at 1024 characters. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fc45b7f6d8
commit
0042240c05
|
|
@ -132,6 +132,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
|||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-12 | PDF export writes the document title and author as UTF-16BE when they are not ASCII, so viewers show accents, currency signs and non-Latin text correctly (#813) |
|
||||
| 2026-10-12 | File › Print selects a system printer, copies, duplex and grayscale, submits PDF jobs through craft-print on a worker and shows spooler errors; browser printing and PDF export remain available |
|
||||
| 2026-10-11 | Command-sequence fuzzing: random command sequences checked against undo/redo, XLSX round-trip and full recalculation; fixes for the table, legacy-array, spill, AutoSum, Watch Window and sheet-copy bugs it found |
|
||||
| 2026-10-11 | Password-protected workbooks open: GridCraft asks for the password and decrypts Agile and Standard encryption (#11) |
|
||||
|
|
|
|||
|
|
@ -516,8 +516,23 @@ fn run_length(data: &[u8]) -> Vec<u8> {
|
|||
out
|
||||
}
|
||||
|
||||
fn pdf_text_string(s: &str) -> String {
|
||||
pdf_string(s)
|
||||
/// Longest Info string written, in characters; longer values are cut on a character boundary.
|
||||
const MAX_INFO_CHARS: usize = 1024;
|
||||
|
||||
/// A PDF text string (document Info entries) for `text`: a literal `( … )` for printable ASCII,
|
||||
/// otherwise UTF-16BE with a byte-order mark as a hex string (ISO 32000-1 7.9.2.2).
|
||||
fn pdf_text_string(text: &str) -> String {
|
||||
let text: String = text.chars().take(MAX_INFO_CHARS).collect();
|
||||
if text.chars().all(|c| matches!(c, ' '..='~')) {
|
||||
return pdf_string(&text);
|
||||
}
|
||||
let mut s = String::with_capacity(text.len() * 4 + 6);
|
||||
s.push_str("<FEFF");
|
||||
for unit in text.encode_utf16() {
|
||||
let _ = write!(s, "{unit:04X}");
|
||||
}
|
||||
s.push('>');
|
||||
s
|
||||
}
|
||||
|
||||
impl PdfDoc {
|
||||
|
|
|
|||
|
|
@ -157,3 +157,43 @@ fn hostile_inputs_never_panic() {
|
|||
let pdf = d.finish();
|
||||
check_xref(&pdf);
|
||||
}
|
||||
|
||||
/// Decodes the `<FEFF…>` hex string following `key` in the file bytes.
|
||||
fn utf16_entry(pdf: &[u8], key: &str) -> String {
|
||||
let at = find(pdf, key.as_bytes()).unwrap() + key.len();
|
||||
let rest = std::str::from_utf8(&pdf[at..at + 9000.min(pdf.len() - at)]).unwrap_or("");
|
||||
let hex = rest.trim_start().strip_prefix("<FEFF").unwrap();
|
||||
let hex = &hex[..hex.find('>').unwrap()];
|
||||
let units: Vec<u16> = (0..hex.len()).step_by(4).map(|i| u16::from_str_radix(&hex[i..i + 4], 16).unwrap()).collect();
|
||||
String::from_utf16(&units).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_strings_with_non_ascii_are_utf16be() {
|
||||
let mut doc = PdfDoc::new();
|
||||
doc.add_page(200.0, 200.0);
|
||||
doc.info.title = "Übersicht €5 – “Q1” 日本 😀".into();
|
||||
doc.info.author = "Müller & Søn".into();
|
||||
let pdf = doc.finish();
|
||||
assert_eq!(utf16_entry(&pdf, "/Title"), "Übersicht €5 – “Q1” 日本 😀");
|
||||
assert_eq!(utf16_entry(&pdf, "/Author"), "Müller & Søn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii_info_strings_stay_literal_and_escaped() {
|
||||
let mut doc = PdfDoc::new();
|
||||
doc.add_page(200.0, 200.0);
|
||||
doc.info.title = r"Q1 (draft) a\b".into();
|
||||
let pdf = doc.finish();
|
||||
assert!(find(&pdf, br"/Title (Q1 \(draft\) a\\b)").is_some());
|
||||
assert!(find(&pdf, b"/Producer (GridCraft PDF)").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_strings_are_capped() {
|
||||
let mut doc = PdfDoc::new();
|
||||
doc.add_page(200.0, 200.0);
|
||||
doc.info.title = "日".repeat(100_000);
|
||||
let pdf = doc.finish();
|
||||
assert_eq!(utf16_entry(&pdf, "/Title").chars().count(), MAX_INFO_CHARS);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue