Skip to main content

otf_pdf_content/
ops.rs

1//! Content-stream tokenizer (ISO 32000-2 §7.8.2): operands followed by an
2//! operator, with inline images (`BI … ID … EI`) read as one operation.
3
4use otf_pdf_syntax::lexer::Token;
5use otf_pdf_syntax::parser::Parser;
6use otf_pdf_syntax::{Dict, Error, Limits, Name, Object, PdfString, Result, Strictness};
7
8/// One operation: an operator and its operands.
9#[derive(Debug, Clone, PartialEq)]
10pub struct Operation {
11    /// The operator, e.g. `Tj` or `re`.
12    pub operator: Vec<u8>,
13    /// Its operands, in order. For `BI` (an inline image): the image
14    /// dictionary and the data as a string.
15    pub operands: Vec<Object>,
16}
17
18/// Operands kept before an operator; more is junk, and is dropped.
19const MAX_OPERANDS: usize = 4096;
20
21/// Parses a content stream into operations. Malformed operands are skipped
22/// (content streams in the wild are full of them), but limits still apply.
23pub fn parse_content(bytes: &[u8], limits: &Limits) -> Result<Vec<Operation>> {
24    let mut parser = Parser::new(bytes, 0, limits, Strictness::Lenient);
25    let mut ops = Vec::new();
26    let mut operands: Vec<Object> = Vec::new();
27    loop {
28        let save = parser.lexer().pos();
29        let token = match parser.lexer().next_token() {
30            Ok(t) => t,
31            // An unterminated string or hex string at the end: stop there.
32            Err(Error::UnexpectedEof) => break,
33            Err(e) => return Err(e),
34        };
35        match token {
36            Token::Eof => break,
37            Token::Keyword(b"BI") => {
38                limits.charge(1)?;
39                let pos = parser.lexer().pos();
40                let rest = bytes.get(pos..).unwrap_or_default();
41                let (dict, data, used) = inline_image(rest, limits);
42                parser.lexer().set_pos(pos + used);
43                ops.push(Operation {
44                    operator: b"BI".to_vec(),
45                    operands: vec![Object::Dict(dict), Object::String(PdfString::literal(data))],
46                });
47                operands.clear();
48            }
49            Token::Keyword(k) if !matches!(k, b"true" | b"false" | b"null") => {
50                limits.charge(1)?;
51                ops.push(Operation {
52                    operator: k.to_vec(),
53                    operands: std::mem::take(&mut operands),
54                });
55            }
56            // Stray closers: skip.
57            Token::ArrayEnd | Token::DictEnd | Token::BraceClose => {}
58            // PostScript-calculator braces only appear in Type 4 functions.
59            Token::BraceOpen => {}
60            _ => {
61                parser.lexer().set_pos(save);
62                match parser.parse_object() {
63                    Ok(obj) => {
64                        if operands.len() < MAX_OPERANDS {
65                            operands.push(obj);
66                        }
67                    }
68                    Err(e @ Error::Limit(_)) => return Err(e),
69                    Err(Error::UnexpectedEof) => break,
70                    Err(_) => {
71                        // Skip one byte past whatever could not be parsed.
72                        parser.lexer().set_pos(save + 1);
73                    }
74                }
75            }
76        }
77    }
78    Ok(ops)
79}
80
81/// Reads an inline image after `BI`: its dictionary up to `ID`, then the
82/// data up to `EI`. Returns the dictionary, the data and the bytes used.
83fn inline_image(bytes: &[u8], limits: &Limits) -> (Dict, Vec<u8>, usize) {
84    let mut parser = Parser::new(bytes, 0, limits, Strictness::Lenient);
85    let mut dict = Dict::new();
86    loop {
87        let save = parser.lexer().pos();
88        match parser.lexer().next_token() {
89            Ok(Token::Keyword(b"ID")) | Ok(Token::Eof) | Err(_) => break,
90            Ok(Token::Name(key)) => match parser.parse_object() {
91                Ok(value) => {
92                    dict.insert(key, value);
93                }
94                Err(_) => break,
95            },
96            Ok(_) => {
97                parser.lexer().set_pos(save + 1);
98            }
99        }
100    }
101    // One whitespace byte separates `ID` from the data.
102    let start = (parser.lexer().pos() + 1).min(bytes.len());
103    let data = bytes.get(start..).unwrap_or_default();
104    // `EI` delimited by whitespace and followed by text that looks like
105    // operators (binary data can contain "EI" by chance).
106    let end = (0..data.len().saturating_sub(1))
107        .find(|&i| {
108            data.get(i..i + 2) == Some(b"EI")
109                && (i == 0
110                    || data
111                        .get(i - 1)
112                        .is_some_and(|&b| otf_pdf_syntax::lexer::is_whitespace(b)))
113                && data
114                    .get(i + 2)
115                    .is_none_or(|&b| otf_pdf_syntax::lexer::is_whitespace(b))
116                && data
117                    .get(i + 2..(i + 12).min(data.len()))
118                    .unwrap_or_default()
119                    .iter()
120                    .all(|&b| b == b'\n' || b == b'\r' || b == b'\t' || (0x20..0x7f).contains(&b))
121        })
122        .unwrap_or(data.len());
123    let mut image = data.get(..end).unwrap_or_default().to_vec();
124    // Drop the whitespace before `EI`.
125    if image
126        .last()
127        .is_some_and(|&b| otf_pdf_syntax::lexer::is_whitespace(b))
128    {
129        image.pop();
130    }
131    (dict, image, (start + end + 2).min(bytes.len()))
132}
133
134/// The name operand at `i`, if it is one.
135pub(crate) fn name_at(operands: &[Object], i: usize) -> Option<&Name> {
136    match operands.get(i) {
137        Some(Object::Name(n)) => Some(n),
138        _ => None,
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    fn ops(input: &[u8]) -> Vec<Operation> {
147        parse_content(input, &Limits::default()).unwrap()
148    }
149
150    #[test]
151    fn operators_and_operands() {
152        let o = ops(b"q 1 0 0 1 10 20 cm BT /F1 12 Tf (Hi) Tj [(A) -50 (B)] TJ ET Q");
153        let names: Vec<_> = o
154            .iter()
155            .map(|op| String::from_utf8_lossy(&op.operator).into_owned())
156            .collect();
157        assert_eq!(names, ["q", "cm", "BT", "Tf", "Tj", "TJ", "ET", "Q"]);
158        assert_eq!(o[1].operands.len(), 6);
159        assert_eq!(o[3].operands[0], Object::Name(Name::from("F1")));
160        assert_eq!(o[5].operands[0].as_array().unwrap().len(), 3);
161    }
162
163    #[test]
164    fn inline_image() {
165        let o = ops(b"q BI /W 2 /H 1 /CS /G /BPC 8 ID \x00EI\xff EI Q");
166        assert_eq!(o.len(), 3);
167        assert_eq!(o[1].operator, b"BI");
168        assert_eq!(o[1].operands[0].as_dict().unwrap().get_int(b"W"), Some(2));
169        // "EI" inside the data is followed by binary, so it is not the end.
170        assert_eq!(o[1].operands[1].as_string(), Some(&b"\x00EI\xff"[..]));
171        assert_eq!(o[2].operator, b"Q");
172    }
173
174    #[test]
175    fn junk_is_skipped() {
176        let o = ops(b"1 2 ] >> } { m (unterminated");
177        assert_eq!(o.len(), 1);
178        assert_eq!(o[0].operator, b"m");
179        assert_eq!(o[0].operands.len(), 2);
180        assert!(ops(b"").is_empty());
181        assert!(ops(b"BI /W 1").len() == 1);
182    }
183
184    #[test]
185    fn keyword_objects_are_operands() {
186        let o = ops(b"true false null d0");
187        assert_eq!(
188            o[0].operands,
189            [Object::Bool(true), Object::Bool(false), Object::Null]
190        );
191    }
192}