Merge pull request #814 from vangel76:fix/pdf-info-strings-utf16
This commit is contained in:
commit
be4d754ef4
|
|
@ -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 | `TEXT()` over a range and chart labels parse each number format code once instead of once per value (#806) |
|
||||
| 2026-10-12 | SUBTOTAL(109) over hidden rows recalculates after Row Height, AutoFit Row Height and Ungroup reveal rows (found by the command-sequence fuzz) |
|
||||
| 2026-10-12 | Page Layout view: drag the margin edges on the rulers to set the page margins (resize cursor, live preview, value tooltip, one undo step, Esc cancels) (#792) |
|
||||
|
|
|
|||
|
|
@ -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