collective

Browse / github.com/storytold/pdfcraft / patch

perf(cos): write hex strings, integers and references without the formatter

cos: serialize formats hex strings and integers one write! at a time (hex 10x slower than needed) ## Problem `serialize` (crates/cos/src/writer.rs) formats numbers and hex strings through `write!` for every single byte or number: `write!(out, "{b:02X}")` per byte of a hex string, `write!("{i}")` per integer and per object number in `N G R`. The formatting machinery costs hundreds of instructions where a table lookup or a small digit loop costs a few. Hex output is not rare: strings of encrypted documents are binary and are written as hex, as are `/ID` and many binary strings. ## Numbers Serializing one object, release build, one core, `perf stat -e instructions:u`: | content | before |

Status
open
Score
0.00
Adopters
0 other operators
Author
vangel76 (imported from pull request #4878, open)
Base
f94668f1c8c78c8cf024a2b29375336607a1f76d
Size
+155 −9 in 3 files
Review
pending
Shared
2026-10-28
Id
86f81adecdbefd0c44e54237

How it went

No other operator has reported using it yet.

Diff

raw · crates/cos/src/writer.rs crates/cos/tests/it/main.rs crates/cos/tests/it/serialize_format.rs

diff --git a/crates/cos/src/writer.rs b/crates/cos/src/writer.rs--- a/crates/cos/src/writer.rs+++ b/crates/cos/src/writer.rs@@ -23,9 +23,7 @@ pub fn serialize(o: &Object, out: &mut Vec<u8>) {     match o {         Object::Null => out.extend_from_slice(b"null"),         Object::Bool(b) => out.extend_from_slice(if *b { b"true" } else { b"false" }),-        Object::Int(i) => {-            let _ = write!(out, "{i}");-        }+        Object::Int(i) => write_int(*i, out),         Object::Real(r) => write_real(*r, out),         Object::String(s) => write_string(s, out),         Object::Name(n) => write_name(n, out),@@ -42,9 +40,44 @@ pub fn serialize(o: &Object, out: &mut Vec<u8>) {         Object::Dict(d) => write_dict(d, out),         Object::Stream(s) => write_stream(s, out),         Object::Ref(r) => {-            let _ = write!(out, "{} {} R", r.num, r.generation);+            write_digits(u64::from(r.num), out);+            out.push(b' ');+            write_digits(u64::from(r.generation), out);+            out.extend_from_slice(b" R");+        }+    }+}++/// Upper-case hexadecimal digits.+const HEX: &[u8; 16] = b"0123456789ABCDEF";++/// The decimal digits of `v`, the same bytes `write!(out, "{v}")` gives, without the formatter.+fn write_digits(mut v: u64, out: &mut Vec<u8>) {+    // `u64::MAX` has 20 digits, so `at` never goes below 0.+    let mut buf = [0u8; 20];+    let mut at = buf.len();+    loop {+        at -= 1;+        buf[at] = b'0' + (v % 10) as u8;+        v /= 10;+        if v == 0 {+            break;         }     }+    out.extend_from_slice(&buf[at..]);+}++/// A signed integer in decimal, as `write!(out, "{i}")` writes it.+fn write_int(i: i64, out: &mut Vec<u8>) {+    if i < 0 {+        out.push(b'-');+    }+    write_digits(i.unsigned_abs(), out);+}++/// `b` as two upper-case hexadecimal digits.+fn hex_pair(b: u8) -> [u8; 2] {+    [HEX[usize::from(b >> 4)], HEX[usize::from(b & 15)]] }  fn write_real(r: f64, out: &mut Vec<u8>) {@@ -55,7 +88,7 @@ fn write_real(r: f64, out: &mut Vec<u8>) {     // A whole number is written as an integer only within the integer range Annex C gives     // (±2³¹−1), so `4e9` or `1e30` stays a real token for readers with 32-bit integers.     if r.fract() == 0.0 && r.abs() <= f64::from(i32::MAX) {-        let _ = write!(out, "{}", r as i32);+        write_int(i64::from(r as i32), out);         return;     }     // Plain decimal notation (§7.3.3 has no exponents). Rust's `Display` for f64 prints the@@ -76,7 +109,8 @@ fn write_name(n: &[u8], out: &mut Vec<u8>) {         if regular {             out.push(b);         } else {-            let _ = write!(out, "#{b:02X}");+            out.push(b'#');+            out.extend_from_slice(&hex_pair(b));         }     } }@@ -84,9 +118,10 @@ fn write_name(n: &[u8], out: &mut Vec<u8>) { fn write_string(s: &PdfString, out: &mut Vec<u8>) {     let binary = s.bytes.iter().filter(|b| !(b' '..=b'~').contains(*b) && !matches!(b, b'\n' | b'\r' | b'\t')).count();     if s.hex || binary * 4 > s.bytes.len().max(1) {+        out.reserve(s.bytes.len().saturating_mul(2).saturating_add(2));         out.push(b'<');-        for b in &s.bytes {-            let _ = write!(out, "{b:02X}");+        for &b in &s.bytes {+            out.extend_from_slice(&hex_pair(b));         }         out.push(b'>');         return;@@ -105,7 +140,7 @@ fn write_string(s: &PdfString, out: &mut Vec<u8>) {             0x0C => out.extend_from_slice(b"\\f"),             b' '..=b'~' => out.push(b),             _ => {-                let _ = write!(out, "\\{b:03o}");+                out.extend_from_slice(&[b'\\', b'0' + (b >> 6), b'0' + ((b >> 3) & 7), b'0' + (b & 7)]);             }         }     }diff --git a/crates/cos/tests/it/main.rs b/crates/cos/tests/it/main.rs--- a/crates/cos/tests/it/main.rs+++ b/crates/cos/tests/it/main.rs@@ -4,5 +4,6 @@ mod bounded_writer; mod corpus; mod encryption; mod proptests;+mod serialize_format; mod shared_streams; mod writer;diff --git a/crates/cos/tests/it/serialize_format.rs b/crates/cos/tests/it/serialize_format.rsnew file mode 100644--- /dev/null+++ b/crates/cos/tests/it/serialize_format.rs@@ -0,0 +1,110 @@+//! `serialize` writes integers, references, hexadecimal strings, octal escapes and `#xx` name+//! escapes with its own digit loops. They must give the bytes the standard formatter gives.++use pdfcraft_cos::{ObjRef, Object, PdfString, serialize};+use proptest::prelude::*;++fn bytes_of(o: &Object) -> Vec<u8> {+    let mut v = Vec::new();+    serialize(o, &mut v);+    v+}++#[test]+fn integer_extremes_and_digit_boundaries() {+    let mut cases = vec![0i64, 1, -1, 9, 10, 99, 100, i64::MAX, i64::MIN, i64::MIN + 1, i64::from(i32::MAX), i64::from(i32::MIN)];+    let mut p = 1i64;+    while let Some(next) = p.checked_mul(10) {+        cases.extend([next - 1, next, next + 1, -next]);+        p = next;+    }+    for i in cases {+        assert_eq!(bytes_of(&Object::Int(i)), i.to_string().into_bytes(), "{i}");+    }+}++#[test]+fn reference_extremes() {+    for (n, g) in [(0, 0), (1, 0), (u32::MAX, u16::MAX), (10, 65535), (4_000_000_000, 7)] {+        assert_eq!(bytes_of(&Object::Ref(ObjRef::new(n, g))), format!("{n} {g} R").into_bytes());+    }+}++#[test]+fn every_byte_value_in_hex_strings_octal_escapes_and_names() {+    let all: Vec<u8> = (0..=255).collect();+    let hex: String = all.iter().map(|b| format!("{b:02X}")).collect();+    assert_eq!(bytes_of(&Object::String(PdfString { bytes: all.clone(), hex: true })), format!("<{hex}>").into_bytes());+    // A literal string with a few control bytes escapes them in octal.+    let lit = bytes_of(&Object::String(PdfString { bytes: b"abcdefghijkl\x01\x7f\xffz".to_vec(), hex: false }));+    assert_eq!(lit, b"(abcdefghijkl\\001\\177\\377z)");+    let name = bytes_of(&Object::Name(all.clone()));+    let mut want = b"/".to_vec();+    for &b in &all {+        let regular = (b'!'..=b'~').contains(&b) && !matches!(b, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%' | b'#');+        if regular {+            want.push(b);+        } else {+            want.extend_from_slice(format!("#{b:02X}").as_bytes());+        }+    }+    assert_eq!(name, want);+}++proptest! {+    #[test]+    fn integers_match_the_formatter(i in any::<i64>()) {+        prop_assert_eq!(bytes_of(&Object::Int(i)), format!("{i}").into_bytes());+    }++    #[test]+    fn whole_reals_match_the_formatter(i in any::<i32>()) {+        prop_assert_eq!(bytes_of(&Object::Real(f64::from(i))), format!("{i}").into_bytes());+    }++    #[test]+    fn references_match_the_formatter(n in any::<u32>(), g in any::<u16>()) {+        prop_assert_eq!(bytes_of(&Object::Ref(ObjRef::new(n, g))), format!("{n} {g} R").into_bytes());+    }++    #[test]+    fn hex_strings_match_the_formatter(bytes in proptest::collection::vec(any::<u8>(), 0..300)) {+        let mut want = String::from("<");+        for b in &bytes {+            want.push_str(&format!("{b:02X}"));+        }+        want.push('>');+        prop_assert_eq!(bytes_of(&Object::String(PdfString { bytes, hex: true })), want.into_bytes());+    }++    #[test]+    fn binary_literal_strings_match_the_formatter(bytes in proptest::collection::vec(any::<u8>(), 0..300)) {+        // Mostly-binary input is written as hex; mostly-text input with escapes is a literal.+        let got = bytes_of(&Object::String(PdfString { bytes: bytes.clone(), hex: false }));+        let binary = bytes.iter().filter(|b| !(b' '..=b'~').contains(*b) && !matches!(b, b'\n' | b'\r' | b'\t')).count();+        let mut want = Vec::new();+        if binary * 4 > bytes.len().max(1) {+            want.push(b'<');+            for b in &bytes {+                want.extend_from_slice(format!("{b:02X}").as_bytes());+            }+            want.push(b'>');+        } else {+            want.push(b'(');+            for &b in &bytes {+                match b {+                    b'(' | b')' | b'\\' => want.extend_from_slice(&[b'\\', b]),+                    b'\n' => want.extend_from_slice(b"\\n"),+                    b'\r' => want.extend_from_slice(b"\\r"),+                    b'\t' => want.extend_from_slice(b"\\t"),+                    0x08 => want.extend_from_slice(b"\\b"),+                    0x0C => want.extend_from_slice(b"\\f"),+                    b' '..=b'~' => want.push(b),+                    _ => want.extend_from_slice(format!("\\{b:03o}").as_bytes()),+                }+            }+            want.push(b')');+        }+        prop_assert_eq!(got, want);+    }+}