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
@@ -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)]); } } }@@ -4,5 +4,6 @@ mod bounded_writer; mod corpus; mod encryption; mod proptests;+mod serialize_format; mod shared_streams; mod writer;new file mode 100644@@ -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);+ }+}