fix(annot): percent-encode non-ASCII bytes written into link /URI

A link annotation whose target contains non-ASCII characters (for example https://example.com/ação) was written into the PDF /URI string as raw UTF-8 bytes, but URIs in PDF are 7-bit ASCII strings (ISO 32000 §12.6.4.8). The saved file then carried an out-of-spec URI that strict readers can reject or misparse.

Percent-encode every byte outside the printable ASCII range when writing the /URI entry. Bytes already inside 0x21..=0x7E pass through unchanged, so existing percent-escapes like %20 are preserved.

Adds a regression test that round-trips a non-ASCII URL and an already-escaped URL through save/reload and asserts the stored URIs are pure ASCII.

Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
This commit is contained in:
Charan Rathore
2026-10-16 01:55:31 +05:30
committed by GitHub
parent 13e3154ab3
commit 46ebc6dcf7
+14 -1
View File
@@ -178,6 +178,19 @@ fn write_style(d: &mut Dict, s: &LinkStyle) {
d.set(b"H".to_vec(), Object::name(s.highlight.code()));
}
/// Bytes of `u` as a 7-bit ASCII URI string (§12.6.4.8).
fn uri_bytes(u: &str) -> Vec<u8> {
let mut out = Vec::with_capacity(u.len());
for &b in u.as_bytes() {
if (0x21..=0x7e).contains(&b) {
out.push(b);
} else {
out.extend_from_slice(format!("%{b:02X}").as_bytes());
}
}
out
}
fn write_action(doc: &Document, d: &mut Dict, a: &LinkAction) -> Result<(), AnnotError> {
match a {
LinkAction::Page(p) => {
@@ -194,7 +207,7 @@ fn write_action(doc: &Document, d: &mut Dict, a: &LinkAction) -> Result<(), Anno
let mut act = Dict::new();
act.set(b"S".to_vec(), Object::name("URI"));
// URIs are 7-bit ASCII strings (§12.6.4.8).
act.set(b"URI".to_vec(), PdfString::literal(u.as_bytes().to_vec()));
act.set(b"URI".to_vec(), PdfString::literal(uri_bytes(u)));
d.set(b"A".to_vec(), Object::Dict(act));
}
LinkAction::Other(_) => {}