use lopdf::Object;
use lopdf::content::Content;
pub fn canonical(content: &[u8]) -> Option<Vec<u8>> {
let before = Content::decode_strict(content).ok()?;
if before.operations.iter().any(|op| op.operator == "BI") {
return None;
}
let out = Tokens::new(content).canonical()?;
let after = Content::decode_strict(&out).ok()?;
let same = before.operations.len() == after.operations.len()
&& before
.operations
.iter()
.zip(&after.operations)
.all(|(a, b)| {
a.operator == b.operator
&& a.operands.len() == b.operands.len()
&& a.operands
.iter()
.zip(&b.operands)
.all(|(x, y)| same_operand(x, y))
});
same.then_some(out)
}
fn same_operand(a: &Object, b: &Object) -> bool {
match (number(a), number(b)) {
(Some(x), Some(y)) => x == y,
_ => format!("{a:?}") == format!("{b:?}"),
}
}
fn number(o: &Object) -> Option<f64> {
match o {
Object::Integer(i) => Some(*i as f64),
Object::Real(r) => Some(f64::from(*r)),
_ => None,
}
}
fn is_whitespace(b: u8) -> bool {
matches!(b, b'\0' | b'\t' | b'\n' | 0x0C | b'\r' | b' ')
}
fn is_delimiter(b: u8) -> bool {
matches!(
b,
b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
)
}
fn is_regular(b: u8) -> bool {
!is_whitespace(b) && !is_delimiter(b)
}
struct Tokens<'a> {
src: &'a [u8],
pos: usize,
out: Vec<u8>,
open: bool,
}
impl<'a> Tokens<'a> {
fn new(src: &'a [u8]) -> Self {
Tokens {
src,
pos: 0,
out: Vec::with_capacity(src.len()),
open: false,
}
}
fn canonical(mut self) -> Option<Vec<u8>> {
while self.pos < self.src.len() {
self.step(self.src[self.pos])?;
}
Some(self.out)
}
fn step(&mut self, b: u8) -> Option<()> {
match b {
_ if is_whitespace(b) => self.pos += 1,
b'%' => self.skip_comment(),
b'(' => self.literal_string()?,
b'<' => self.angle()?,
b'/' => self.name(),
_ if is_delimiter(b) => self.delimiter(b)?,
_ => self.regular(),
}
Some(())
}
fn emit(&mut self, token: &[u8], regular: bool) {
if regular && self.open {
self.out.push(b' ');
}
self.out.extend_from_slice(token);
self.open = regular;
}
fn skip_comment(&mut self) {
while self.pos < self.src.len() && !matches!(self.src[self.pos], b'\n' | b'\r') {
self.pos += 1;
}
}
fn literal_string(&mut self) -> Option<()> {
let start = self.pos;
let mut depth = 0usize;
loop {
let b = *self.src.get(self.pos)?;
self.pos += 1;
match b {
b'\\' => self.pos += 1,
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
}
let token = self.src[start..self.pos.min(self.src.len())].to_vec();
self.emit(&token, false);
Some(())
}
fn angle(&mut self) -> Option<()> {
if self.src.get(self.pos + 1) == Some(&b'<') {
self.pos += 2;
self.emit(b"<<", false);
return Some(());
}
let mut token = vec![b'<'];
self.pos += 1;
loop {
let b = *self.src.get(self.pos)?;
self.pos += 1;
if b == b'>' {
break;
}
if !is_whitespace(b) {
token.push(b);
}
}
token.push(b'>');
self.emit(&token, false);
Some(())
}
fn name(&mut self) {
let start = self.pos;
self.pos += 1;
while self.pos < self.src.len() && is_regular(self.src[self.pos]) {
self.pos += 1;
}
let token = self.src[start..self.pos].to_vec();
self.emit(&token, false);
self.open = true;
}
fn delimiter(&mut self, b: u8) -> Option<()> {
let token: &[u8] = match b {
b'[' => b"[",
b']' => b"]",
b'{' => b"{",
b'}' => b"}",
b'>' if self.src.get(self.pos + 1) == Some(&b'>') => b">>",
_ => return None,
};
self.pos += token.len();
self.emit(token, false);
Some(())
}
fn regular(&mut self) {
let start = self.pos;
while self.pos < self.src.len() && is_regular(self.src[self.pos]) {
self.pos += 1;
}
let token = &self.src[start..self.pos];
let token = canonical_number(token).unwrap_or_else(|| token.to_vec());
self.emit(&token, true);
}
}
fn canonical_number(token: &[u8]) -> Option<Vec<u8>> {
let (negative, digits) = match token.first()? {
b'-' => (true, &token[1..]),
b'+' => (false, &token[1..]),
_ => (false, token),
};
let dots = digits.iter().filter(|&&b| b == b'.').count();
if digits.is_empty() || dots > 1 || !digits.iter().all(|b| b.is_ascii_digit() || *b == b'.') {
return None;
}
let (int, frac) = match digits.iter().position(|&b| b == b'.') {
Some(i) => (&digits[..i], &digits[i + 1..]),
None => (digits, &digits[digits.len()..]),
};
let int = &int[int.iter().take_while(|&&b| b == b'0').count()..];
let frac = &frac[..frac.len() - frac.iter().rev().take_while(|&&b| b == b'0').count()];
let mut out = Vec::with_capacity(token.len());
if negative && !(int.is_empty() && frac.is_empty()) {
out.push(b'-');
}
if int.is_empty() && frac.is_empty() {
out.push(b'0');
} else {
out.extend_from_slice(int);
if !frac.is_empty() {
out.push(b'.');
out.extend_from_slice(frac);
}
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn canon(s: &str) -> String {
String::from_utf8(canonical(s.as_bytes()).expect("canonical")).unwrap()
}
#[test]
fn whitespace_comments_and_numbers_are_normalized() {
assert_eq!(
canon("q\r\n 1.50000 0.000 +0 -0.0 10.00 20.5000 cm % comment\n/Im1 Do\nQ"),
"q 1.5 0 0 0 10 20.5 cm/Im1 Do Q"
);
assert_eq!(
canon("BT /F1 12 Tf ( a\\)(b) ) Tj [ (x) -20 <41 42> ] TJ ET"),
"BT/F1 12 Tf( a\\)(b) )Tj[(x)-20<4142>]TJ ET"
);
assert_eq!(
canon("/Span << /MCID 3 >> BDC EMC"),
"/Span<</MCID 3>>BDC EMC"
);
assert_eq!(canon(".5 -.25 007.10 -0 w"), ".5 -.25 7.1 0 w");
}
#[test]
fn unparseable_and_inline_image_streams_are_left_alone() {
assert!(canonical(b"-. 0 Td").is_none());
assert!(canonical(b"(unterminated").is_none());
assert!(canonical(b"BI /W 1 /H 1 /CS /G /BPC 8 ID \x00 EI").is_none());
assert!(canonical(b"").is_some());
}
#[test]
fn number_edge_cases() {
let n = |s: &str| canonical_number(s.as_bytes()).map(|v| String::from_utf8(v).unwrap());
assert_eq!(n("+12"), Some("12".into()));
assert_eq!(n("-000"), Some("0".into()));
assert_eq!(n("100"), Some("100".into()));
assert_eq!(n("1.0"), Some("1".into()));
assert_eq!(n("1.2.3"), None);
assert_eq!(n("Tj"), None);
assert_eq!(n("-"), None);
}
}