Skip to main content

pdfplumber_parse/
tokenizer.rs

1//! Content stream tokenizer for PDF operator/operand parsing.
2//!
3//! Parses raw PDF content stream bytes into a sequence of [`Operator`]s,
4//! each carrying its [`Operand`] arguments. This is the foundation for
5//! the content stream interpreter.
6
7use crate::error::BackendError;
8
9/// A PDF content stream operand value.
10#[derive(Debug, Clone, PartialEq)]
11pub enum Operand {
12    /// Integer number (e.g., `42`, `-7`).
13    Integer(i64),
14    /// Real (floating-point) number (e.g., `3.14`, `.5`).
15    Real(f64),
16    /// Name object (e.g., `/F1`, `/DeviceRGB`). Stored without the leading `/`.
17    Name(String),
18    /// Literal string delimited by parentheses, stored as raw bytes.
19    LiteralString(Vec<u8>),
20    /// Hexadecimal string delimited by angle brackets, stored as decoded bytes.
21    HexString(Vec<u8>),
22    /// Array of operands (e.g., `[1 2 3]`).
23    Array(Vec<Operand>),
24    /// Boolean value (`true` or `false`).
25    Boolean(bool),
26    /// The null object.
27    Null,
28}
29
30/// A PDF content stream operator with its preceding operands.
31#[derive(Debug, Clone, PartialEq)]
32pub struct Operator {
33    /// Operator name (e.g., `"BT"`, `"Tf"`, `"Tj"`, `"m"`).
34    pub name: String,
35    /// Operands that preceded this operator on the operand stack.
36    pub operands: Vec<Operand>,
37}
38
39/// Inline image data captured from BI...ID...EI sequences.
40#[derive(Debug, Clone, PartialEq)]
41pub struct InlineImageData {
42    /// Dictionary entries between BI and ID as key-value pairs.
43    pub dict: Vec<(String, Operand)>,
44    /// Raw image data bytes between ID and EI.
45    pub data: Vec<u8>,
46}
47
48/// Parse PDF content stream bytes into a sequence of operators.
49///
50/// Each operator collects the operands that preceded it on the operand stack.
51/// Comments (% to end of line) are stripped. Inline images (BI/ID/EI) are
52/// handled as a special case.
53///
54/// # Errors
55///
56/// Returns [`BackendError::Interpreter`] for malformed content streams.
57pub fn tokenize(input: &[u8]) -> Result<Vec<Operator>, BackendError> {
58    let mut ops = Vec::new();
59    let mut operand_stack: Vec<Operand> = Vec::new();
60    let mut pos = 0;
61
62    while pos < input.len() {
63        skip_whitespace_and_comments(input, &mut pos);
64        if pos >= input.len() {
65            break;
66        }
67
68        let b = input[pos];
69
70        match b {
71            // Literal string
72            b'(' => {
73                let s = parse_literal_string(input, &mut pos)?;
74                operand_stack.push(Operand::LiteralString(s));
75            }
76            // Hex string
77            b'<' => {
78                if pos + 1 < input.len() && input[pos + 1] == b'<' {
79                    // << is a dictionary start — not valid in content streams as operand,
80                    // but handle gracefully by treating as unknown token
81                    return Err(BackendError::Interpreter(
82                        "unexpected '<<' in content stream".to_string(),
83                    ));
84                }
85                let s = parse_hex_string(input, &mut pos)?;
86                operand_stack.push(Operand::HexString(s));
87            }
88            // Array start
89            b'[' => {
90                pos += 1; // skip '['
91                let arr = parse_array(input, &mut pos)?;
92                operand_stack.push(Operand::Array(arr));
93            }
94            // Name
95            b'/' => {
96                let name = parse_name(input, &mut pos);
97                operand_stack.push(Operand::Name(name));
98            }
99            // Number (digit, sign, or decimal point)
100            b'0'..=b'9' | b'+' | b'-' | b'.' => {
101                let num = parse_number(input, &mut pos)?;
102                operand_stack.push(num);
103            }
104            // Keyword (operator, boolean, null)
105            b'a'..=b'z' | b'A'..=b'Z' | b'*' | b'\'' | b'"' => {
106                let keyword = parse_keyword(input, &mut pos);
107                match keyword.as_str() {
108                    "true" => operand_stack.push(Operand::Boolean(true)),
109                    "false" => operand_stack.push(Operand::Boolean(false)),
110                    "null" => operand_stack.push(Operand::Null),
111                    "BI" => {
112                        // Inline image: parse BI <dict> ID <data> EI
113                        let (dict, data) = parse_inline_image(input, &mut pos)?;
114                        ops.push(Operator {
115                            name: "BI".to_string(),
116                            operands: vec![
117                                Operand::Array(
118                                    dict.into_iter()
119                                        .flat_map(|(k, v)| vec![Operand::Name(k), v])
120                                        .collect(),
121                                ),
122                                Operand::LiteralString(data),
123                            ],
124                        });
125                    }
126                    _ => {
127                        // It's an operator
128                        ops.push(Operator {
129                            name: keyword,
130                            operands: std::mem::take(&mut operand_stack),
131                        });
132                    }
133                }
134            }
135            // Array end — shouldn't appear at top level
136            b']' => {
137                return Err(BackendError::Interpreter(
138                    "unexpected ']' outside array".to_string(),
139                ));
140            }
141            _ => {
142                // Skip unknown bytes
143                pos += 1;
144            }
145        }
146    }
147
148    Ok(ops)
149}
150
151/// Returns `true` if `b` is a PDF whitespace character.
152fn is_whitespace(b: u8) -> bool {
153    matches!(b, b' ' | b'\t' | b'\r' | b'\n' | 0x0C | 0x00)
154}
155
156/// Returns `true` if `b` is a PDF delimiter character.
157fn is_delimiter(b: u8) -> bool {
158    matches!(
159        b,
160        b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
161    )
162}
163
164/// Skip whitespace and comments.
165fn skip_whitespace_and_comments(input: &[u8], pos: &mut usize) {
166    while *pos < input.len() {
167        if is_whitespace(input[*pos]) {
168            *pos += 1;
169        } else if input[*pos] == b'%' {
170            // Comment — skip to end of line
171            while *pos < input.len() && input[*pos] != b'\n' && input[*pos] != b'\r' {
172                *pos += 1;
173            }
174        } else {
175            break;
176        }
177    }
178}
179
180/// Parse a literal string `(...)` with balanced parentheses and escape sequences.
181fn parse_literal_string(input: &[u8], pos: &mut usize) -> Result<Vec<u8>, BackendError> {
182    debug_assert_eq!(input[*pos], b'(');
183    *pos += 1; // skip opening '('
184
185    let mut result = Vec::new();
186    let mut depth = 1u32;
187
188    while *pos < input.len() {
189        let b = input[*pos];
190        match b {
191            b'(' => {
192                depth += 1;
193                result.push(b'(');
194                *pos += 1;
195            }
196            b')' => {
197                depth -= 1;
198                if depth == 0 {
199                    *pos += 1; // skip closing ')'
200                    return Ok(result);
201                }
202                result.push(b')');
203                *pos += 1;
204            }
205            b'\\' => {
206                *pos += 1;
207                if *pos >= input.len() {
208                    return Err(BackendError::Interpreter(
209                        "unterminated escape in literal string".to_string(),
210                    ));
211                }
212                let escaped = input[*pos];
213                match escaped {
214                    b'n' => result.push(b'\n'),
215                    b'r' => result.push(b'\r'),
216                    b't' => result.push(b'\t'),
217                    b'b' => result.push(0x08),
218                    b'f' => result.push(0x0C),
219                    b'(' => result.push(b'('),
220                    b')' => result.push(b')'),
221                    b'\\' => result.push(b'\\'),
222                    b'\r' => {
223                        // Backslash + CR (or CR+LF) = line continuation
224                        *pos += 1;
225                        if *pos < input.len() && input[*pos] == b'\n' {
226                            *pos += 1;
227                        }
228                        continue;
229                    }
230                    b'\n' => {
231                        // Backslash + LF = line continuation
232                        *pos += 1;
233                        continue;
234                    }
235                    b'0'..=b'7' => {
236                        // Octal escape (1-3 digits)
237                        let mut val = escaped - b'0';
238                        for _ in 0..2 {
239                            if *pos + 1 < input.len()
240                                && input[*pos + 1] >= b'0'
241                                && input[*pos + 1] <= b'7'
242                            {
243                                *pos += 1;
244                                val = val * 8 + (input[*pos] - b'0');
245                            } else {
246                                break;
247                            }
248                        }
249                        result.push(val);
250                        *pos += 1;
251                        continue;
252                    }
253                    _ => {
254                        // Unknown escape — just include the character
255                        result.push(escaped);
256                    }
257                }
258                *pos += 1;
259            }
260            _ => {
261                result.push(b);
262                *pos += 1;
263            }
264        }
265    }
266
267    Err(BackendError::Interpreter(
268        "unterminated literal string".to_string(),
269    ))
270}
271
272/// Parse a hex string `<...>`.
273fn parse_hex_string(input: &[u8], pos: &mut usize) -> Result<Vec<u8>, BackendError> {
274    debug_assert_eq!(input[*pos], b'<');
275    *pos += 1; // skip '<'
276
277    let mut hex_chars = Vec::new();
278    while *pos < input.len() {
279        let b = input[*pos];
280        if b == b'>' {
281            *pos += 1; // skip '>'
282            break;
283        }
284        if is_whitespace(b) {
285            *pos += 1;
286            continue;
287        }
288        hex_chars.push(b);
289        *pos += 1;
290    }
291
292    // If odd number of hex digits, append a trailing 0
293    if hex_chars.len() % 2 != 0 {
294        hex_chars.push(b'0');
295    }
296
297    let mut result = Vec::with_capacity(hex_chars.len() / 2);
298    for chunk in hex_chars.chunks(2) {
299        let hi = hex_digit(chunk[0])?;
300        let lo = hex_digit(chunk[1])?;
301        result.push((hi << 4) | lo);
302    }
303
304    Ok(result)
305}
306
307/// Convert a hex digit character to its value (0-15).
308fn hex_digit(b: u8) -> Result<u8, BackendError> {
309    match b {
310        b'0'..=b'9' => Ok(b - b'0'),
311        b'a'..=b'f' => Ok(b - b'a' + 10),
312        b'A'..=b'F' => Ok(b - b'A' + 10),
313        _ => Err(BackendError::Interpreter(format!(
314            "invalid hex digit: {:?}",
315            b as char
316        ))),
317    }
318}
319
320/// Parse an array until `]`. Assumes `[` already consumed.
321fn parse_array(input: &[u8], pos: &mut usize) -> Result<Vec<Operand>, BackendError> {
322    let mut elements = Vec::new();
323
324    loop {
325        skip_whitespace_and_comments(input, pos);
326        if *pos >= input.len() {
327            return Err(BackendError::Interpreter("unterminated array".to_string()));
328        }
329
330        if input[*pos] == b']' {
331            *pos += 1; // skip ']'
332            return Ok(elements);
333        }
334
335        let b = input[*pos];
336        match b {
337            b'(' => {
338                let s = parse_literal_string(input, pos)?;
339                elements.push(Operand::LiteralString(s));
340            }
341            b'<' => {
342                let s = parse_hex_string(input, pos)?;
343                elements.push(Operand::HexString(s));
344            }
345            b'[' => {
346                *pos += 1;
347                let arr = parse_array(input, pos)?;
348                elements.push(Operand::Array(arr));
349            }
350            b'/' => {
351                let name = parse_name(input, pos);
352                elements.push(Operand::Name(name));
353            }
354            b'0'..=b'9' | b'+' | b'-' | b'.' => {
355                let num = parse_number(input, pos)?;
356                elements.push(num);
357            }
358            b'a'..=b'z' | b'A'..=b'Z' => {
359                let keyword = parse_keyword(input, pos);
360                match keyword.as_str() {
361                    "true" => elements.push(Operand::Boolean(true)),
362                    "false" => elements.push(Operand::Boolean(false)),
363                    "null" => elements.push(Operand::Null),
364                    _ => {
365                        // In TJ arrays, operators don't appear — treat as name-like
366                        elements.push(Operand::Name(keyword));
367                    }
368                }
369            }
370            _ => {
371                return Err(BackendError::Interpreter(format!(
372                    "unexpected byte in array: 0x{:02X}",
373                    b
374                )));
375            }
376        }
377    }
378}
379
380/// Parse a `/Name` token. Assumes current byte is `/`.
381fn parse_name(input: &[u8], pos: &mut usize) -> String {
382    debug_assert_eq!(input[*pos], b'/');
383    *pos += 1; // skip '/'
384
385    let start = *pos;
386    while *pos < input.len() && !is_whitespace(input[*pos]) && !is_delimiter(input[*pos]) {
387        *pos += 1;
388    }
389
390    // Handle #XX hex escapes in names
391    let raw = &input[start..*pos];
392    let mut name = Vec::with_capacity(raw.len());
393    let mut i = 0;
394    while i < raw.len() {
395        if raw[i] == b'#' && i + 2 < raw.len() {
396            if let (Ok(hi), Ok(lo)) = (hex_digit(raw[i + 1]), hex_digit(raw[i + 2])) {
397                name.push((hi << 4) | lo);
398                i += 3;
399                continue;
400            }
401        }
402        name.push(raw[i]);
403        i += 1;
404    }
405
406    String::from_utf8_lossy(&name).into_owned()
407}
408
409/// Parse a number (integer or real).
410fn parse_number(input: &[u8], pos: &mut usize) -> Result<Operand, BackendError> {
411    let start = *pos;
412    let mut has_dot = false;
413
414    // Sign
415    if *pos < input.len() && (input[*pos] == b'+' || input[*pos] == b'-') {
416        *pos += 1;
417    }
418
419    // Digits and decimal point
420    while *pos < input.len() {
421        let b = input[*pos];
422        if b == b'.' {
423            if has_dot {
424                break; // second dot — stop
425            }
426            has_dot = true;
427            *pos += 1;
428        } else if b.is_ascii_digit() {
429            *pos += 1;
430        } else {
431            break;
432        }
433    }
434
435    let token = &input[start..*pos];
436    let s = std::str::from_utf8(token)
437        .map_err(|_| BackendError::Interpreter("invalid UTF-8 in number token".to_string()))?;
438
439    if has_dot {
440        let val: f64 = s
441            .parse()
442            .map_err(|_| BackendError::Interpreter(format!("invalid real number: {s}")))?;
443        Ok(Operand::Real(val))
444    } else {
445        let val: i64 = s
446            .parse()
447            .map_err(|_| BackendError::Interpreter(format!("invalid integer: {s}")))?;
448        Ok(Operand::Integer(val))
449    }
450}
451
452/// Parse a keyword (alphabetic + `*` + `'` + `"`).
453fn parse_keyword(input: &[u8], pos: &mut usize) -> String {
454    let start = *pos;
455    while *pos < input.len() {
456        let b = input[*pos];
457        if b.is_ascii_alphabetic() || b == b'*' || b == b'\'' || b == b'"' {
458            *pos += 1;
459        } else {
460            break;
461        }
462    }
463    String::from_utf8_lossy(&input[start..*pos]).into_owned()
464}
465
466/// Inline image dictionary entries: key-value pairs.
467type InlineImageDict = Vec<(String, Operand)>;
468
469/// Parse inline image data: `BI <dict entries> ID <data> EI`.
470/// Called after `BI` keyword has been consumed.
471fn parse_inline_image(
472    input: &[u8],
473    pos: &mut usize,
474) -> Result<(InlineImageDict, Vec<u8>), BackendError> {
475    // Parse dictionary entries until ID keyword
476    let mut dict = Vec::new();
477
478    loop {
479        skip_whitespace_and_comments(input, pos);
480        if *pos >= input.len() {
481            return Err(BackendError::Interpreter(
482                "unterminated inline image (missing ID)".to_string(),
483            ));
484        }
485
486        // Check for ID keyword
487        if *pos + 1 < input.len()
488            && input[*pos] == b'I'
489            && input[*pos + 1] == b'D'
490            && (*pos + 2 >= input.len() || is_whitespace(input[*pos + 2]))
491        {
492            *pos += 2; // skip "ID"
493            // Skip single whitespace byte after ID
494            if *pos < input.len() && is_whitespace(input[*pos]) {
495                *pos += 1;
496            }
497            break;
498        }
499
500        // Parse key (name)
501        if input[*pos] != b'/' {
502            return Err(BackendError::Interpreter(
503                "expected name key in inline image dictionary".to_string(),
504            ));
505        }
506        let key = parse_name(input, pos);
507
508        // Parse value
509        skip_whitespace_and_comments(input, pos);
510        if *pos >= input.len() {
511            return Err(BackendError::Interpreter(
512                "unterminated inline image dictionary".to_string(),
513            ));
514        }
515
516        let value = parse_inline_image_value(input, pos)?;
517        dict.push((key, value));
518    }
519
520    // Read image data until EI
521    let data_start = *pos;
522    // Look for EI preceded by whitespace
523    while *pos < input.len() {
524        if *pos + 2 <= input.len()
525            && (*pos == data_start || is_whitespace(input[*pos - 1]))
526            && input[*pos] == b'E'
527            && input[*pos + 1] == b'I'
528            && (*pos + 2 >= input.len()
529                || is_whitespace(input[*pos + 2])
530                || is_delimiter(input[*pos + 2]))
531        {
532            let data = input[data_start..*pos].to_vec();
533            // Trim trailing whitespace from data
534            let data = if data.last().is_some_and(|&b| is_whitespace(b)) {
535                data[..data.len() - 1].to_vec()
536            } else {
537                data
538            };
539            *pos += 2; // skip "EI"
540            return Ok((dict, data));
541        }
542        *pos += 1;
543    }
544
545    Err(BackendError::Interpreter(
546        "unterminated inline image (missing EI)".to_string(),
547    ))
548}
549
550/// Parse a single value in an inline image dictionary.
551fn parse_inline_image_value(input: &[u8], pos: &mut usize) -> Result<Operand, BackendError> {
552    let b = input[*pos];
553    match b {
554        b'/' => Ok(Operand::Name(parse_name(input, pos))),
555        b'(' => Ok(Operand::LiteralString(parse_literal_string(input, pos)?)),
556        b'<' => Ok(Operand::HexString(parse_hex_string(input, pos)?)),
557        b'[' => {
558            *pos += 1;
559            Ok(Operand::Array(parse_array(input, pos)?))
560        }
561        b'0'..=b'9' | b'+' | b'-' | b'.' => parse_number(input, pos),
562        b'a'..=b'z' | b'A'..=b'Z' => {
563            let kw = parse_keyword(input, pos);
564            match kw.as_str() {
565                "true" => Ok(Operand::Boolean(true)),
566                "false" => Ok(Operand::Boolean(false)),
567                "null" => Ok(Operand::Null),
568                _ => Ok(Operand::Name(kw)),
569            }
570        }
571        _ => Err(BackendError::Interpreter(format!(
572            "unexpected byte in inline image value: 0x{:02X}",
573            b
574        ))),
575    }
576}
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581
582    // ---- Operand parsing tests ----
583
584    #[test]
585    fn parse_integer() {
586        let ops = tokenize(b"42 m").unwrap();
587        assert_eq!(ops.len(), 1);
588        assert_eq!(ops[0].name, "m");
589        assert_eq!(ops[0].operands, vec![Operand::Integer(42)]);
590    }
591
592    #[test]
593    fn parse_negative_integer() {
594        let ops = tokenize(b"-7 Td").unwrap();
595        assert_eq!(ops.len(), 1);
596        assert_eq!(ops[0].operands, vec![Operand::Integer(-7)]);
597    }
598
599    #[test]
600    fn parse_real_number() {
601        let ops = tokenize(b"3.14 w").unwrap();
602        assert_eq!(ops.len(), 1);
603        assert_eq!(ops[0].operands, vec![Operand::Real(3.14)]);
604    }
605
606    #[test]
607    fn parse_real_leading_dot() {
608        let ops = tokenize(b".5 w").unwrap();
609        assert_eq!(ops.len(), 1);
610        assert_eq!(ops[0].operands, vec![Operand::Real(0.5)]);
611    }
612
613    #[test]
614    fn parse_negative_real() {
615        let ops = tokenize(b"-.002 w").unwrap();
616        assert_eq!(ops.len(), 1);
617        assert_eq!(ops[0].operands, vec![Operand::Real(-0.002)]);
618    }
619
620    #[test]
621    fn parse_name_operand() {
622        let ops = tokenize(b"/F1 12 Tf").unwrap();
623        assert_eq!(ops.len(), 1);
624        assert_eq!(ops[0].name, "Tf");
625        assert_eq!(
626            ops[0].operands,
627            vec![Operand::Name("F1".to_string()), Operand::Integer(12)]
628        );
629    }
630
631    #[test]
632    fn parse_name_with_hex_escape() {
633        let ops = tokenize(b"/F#231 12 Tf").unwrap();
634        assert_eq!(ops[0].operands[0], Operand::Name("F#1".to_string()));
635    }
636
637    #[test]
638    fn parse_literal_string_simple() {
639        let ops = tokenize(b"(Hello) Tj").unwrap();
640        assert_eq!(ops.len(), 1);
641        assert_eq!(ops[0].name, "Tj");
642        assert_eq!(
643            ops[0].operands,
644            vec![Operand::LiteralString(b"Hello".to_vec())]
645        );
646    }
647
648    #[test]
649    fn parse_literal_string_escaped_chars() {
650        let ops = tokenize(b"(line1\\nline2) Tj").unwrap();
651        assert_eq!(
652            ops[0].operands,
653            vec![Operand::LiteralString(b"line1\nline2".to_vec())]
654        );
655    }
656
657    #[test]
658    fn parse_literal_string_balanced_parens() {
659        let ops = tokenize(b"(a(b)c) Tj").unwrap();
660        assert_eq!(
661            ops[0].operands,
662            vec![Operand::LiteralString(b"a(b)c".to_vec())]
663        );
664    }
665
666    #[test]
667    fn parse_literal_string_octal_escape() {
668        // \101 = 'A' (65)
669        let ops = tokenize(b"(\\101) Tj").unwrap();
670        assert_eq!(ops[0].operands, vec![Operand::LiteralString(vec![65])]);
671    }
672
673    #[test]
674    fn parse_hex_string() {
675        let ops = tokenize(b"<48656C6C6F> Tj").unwrap();
676        assert_eq!(ops.len(), 1);
677        assert_eq!(ops[0].operands, vec![Operand::HexString(b"Hello".to_vec())]);
678    }
679
680    #[test]
681    fn parse_hex_string_odd_digits() {
682        // Odd number of hex digits: trailing 0 appended → <ABC> = <ABC0>
683        let ops = tokenize(b"<ABC> Tj").unwrap();
684        assert_eq!(ops[0].operands, vec![Operand::HexString(vec![0xAB, 0xC0])]);
685    }
686
687    #[test]
688    fn parse_hex_string_with_whitespace() {
689        let ops = tokenize(b"<48 65 6C 6C 6F> Tj").unwrap();
690        assert_eq!(ops[0].operands, vec![Operand::HexString(b"Hello".to_vec())]);
691    }
692
693    #[test]
694    fn parse_array_operand() {
695        let ops = tokenize(b"[1 2 3] re").unwrap();
696        assert_eq!(ops.len(), 1);
697        assert_eq!(
698            ops[0].operands,
699            vec![Operand::Array(vec![
700                Operand::Integer(1),
701                Operand::Integer(2),
702                Operand::Integer(3),
703            ])]
704        );
705    }
706
707    #[test]
708    fn parse_boolean_true() {
709        let ops = tokenize(b"true m").unwrap();
710        assert_eq!(ops[0].operands, vec![Operand::Boolean(true)]);
711    }
712
713    #[test]
714    fn parse_boolean_false() {
715        let ops = tokenize(b"false m").unwrap();
716        assert_eq!(ops[0].operands, vec![Operand::Boolean(false)]);
717    }
718
719    #[test]
720    fn parse_null_operand() {
721        let ops = tokenize(b"null m").unwrap();
722        assert_eq!(ops[0].operands, vec![Operand::Null]);
723    }
724
725    // ---- Operator parsing tests ----
726
727    #[test]
728    fn parse_bt_et() {
729        let ops = tokenize(b"BT ET").unwrap();
730        assert_eq!(ops.len(), 2);
731        assert_eq!(ops[0].name, "BT");
732        assert!(ops[0].operands.is_empty());
733        assert_eq!(ops[1].name, "ET");
734        assert!(ops[1].operands.is_empty());
735    }
736
737    #[test]
738    fn parse_tf_operator() {
739        let ops = tokenize(b"/F1 12 Tf").unwrap();
740        assert_eq!(ops.len(), 1);
741        assert_eq!(ops[0].name, "Tf");
742        assert_eq!(
743            ops[0].operands,
744            vec![Operand::Name("F1".to_string()), Operand::Integer(12)]
745        );
746    }
747
748    #[test]
749    fn parse_td_operator() {
750        let ops = tokenize(b"72 700 Td").unwrap();
751        assert_eq!(ops.len(), 1);
752        assert_eq!(ops[0].name, "Td");
753        assert_eq!(
754            ops[0].operands,
755            vec![Operand::Integer(72), Operand::Integer(700)]
756        );
757    }
758
759    #[test]
760    fn parse_tj_operator() {
761        let ops = tokenize(b"(Hello World) Tj").unwrap();
762        assert_eq!(ops.len(), 1);
763        assert_eq!(ops[0].name, "Tj");
764        assert_eq!(
765            ops[0].operands,
766            vec![Operand::LiteralString(b"Hello World".to_vec())]
767        );
768    }
769
770    #[test]
771    fn parse_tj_array_with_kerning() {
772        let ops = tokenize(b"[(H) -20 (ello)] TJ").unwrap();
773        assert_eq!(ops.len(), 1);
774        assert_eq!(ops[0].name, "TJ");
775        assert_eq!(
776            ops[0].operands,
777            vec![Operand::Array(vec![
778                Operand::LiteralString(b"H".to_vec()),
779                Operand::Integer(-20),
780                Operand::LiteralString(b"ello".to_vec()),
781            ])]
782        );
783    }
784
785    #[test]
786    fn parse_path_operators() {
787        let ops = tokenize(b"100 200 m 300 400 l S").unwrap();
788        assert_eq!(ops.len(), 3);
789        assert_eq!(ops[0].name, "m");
790        assert_eq!(
791            ops[0].operands,
792            vec![Operand::Integer(100), Operand::Integer(200)]
793        );
794        assert_eq!(ops[1].name, "l");
795        assert_eq!(
796            ops[1].operands,
797            vec![Operand::Integer(300), Operand::Integer(400)]
798        );
799        assert_eq!(ops[2].name, "S");
800        assert!(ops[2].operands.is_empty());
801    }
802
803    #[test]
804    fn parse_re_operator() {
805        let ops = tokenize(b"10 20 100 50 re f").unwrap();
806        assert_eq!(ops.len(), 2);
807        assert_eq!(ops[0].name, "re");
808        assert_eq!(
809            ops[0].operands,
810            vec![
811                Operand::Integer(10),
812                Operand::Integer(20),
813                Operand::Integer(100),
814                Operand::Integer(50),
815            ]
816        );
817        assert_eq!(ops[1].name, "f");
818    }
819
820    #[test]
821    fn parse_f_star_operator() {
822        let ops = tokenize(b"f*").unwrap();
823        assert_eq!(ops.len(), 1);
824        assert_eq!(ops[0].name, "f*");
825    }
826
827    // ---- Comment handling ----
828
829    #[test]
830    fn skip_comments() {
831        let ops = tokenize(b"% this is a comment\nBT ET").unwrap();
832        assert_eq!(ops.len(), 2);
833        assert_eq!(ops[0].name, "BT");
834        assert_eq!(ops[1].name, "ET");
835    }
836
837    #[test]
838    fn inline_comment_between_operators() {
839        let ops = tokenize(b"BT % begin text\n/F1 12 Tf\nET").unwrap();
840        assert_eq!(ops.len(), 3);
841        assert_eq!(ops[0].name, "BT");
842        assert_eq!(ops[1].name, "Tf");
843        assert_eq!(ops[2].name, "ET");
844    }
845
846    // ---- Mixed content stream tests ----
847
848    #[test]
849    fn parse_typical_text_stream() {
850        let stream = b"BT\n/F1 12 Tf\n72 700 Td\n(Hello World) Tj\nET";
851        let ops = tokenize(stream).unwrap();
852        assert_eq!(ops.len(), 5);
853        assert_eq!(ops[0].name, "BT");
854        assert_eq!(ops[1].name, "Tf");
855        assert_eq!(ops[1].operands.len(), 2);
856        assert_eq!(ops[2].name, "Td");
857        assert_eq!(ops[2].operands.len(), 2);
858        assert_eq!(ops[3].name, "Tj");
859        assert_eq!(ops[3].operands.len(), 1);
860        assert_eq!(ops[4].name, "ET");
861    }
862
863    #[test]
864    fn parse_mixed_text_and_graphics() {
865        let stream = b"q\n1 0 0 1 72 720 cm\nBT\n/F1 12 Tf\n(Test) Tj\nET\n100 200 300 400 re S\nQ";
866        let ops = tokenize(stream).unwrap();
867        assert_eq!(ops[0].name, "q");
868        assert_eq!(ops[1].name, "cm");
869        assert_eq!(ops[1].operands.len(), 6);
870        assert_eq!(ops[2].name, "BT");
871        assert_eq!(ops[3].name, "Tf");
872        assert_eq!(ops[4].name, "Tj");
873        assert_eq!(ops[5].name, "ET");
874        assert_eq!(ops[6].name, "re");
875        assert_eq!(ops[7].name, "S");
876        assert_eq!(ops[8].name, "Q");
877    }
878
879    #[test]
880    fn parse_color_operators() {
881        let ops = tokenize(b"0.5 g\n1 0 0 RG").unwrap();
882        assert_eq!(ops.len(), 2);
883        assert_eq!(ops[0].name, "g");
884        assert_eq!(ops[0].operands, vec![Operand::Real(0.5)]);
885        assert_eq!(ops[1].name, "RG");
886        assert_eq!(
887            ops[1].operands,
888            vec![
889                Operand::Integer(1),
890                Operand::Integer(0),
891                Operand::Integer(0),
892            ]
893        );
894    }
895
896    #[test]
897    fn parse_quote_operator() {
898        let ops = tokenize(b"(text) '").unwrap();
899        assert_eq!(ops.len(), 1);
900        assert_eq!(ops[0].name, "'");
901        assert_eq!(
902            ops[0].operands,
903            vec![Operand::LiteralString(b"text".to_vec())]
904        );
905    }
906
907    #[test]
908    fn parse_double_quote_operator() {
909        let ops = tokenize(b"1 2 (text) \"").unwrap();
910        assert_eq!(ops.len(), 1);
911        assert_eq!(ops[0].name, "\"");
912        assert_eq!(
913            ops[0].operands,
914            vec![
915                Operand::Integer(1),
916                Operand::Integer(2),
917                Operand::LiteralString(b"text".to_vec()),
918            ]
919        );
920    }
921
922    #[test]
923    fn parse_empty_stream() {
924        let ops = tokenize(b"").unwrap();
925        assert!(ops.is_empty());
926    }
927
928    #[test]
929    fn parse_whitespace_only() {
930        let ops = tokenize(b"   \t\n\r  ").unwrap();
931        assert!(ops.is_empty());
932    }
933
934    #[test]
935    fn parse_inline_image() {
936        // Space after ID is the mandatory single whitespace separator (per PDF spec)
937        let stream = b"BI\n/W 2 /H 2 /CS /G /BPC 8\nID \x00\xFF\x00\xFF\nEI";
938        let ops = tokenize(stream).unwrap();
939        assert_eq!(ops.len(), 1);
940        assert_eq!(ops[0].name, "BI");
941        // First operand is array of key-value pairs flattened
942        if let Operand::Array(ref entries) = ops[0].operands[0] {
943            // W=2, H=2, CS=G, BPC=8 → 8 elements (4 pairs)
944            assert_eq!(entries.len(), 8);
945            assert_eq!(entries[0], Operand::Name("W".to_string()));
946            assert_eq!(entries[1], Operand::Integer(2));
947        } else {
948            panic!("expected array operand for BI dict");
949        }
950        // Second operand is the raw data
951        if let Operand::LiteralString(ref data) = ops[0].operands[1] {
952            assert_eq!(data, &[0x00, 0xFF, 0x00, 0xFF]);
953        } else {
954            panic!("expected literal string operand for BI data");
955        }
956    }
957
958    // ---- Edge cases ----
959
960    #[test]
961    fn parse_positive_sign_number() {
962        let ops = tokenize(b"+5 m").unwrap();
963        assert_eq!(ops[0].operands, vec![Operand::Integer(5)]);
964    }
965
966    #[test]
967    fn parse_zero() {
968        let ops = tokenize(b"0 m").unwrap();
969        assert_eq!(ops[0].operands, vec![Operand::Integer(0)]);
970    }
971
972    #[test]
973    fn parse_zero_real() {
974        let ops = tokenize(b"0.0 m").unwrap();
975        assert_eq!(ops[0].operands, vec![Operand::Real(0.0)]);
976    }
977
978    #[test]
979    fn parse_multiple_operators_no_operands() {
980        let ops = tokenize(b"q Q n W").unwrap();
981        assert_eq!(ops.len(), 4);
982        assert_eq!(ops[0].name, "q");
983        assert_eq!(ops[1].name, "Q");
984        assert_eq!(ops[2].name, "n");
985        assert_eq!(ops[3].name, "W");
986    }
987
988    #[test]
989    fn parse_text_matrix() {
990        let ops = tokenize(b"1 0 0 1 72 700 Tm").unwrap();
991        assert_eq!(ops.len(), 1);
992        assert_eq!(ops[0].name, "Tm");
993        assert_eq!(ops[0].operands.len(), 6);
994    }
995
996    #[test]
997    fn unterminated_literal_string_error() {
998        let result = tokenize(b"(unclosed");
999        assert!(result.is_err());
1000    }
1001
1002    #[test]
1003    fn unterminated_array_error() {
1004        let result = tokenize(b"[1 2 3");
1005        assert!(result.is_err());
1006    }
1007
1008    #[test]
1009    fn unexpected_array_close_error() {
1010        let result = tokenize(b"]");
1011        assert!(result.is_err());
1012    }
1013
1014    #[test]
1015    fn parse_do_operator() {
1016        let ops = tokenize(b"/Im0 Do").unwrap();
1017        assert_eq!(ops.len(), 1);
1018        assert_eq!(ops[0].name, "Do");
1019        assert_eq!(ops[0].operands, vec![Operand::Name("Im0".to_string())]);
1020    }
1021
1022    #[test]
1023    fn parse_scn_operator() {
1024        let ops = tokenize(b"0.5 0.2 0.8 scn").unwrap();
1025        assert_eq!(ops.len(), 1);
1026        assert_eq!(ops[0].name, "scn");
1027        assert_eq!(ops[0].operands.len(), 3);
1028    }
1029
1030    #[test]
1031    fn parse_dash_pattern() {
1032        let ops = tokenize(b"[3 5] 6 d").unwrap();
1033        assert_eq!(ops.len(), 1);
1034        assert_eq!(ops[0].name, "d");
1035        assert_eq!(
1036            ops[0].operands,
1037            vec![
1038                Operand::Array(vec![Operand::Integer(3), Operand::Integer(5)]),
1039                Operand::Integer(6),
1040            ]
1041        );
1042    }
1043
1044    #[test]
1045    fn parse_consecutive_strings() {
1046        let ops = tokenize(b"(abc) (def) Tj").unwrap();
1047        assert_eq!(ops.len(), 1);
1048        assert_eq!(ops[0].name, "Tj");
1049        assert_eq!(ops[0].operands.len(), 2);
1050    }
1051}