Skip to main content

oxidize_pdf/parser/
content.rs

1//! PDF Content Stream Parser - Complete support for PDF graphics operators
2//!
3//! This module implements comprehensive parsing of PDF content streams according to the PDF specification.
4//! Content streams contain the actual drawing instructions (operators) that render text, graphics, and images
5//! on PDF pages.
6//!
7//! # Overview
8//!
9//! Content streams are sequences of PDF operators that describe:
10//! - Text positioning and rendering
11//! - Path construction and painting
12//! - Color and graphics state management
13//! - Image and XObject placement
14//! - Coordinate transformations
15//!
16//! # Architecture
17//!
18//! The parser is divided into two main components:
19//! - `ContentTokenizer`: Low-level tokenization of content stream bytes
20//! - `ContentParser`: High-level parsing of tokens into structured operations
21//!
22//! # Example
23//!
24//! ```rust,no_run
25//! use oxidize_pdf::parser::content::{ContentParser, ContentOperation};
26//!
27//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
28//! // Parse a content stream
29//! let content_stream = b"BT /F1 12 Tf 100 200 Td (Hello World) Tj ET";
30//! let operations = ContentParser::parse_content(content_stream)?;
31//!
32//! // Process operations
33//! for op in operations {
34//!     match op {
35//!         ContentOperation::BeginText => println!("Start text object"),
36//!         ContentOperation::SetFont(name, size) => println!("Font: {} at {}", name, size),
37//!         ContentOperation::ShowText(text) => println!("Text: {:?}", text),
38//!         _ => {}
39//!     }
40//! }
41//! # Ok(())
42//! # }
43//! ```
44//!
45//! # Supported Operators
46//!
47//! This parser supports all standard PDF operators including:
48//! - Text operators (BT, ET, Tj, TJ, Tf, Td, etc.)
49//! - Graphics state operators (q, Q, cm, w, J, etc.)
50//! - Path construction operators (m, l, c, re, h)
51//! - Path painting operators (S, f, B, n, etc.)
52//! - Color operators (g, rg, k, cs, scn, etc.)
53//! - XObject operators (Do)
54//! - Marked content operators (BMC, BDC, EMC, etc.)
55
56use super::{ParseError, ParseResult};
57use crate::objects::Object;
58use std::collections::HashMap;
59
60/// A single value inside a marked-content properties dictionary or array.
61///
62/// PDF marked-content properties (BDC, DP) carry typed values: strings,
63/// integers, real numbers, names, arrays, and nested dictionaries. The
64/// previous `HashMap<String, String>` carrier was lossy for `/ActualText`
65/// (UTF-16BE bytes mangled by `String::from_utf8_lossy`) and for `/MCID`
66/// (integer values stored as their decimal string representation). This
67/// enum preserves the original token type and bytes; decoding happens
68/// lazily at the extractor level (e.g. UTF-16BE detection via BOM).
69///
70/// Hex strings (`<FEFF00660069>`) and literal strings (`(text)`) both
71/// land here as `MarkedContentValue::String(Vec<u8>)` because both are
72/// raw byte sequences at the PDF tokenizer level.
73#[derive(Debug, Clone, PartialEq)]
74pub enum MarkedContentValue {
75    /// Raw PDF string bytes (from either `Token::String` or `Token::HexString`).
76    /// Decoded lazily by consumers — UTF-16BE detection via BOM happens in the
77    /// extractor's `decode_pdf_string` helper.
78    String(Vec<u8>),
79    /// PDF integer (e.g. `/MCID 0`).
80    Integer(i64),
81    /// PDF real number.
82    Real(f64),
83    /// PDF name token (e.g. `/Pagination`).
84    Name(String),
85    /// PDF array; nested values are themselves `MarkedContentValue`.
86    Array(Vec<MarkedContentValue>),
87    /// Nested dictionary; keys are PDF name strings (the leading `/` is stripped).
88    Dict(HashMap<String, MarkedContentValue>),
89}
90
91/// Properties operand of a BDC/DP operator. Two shapes per ISO 32000-1
92/// §14.6.2:
93///
94/// - **Inline**: the second BDC operand is an inline dictionary literal
95///   (`<< /MCID 0 /ActualText (fi) >>`). Keys map to `MarkedContentValue`.
96/// - **ResourceRef**: the second BDC operand is a name (`/PropsName`) that
97///   references the page's `/Resources /Properties /<name>` dictionary.
98///   Resolution against the page's resource tree happens in the extractor
99///   (parser does not have access to the page object).
100#[derive(Debug, Clone, PartialEq)]
101pub enum MarkedContentProps {
102    Inline(HashMap<String, MarkedContentValue>),
103    ResourceRef(String),
104}
105
106/// Represents a single operator in a PDF content stream.
107///
108/// Each variant corresponds to a specific PDF operator and carries the associated
109/// operands. These operations form a complete instruction set for rendering PDF content.
110///
111/// # Categories
112///
113/// Operations are grouped into several categories:
114/// - **Text Object**: BeginText, EndText
115/// - **Text State**: Font, spacing, scaling, rendering mode
116/// - **Text Positioning**: Matrix transforms, moves, line advances
117/// - **Text Showing**: Display text with various formatting
118/// - **Graphics State**: Save/restore, transforms, line properties
119/// - **Path Construction**: Move, line, curve, rectangle operations
120/// - **Path Painting**: Stroke, fill, clipping operations
121/// - **Color**: RGB, CMYK, grayscale, and color space operations
122/// - **XObject**: External graphics and form placement
123/// - **Marked Content**: Semantic tagging for accessibility
124///
125/// # Example
126///
127/// ```rust
128/// use oxidize_pdf::parser::content::{ContentOperation};
129///
130/// // Text operation
131/// let op1 = ContentOperation::ShowText(b"Hello".to_vec());
132///
133/// // Graphics operation
134/// let op2 = ContentOperation::SetLineWidth(2.0);
135///
136/// // Path operation
137/// let op3 = ContentOperation::Rectangle(10.0, 10.0, 100.0, 50.0);
138/// ```
139#[derive(Debug, Clone, PartialEq)]
140pub enum ContentOperation {
141    // Text object operators
142    /// Begin a text object (BT operator).
143    /// All text showing operations must occur within a text object.
144    BeginText,
145
146    /// End a text object (ET operator).
147    /// Closes the current text object started with BeginText.
148    EndText,
149
150    // Text state operators
151    /// Set character spacing (Tc operator).
152    /// Additional space between characters in unscaled text units.
153    SetCharSpacing(f32),
154
155    /// Set word spacing (Tw operator).
156    /// Additional space for ASCII space character (0x20) in unscaled text units.
157    SetWordSpacing(f32),
158
159    /// Set horizontal text scaling (Tz operator).
160    /// Percentage of normal width (100 = normal).
161    SetHorizontalScaling(f32),
162
163    /// Set text leading (TL operator).
164    /// Vertical distance between baselines for T* operator.
165    SetLeading(f32),
166
167    /// Set font and size (Tf operator).
168    /// Font name must match a key in the Resources/Font dictionary.
169    SetFont(String, f32),
170
171    /// Set text rendering mode (Tr operator).
172    /// 0=fill, 1=stroke, 2=fill+stroke, 3=invisible, 4=fill+clip, 5=stroke+clip, 6=fill+stroke+clip, 7=clip
173    SetTextRenderMode(i32),
174
175    /// Set text rise (Ts operator).
176    /// Vertical displacement for superscripts/subscripts in text units.
177    SetTextRise(f32),
178
179    // Text positioning operators
180    /// Move text position (Td operator).
181    /// Translates the text matrix by (tx, ty).
182    MoveText(f32, f32),
183
184    /// Move text position and set leading (TD operator).
185    /// Equivalent to: -ty TL tx ty Td
186    MoveTextSetLeading(f32, f32),
187
188    /// Set text matrix directly (Tm operator).
189    /// Parameters: [a, b, c, d, e, f] for transformation matrix.
190    SetTextMatrix(f32, f32, f32, f32, f32, f32),
191
192    /// Move to start of next line (T* operator).
193    /// Uses the current leading value set with TL.
194    NextLine,
195
196    // Text showing operators
197    /// Show text string (Tj operator).
198    /// The bytes are encoded according to the current font's encoding.
199    ShowText(Vec<u8>),
200
201    /// Show text with individual positioning (TJ operator).
202    /// Array elements can be strings or position adjustments.
203    ShowTextArray(Vec<TextElement>),
204
205    /// Move to next line and show text (' operator).
206    /// Equivalent to: T* string Tj
207    NextLineShowText(Vec<u8>),
208
209    /// Set spacing, move to next line, and show text (" operator).
210    /// Equivalent to: word_spacing Tw char_spacing Tc string '
211    SetSpacingNextLineShowText(f32, f32, Vec<u8>),
212
213    // Graphics state operators
214    /// Save current graphics state (q operator).
215    /// Pushes the entire graphics state onto a stack.
216    SaveGraphicsState,
217
218    /// Restore graphics state (Q operator).
219    /// Pops the graphics state from the stack.
220    RestoreGraphicsState,
221
222    /// Concatenate matrix to current transformation matrix (cm operator).
223    /// Modifies the CTM: CTM' = CTM × [a b c d e f]
224    SetTransformMatrix(f32, f32, f32, f32, f32, f32),
225
226    /// Set line width (w operator) in user space units.
227    SetLineWidth(f32),
228
229    /// Set line cap style (J operator).
230    /// 0=butt cap, 1=round cap, 2=projecting square cap
231    SetLineCap(i32),
232
233    /// Set line join style (j operator).
234    /// 0=miter join, 1=round join, 2=bevel join
235    SetLineJoin(i32),
236
237    /// Set miter limit (M operator).
238    /// Maximum ratio of miter length to line width.
239    SetMiterLimit(f32),
240
241    /// Set dash pattern (d operator).
242    /// Array of dash/gap lengths and starting phase.
243    SetDashPattern(Vec<f32>, f32),
244
245    /// Set rendering intent (ri operator).
246    /// Color rendering intent: /AbsoluteColorimetric, /RelativeColorimetric, /Saturation, /Perceptual
247    SetIntent(String),
248
249    /// Set flatness tolerance (i operator).
250    /// Maximum error when rendering curves as line segments.
251    SetFlatness(f32),
252
253    /// Set graphics state from parameter dictionary (gs operator).
254    /// References ExtGState resource dictionary.
255    SetGraphicsStateParams(String),
256
257    // Path construction operators
258    /// Begin new subpath at point (m operator).
259    MoveTo(f32, f32),
260
261    /// Append straight line segment (l operator).
262    LineTo(f32, f32),
263
264    /// Append cubic Bézier curve (c operator).
265    /// Control points: (x1,y1), (x2,y2), endpoint: (x3,y3)
266    CurveTo(f32, f32, f32, f32, f32, f32),
267
268    /// Append cubic Bézier curve with first control point = current point (v operator).
269    CurveToV(f32, f32, f32, f32),
270
271    /// Append cubic Bézier curve with second control point = endpoint (y operator).
272    CurveToY(f32, f32, f32, f32),
273
274    /// Close current subpath (h operator).
275    /// Appends straight line to starting point.
276    ClosePath,
277
278    /// Append rectangle as complete subpath (re operator).
279    /// Parameters: x, y, width, height
280    Rectangle(f32, f32, f32, f32),
281
282    // Path painting operators
283    /// Stroke the path (S operator).
284    Stroke,
285
286    /// Close and stroke the path (s operator).
287    /// Equivalent to: h S
288    CloseStroke,
289
290    /// Fill the path using nonzero winding rule (f or F operator).
291    Fill,
292
293    /// Fill the path using even-odd rule (f* operator).
294    FillEvenOdd,
295
296    /// Fill then stroke the path (B operator).
297    /// Uses nonzero winding rule.
298    FillStroke,
299
300    /// Fill then stroke using even-odd rule (B* operator).
301    FillStrokeEvenOdd,
302
303    /// Close, fill, and stroke the path (b operator).
304    /// Equivalent to: h B
305    CloseFillStroke,
306
307    /// Close, fill, and stroke using even-odd rule (b* operator).
308    CloseFillStrokeEvenOdd,
309
310    /// End path without filling or stroking (n operator).
311    /// Used primarily before clipping.
312    EndPath,
313
314    // Clipping path operators
315    Clip,        // W
316    ClipEvenOdd, // W*
317
318    // Color operators
319    /// Set stroking color space (CS operator).
320    /// References ColorSpace resource dictionary.
321    SetStrokingColorSpace(String),
322
323    /// Set non-stroking color space (cs operator).
324    /// References ColorSpace resource dictionary.
325    SetNonStrokingColorSpace(String),
326
327    /// Set stroking color (SC, SCN operators).
328    /// Number of components depends on current color space.
329    SetStrokingColor(Vec<f32>),
330
331    /// Set non-stroking color (sc, scn operators).
332    /// Number of components depends on current color space.
333    SetNonStrokingColor(Vec<f32>),
334
335    /// Set stroking color to DeviceGray (G operator).
336    /// 0.0 = black, 1.0 = white
337    SetStrokingGray(f32),
338
339    /// Set non-stroking color to DeviceGray (g operator).
340    SetNonStrokingGray(f32),
341
342    /// Set stroking color to DeviceRGB (RG operator).
343    /// Components range from 0.0 to 1.0.
344    SetStrokingRGB(f32, f32, f32),
345
346    /// Set non-stroking color to DeviceRGB (rg operator).
347    SetNonStrokingRGB(f32, f32, f32),
348
349    /// Set stroking color to DeviceCMYK (K operator).
350    SetStrokingCMYK(f32, f32, f32, f32),
351
352    /// Set non-stroking color to DeviceCMYK (k operator).
353    SetNonStrokingCMYK(f32, f32, f32, f32),
354
355    // Shading operators
356    ShadingFill(String), // sh
357
358    // Inline image operators
359    /// Begin inline image (BI operator)
360    BeginInlineImage,
361    /// Inline image with parsed dictionary and data
362    InlineImage {
363        /// Image parameters (width, height, colorspace, etc.)
364        params: HashMap<String, Object>,
365        /// Raw image data
366        data: Vec<u8>,
367    },
368
369    // XObject operators
370    /// Paint external object (Do operator).
371    /// References XObject resource dictionary (images, forms).
372    PaintXObject(String),
373
374    // Marked content operators
375    BeginMarkedContent(String),                                    // BMC
376    BeginMarkedContentWithProps(String, MarkedContentProps),       // BDC
377    EndMarkedContent,                                              // EMC
378    DefineMarkedContentPoint(String),                              // MP
379    DefineMarkedContentPointWithProps(String, MarkedContentProps), // DP
380
381    // Compatibility operators
382    BeginCompatibility, // BX
383    EndCompatibility,   // EX
384}
385
386/// Represents a text element in a TJ array for ShowTextArray operations.
387///
388/// The TJ operator takes an array of strings and position adjustments,
389/// allowing fine control over character and word spacing.
390///
391/// # Example
392///
393/// ```rust
394/// use oxidize_pdf::parser::content::{TextElement, ContentOperation};
395///
396/// // TJ array: [(Hello) -50 (World)]
397/// let tj_array = vec![
398///     TextElement::Text(b"Hello".to_vec()),
399///     TextElement::Spacing(-50.0), // Move left 50 units
400///     TextElement::Text(b"World".to_vec()),
401/// ];
402/// let op = ContentOperation::ShowTextArray(tj_array);
403/// ```
404#[derive(Debug, Clone, PartialEq)]
405pub enum TextElement {
406    /// Text string to show
407    Text(Vec<u8>),
408    /// Position adjustment in thousandths of text space units
409    /// Negative values move to the right (decrease spacing)
410    Spacing(f32),
411}
412
413/// Token types in content streams
414#[derive(Debug, Clone, PartialEq)]
415pub(super) enum Token {
416    Number(f32),
417    Integer(i32),
418    String(Vec<u8>),
419    HexString(Vec<u8>),
420    Name(String),
421    Operator(String),
422    ArrayStart,
423    ArrayEnd,
424    DictStart,
425    DictEnd,
426    /// Raw binary data between ID and EI in an inline image.
427    /// The tokenizer captures this as opaque bytes to prevent
428    /// binary image data from being mis-parsed as operators.
429    InlineImageData(Vec<u8>),
430}
431
432/// Content stream tokenizer
433pub struct ContentTokenizer<'a> {
434    input: &'a [u8],
435    position: usize,
436    /// Set after returning an "ID" operator token.
437    /// The next call to next_token() will read raw inline image bytes.
438    in_inline_image: bool,
439}
440
441impl<'a> ContentTokenizer<'a> {
442    /// Create a new tokenizer for the given input
443    pub fn new(input: &'a [u8]) -> Self {
444        Self {
445            input,
446            position: 0,
447            in_inline_image: false,
448        }
449    }
450
451    /// Get the next token from the stream
452    pub(super) fn next_token(&mut self) -> ParseResult<Option<Token>> {
453        // If we just returned an "ID" token, read raw inline image binary data
454        if self.in_inline_image {
455            self.in_inline_image = false;
456            return self.read_inline_image_data();
457        }
458
459        self.skip_whitespace();
460
461        if self.position >= self.input.len() {
462            return Ok(None);
463        }
464
465        let ch = self.input[self.position];
466
467        match ch {
468            // Numbers
469            b'+' | b'-' | b'.' | b'0'..=b'9' => self.read_number(),
470
471            // Strings
472            b'(' => self.read_literal_string(),
473            b'<' => {
474                if self.peek_next() == Some(b'<') {
475                    self.position += 2;
476                    Ok(Some(Token::DictStart))
477                } else {
478                    self.read_hex_string()
479                }
480            }
481            b'>' => {
482                if self.peek_next() == Some(b'>') {
483                    self.position += 2;
484                    Ok(Some(Token::DictEnd))
485                } else {
486                    Err(ParseError::SyntaxError {
487                        position: self.position,
488                        message: "Unexpected '>'".to_string(),
489                    })
490                }
491            }
492
493            // Arrays
494            b'[' => {
495                self.position += 1;
496                Ok(Some(Token::ArrayStart))
497            }
498            b']' => {
499                self.position += 1;
500                Ok(Some(Token::ArrayEnd))
501            }
502
503            // Names
504            b'/' => self.read_name(),
505
506            // Skip unhandled delimiters (corrupted content / binary data recovery)
507            // These bytes are delimiters in read_operator() but have no valid meaning
508            // at the top level of a content stream. Skipping them prevents infinite loops
509            // where read_operator() would return an empty operator without advancing.
510            b';' | b')' | b'{' | b'}' => {
511                self.position += 1;
512                self.next_token() // Recursively get next valid token
513            }
514
515            // Operators or other tokens
516            _ => {
517                let token = self.read_operator()?;
518                // After "ID" operator, switch to raw binary mode for inline image data
519                if let Some(Token::Operator(ref op)) = token {
520                    if op == "ID" {
521                        self.in_inline_image = true;
522                    }
523                }
524                Ok(token)
525            }
526        }
527    }
528
529    fn skip_whitespace(&mut self) {
530        while self.position < self.input.len() {
531            match self.input[self.position] {
532                b' ' | b'\t' | b'\r' | b'\n' | b'\x0C' => self.position += 1,
533                b'%' => self.skip_comment(),
534                _ => break,
535            }
536        }
537    }
538
539    fn skip_comment(&mut self) {
540        while self.position < self.input.len() && self.input[self.position] != b'\n' {
541            self.position += 1;
542        }
543    }
544
545    fn peek_next(&self) -> Option<u8> {
546        if self.position + 1 < self.input.len() {
547            Some(self.input[self.position + 1])
548        } else {
549            None
550        }
551    }
552
553    fn read_number(&mut self) -> ParseResult<Option<Token>> {
554        let start = self.position;
555        let mut has_dot = false;
556
557        // Handle optional sign
558        if self.position < self.input.len()
559            && (self.input[self.position] == b'+' || self.input[self.position] == b'-')
560        {
561            self.position += 1;
562        }
563
564        // Read digits and optional decimal point
565        while self.position < self.input.len() {
566            match self.input[self.position] {
567                b'0'..=b'9' => self.position += 1,
568                b'.' if !has_dot => {
569                    has_dot = true;
570                    self.position += 1;
571                }
572                _ => break,
573            }
574        }
575
576        let num_str = std::str::from_utf8(&self.input[start..self.position]).map_err(|_| {
577            ParseError::SyntaxError {
578                position: start,
579                message: "Invalid number format".to_string(),
580            }
581        })?;
582
583        if has_dot {
584            let value = num_str
585                .parse::<f32>()
586                .map_err(|_| ParseError::SyntaxError {
587                    position: start,
588                    message: "Invalid float number".to_string(),
589                })?;
590            Ok(Some(Token::Number(value)))
591        } else {
592            let value = num_str
593                .parse::<i32>()
594                .map_err(|_| ParseError::SyntaxError {
595                    position: start,
596                    message: "Invalid integer number".to_string(),
597                })?;
598            Ok(Some(Token::Integer(value)))
599        }
600    }
601
602    fn read_literal_string(&mut self) -> ParseResult<Option<Token>> {
603        self.position += 1; // Skip opening '('
604        let mut result = Vec::new();
605        let mut paren_depth = 1;
606        let mut escape = false;
607
608        while self.position < self.input.len() && paren_depth > 0 {
609            let ch = self.input[self.position];
610            self.position += 1;
611
612            if escape {
613                match ch {
614                    b'n' => result.push(b'\n'),
615                    b'r' => result.push(b'\r'),
616                    b't' => result.push(b'\t'),
617                    b'b' => result.push(b'\x08'),
618                    b'f' => result.push(b'\x0C'),
619                    b'(' => result.push(b'('),
620                    b')' => result.push(b')'),
621                    b'\\' => result.push(b'\\'),
622                    b'0'..=b'7' => {
623                        // Octal escape sequence
624                        self.position -= 1;
625                        let octal_value = self.read_octal_escape()?;
626                        result.push(octal_value);
627                    }
628                    _ => result.push(ch), // Unknown escape, treat as literal
629                }
630                escape = false;
631            } else {
632                match ch {
633                    b'\\' => escape = true,
634                    b'(' => {
635                        paren_depth += 1;
636                        result.push(ch);
637                    }
638                    b')' => {
639                        paren_depth -= 1;
640                        if paren_depth > 0 {
641                            result.push(ch);
642                        }
643                    }
644                    _ => result.push(ch),
645                }
646            }
647        }
648
649        Ok(Some(Token::String(result)))
650    }
651
652    fn read_octal_escape(&mut self) -> ParseResult<u8> {
653        // Use u16 to avoid overflow panic on malformed octal sequences (e.g. \777).
654        // Per ISO 32000-1:2008 §7.3.4.2: "high-order overflow shall be ignored".
655        let mut value = 0u16;
656        let mut count = 0;
657
658        while count < 3 && self.position < self.input.len() {
659            match self.input[self.position] {
660                b'0'..=b'7' => {
661                    value = value * 8 + u16::from(self.input[self.position] - b'0');
662                    self.position += 1;
663                    count += 1;
664                }
665                _ => break,
666            }
667        }
668
669        Ok(value as u8)
670    }
671
672    fn read_hex_string(&mut self) -> ParseResult<Option<Token>> {
673        self.position += 1; // Skip opening '<'
674        let mut result = Vec::new();
675        let mut nibble = None;
676
677        while self.position < self.input.len() {
678            let ch = self.input[self.position];
679
680            match ch {
681                b'>' => {
682                    self.position += 1;
683                    // Handle odd number of hex digits
684                    if let Some(n) = nibble {
685                        result.push(n << 4);
686                    }
687                    return Ok(Some(Token::HexString(result)));
688                }
689                b'0'..=b'9' | b'A'..=b'F' | b'a'..=b'f' => {
690                    let digit = if ch <= b'9' {
691                        ch - b'0'
692                    } else if ch <= b'F' {
693                        ch - b'A' + 10
694                    } else {
695                        ch - b'a' + 10
696                    };
697
698                    if let Some(n) = nibble {
699                        result.push((n << 4) | digit);
700                        nibble = None;
701                    } else {
702                        nibble = Some(digit);
703                    }
704                    self.position += 1;
705                }
706                b' ' | b'\t' | b'\r' | b'\n' | b'\x0C' => {
707                    // Skip whitespace in hex strings
708                    self.position += 1;
709                }
710                _ => {
711                    return Err(ParseError::SyntaxError {
712                        position: self.position,
713                        message: format!("Invalid character in hex string: {:?}", ch as char),
714                    });
715                }
716            }
717        }
718
719        Err(ParseError::SyntaxError {
720            position: self.position,
721            message: "Unterminated hex string".to_string(),
722        })
723    }
724
725    fn read_name(&mut self) -> ParseResult<Option<Token>> {
726        self.position += 1; // Skip '/'
727        let start = self.position;
728
729        while self.position < self.input.len() {
730            let ch = self.input[self.position];
731            match ch {
732                b' ' | b'\t' | b'\r' | b'\n' | b'\x0C' | b'(' | b')' | b'<' | b'>' | b'['
733                | b']' | b'{' | b'}' | b'/' | b'%' => break,
734                b'#' => {
735                    // Handle hex escape in name
736                    self.position += 1;
737                    if self.position + 1 < self.input.len() {
738                        self.position += 2;
739                    }
740                }
741                _ => self.position += 1,
742            }
743        }
744
745        let name_bytes = &self.input[start..self.position];
746        let name = self.decode_name(name_bytes)?;
747        Ok(Some(Token::Name(name)))
748    }
749
750    fn decode_name(&self, bytes: &[u8]) -> ParseResult<String> {
751        let mut result = Vec::new();
752        let mut i = 0;
753
754        while i < bytes.len() {
755            if bytes[i] == b'#' && i + 2 < bytes.len() {
756                // Hex escape
757                let hex_str = std::str::from_utf8(&bytes[i + 1..i + 3]).map_err(|_| {
758                    ParseError::SyntaxError {
759                        position: self.position,
760                        message: "Invalid hex escape in name".to_string(),
761                    }
762                })?;
763                let value =
764                    u8::from_str_radix(hex_str, 16).map_err(|_| ParseError::SyntaxError {
765                        position: self.position,
766                        message: "Invalid hex escape in name".to_string(),
767                    })?;
768                result.push(value);
769                i += 3;
770            } else {
771                result.push(bytes[i]);
772                i += 1;
773            }
774        }
775
776        String::from_utf8(result).map_err(|_| ParseError::SyntaxError {
777            position: self.position,
778            message: "Invalid UTF-8 in name".to_string(),
779        })
780    }
781
782    fn read_operator(&mut self) -> ParseResult<Option<Token>> {
783        let start = self.position;
784
785        while self.position < self.input.len() {
786            let ch = self.input[self.position];
787            match ch {
788                b' ' | b'\t' | b'\r' | b'\n' | b'\x0C' | b'(' | b')' | b'<' | b'>' | b'['
789                | b']' | b'{' | b'}' | b'/' | b'%' | b';' => break,
790                _ => self.position += 1,
791            }
792        }
793
794        let op_bytes = &self.input[start..self.position];
795        let op = std::str::from_utf8(op_bytes).map_err(|_| ParseError::SyntaxError {
796            position: start,
797            message: "Invalid operator".to_string(),
798        })?;
799
800        Ok(Some(Token::Operator(op.to_string())))
801    }
802
803    /// Read raw binary data for an inline image (between ID and EI).
804    ///
805    /// Per PDF spec §4.8.6, after the ID operator and a single whitespace byte,
806    /// all subsequent bytes are raw image data until the EI marker is found.
807    /// The EI marker is: whitespace + 'E' + 'I' + (whitespace, delimiter, or EOF).
808    fn read_inline_image_data(&mut self) -> ParseResult<Option<Token>> {
809        // Skip single whitespace byte after ID (per PDF spec §4.8.6)
810        if self.position < self.input.len() {
811            let ch = self.input[self.position];
812            if ch == b' ' || ch == b'\n' || ch == b'\r' || ch == b'\t' {
813                self.position += 1;
814                // Handle \r\n as single whitespace
815                if ch == b'\r'
816                    && self.position < self.input.len()
817                    && self.input[self.position] == b'\n'
818                {
819                    self.position += 1;
820                }
821            }
822        }
823
824        let start = self.position;
825
826        // Scan for EI marker: preceded by whitespace + 'E' + 'I' + (whitespace/delimiter/EOF)
827        while self.position + 1 < self.input.len() {
828            let preceded_by_whitespace = self.position == start
829                || matches!(
830                    self.input[self.position - 1],
831                    b' ' | b'\t' | b'\r' | b'\n' | b'\x0C'
832                );
833
834            if preceded_by_whitespace
835                && self.input[self.position] == b'E'
836                && self.input[self.position + 1] == b'I'
837            {
838                let after_ei = self.position + 2;
839                let followed_by_boundary = after_ei >= self.input.len()
840                    || matches!(
841                        self.input[after_ei],
842                        b' ' | b'\t' | b'\r' | b'\n' | b'\x0C' | b'/' | b'<' | b'(' | b'[' | b'%'
843                    );
844
845                if followed_by_boundary {
846                    // Trim trailing whitespace that preceded EI from the data
847                    let mut end = self.position;
848                    if end > start
849                        && matches!(self.input[end - 1], b' ' | b'\t' | b'\r' | b'\n' | b'\x0C')
850                    {
851                        end -= 1;
852                    }
853                    let data = self.input[start..end].to_vec();
854                    self.position = after_ei; // Skip past "EI"
855                    return Ok(Some(Token::InlineImageData(data)));
856                }
857            }
858            self.position += 1;
859        }
860
861        // No EI found — return remaining bytes as best-effort recovery
862        let data = self.input[start..].to_vec();
863        self.position = self.input.len();
864        Ok(Some(Token::InlineImageData(data)))
865    }
866}
867
868/// High-level content stream parser.
869///
870/// Converts tokenized content streams into structured `ContentOperation` values.
871/// This parser handles the operand stack and operator parsing according to PDF specifications.
872///
873/// # Usage
874///
875/// The parser is typically used through its static methods:
876///
877/// ```rust
878/// use oxidize_pdf::parser::content::ContentParser;
879///
880/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
881/// let content = b"q 1 0 0 1 50 50 cm 100 100 200 150 re S Q";
882/// let operations = ContentParser::parse(content)?;
883/// # Ok(())
884/// # }
885/// ```
886pub struct ContentParser {
887    tokens: Vec<Token>,
888    position: usize,
889}
890
891pub(crate) struct ParsedType3CharProc {
892    pub(crate) width: (f64, f64),
893    pub(crate) bbox: Option<[f64; 4]>,
894    pub(crate) operations: Vec<ContentOperation>,
895}
896
897impl ContentParser {
898    /// Create a new content parser
899    pub fn new(_content: &[u8]) -> Self {
900        Self {
901            tokens: Vec::new(),
902            position: 0,
903        }
904    }
905
906    /// Parse a content stream into a vector of operators.
907    ///
908    /// This is a convenience method that creates a parser and processes the entire stream.
909    ///
910    /// # Arguments
911    ///
912    /// * `content` - Raw content stream bytes (may be compressed)
913    ///
914    /// # Returns
915    ///
916    /// A vector of parsed `ContentOperation` values in the order they appear.
917    ///
918    /// # Errors
919    ///
920    /// Returns an error if:
921    /// - Invalid operator syntax is encountered
922    /// - Operators have incorrect number/type of operands
923    /// - Unknown operators are found
924    ///
925    /// # Example
926    ///
927    /// ```rust
928    /// use oxidize_pdf::parser::content::{ContentParser, ContentOperation};
929    ///
930    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
931    /// let content = b"BT /F1 12 Tf 100 200 Td (Hello) Tj ET";
932    /// let operations = ContentParser::parse(content)?;
933    ///
934    /// assert_eq!(operations.len(), 5);
935    /// assert!(matches!(operations[0], ContentOperation::BeginText));
936    /// # Ok(())
937    /// # }
938    /// ```
939    pub fn parse(content: &[u8]) -> ParseResult<Vec<ContentOperation>> {
940        Self::parse_content(content)
941    }
942
943    /// Parse a content stream without best-effort recovery.
944    ///
945    /// Unlike [`Self::parse`], malformed or unknown operators are returned to
946    /// the caller. This is intended for self-contained programs such as Type 3
947    /// character procedures, where silently dropping an operation could alter
948    /// the rendered glyph.
949    pub fn parse_strict(content: &[u8]) -> ParseResult<Vec<ContentOperation>> {
950        let mut tokenizer = ContentTokenizer::new(content);
951        let mut tokens = Vec::new();
952        while let Some(token) = tokenizer.next_token()? {
953            tokens.push(token);
954        }
955        Self {
956            tokens,
957            position: 0,
958        }
959        .parse_operators_strict()
960    }
961
962    pub(crate) fn parse_type3_charproc(content: &[u8]) -> ParseResult<ParsedType3CharProc> {
963        let mut tokenizer = ContentTokenizer::new(content);
964        let mut tokens = Vec::new();
965        while let Some(token) = tokenizer.next_token()? {
966            tokens.push(token);
967        }
968        let mut parser = Self {
969            tokens,
970            position: 0,
971        };
972        let mut operations = Vec::new();
973        let mut operands = Vec::new();
974        let mut metrics = None;
975        while parser.position < parser.tokens.len() {
976            let token = parser.tokens[parser.position].clone();
977            parser.position += 1;
978            match token {
979                Token::Operator(op) if op == "d0" => {
980                    if metrics.is_some() || !operations.is_empty() {
981                        return Err(ParseError::SyntaxError {
982                            position: parser.position,
983                            message: "d0/d1 must be the first Type 3 CharProc operator".into(),
984                        });
985                    }
986                    let wy = parser.pop_number(&mut operands)?;
987                    let wx = parser.pop_number(&mut operands)?;
988                    if !operands.is_empty() {
989                        return Err(ParseError::SyntaxError {
990                            position: parser.position,
991                            message: "d0 has extra operands".into(),
992                        });
993                    }
994                    metrics = Some(((f64::from(wx), f64::from(wy)), None));
995                }
996                Token::Operator(op) if op == "d1" => {
997                    if metrics.is_some() || !operations.is_empty() {
998                        return Err(ParseError::SyntaxError {
999                            position: parser.position,
1000                            message: "d0/d1 must be the first Type 3 CharProc operator".into(),
1001                        });
1002                    }
1003                    let ury = parser.pop_number(&mut operands)?;
1004                    let urx = parser.pop_number(&mut operands)?;
1005                    let lly = parser.pop_number(&mut operands)?;
1006                    let llx = parser.pop_number(&mut operands)?;
1007                    let wy = parser.pop_number(&mut operands)?;
1008                    let wx = parser.pop_number(&mut operands)?;
1009                    if !operands.is_empty() {
1010                        return Err(ParseError::SyntaxError {
1011                            position: parser.position,
1012                            message: "d1 has extra operands".into(),
1013                        });
1014                    }
1015                    metrics = Some((
1016                        (f64::from(wx), f64::from(wy)),
1017                        Some([
1018                            f64::from(llx),
1019                            f64::from(lly),
1020                            f64::from(urx),
1021                            f64::from(ury),
1022                        ]),
1023                    ));
1024                }
1025                Token::Operator(op) => {
1026                    operations.push(parser.parse_operator(&op, &mut operands)?);
1027                }
1028                operand => operands.push(operand),
1029            }
1030        }
1031        if !operands.is_empty() {
1032            return Err(ParseError::SyntaxError {
1033                position: parser.position,
1034                message: "trailing operands without an operator".into(),
1035            });
1036        }
1037        let (width, bbox) = metrics.ok_or_else(|| ParseError::SyntaxError {
1038            position: 0,
1039            message: "Type 3 CharProc must begin with d0 or d1".into(),
1040        })?;
1041        Ok(ParsedType3CharProc {
1042            width,
1043            bbox,
1044            operations,
1045        })
1046    }
1047
1048    /// Parse a content stream into a vector of operators.
1049    ///
1050    /// This method tokenizes the input and converts it to operations.
1051    /// It handles the PDF postfix notation where operands precede operators.
1052    pub fn parse_content(content: &[u8]) -> ParseResult<Vec<ContentOperation>> {
1053        let mut tokenizer = ContentTokenizer::new(content);
1054        let mut tokens = Vec::new();
1055
1056        // Tokenize the entire stream. Best-effort recovery (issue #319):
1057        // if the tokenizer hits an unrecoverable construct (e.g. an
1058        // unterminated hex string), stop there but KEEP every token parsed
1059        // so far instead of discarding the whole page. `next_token` already
1060        // recovers internally from skippable garbage; a hard error here is
1061        // the tail of the stream, and losing the tail beats losing the page.
1062        loop {
1063            match tokenizer.next_token() {
1064                Ok(Some(token)) => tokens.push(token),
1065                Ok(None) => break,
1066                Err(_e) => {
1067                    tracing::debug!("content tokenizer stopped early: {_e}");
1068                    break;
1069                }
1070            }
1071        }
1072
1073        let mut parser = Self {
1074            tokens,
1075            position: 0,
1076        };
1077
1078        parser.parse_operators()
1079    }
1080
1081    fn parse_operators(&mut self) -> ParseResult<Vec<ContentOperation>> {
1082        let mut operators = Vec::new();
1083        let mut operand_stack: Vec<Token> = Vec::new();
1084
1085        while self.position < self.tokens.len() {
1086            let token = self.tokens[self.position].clone();
1087            self.position += 1;
1088
1089            match &token {
1090                Token::Operator(op) => {
1091                    // Best-effort recovery (issue #319): a single malformed
1092                    // operator must NOT discard the entire content stream.
1093                    // Content streams are not safety-critical; an operand
1094                    // mismatch on one operator (e.g. a `Td` missing its
1095                    // numbers, common in some producers) previously aborted
1096                    // the whole page via `?`, so the extractor dropped every
1097                    // valid operator before it. Instead, skip the bad
1098                    // operator, resync by clearing its pending operands, and
1099                    // continue parsing the rest of the stream.
1100                    match self.parse_operator(op, &mut operand_stack) {
1101                        Ok(operator) => operators.push(operator),
1102                        Err(_e) => {
1103                            tracing::debug!("skipping malformed content operator '{op}': {_e}");
1104                            operand_stack.clear();
1105                        }
1106                    }
1107                }
1108                _ => {
1109                    // Not an operator, push to operand stack
1110                    operand_stack.push(token);
1111                }
1112            }
1113        }
1114
1115        Ok(operators)
1116    }
1117
1118    fn parse_operators_strict(&mut self) -> ParseResult<Vec<ContentOperation>> {
1119        let mut operators = Vec::new();
1120        let mut operand_stack = Vec::new();
1121        while self.position < self.tokens.len() {
1122            let token = self.tokens[self.position].clone();
1123            self.position += 1;
1124            match token {
1125                Token::Operator(op) => {
1126                    operators.push(self.parse_operator(&op, &mut operand_stack)?);
1127                }
1128                operand => operand_stack.push(operand),
1129            }
1130        }
1131        if operand_stack.is_empty() {
1132            Ok(operators)
1133        } else {
1134            Err(ParseError::SyntaxError {
1135                position: self.position,
1136                message: "trailing operands without an operator".to_string(),
1137            })
1138        }
1139    }
1140
1141    fn parse_operator(
1142        &mut self,
1143        op: &str,
1144        operands: &mut Vec<Token>,
1145    ) -> ParseResult<ContentOperation> {
1146        let operator = match op {
1147            // Text object operators
1148            "BT" => ContentOperation::BeginText,
1149            "ET" => ContentOperation::EndText,
1150
1151            // Text state operators
1152            "Tc" => {
1153                let spacing = self.pop_number(operands)?;
1154                ContentOperation::SetCharSpacing(spacing)
1155            }
1156            "Tw" => {
1157                let spacing = self.pop_number(operands)?;
1158                ContentOperation::SetWordSpacing(spacing)
1159            }
1160            "Tz" => {
1161                let scale = self.pop_number(operands)?;
1162                ContentOperation::SetHorizontalScaling(scale)
1163            }
1164            "TL" => {
1165                let leading = self.pop_number(operands)?;
1166                ContentOperation::SetLeading(leading)
1167            }
1168            "Tf" => {
1169                let size = self.pop_number(operands)?;
1170                let font = self.pop_name(operands)?;
1171                ContentOperation::SetFont(font, size)
1172            }
1173            "Tr" => {
1174                let mode = self.pop_integer(operands)?;
1175                ContentOperation::SetTextRenderMode(mode)
1176            }
1177            "Ts" => {
1178                let rise = self.pop_number(operands)?;
1179                ContentOperation::SetTextRise(rise)
1180            }
1181
1182            // Text positioning operators
1183            "Td" => {
1184                let ty = self.pop_number(operands)?;
1185                let tx = self.pop_number(operands)?;
1186                ContentOperation::MoveText(tx, ty)
1187            }
1188            "TD" => {
1189                let ty = self.pop_number(operands)?;
1190                let tx = self.pop_number(operands)?;
1191                ContentOperation::MoveTextSetLeading(tx, ty)
1192            }
1193            "Tm" => {
1194                let f = self.pop_number(operands)?;
1195                let e = self.pop_number(operands)?;
1196                let d = self.pop_number(operands)?;
1197                let c = self.pop_number(operands)?;
1198                let b = self.pop_number(operands)?;
1199                let a = self.pop_number(operands)?;
1200                ContentOperation::SetTextMatrix(a, b, c, d, e, f)
1201            }
1202            "T*" => ContentOperation::NextLine,
1203
1204            // Text showing operators
1205            "Tj" => {
1206                let text = self.pop_string(operands)?;
1207                ContentOperation::ShowText(text)
1208            }
1209            "TJ" => {
1210                let array = self.pop_array(operands)?;
1211                let elements = self.parse_text_array(array)?;
1212                ContentOperation::ShowTextArray(elements)
1213            }
1214            "'" => {
1215                let text = self.pop_string(operands)?;
1216                ContentOperation::NextLineShowText(text)
1217            }
1218            "\"" => {
1219                // ISO 32000-1 §9.4.3: operand order is `aw ac string "`
1220                // (aw at the bottom of the operand stack). `pop_*` is LIFO,
1221                // so we pop string first, then `ac`, then `aw`. The enum
1222                // variant is `(word_spacing, char_spacing, text)` to match
1223                // the spec field names — pass aw first, then ac.
1224                let text = self.pop_string(operands)?;
1225                let ac = self.pop_number(operands)?;
1226                let aw = self.pop_number(operands)?;
1227                ContentOperation::SetSpacingNextLineShowText(aw, ac, text)
1228            }
1229
1230            // Graphics state operators
1231            "q" => ContentOperation::SaveGraphicsState,
1232            "Q" => ContentOperation::RestoreGraphicsState,
1233            "cm" => {
1234                let f = self.pop_number(operands)?;
1235                let e = self.pop_number(operands)?;
1236                let d = self.pop_number(operands)?;
1237                let c = self.pop_number(operands)?;
1238                let b = self.pop_number(operands)?;
1239                let a = self.pop_number(operands)?;
1240                ContentOperation::SetTransformMatrix(a, b, c, d, e, f)
1241            }
1242            "w" => {
1243                let width = self.pop_number(operands)?;
1244                ContentOperation::SetLineWidth(width)
1245            }
1246            "J" => {
1247                let cap = self.pop_integer(operands)?;
1248                ContentOperation::SetLineCap(cap)
1249            }
1250            "j" => {
1251                let join = self.pop_integer(operands)?;
1252                ContentOperation::SetLineJoin(join)
1253            }
1254            "M" => {
1255                let limit = self.pop_number(operands)?;
1256                ContentOperation::SetMiterLimit(limit)
1257            }
1258            "d" => {
1259                let phase = self.pop_number(operands)?;
1260                let array = self.pop_array(operands)?;
1261                let pattern = self.parse_dash_array(array)?;
1262                ContentOperation::SetDashPattern(pattern, phase)
1263            }
1264            "ri" => {
1265                let intent = self.pop_name(operands)?;
1266                ContentOperation::SetIntent(intent)
1267            }
1268            "i" => {
1269                let flatness = self.pop_number(operands)?;
1270                ContentOperation::SetFlatness(flatness)
1271            }
1272            "gs" => {
1273                let name = self.pop_name(operands)?;
1274                ContentOperation::SetGraphicsStateParams(name)
1275            }
1276
1277            // Path construction operators
1278            "m" => {
1279                let y = self.pop_number(operands)?;
1280                let x = self.pop_number(operands)?;
1281                ContentOperation::MoveTo(x, y)
1282            }
1283            "l" => {
1284                let y = self.pop_number(operands)?;
1285                let x = self.pop_number(operands)?;
1286                ContentOperation::LineTo(x, y)
1287            }
1288            "c" => {
1289                let y3 = self.pop_number(operands)?;
1290                let x3 = self.pop_number(operands)?;
1291                let y2 = self.pop_number(operands)?;
1292                let x2 = self.pop_number(operands)?;
1293                let y1 = self.pop_number(operands)?;
1294                let x1 = self.pop_number(operands)?;
1295                ContentOperation::CurveTo(x1, y1, x2, y2, x3, y3)
1296            }
1297            "v" => {
1298                let y3 = self.pop_number(operands)?;
1299                let x3 = self.pop_number(operands)?;
1300                let y2 = self.pop_number(operands)?;
1301                let x2 = self.pop_number(operands)?;
1302                ContentOperation::CurveToV(x2, y2, x3, y3)
1303            }
1304            "y" => {
1305                let y3 = self.pop_number(operands)?;
1306                let x3 = self.pop_number(operands)?;
1307                let y1 = self.pop_number(operands)?;
1308                let x1 = self.pop_number(operands)?;
1309                ContentOperation::CurveToY(x1, y1, x3, y3)
1310            }
1311            "h" => ContentOperation::ClosePath,
1312            "re" => {
1313                let height = self.pop_number(operands)?;
1314                let width = self.pop_number(operands)?;
1315                let y = self.pop_number(operands)?;
1316                let x = self.pop_number(operands)?;
1317                ContentOperation::Rectangle(x, y, width, height)
1318            }
1319
1320            // Path painting operators
1321            "S" => ContentOperation::Stroke,
1322            "s" => ContentOperation::CloseStroke,
1323            "f" | "F" => ContentOperation::Fill,
1324            "f*" => ContentOperation::FillEvenOdd,
1325            "B" => ContentOperation::FillStroke,
1326            "B*" => ContentOperation::FillStrokeEvenOdd,
1327            "b" => ContentOperation::CloseFillStroke,
1328            "b*" => ContentOperation::CloseFillStrokeEvenOdd,
1329            "n" => ContentOperation::EndPath,
1330
1331            // Clipping path operators
1332            "W" => ContentOperation::Clip,
1333            "W*" => ContentOperation::ClipEvenOdd,
1334
1335            // Color operators
1336            "CS" => {
1337                let name = self.pop_name(operands)?;
1338                ContentOperation::SetStrokingColorSpace(name)
1339            }
1340            "cs" => {
1341                let name = self.pop_name(operands)?;
1342                ContentOperation::SetNonStrokingColorSpace(name)
1343            }
1344            "SC" | "SCN" => {
1345                let components = self.pop_color_components(operands)?;
1346                ContentOperation::SetStrokingColor(components)
1347            }
1348            "sc" | "scn" => {
1349                let components = self.pop_color_components(operands)?;
1350                ContentOperation::SetNonStrokingColor(components)
1351            }
1352            "G" => {
1353                let gray = self.pop_number(operands)?;
1354                ContentOperation::SetStrokingGray(gray)
1355            }
1356            "g" => {
1357                let gray = self.pop_number(operands)?;
1358                ContentOperation::SetNonStrokingGray(gray)
1359            }
1360            "RG" => {
1361                let b = self.pop_number(operands)?;
1362                let g = self.pop_number(operands)?;
1363                let r = self.pop_number(operands)?;
1364                ContentOperation::SetStrokingRGB(r, g, b)
1365            }
1366            "rg" => {
1367                let b = self.pop_number(operands)?;
1368                let g = self.pop_number(operands)?;
1369                let r = self.pop_number(operands)?;
1370                ContentOperation::SetNonStrokingRGB(r, g, b)
1371            }
1372            "K" => {
1373                let k = self.pop_number(operands)?;
1374                let y = self.pop_number(operands)?;
1375                let m = self.pop_number(operands)?;
1376                let c = self.pop_number(operands)?;
1377                ContentOperation::SetStrokingCMYK(c, m, y, k)
1378            }
1379            "k" => {
1380                let k = self.pop_number(operands)?;
1381                let y = self.pop_number(operands)?;
1382                let m = self.pop_number(operands)?;
1383                let c = self.pop_number(operands)?;
1384                ContentOperation::SetNonStrokingCMYK(c, m, y, k)
1385            }
1386
1387            // Shading operators
1388            "sh" => {
1389                let name = self.pop_name(operands)?;
1390                ContentOperation::ShadingFill(name)
1391            }
1392
1393            // XObject operators
1394            "Do" => {
1395                let name = self.pop_name(operands)?;
1396                ContentOperation::PaintXObject(name)
1397            }
1398
1399            // Marked content operators
1400            "BMC" => {
1401                let tag = self.pop_name(operands)?;
1402                ContentOperation::BeginMarkedContent(tag)
1403            }
1404            "BDC" => {
1405                let props = self.pop_dict_or_name(operands)?;
1406                let tag = self.pop_name(operands)?;
1407                ContentOperation::BeginMarkedContentWithProps(tag, props)
1408            }
1409            "EMC" => ContentOperation::EndMarkedContent,
1410            "MP" => {
1411                let tag = self.pop_name(operands)?;
1412                ContentOperation::DefineMarkedContentPoint(tag)
1413            }
1414            "DP" => {
1415                let props = self.pop_dict_or_name(operands)?;
1416                let tag = self.pop_name(operands)?;
1417                ContentOperation::DefineMarkedContentPointWithProps(tag, props)
1418            }
1419
1420            // Compatibility operators
1421            "BX" => ContentOperation::BeginCompatibility,
1422            "EX" => ContentOperation::EndCompatibility,
1423
1424            // Inline images are handled specially
1425            "BI" => {
1426                operands.clear(); // Clear any remaining operands
1427                self.parse_inline_image()?
1428            }
1429
1430            _ => {
1431                return Err(ParseError::SyntaxError {
1432                    position: self.position,
1433                    message: format!("Unknown operator: {op}"),
1434                });
1435            }
1436        };
1437
1438        operands.clear(); // Clear operands after processing
1439        Ok(operator)
1440    }
1441
1442    // Helper methods for popping operands
1443    fn pop_number(&self, operands: &mut Vec<Token>) -> ParseResult<f32> {
1444        match operands.pop() {
1445            Some(Token::Number(n)) => Ok(n),
1446            Some(Token::Integer(i)) => Ok(i as f32),
1447            _ => Err(ParseError::SyntaxError {
1448                position: self.position,
1449                message: "Expected number operand".to_string(),
1450            }),
1451        }
1452    }
1453
1454    fn pop_integer(&self, operands: &mut Vec<Token>) -> ParseResult<i32> {
1455        match operands.pop() {
1456            Some(Token::Integer(i)) => Ok(i),
1457            _ => Err(ParseError::SyntaxError {
1458                position: self.position,
1459                message: "Expected integer operand".to_string(),
1460            }),
1461        }
1462    }
1463
1464    fn pop_name(&self, operands: &mut Vec<Token>) -> ParseResult<String> {
1465        match operands.pop() {
1466            Some(Token::Name(n)) => Ok(n),
1467            _ => Err(ParseError::SyntaxError {
1468                position: self.position,
1469                message: "Expected name operand".to_string(),
1470            }),
1471        }
1472    }
1473
1474    fn pop_string(&self, operands: &mut Vec<Token>) -> ParseResult<Vec<u8>> {
1475        match operands.pop() {
1476            Some(Token::String(s)) => Ok(s),
1477            Some(Token::HexString(s)) => Ok(s),
1478            _ => Err(ParseError::SyntaxError {
1479                position: self.position,
1480                message: "Expected string operand".to_string(),
1481            }),
1482        }
1483    }
1484
1485    fn pop_array(&self, operands: &mut Vec<Token>) -> ParseResult<Vec<Token>> {
1486        // First check if we have an ArrayEnd at the top (which we should for a complete array)
1487        let has_array_end = matches!(operands.last(), Some(Token::ArrayEnd));
1488        if has_array_end {
1489            operands.pop(); // Remove the ArrayEnd
1490        }
1491
1492        let mut array = Vec::new();
1493        let mut found_start = false;
1494
1495        // Pop tokens until we find ArrayStart
1496        while let Some(token) = operands.pop() {
1497            match token {
1498                Token::ArrayStart => {
1499                    found_start = true;
1500                    break;
1501                }
1502                Token::ArrayEnd => {
1503                    // Skip any additional ArrayEnd tokens (shouldn't happen in well-formed PDFs)
1504                    continue;
1505                }
1506                _ => array.push(token),
1507            }
1508        }
1509
1510        if !found_start {
1511            return Err(ParseError::SyntaxError {
1512                position: self.position,
1513                message: "Expected array".to_string(),
1514            });
1515        }
1516
1517        array.reverse(); // We collected in reverse order
1518        Ok(array)
1519    }
1520
1521    fn pop_dict_or_name(&self, operands: &mut Vec<Token>) -> ParseResult<MarkedContentProps> {
1522        let token = operands.pop().ok_or_else(|| ParseError::SyntaxError {
1523            position: self.position,
1524            message: "Expected dict or name operand for BDC/DP".to_string(),
1525        })?;
1526
1527        match token {
1528            Token::Name(name) => Ok(MarkedContentProps::ResourceRef(name)),
1529            Token::DictEnd => {
1530                // Inline dictionary. Stack layout (newest on top):
1531                //   ... DictStart Name(k1) Value(v1) Name(k2) Value(v2) DictEnd
1532                // We pop value-then-key pairs in reverse until we hit DictStart.
1533                let mut map: HashMap<String, MarkedContentValue> = HashMap::new();
1534                loop {
1535                    let next = operands.pop().ok_or_else(|| ParseError::SyntaxError {
1536                        position: self.position,
1537                        message: "Unterminated inline dict in BDC/DP".to_string(),
1538                    })?;
1539                    if matches!(next, Token::DictStart) {
1540                        break;
1541                    }
1542                    let value = Self::token_to_mc_value(next, operands)?;
1543                    let key = match operands.pop() {
1544                        Some(Token::Name(k)) => k,
1545                        Some(other) => {
1546                            return Err(ParseError::SyntaxError {
1547                                position: self.position,
1548                                message: format!(
1549                                    "Expected Name as inline dict key, got {:?}",
1550                                    other
1551                                ),
1552                            });
1553                        }
1554                        None => {
1555                            return Err(ParseError::SyntaxError {
1556                                position: self.position,
1557                                message: "Unterminated inline dict (missing key)".to_string(),
1558                            });
1559                        }
1560                    };
1561                    map.insert(key, value);
1562                }
1563                Ok(MarkedContentProps::Inline(map))
1564            }
1565            other => Err(ParseError::SyntaxError {
1566                position: self.position,
1567                message: format!("Expected name or inline dict for BDC/DP, got {:?}", other),
1568            }),
1569        }
1570    }
1571
1572    /// Convert a popped token to a `MarkedContentValue`. For `ArrayEnd` and
1573    /// `DictEnd` tokens we recursively collect the matching container; all
1574    /// other tokens map to leaf variants.
1575    fn token_to_mc_value(
1576        token: Token,
1577        operands: &mut Vec<Token>,
1578    ) -> ParseResult<MarkedContentValue> {
1579        match token {
1580            Token::String(b) | Token::HexString(b) => Ok(MarkedContentValue::String(b)),
1581            Token::Integer(i) => Ok(MarkedContentValue::Integer(i as i64)),
1582            Token::Number(f) => Ok(MarkedContentValue::Real(f as f64)),
1583            Token::Name(n) => Ok(MarkedContentValue::Name(n)),
1584            Token::ArrayEnd => {
1585                let mut items: Vec<MarkedContentValue> = Vec::new();
1586                loop {
1587                    let next = operands.pop().ok_or_else(|| ParseError::SyntaxError {
1588                        position: 0,
1589                        message: "Unterminated array in marked-content props".to_string(),
1590                    })?;
1591                    if matches!(next, Token::ArrayStart) {
1592                        break;
1593                    }
1594                    items.push(Self::token_to_mc_value(next, operands)?);
1595                }
1596                items.reverse();
1597                Ok(MarkedContentValue::Array(items))
1598            }
1599            Token::DictEnd => {
1600                let mut nested: HashMap<String, MarkedContentValue> = HashMap::new();
1601                loop {
1602                    let next = operands.pop().ok_or_else(|| ParseError::SyntaxError {
1603                        position: 0,
1604                        message: "Unterminated nested dict in marked-content props".to_string(),
1605                    })?;
1606                    if matches!(next, Token::DictStart) {
1607                        break;
1608                    }
1609                    let value = Self::token_to_mc_value(next, operands)?;
1610                    let key = match operands.pop() {
1611                        Some(Token::Name(k)) => k,
1612                        _ => {
1613                            return Err(ParseError::SyntaxError {
1614                                position: 0,
1615                                message: "Expected name key in nested dict".to_string(),
1616                            });
1617                        }
1618                    };
1619                    nested.insert(key, value);
1620                }
1621                Ok(MarkedContentValue::Dict(nested))
1622            }
1623            other => Err(ParseError::SyntaxError {
1624                position: 0,
1625                message: format!("Unexpected token type in marked-content value: {:?}", other),
1626            }),
1627        }
1628    }
1629
1630    fn pop_color_components(&self, operands: &mut Vec<Token>) -> ParseResult<Vec<f32>> {
1631        let mut components = Vec::new();
1632
1633        // Pop all numeric values from the stack
1634        while let Some(token) = operands.last() {
1635            match token {
1636                Token::Number(n) => {
1637                    components.push(*n);
1638                    operands.pop();
1639                }
1640                Token::Integer(i) => {
1641                    components.push(*i as f32);
1642                    operands.pop();
1643                }
1644                _ => break,
1645            }
1646        }
1647
1648        components.reverse();
1649        Ok(components)
1650    }
1651
1652    fn parse_text_array(&self, tokens: Vec<Token>) -> ParseResult<Vec<TextElement>> {
1653        let mut elements = Vec::new();
1654
1655        for token in tokens {
1656            match token {
1657                Token::String(s) | Token::HexString(s) => {
1658                    elements.push(TextElement::Text(s));
1659                }
1660                Token::Number(n) => {
1661                    elements.push(TextElement::Spacing(n));
1662                }
1663                Token::Integer(i) => {
1664                    elements.push(TextElement::Spacing(i as f32));
1665                }
1666                _ => {
1667                    return Err(ParseError::SyntaxError {
1668                        position: self.position,
1669                        message: "Invalid element in text array".to_string(),
1670                    });
1671                }
1672            }
1673        }
1674
1675        Ok(elements)
1676    }
1677
1678    fn parse_dash_array(&self, tokens: Vec<Token>) -> ParseResult<Vec<f32>> {
1679        let mut pattern = Vec::new();
1680
1681        for token in tokens {
1682            match token {
1683                Token::Number(n) => pattern.push(n),
1684                Token::Integer(i) => pattern.push(i as f32),
1685                _ => {
1686                    return Err(ParseError::SyntaxError {
1687                        position: self.position,
1688                        message: "Invalid element in dash array".to_string(),
1689                    });
1690                }
1691            }
1692        }
1693
1694        Ok(pattern)
1695    }
1696
1697    fn parse_inline_image(&mut self) -> ParseResult<ContentOperation> {
1698        // Parse inline image dictionary until we find ID
1699        let mut params = HashMap::new();
1700
1701        while self.position < self.tokens.len() {
1702            // Check if we've reached the ID operator
1703            if let Token::Operator(op) = &self.tokens[self.position] {
1704                if op == "ID" {
1705                    self.position += 1;
1706                    break;
1707                }
1708            }
1709
1710            // Parse key-value pairs for image parameters
1711            // Keys are abbreviated in inline images:
1712            // /W -> Width, /H -> Height, /CS -> ColorSpace, /BPC -> BitsPerComponent
1713            // /F -> Filter, /DP -> DecodeParms, /IM -> ImageMask, /I -> Interpolate
1714            if let Token::Name(key) = &self.tokens[self.position] {
1715                self.position += 1;
1716                if self.position >= self.tokens.len() {
1717                    break;
1718                }
1719
1720                // Parse the value
1721                let value = match &self.tokens[self.position] {
1722                    Token::Integer(n) => Object::Integer(*n as i64),
1723                    Token::Number(n) => Object::Real(*n as f64),
1724                    Token::Name(s) => Object::Name(expand_inline_name(s)),
1725                    Token::String(s) => Object::String(String::from_utf8_lossy(s).to_string()),
1726                    Token::HexString(s) => Object::String(String::from_utf8_lossy(s).to_string()),
1727                    _ => Object::Null,
1728                };
1729
1730                // Expand abbreviated keys to full names
1731                let full_key = expand_inline_key(key);
1732                params.insert(full_key, value);
1733                self.position += 1;
1734            } else {
1735                self.position += 1;
1736            }
1737        }
1738
1739        // Get inline image data from dedicated InlineImageData token
1740        // (the tokenizer reads raw bytes between ID whitespace and EI)
1741        let data = if self.position < self.tokens.len() {
1742            if let Token::InlineImageData(bytes) = &self.tokens[self.position] {
1743                let d = bytes.clone();
1744                self.position += 1;
1745                d
1746            } else {
1747                // Fallback: collect tokens until EI (for backwards compat with edge cases)
1748                self.collect_inline_image_data_from_tokens()?
1749            }
1750        } else {
1751            Vec::new()
1752        };
1753
1754        Ok(ContentOperation::InlineImage { params, data })
1755    }
1756
1757    /// Fallback data collection when InlineImageData token is not present.
1758    /// This handles edge cases where the tokenizer couldn't detect the ID/EI boundary.
1759    fn collect_inline_image_data_from_tokens(&mut self) -> ParseResult<Vec<u8>> {
1760        let mut data = Vec::new();
1761        while self.position < self.tokens.len() {
1762            if let Token::Operator(op) = &self.tokens[self.position] {
1763                if op == "EI" {
1764                    self.position += 1;
1765                    break;
1766                }
1767            }
1768            match &self.tokens[self.position] {
1769                Token::String(bytes) | Token::HexString(bytes) => {
1770                    data.extend_from_slice(bytes);
1771                }
1772                Token::Integer(n) => data.extend_from_slice(n.to_string().as_bytes()),
1773                Token::Number(n) => data.extend_from_slice(n.to_string().as_bytes()),
1774                Token::Name(s) | Token::Operator(s) => data.extend_from_slice(s.as_bytes()),
1775                _ => {}
1776            }
1777            self.position += 1;
1778        }
1779        Ok(data)
1780    }
1781}
1782
1783/// Expand abbreviated inline image key names to full names
1784fn expand_inline_key(key: &str) -> String {
1785    match key {
1786        "W" => "Width".to_string(),
1787        "H" => "Height".to_string(),
1788        "CS" | "ColorSpace" => "ColorSpace".to_string(),
1789        "BPC" | "BitsPerComponent" => "BitsPerComponent".to_string(),
1790        "F" => "Filter".to_string(),
1791        "DP" | "DecodeParms" => "DecodeParms".to_string(),
1792        "IM" => "ImageMask".to_string(),
1793        "I" => "Interpolate".to_string(),
1794        "Intent" => "Intent".to_string(),
1795        "D" => "Decode".to_string(),
1796        _ => key.to_string(),
1797    }
1798}
1799
1800/// Expand abbreviated inline image color space names
1801fn expand_inline_name(name: &str) -> String {
1802    match name {
1803        "G" => "DeviceGray".to_string(),
1804        "RGB" => "DeviceRGB".to_string(),
1805        "CMYK" => "DeviceCMYK".to_string(),
1806        "I" => "Indexed".to_string(),
1807        "AHx" => "ASCIIHexDecode".to_string(),
1808        "A85" => "ASCII85Decode".to_string(),
1809        "LZW" => "LZWDecode".to_string(),
1810        "Fl" => "FlateDecode".to_string(),
1811        "RL" => "RunLengthDecode".to_string(),
1812        "DCT" => "DCTDecode".to_string(),
1813        "CCF" => "CCITTFaxDecode".to_string(),
1814        _ => name.to_string(),
1815    }
1816}
1817
1818#[cfg(test)]
1819mod tests {
1820    use super::*;
1821
1822    #[test]
1823    fn test_tokenize_numbers() {
1824        let input = b"123 -45 3.14159 -0.5 .5";
1825        let mut tokenizer = ContentTokenizer::new(input);
1826
1827        assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Integer(123)));
1828        assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Integer(-45)));
1829        assert_eq!(
1830            tokenizer.next_token().unwrap(),
1831            Some(Token::Number(3.14159))
1832        );
1833        assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Number(-0.5)));
1834        assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Number(0.5)));
1835        assert_eq!(tokenizer.next_token().unwrap(), None);
1836    }
1837
1838    #[test]
1839    fn test_tokenize_strings() {
1840        let input = b"(Hello World) (Hello\\nWorld) (Nested (paren))";
1841        let mut tokenizer = ContentTokenizer::new(input);
1842
1843        assert_eq!(
1844            tokenizer.next_token().unwrap(),
1845            Some(Token::String(b"Hello World".to_vec()))
1846        );
1847        assert_eq!(
1848            tokenizer.next_token().unwrap(),
1849            Some(Token::String(b"Hello\nWorld".to_vec()))
1850        );
1851        assert_eq!(
1852            tokenizer.next_token().unwrap(),
1853            Some(Token::String(b"Nested (paren)".to_vec()))
1854        );
1855    }
1856
1857    #[test]
1858    fn test_tokenize_hex_strings() {
1859        let input = b"<48656C6C6F> <48 65 6C 6C 6F>";
1860        let mut tokenizer = ContentTokenizer::new(input);
1861
1862        assert_eq!(
1863            tokenizer.next_token().unwrap(),
1864            Some(Token::HexString(b"Hello".to_vec()))
1865        );
1866        assert_eq!(
1867            tokenizer.next_token().unwrap(),
1868            Some(Token::HexString(b"Hello".to_vec()))
1869        );
1870    }
1871
1872    #[test]
1873    fn test_tokenize_names() {
1874        let input = b"/Name /Name#20with#20spaces /A#42C";
1875        let mut tokenizer = ContentTokenizer::new(input);
1876
1877        assert_eq!(
1878            tokenizer.next_token().unwrap(),
1879            Some(Token::Name("Name".to_string()))
1880        );
1881        assert_eq!(
1882            tokenizer.next_token().unwrap(),
1883            Some(Token::Name("Name with spaces".to_string()))
1884        );
1885        assert_eq!(
1886            tokenizer.next_token().unwrap(),
1887            Some(Token::Name("ABC".to_string()))
1888        );
1889    }
1890
1891    #[test]
1892    fn test_tokenize_operators() {
1893        let input = b"BT Tj ET q Q";
1894        let mut tokenizer = ContentTokenizer::new(input);
1895
1896        assert_eq!(
1897            tokenizer.next_token().unwrap(),
1898            Some(Token::Operator("BT".to_string()))
1899        );
1900        assert_eq!(
1901            tokenizer.next_token().unwrap(),
1902            Some(Token::Operator("Tj".to_string()))
1903        );
1904        assert_eq!(
1905            tokenizer.next_token().unwrap(),
1906            Some(Token::Operator("ET".to_string()))
1907        );
1908        assert_eq!(
1909            tokenizer.next_token().unwrap(),
1910            Some(Token::Operator("q".to_string()))
1911        );
1912        assert_eq!(
1913            tokenizer.next_token().unwrap(),
1914            Some(Token::Operator("Q".to_string()))
1915        );
1916    }
1917
1918    #[test]
1919    fn test_parse_text_operators() {
1920        let content = b"BT /F1 12 Tf 100 200 Td (Hello World) Tj ET";
1921        let operators = ContentParser::parse(content).unwrap();
1922
1923        assert_eq!(operators.len(), 5);
1924        assert_eq!(operators[0], ContentOperation::BeginText);
1925        assert_eq!(
1926            operators[1],
1927            ContentOperation::SetFont("F1".to_string(), 12.0)
1928        );
1929        assert_eq!(operators[2], ContentOperation::MoveText(100.0, 200.0));
1930        assert_eq!(
1931            operators[3],
1932            ContentOperation::ShowText(b"Hello World".to_vec())
1933        );
1934        assert_eq!(operators[4], ContentOperation::EndText);
1935    }
1936
1937    #[test]
1938    fn test_parse_graphics_operators() {
1939        let content = b"q 1 0 0 1 50 50 cm 2 w 0 0 100 100 re S Q";
1940        let operators = ContentParser::parse(content).unwrap();
1941
1942        assert_eq!(operators.len(), 6);
1943        assert_eq!(operators[0], ContentOperation::SaveGraphicsState);
1944        assert_eq!(
1945            operators[1],
1946            ContentOperation::SetTransformMatrix(1.0, 0.0, 0.0, 1.0, 50.0, 50.0)
1947        );
1948        assert_eq!(operators[2], ContentOperation::SetLineWidth(2.0));
1949        assert_eq!(
1950            operators[3],
1951            ContentOperation::Rectangle(0.0, 0.0, 100.0, 100.0)
1952        );
1953        assert_eq!(operators[4], ContentOperation::Stroke);
1954        assert_eq!(operators[5], ContentOperation::RestoreGraphicsState);
1955    }
1956
1957    #[test]
1958    fn test_parse_color_operators() {
1959        let content = b"0.5 g 1 0 0 rg 0 0 0 1 k";
1960        let operators = ContentParser::parse(content).unwrap();
1961
1962        assert_eq!(operators.len(), 3);
1963        assert_eq!(operators[0], ContentOperation::SetNonStrokingGray(0.5));
1964        assert_eq!(
1965            operators[1],
1966            ContentOperation::SetNonStrokingRGB(1.0, 0.0, 0.0)
1967        );
1968        assert_eq!(
1969            operators[2],
1970            ContentOperation::SetNonStrokingCMYK(0.0, 0.0, 0.0, 1.0)
1971        );
1972    }
1973
1974    // Comprehensive tests for all ContentOperation variants
1975    mod comprehensive_tests {
1976        use super::*;
1977
1978        #[test]
1979        fn test_all_text_operators() {
1980            // Test basic text operators that work with current parser
1981            let content = b"BT 5 Tc 10 Tw 120 Tz 15 TL /F1 12 Tf 1 Tr 5 Ts 100 200 Td 50 150 TD T* (Hello) Tj ET";
1982            let operators = ContentParser::parse(content).unwrap();
1983
1984            assert_eq!(operators[0], ContentOperation::BeginText);
1985            assert_eq!(operators[1], ContentOperation::SetCharSpacing(5.0));
1986            assert_eq!(operators[2], ContentOperation::SetWordSpacing(10.0));
1987            assert_eq!(operators[3], ContentOperation::SetHorizontalScaling(120.0));
1988            assert_eq!(operators[4], ContentOperation::SetLeading(15.0));
1989            assert_eq!(
1990                operators[5],
1991                ContentOperation::SetFont("F1".to_string(), 12.0)
1992            );
1993            assert_eq!(operators[6], ContentOperation::SetTextRenderMode(1));
1994            assert_eq!(operators[7], ContentOperation::SetTextRise(5.0));
1995            assert_eq!(operators[8], ContentOperation::MoveText(100.0, 200.0));
1996            assert_eq!(
1997                operators[9],
1998                ContentOperation::MoveTextSetLeading(50.0, 150.0)
1999            );
2000            assert_eq!(operators[10], ContentOperation::NextLine);
2001            assert_eq!(operators[11], ContentOperation::ShowText(b"Hello".to_vec()));
2002            assert_eq!(operators[12], ContentOperation::EndText);
2003        }
2004
2005        #[test]
2006        fn test_all_graphics_state_operators() {
2007            // Test basic graphics state operators without arrays
2008            let content = b"q Q 1 0 0 1 50 50 cm 2 w 1 J 2 j 10 M /GS1 gs 0.5 i /Perceptual ri";
2009            let operators = ContentParser::parse(content).unwrap();
2010
2011            assert_eq!(operators[0], ContentOperation::SaveGraphicsState);
2012            assert_eq!(operators[1], ContentOperation::RestoreGraphicsState);
2013            assert_eq!(
2014                operators[2],
2015                ContentOperation::SetTransformMatrix(1.0, 0.0, 0.0, 1.0, 50.0, 50.0)
2016            );
2017            assert_eq!(operators[3], ContentOperation::SetLineWidth(2.0));
2018            assert_eq!(operators[4], ContentOperation::SetLineCap(1));
2019            assert_eq!(operators[5], ContentOperation::SetLineJoin(2));
2020            assert_eq!(operators[6], ContentOperation::SetMiterLimit(10.0));
2021            assert_eq!(
2022                operators[7],
2023                ContentOperation::SetGraphicsStateParams("GS1".to_string())
2024            );
2025            assert_eq!(operators[8], ContentOperation::SetFlatness(0.5));
2026            assert_eq!(
2027                operators[9],
2028                ContentOperation::SetIntent("Perceptual".to_string())
2029            );
2030        }
2031
2032        #[test]
2033        fn test_all_path_construction_operators() {
2034            let content = b"100 200 m 150 200 l 200 200 250 250 300 200 c 250 180 300 200 v 200 180 300 200 y h 50 50 100 100 re";
2035            let operators = ContentParser::parse(content).unwrap();
2036
2037            assert_eq!(operators[0], ContentOperation::MoveTo(100.0, 200.0));
2038            assert_eq!(operators[1], ContentOperation::LineTo(150.0, 200.0));
2039            assert_eq!(
2040                operators[2],
2041                ContentOperation::CurveTo(200.0, 200.0, 250.0, 250.0, 300.0, 200.0)
2042            );
2043            assert_eq!(
2044                operators[3],
2045                ContentOperation::CurveToV(250.0, 180.0, 300.0, 200.0)
2046            );
2047            assert_eq!(
2048                operators[4],
2049                ContentOperation::CurveToY(200.0, 180.0, 300.0, 200.0)
2050            );
2051            assert_eq!(operators[5], ContentOperation::ClosePath);
2052            assert_eq!(
2053                operators[6],
2054                ContentOperation::Rectangle(50.0, 50.0, 100.0, 100.0)
2055            );
2056        }
2057
2058        #[test]
2059        fn test_all_path_painting_operators() {
2060            let content = b"S s f F f* B B* b b* n W W*";
2061            let operators = ContentParser::parse(content).unwrap();
2062
2063            assert_eq!(operators[0], ContentOperation::Stroke);
2064            assert_eq!(operators[1], ContentOperation::CloseStroke);
2065            assert_eq!(operators[2], ContentOperation::Fill);
2066            assert_eq!(operators[3], ContentOperation::Fill); // F is alias for f
2067            assert_eq!(operators[4], ContentOperation::FillEvenOdd);
2068            assert_eq!(operators[5], ContentOperation::FillStroke);
2069            assert_eq!(operators[6], ContentOperation::FillStrokeEvenOdd);
2070            assert_eq!(operators[7], ContentOperation::CloseFillStroke);
2071            assert_eq!(operators[8], ContentOperation::CloseFillStrokeEvenOdd);
2072            assert_eq!(operators[9], ContentOperation::EndPath);
2073            assert_eq!(operators[10], ContentOperation::Clip);
2074            assert_eq!(operators[11], ContentOperation::ClipEvenOdd);
2075        }
2076
2077        #[test]
2078        fn test_all_color_operators() {
2079            // Test basic color operators that work with current parser
2080            let content = b"/DeviceRGB CS /DeviceGray cs 0.7 G 0.4 g 1 0 0 RG 0 1 0 rg 0 0 0 1 K 0.2 0.3 0.4 0.5 k /Shade1 sh";
2081            let operators = ContentParser::parse(content).unwrap();
2082
2083            assert_eq!(
2084                operators[0],
2085                ContentOperation::SetStrokingColorSpace("DeviceRGB".to_string())
2086            );
2087            assert_eq!(
2088                operators[1],
2089                ContentOperation::SetNonStrokingColorSpace("DeviceGray".to_string())
2090            );
2091            assert_eq!(operators[2], ContentOperation::SetStrokingGray(0.7));
2092            assert_eq!(operators[3], ContentOperation::SetNonStrokingGray(0.4));
2093            assert_eq!(
2094                operators[4],
2095                ContentOperation::SetStrokingRGB(1.0, 0.0, 0.0)
2096            );
2097            assert_eq!(
2098                operators[5],
2099                ContentOperation::SetNonStrokingRGB(0.0, 1.0, 0.0)
2100            );
2101            assert_eq!(
2102                operators[6],
2103                ContentOperation::SetStrokingCMYK(0.0, 0.0, 0.0, 1.0)
2104            );
2105            assert_eq!(
2106                operators[7],
2107                ContentOperation::SetNonStrokingCMYK(0.2, 0.3, 0.4, 0.5)
2108            );
2109            assert_eq!(
2110                operators[8],
2111                ContentOperation::ShadingFill("Shade1".to_string())
2112            );
2113        }
2114
2115        #[test]
2116        fn test_xobject_and_marked_content_operators() {
2117            // Test basic XObject and marked content operators
2118            let content = b"/Image1 Do /MC1 BMC EMC /MP1 MP BX EX";
2119            let operators = ContentParser::parse(content).unwrap();
2120
2121            assert_eq!(
2122                operators[0],
2123                ContentOperation::PaintXObject("Image1".to_string())
2124            );
2125            assert_eq!(
2126                operators[1],
2127                ContentOperation::BeginMarkedContent("MC1".to_string())
2128            );
2129            assert_eq!(operators[2], ContentOperation::EndMarkedContent);
2130            assert_eq!(
2131                operators[3],
2132                ContentOperation::DefineMarkedContentPoint("MP1".to_string())
2133            );
2134            assert_eq!(operators[4], ContentOperation::BeginCompatibility);
2135            assert_eq!(operators[5], ContentOperation::EndCompatibility);
2136        }
2137
2138        #[test]
2139        fn test_complex_content_stream() {
2140            let content = b"q 0.5 0 0 0.5 100 100 cm BT /F1 12 Tf 0 0 Td (Complex) Tj ET Q";
2141            let operators = ContentParser::parse(content).unwrap();
2142
2143            assert_eq!(operators.len(), 8);
2144            assert_eq!(operators[0], ContentOperation::SaveGraphicsState);
2145            assert_eq!(
2146                operators[1],
2147                ContentOperation::SetTransformMatrix(0.5, 0.0, 0.0, 0.5, 100.0, 100.0)
2148            );
2149            assert_eq!(operators[2], ContentOperation::BeginText);
2150            assert_eq!(
2151                operators[3],
2152                ContentOperation::SetFont("F1".to_string(), 12.0)
2153            );
2154            assert_eq!(operators[4], ContentOperation::MoveText(0.0, 0.0));
2155            assert_eq!(
2156                operators[5],
2157                ContentOperation::ShowText(b"Complex".to_vec())
2158            );
2159            assert_eq!(operators[6], ContentOperation::EndText);
2160            assert_eq!(operators[7], ContentOperation::RestoreGraphicsState);
2161        }
2162
2163        #[test]
2164        fn test_tokenizer_whitespace_handling() {
2165            let input = b"  \t\n\r  BT  \t\n  /F1   12.5  \t Tf  \n\r  ET  ";
2166            let mut tokenizer = ContentTokenizer::new(input);
2167
2168            assert_eq!(
2169                tokenizer.next_token().unwrap(),
2170                Some(Token::Operator("BT".to_string()))
2171            );
2172            assert_eq!(
2173                tokenizer.next_token().unwrap(),
2174                Some(Token::Name("F1".to_string()))
2175            );
2176            assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Number(12.5)));
2177            assert_eq!(
2178                tokenizer.next_token().unwrap(),
2179                Some(Token::Operator("Tf".to_string()))
2180            );
2181            assert_eq!(
2182                tokenizer.next_token().unwrap(),
2183                Some(Token::Operator("ET".to_string()))
2184            );
2185            assert_eq!(tokenizer.next_token().unwrap(), None);
2186        }
2187
2188        #[test]
2189        fn test_tokenizer_edge_cases() {
2190            // Test basic number formats that are actually supported
2191            let input = b"0 .5 -.5 +.5 123. .123 1.23 -1.23";
2192            let mut tokenizer = ContentTokenizer::new(input);
2193
2194            assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Integer(0)));
2195            assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Number(0.5)));
2196            assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Number(-0.5)));
2197            assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Number(0.5)));
2198            assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Number(123.0)));
2199            assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Number(0.123)));
2200            assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Number(1.23)));
2201            assert_eq!(tokenizer.next_token().unwrap(), Some(Token::Number(-1.23)));
2202        }
2203
2204        #[test]
2205        fn test_string_parsing_edge_cases() {
2206            let input = b"(Simple) (With\\\\backslash) (With\\)paren) (With\\newline) (With\\ttab) (With\\rcarriage) (With\\bbackspace) (With\\fformfeed) (With\\(leftparen) (With\\)rightparen) (With\\377octal) (With\\dddoctal)";
2207            let mut tokenizer = ContentTokenizer::new(input);
2208
2209            assert_eq!(
2210                tokenizer.next_token().unwrap(),
2211                Some(Token::String(b"Simple".to_vec()))
2212            );
2213            assert_eq!(
2214                tokenizer.next_token().unwrap(),
2215                Some(Token::String(b"With\\backslash".to_vec()))
2216            );
2217            assert_eq!(
2218                tokenizer.next_token().unwrap(),
2219                Some(Token::String(b"With)paren".to_vec()))
2220            );
2221            assert_eq!(
2222                tokenizer.next_token().unwrap(),
2223                Some(Token::String(b"With\newline".to_vec()))
2224            );
2225            assert_eq!(
2226                tokenizer.next_token().unwrap(),
2227                Some(Token::String(b"With\ttab".to_vec()))
2228            );
2229            assert_eq!(
2230                tokenizer.next_token().unwrap(),
2231                Some(Token::String(b"With\rcarriage".to_vec()))
2232            );
2233            assert_eq!(
2234                tokenizer.next_token().unwrap(),
2235                Some(Token::String(b"With\x08backspace".to_vec()))
2236            );
2237            assert_eq!(
2238                tokenizer.next_token().unwrap(),
2239                Some(Token::String(b"With\x0Cformfeed".to_vec()))
2240            );
2241            assert_eq!(
2242                tokenizer.next_token().unwrap(),
2243                Some(Token::String(b"With(leftparen".to_vec()))
2244            );
2245            assert_eq!(
2246                tokenizer.next_token().unwrap(),
2247                Some(Token::String(b"With)rightparen".to_vec()))
2248            );
2249        }
2250
2251        #[test]
2252        fn test_hex_string_parsing() {
2253            let input = b"<48656C6C6F> <48 65 6C 6C 6F> <48656C6C6F57> <48656C6C6F5>";
2254            let mut tokenizer = ContentTokenizer::new(input);
2255
2256            assert_eq!(
2257                tokenizer.next_token().unwrap(),
2258                Some(Token::HexString(b"Hello".to_vec()))
2259            );
2260            assert_eq!(
2261                tokenizer.next_token().unwrap(),
2262                Some(Token::HexString(b"Hello".to_vec()))
2263            );
2264            assert_eq!(
2265                tokenizer.next_token().unwrap(),
2266                Some(Token::HexString(b"HelloW".to_vec()))
2267            );
2268            assert_eq!(
2269                tokenizer.next_token().unwrap(),
2270                Some(Token::HexString(b"Hello\x50".to_vec()))
2271            );
2272        }
2273
2274        #[test]
2275        fn test_name_parsing_edge_cases() {
2276            let input = b"/Name /Name#20with#20spaces /Name#23with#23hash /Name#2Fwith#2Fslash /#45mptyName";
2277            let mut tokenizer = ContentTokenizer::new(input);
2278
2279            assert_eq!(
2280                tokenizer.next_token().unwrap(),
2281                Some(Token::Name("Name".to_string()))
2282            );
2283            assert_eq!(
2284                tokenizer.next_token().unwrap(),
2285                Some(Token::Name("Name with spaces".to_string()))
2286            );
2287            assert_eq!(
2288                tokenizer.next_token().unwrap(),
2289                Some(Token::Name("Name#with#hash".to_string()))
2290            );
2291            assert_eq!(
2292                tokenizer.next_token().unwrap(),
2293                Some(Token::Name("Name/with/slash".to_string()))
2294            );
2295            assert_eq!(
2296                tokenizer.next_token().unwrap(),
2297                Some(Token::Name("EmptyName".to_string()))
2298            );
2299        }
2300
2301        #[test]
2302        fn test_operator_parsing_edge_cases() {
2303            let content = b"q q q Q Q Q BT BT ET ET";
2304            let operators = ContentParser::parse(content).unwrap();
2305
2306            assert_eq!(operators.len(), 10);
2307            assert_eq!(operators[0], ContentOperation::SaveGraphicsState);
2308            assert_eq!(operators[1], ContentOperation::SaveGraphicsState);
2309            assert_eq!(operators[2], ContentOperation::SaveGraphicsState);
2310            assert_eq!(operators[3], ContentOperation::RestoreGraphicsState);
2311            assert_eq!(operators[4], ContentOperation::RestoreGraphicsState);
2312            assert_eq!(operators[5], ContentOperation::RestoreGraphicsState);
2313            assert_eq!(operators[6], ContentOperation::BeginText);
2314            assert_eq!(operators[7], ContentOperation::BeginText);
2315            assert_eq!(operators[8], ContentOperation::EndText);
2316            assert_eq!(operators[9], ContentOperation::EndText);
2317        }
2318
2319        #[test]
2320        fn test_error_handling_insufficient_operands() {
2321            // Best-effort recovery (issue #319): `Td` is missing its y
2322            // coordinate. The malformed operator is skipped, but a following
2323            // valid text operator must still be recovered — the page is NOT
2324            // discarded wholesale.
2325            let content = b"100 Td (kept) Tj";
2326            let ops = ContentParser::parse(content).expect("recovers from bad Td");
2327            assert!(
2328                ops.iter()
2329                    .any(|op| matches!(op, ContentOperation::ShowText(t) if t == b"kept")),
2330                "valid Tj after the malformed Td must survive: {ops:?}"
2331            );
2332        }
2333
2334        #[test]
2335        fn test_error_handling_invalid_operator() {
2336            // Unknown operator `INVALID` is skipped; the following valid
2337            // MoveTo survives (issue #319 recovery contract).
2338            let content = b"100 200 INVALID 10 20 m";
2339            let ops = ContentParser::parse(content).expect("recovers from unknown operator");
2340            assert!(
2341                ops.iter()
2342                    .any(|op| matches!(op, ContentOperation::MoveTo(_, _))),
2343                "valid MoveTo after the unknown operator must survive: {ops:?}"
2344            );
2345        }
2346
2347        #[test]
2348        fn test_error_handling_malformed_string() {
2349            // Test that the tokenizer handles malformed strings appropriately
2350            let input = b"(Unclosed string";
2351            let mut tokenizer = ContentTokenizer::new(input);
2352            let result = tokenizer.next_token();
2353            // The current implementation may not detect this as an error
2354            // so we'll just test that we get some result
2355            assert!(result.is_ok() || result.is_err());
2356        }
2357
2358        #[test]
2359        fn test_error_handling_malformed_hex_string() {
2360            let input = b"<48656C6C6G>";
2361            let mut tokenizer = ContentTokenizer::new(input);
2362            let result = tokenizer.next_token();
2363            assert!(result.is_err());
2364        }
2365
2366        #[test]
2367        fn test_error_handling_malformed_name() {
2368            let input = b"/Name#GG";
2369            let mut tokenizer = ContentTokenizer::new(input);
2370            let result = tokenizer.next_token();
2371            assert!(result.is_err());
2372        }
2373
2374        #[test]
2375        fn test_empty_content_stream() {
2376            let content = b"";
2377            let operators = ContentParser::parse(content).unwrap();
2378            assert_eq!(operators.len(), 0);
2379        }
2380
2381        #[test]
2382        fn test_whitespace_only_content_stream() {
2383            let content = b"   \t\n\r   ";
2384            let operators = ContentParser::parse(content).unwrap();
2385            assert_eq!(operators.len(), 0);
2386        }
2387
2388        #[test]
2389        fn test_mixed_integer_and_real_operands() {
2390            // Test with simple operands that work with current parser
2391            let content = b"100 200 m 150 200 l";
2392            let operators = ContentParser::parse(content).unwrap();
2393
2394            assert_eq!(operators.len(), 2);
2395            assert_eq!(operators[0], ContentOperation::MoveTo(100.0, 200.0));
2396            assert_eq!(operators[1], ContentOperation::LineTo(150.0, 200.0));
2397        }
2398
2399        #[test]
2400        fn test_negative_operands() {
2401            let content = b"-100 -200 Td -50.5 -75.2 TD";
2402            let operators = ContentParser::parse(content).unwrap();
2403
2404            assert_eq!(operators.len(), 2);
2405            assert_eq!(operators[0], ContentOperation::MoveText(-100.0, -200.0));
2406            assert_eq!(
2407                operators[1],
2408                ContentOperation::MoveTextSetLeading(-50.5, -75.2)
2409            );
2410        }
2411
2412        #[test]
2413        fn test_large_numbers() {
2414            let content = b"999999.999999 -999999.999999 m";
2415            let operators = ContentParser::parse(content).unwrap();
2416
2417            assert_eq!(operators.len(), 1);
2418            assert_eq!(
2419                operators[0],
2420                ContentOperation::MoveTo(999999.999999, -999999.999999)
2421            );
2422        }
2423
2424        #[test]
2425        fn test_scientific_notation() {
2426            // Test with simple decimal numbers since scientific notation isn't implemented
2427            let content = b"123.45 -456.78 m";
2428            let operators = ContentParser::parse(content).unwrap();
2429
2430            assert_eq!(operators.len(), 1);
2431            assert_eq!(operators[0], ContentOperation::MoveTo(123.45, -456.78));
2432        }
2433
2434        #[test]
2435        fn test_show_text_array_complex() {
2436            // `TJ` expects an array operand, not a plain string. The malformed
2437            // operator is skipped; a following valid Tj is recovered
2438            // (issue #319 recovery contract).
2439            let content = b"(Hello) TJ (kept) Tj";
2440            let ops = ContentParser::parse(content).expect("recovers from malformed TJ");
2441            assert!(
2442                ops.iter()
2443                    .any(|op| matches!(op, ContentOperation::ShowText(t) if t == b"kept")),
2444                "valid Tj after the malformed TJ must survive: {ops:?}"
2445            );
2446        }
2447
2448        #[test]
2449        fn test_dash_pattern_empty() {
2450            // `d` needs an array operand; the malformed operator is skipped and
2451            // a following valid MoveTo survives (issue #319 recovery contract).
2452            let content = b"0 d 10 20 m";
2453            let ops = ContentParser::parse(content).expect("recovers from malformed d");
2454            assert!(
2455                ops.iter()
2456                    .any(|op| matches!(op, ContentOperation::MoveTo(_, _))),
2457                "valid MoveTo after the malformed dash op must survive: {ops:?}"
2458            );
2459        }
2460
2461        #[test]
2462        fn test_dash_pattern_complex() {
2463            // Same recovery contract with a real-number operand before `d`.
2464            let content = b"2.5 d 10 20 m";
2465            let ops = ContentParser::parse(content).expect("recovers from malformed d");
2466            assert!(
2467                ops.iter()
2468                    .any(|op| matches!(op, ContentOperation::MoveTo(_, _))),
2469                "valid MoveTo after the malformed dash op must survive: {ops:?}"
2470            );
2471        }
2472
2473        #[test]
2474        fn test_pop_array_removes_array_end() {
2475            // Test that pop_array correctly handles ArrayEnd tokens
2476            let parser = ContentParser::new(b"");
2477
2478            // Test normal array: [1 2 3]
2479            let mut operands = vec![
2480                Token::ArrayStart,
2481                Token::Integer(1),
2482                Token::Integer(2),
2483                Token::Integer(3),
2484                Token::ArrayEnd,
2485            ];
2486            let result = parser.pop_array(&mut operands).unwrap();
2487            assert_eq!(result.len(), 3);
2488            assert!(operands.is_empty());
2489
2490            // Test array without ArrayEnd (backwards compatibility)
2491            let mut operands = vec![Token::ArrayStart, Token::Number(1.5), Token::Number(2.5)];
2492            let result = parser.pop_array(&mut operands).unwrap();
2493            assert_eq!(result.len(), 2);
2494            assert!(operands.is_empty());
2495        }
2496
2497        #[test]
2498        fn test_dash_array_parsing_valid() {
2499            // Test that parser correctly parses valid dash arrays
2500            let parser = ContentParser::new(b"");
2501
2502            // Test with valid numbers only
2503            let valid_tokens = vec![Token::Number(3.0), Token::Integer(2)];
2504            let result = parser.parse_dash_array(valid_tokens).unwrap();
2505            assert_eq!(result, vec![3.0, 2.0]);
2506
2507            // Test empty dash array
2508            let empty_tokens = vec![];
2509            let result = parser.parse_dash_array(empty_tokens).unwrap();
2510            let expected: Vec<f32> = vec![];
2511            assert_eq!(result, expected);
2512        }
2513
2514        #[test]
2515        fn test_text_array_parsing_valid() {
2516            // Test that parser correctly parses valid text arrays
2517            let parser = ContentParser::new(b"");
2518
2519            // Test with valid elements only
2520            let valid_tokens = vec![
2521                Token::String(b"Hello".to_vec()),
2522                Token::Number(-100.0),
2523                Token::String(b"World".to_vec()),
2524            ];
2525            let result = parser.parse_text_array(valid_tokens).unwrap();
2526            assert_eq!(result.len(), 3);
2527        }
2528
2529        #[test]
2530        fn test_inline_image_handling() {
2531            let content = b"BI /W 100 /H 100 /BPC 8 /CS /RGB ID some_image_data EI";
2532            let operators = ContentParser::parse(content).unwrap();
2533
2534            assert_eq!(operators.len(), 1);
2535            match &operators[0] {
2536                ContentOperation::InlineImage { params, data: _ } => {
2537                    // Check parsed parameters
2538                    assert_eq!(params.get("Width"), Some(&Object::Integer(100)));
2539                    assert_eq!(params.get("Height"), Some(&Object::Integer(100)));
2540                    assert_eq!(params.get("BitsPerComponent"), Some(&Object::Integer(8)));
2541                    assert_eq!(
2542                        params.get("ColorSpace"),
2543                        Some(&Object::Name("DeviceRGB".to_string()))
2544                    );
2545                    // Data field is not captured, just verify params
2546                }
2547                _ => panic!("Expected InlineImage operation"),
2548            }
2549        }
2550
2551        #[test]
2552        fn test_inline_image_with_filter() {
2553            let content = b"BI /W 50 /H 50 /CS /G /BPC 1 /F /AHx ID 00FF00FF EI";
2554            let operators = ContentParser::parse(content).unwrap();
2555
2556            assert_eq!(operators.len(), 1);
2557            match &operators[0] {
2558                ContentOperation::InlineImage { params, data: _ } => {
2559                    assert_eq!(params.get("Width"), Some(&Object::Integer(50)));
2560                    assert_eq!(params.get("Height"), Some(&Object::Integer(50)));
2561                    assert_eq!(
2562                        params.get("ColorSpace"),
2563                        Some(&Object::Name("DeviceGray".to_string()))
2564                    );
2565                    assert_eq!(params.get("BitsPerComponent"), Some(&Object::Integer(1)));
2566                    assert_eq!(
2567                        params.get("Filter"),
2568                        Some(&Object::Name("ASCIIHexDecode".to_string()))
2569                    );
2570                }
2571                _ => panic!("Expected InlineImage operation"),
2572            }
2573        }
2574
2575        #[test]
2576        fn test_content_parser_performance() {
2577            let mut content = Vec::new();
2578            for i in 0..1000 {
2579                content.extend_from_slice(format!("{} {} m ", i, i + 1).as_bytes());
2580            }
2581
2582            let start = std::time::Instant::now();
2583            let operators = ContentParser::parse(&content).unwrap();
2584            let duration = start.elapsed();
2585
2586            assert_eq!(operators.len(), 1000);
2587            assert!(duration.as_millis() < 100); // Should parse 1000 operators in under 100ms
2588        }
2589
2590        #[test]
2591        fn test_tokenizer_performance() {
2592            let mut input = Vec::new();
2593            for i in 0..1000 {
2594                input.extend_from_slice(format!("{} {} ", i, i + 1).as_bytes());
2595            }
2596
2597            let start = std::time::Instant::now();
2598            let mut tokenizer = ContentTokenizer::new(&input);
2599            let mut count = 0;
2600            while tokenizer.next_token().unwrap().is_some() {
2601                count += 1;
2602            }
2603            let duration = start.elapsed();
2604
2605            assert_eq!(count, 2000); // 1000 pairs of numbers
2606            assert!(duration.as_millis() < 50); // Should tokenize 2000 tokens in under 50ms
2607        }
2608
2609        #[test]
2610        fn test_memory_usage_large_content() {
2611            let mut content = Vec::new();
2612            for i in 0..10000 {
2613                content.extend_from_slice(
2614                    format!("{} {} {} {} {} {} c ", i, i + 1, i + 2, i + 3, i + 4, i + 5)
2615                        .as_bytes(),
2616                );
2617            }
2618
2619            let operators = ContentParser::parse(&content).unwrap();
2620            assert_eq!(operators.len(), 10000);
2621
2622            // Verify all operations are CurveTo
2623            for op in operators {
2624                matches!(op, ContentOperation::CurveTo(_, _, _, _, _, _));
2625            }
2626        }
2627
2628        #[test]
2629        fn test_concurrent_parsing() {
2630            use std::sync::Arc;
2631            use std::thread;
2632
2633            let content = Arc::new(b"BT /F1 12 Tf 100 200 Td (Hello) Tj ET".to_vec());
2634            let handles: Vec<_> = (0..10)
2635                .map(|_| {
2636                    let content_clone = content.clone();
2637                    thread::spawn(move || ContentParser::parse(&content_clone).unwrap())
2638                })
2639                .collect();
2640
2641            for handle in handles {
2642                let operators = handle.join().unwrap();
2643                assert_eq!(operators.len(), 5);
2644                assert_eq!(operators[0], ContentOperation::BeginText);
2645                assert_eq!(operators[4], ContentOperation::EndText);
2646            }
2647        }
2648
2649        // ========== NEW COMPREHENSIVE TESTS ==========
2650
2651        #[test]
2652        fn test_tokenizer_hex_string_edge_cases() {
2653            let mut tokenizer = ContentTokenizer::new(b"<>");
2654            let token = tokenizer.next_token().unwrap().unwrap();
2655            match token {
2656                Token::HexString(data) => assert!(data.is_empty()),
2657                _ => panic!("Expected empty hex string"),
2658            }
2659
2660            // Odd number of hex digits
2661            let mut tokenizer = ContentTokenizer::new(b"<123>");
2662            let token = tokenizer.next_token().unwrap().unwrap();
2663            match token {
2664                Token::HexString(data) => assert_eq!(data, vec![0x12, 0x30]),
2665                _ => panic!("Expected hex string with odd digits"),
2666            }
2667
2668            // Hex string with whitespace
2669            let mut tokenizer = ContentTokenizer::new(b"<12 34\t56\n78>");
2670            let token = tokenizer.next_token().unwrap().unwrap();
2671            match token {
2672                Token::HexString(data) => assert_eq!(data, vec![0x12, 0x34, 0x56, 0x78]),
2673                _ => panic!("Expected hex string with whitespace"),
2674            }
2675        }
2676
2677        #[test]
2678        fn test_tokenizer_literal_string_escape_sequences() {
2679            // Test all standard escape sequences
2680            let mut tokenizer = ContentTokenizer::new(b"(\\n\\r\\t\\b\\f\\(\\)\\\\)");
2681            let token = tokenizer.next_token().unwrap().unwrap();
2682            match token {
2683                Token::String(data) => {
2684                    assert_eq!(
2685                        data,
2686                        vec![b'\n', b'\r', b'\t', 0x08, 0x0C, b'(', b')', b'\\']
2687                    );
2688                }
2689                _ => panic!("Expected string with escapes"),
2690            }
2691
2692            // Test octal escape sequences
2693            let mut tokenizer = ContentTokenizer::new(b"(\\101\\040\\377)");
2694            let token = tokenizer.next_token().unwrap().unwrap();
2695            match token {
2696                Token::String(data) => assert_eq!(data, vec![b'A', b' ', 255]),
2697                _ => panic!("Expected string with octal escapes"),
2698            }
2699        }
2700
2701        #[test]
2702        fn test_tokenizer_nested_parentheses() {
2703            let mut tokenizer = ContentTokenizer::new(b"(outer (inner) text)");
2704            let token = tokenizer.next_token().unwrap().unwrap();
2705            match token {
2706                Token::String(data) => {
2707                    assert_eq!(data, b"outer (inner) text");
2708                }
2709                _ => panic!("Expected string with nested parentheses"),
2710            }
2711
2712            // Multiple levels of nesting
2713            let mut tokenizer = ContentTokenizer::new(b"(level1 (level2 (level3) back2) back1)");
2714            let token = tokenizer.next_token().unwrap().unwrap();
2715            match token {
2716                Token::String(data) => {
2717                    assert_eq!(data, b"level1 (level2 (level3) back2) back1");
2718                }
2719                _ => panic!("Expected string with deep nesting"),
2720            }
2721        }
2722
2723        #[test]
2724        fn test_tokenizer_name_hex_escapes() {
2725            let mut tokenizer = ContentTokenizer::new(b"/Name#20With#20Spaces");
2726            let token = tokenizer.next_token().unwrap().unwrap();
2727            match token {
2728                Token::Name(name) => assert_eq!(name, "Name With Spaces"),
2729                _ => panic!("Expected name with hex escapes"),
2730            }
2731
2732            // Test various special characters
2733            let mut tokenizer = ContentTokenizer::new(b"/Special#2F#28#29#3C#3E");
2734            let token = tokenizer.next_token().unwrap().unwrap();
2735            match token {
2736                Token::Name(name) => assert_eq!(name, "Special/()<>"),
2737                _ => panic!("Expected name with special character escapes"),
2738            }
2739        }
2740
2741        #[test]
2742        fn test_tokenizer_number_edge_cases() {
2743            // Very large integers
2744            let mut tokenizer = ContentTokenizer::new(b"2147483647");
2745            let token = tokenizer.next_token().unwrap().unwrap();
2746            match token {
2747                Token::Integer(n) => assert_eq!(n, 2147483647),
2748                _ => panic!("Expected large integer"),
2749            }
2750
2751            // Very small numbers
2752            let mut tokenizer = ContentTokenizer::new(b"0.00001");
2753            let token = tokenizer.next_token().unwrap().unwrap();
2754            match token {
2755                Token::Number(n) => assert!((n - 0.00001).abs() < f32::EPSILON),
2756                _ => panic!("Expected small float"),
2757            }
2758
2759            // Numbers starting with dot
2760            let mut tokenizer = ContentTokenizer::new(b".5");
2761            let token = tokenizer.next_token().unwrap().unwrap();
2762            match token {
2763                Token::Number(n) => assert!((n - 0.5).abs() < f32::EPSILON),
2764                _ => panic!("Expected float starting with dot"),
2765            }
2766        }
2767
2768        #[test]
2769        fn test_parser_complex_path_operations() {
2770            let content = b"100 200 m 150 200 l 150 250 l 100 250 l h f";
2771            let operators = ContentParser::parse(content).unwrap();
2772
2773            assert_eq!(operators.len(), 6);
2774            assert_eq!(operators[0], ContentOperation::MoveTo(100.0, 200.0));
2775            assert_eq!(operators[1], ContentOperation::LineTo(150.0, 200.0));
2776            assert_eq!(operators[2], ContentOperation::LineTo(150.0, 250.0));
2777            assert_eq!(operators[3], ContentOperation::LineTo(100.0, 250.0));
2778            assert_eq!(operators[4], ContentOperation::ClosePath);
2779            assert_eq!(operators[5], ContentOperation::Fill);
2780        }
2781
2782        #[test]
2783        fn test_parser_bezier_curves() {
2784            let content = b"100 100 150 50 200 150 c";
2785            let operators = ContentParser::parse(content).unwrap();
2786
2787            assert_eq!(operators.len(), 1);
2788            match &operators[0] {
2789                ContentOperation::CurveTo(x1, y1, x2, y2, x3, y3) => {
2790                    // Values are parsed in reverse order: last 6 values for c operator
2791                    // Stack order: 100 100 150 50 200 150
2792                    // Pop order: x1=100, y1=100, x2=150, y2=50, x3=200, y3=150
2793                    assert!(x1.is_finite() && y1.is_finite());
2794                    assert!(x2.is_finite() && y2.is_finite());
2795                    assert!(x3.is_finite() && y3.is_finite());
2796                    // Verify we have 6 coordinate values
2797                    assert!(*x1 >= 50.0 && *x1 <= 200.0);
2798                    assert!(*y1 >= 50.0 && *y1 <= 200.0);
2799                }
2800                _ => panic!("Expected CurveTo operation"),
2801            }
2802        }
2803
2804        #[test]
2805        fn test_parser_color_operations() {
2806            let content = b"0.5 g 1 0 0 rg 0 1 0 1 k /DeviceRGB cs 0.2 0.4 0.6 sc";
2807            let operators = ContentParser::parse(content).unwrap();
2808
2809            assert_eq!(operators.len(), 5);
2810            match &operators[0] {
2811                ContentOperation::SetNonStrokingGray(gray) => assert_eq!(*gray, 0.5),
2812                _ => panic!("Expected SetNonStrokingGray"),
2813            }
2814            match &operators[1] {
2815                ContentOperation::SetNonStrokingRGB(r, g, b) => {
2816                    assert_eq!((*r, *g, *b), (1.0, 0.0, 0.0));
2817                }
2818                _ => panic!("Expected SetNonStrokingRGB"),
2819            }
2820        }
2821
2822        #[test]
2823        fn test_parser_text_positioning_advanced() {
2824            let content = b"BT 1 0 0 1 100 200 Tm 0 TL 10 TL (Line 1) ' (Line 2) ' ET";
2825            let operators = ContentParser::parse(content).unwrap();
2826
2827            assert_eq!(operators.len(), 7);
2828            assert_eq!(operators[0], ContentOperation::BeginText);
2829            match &operators[1] {
2830                ContentOperation::SetTextMatrix(a, b, c, d, e, f) => {
2831                    assert_eq!((*a, *b, *c, *d, *e, *f), (1.0, 0.0, 0.0, 1.0, 100.0, 200.0));
2832                }
2833                _ => panic!("Expected SetTextMatrix"),
2834            }
2835            assert_eq!(operators[6], ContentOperation::EndText);
2836        }
2837
2838        #[test]
2839        fn test_parser_graphics_state_operations() {
2840            let content = b"q 2 0 0 2 100 100 cm 5 w 1 J 2 j 10 M Q";
2841            let operators = ContentParser::parse(content).unwrap();
2842
2843            assert_eq!(operators.len(), 7);
2844            assert_eq!(operators[0], ContentOperation::SaveGraphicsState);
2845            match &operators[1] {
2846                ContentOperation::SetTransformMatrix(a, b, c, d, e, f) => {
2847                    assert_eq!((*a, *b, *c, *d, *e, *f), (2.0, 0.0, 0.0, 2.0, 100.0, 100.0));
2848                }
2849                _ => panic!("Expected SetTransformMatrix"),
2850            }
2851            assert_eq!(operators[6], ContentOperation::RestoreGraphicsState);
2852        }
2853
2854        #[test]
2855        fn test_parser_xobject_operations() {
2856            let content = b"/Image1 Do /Form2 Do /Pattern3 Do";
2857            let operators = ContentParser::parse(content).unwrap();
2858
2859            assert_eq!(operators.len(), 3);
2860            for (i, expected_name) in ["Image1", "Form2", "Pattern3"].iter().enumerate() {
2861                match &operators[i] {
2862                    ContentOperation::PaintXObject(name) => assert_eq!(name, expected_name),
2863                    _ => panic!("Expected PaintXObject"),
2864                }
2865            }
2866        }
2867
2868        #[test]
2869        fn test_parser_marked_content_operations() {
2870            let content = b"/P BMC (Tagged content) Tj EMC";
2871            let operators = ContentParser::parse(content).unwrap();
2872
2873            assert_eq!(operators.len(), 3);
2874            match &operators[0] {
2875                ContentOperation::BeginMarkedContent(tag) => assert_eq!(tag, "P"),
2876                _ => panic!("Expected BeginMarkedContent"),
2877            }
2878            assert_eq!(operators[2], ContentOperation::EndMarkedContent);
2879        }
2880
2881        #[test]
2882        fn test_parser_error_handling_invalid_operators() {
2883            // Best-effort recovery contract (issue #319).
2884
2885            // Missing operands for `m`: the malformed operator is skipped but
2886            // a following valid `l` is recovered.
2887            let content = b"m 10 20 l";
2888            let ops = ContentParser::parse(content).expect("recovers from operand-less m");
2889            assert!(
2890                ops.iter()
2891                    .any(|op| matches!(op, ContentOperation::LineTo(_, _))),
2892                "valid LineTo after the operand-less m must survive: {ops:?}"
2893            );
2894
2895            // Unterminated hex string: the tokenizer stops at the malformed
2896            // tail but keeps every token before it, so valid text ahead of the
2897            // bad hex is still extracted.
2898            let content = b"(kept) Tj <ABC DEF";
2899            let ops = ContentParser::parse(content).expect("recovers, keeping pre-error tokens");
2900            assert!(
2901                ops.iter()
2902                    .any(|op| matches!(op, ContentOperation::ShowText(t) if t == b"kept")),
2903                "text before the unterminated hex must survive: {ops:?}"
2904            );
2905
2906            // Numbers without an operator parse OK (no operator attempted).
2907            let content = b"100 200 300";
2908            assert!(ContentParser::parse(content).is_ok());
2909        }
2910
2911        #[test]
2912        fn test_parser_whitespace_tolerance() {
2913            let content = b"  \n\t  100   \r\n  200  \t m  \n";
2914            let operators = ContentParser::parse(content).unwrap();
2915
2916            assert_eq!(operators.len(), 1);
2917            assert_eq!(operators[0], ContentOperation::MoveTo(100.0, 200.0));
2918        }
2919
2920        #[test]
2921        fn test_tokenizer_comment_handling() {
2922            let content = b"100 % This is a comment\n200 m % Another comment";
2923            let operators = ContentParser::parse(content).unwrap();
2924
2925            assert_eq!(operators.len(), 1);
2926            assert_eq!(operators[0], ContentOperation::MoveTo(100.0, 200.0));
2927        }
2928
2929        #[test]
2930        fn test_parser_stream_with_binary_data() {
2931            // Test content stream with comment containing binary-like data
2932            let content = b"100 200 m % Comment with \xFF binary\n150 250 l";
2933
2934            let operators = ContentParser::parse(content).unwrap();
2935            assert_eq!(operators.len(), 2);
2936            assert_eq!(operators[0], ContentOperation::MoveTo(100.0, 200.0));
2937            assert_eq!(operators[1], ContentOperation::LineTo(150.0, 250.0));
2938        }
2939
2940        #[test]
2941        fn test_tokenizer_array_parsing() {
2942            // Test simple operations that don't require complex array parsing
2943            let content = b"100 200 m 150 250 l";
2944            let operators = ContentParser::parse(content).unwrap();
2945
2946            assert_eq!(operators.len(), 2);
2947            assert_eq!(operators[0], ContentOperation::MoveTo(100.0, 200.0));
2948            assert_eq!(operators[1], ContentOperation::LineTo(150.0, 250.0));
2949        }
2950
2951        #[test]
2952        fn test_parser_rectangle_operations() {
2953            let content = b"10 20 100 50 re 0 0 200 300 re";
2954            let operators = ContentParser::parse(content).unwrap();
2955
2956            assert_eq!(operators.len(), 2);
2957            match &operators[0] {
2958                ContentOperation::Rectangle(x, y, width, height) => {
2959                    assert_eq!((*x, *y, *width, *height), (10.0, 20.0, 100.0, 50.0));
2960                }
2961                _ => panic!("Expected Rectangle operation"),
2962            }
2963            match &operators[1] {
2964                ContentOperation::Rectangle(x, y, width, height) => {
2965                    assert_eq!((*x, *y, *width, *height), (0.0, 0.0, 200.0, 300.0));
2966                }
2967                _ => panic!("Expected Rectangle operation"),
2968            }
2969        }
2970
2971        #[test]
2972        fn test_parser_clipping_operations() {
2973            let content = b"100 100 50 50 re W n 200 200 75 75 re W* n";
2974            let operators = ContentParser::parse(content).unwrap();
2975
2976            assert_eq!(operators.len(), 6);
2977            assert_eq!(operators[1], ContentOperation::Clip);
2978            assert_eq!(operators[2], ContentOperation::EndPath);
2979            assert_eq!(operators[4], ContentOperation::ClipEvenOdd);
2980            assert_eq!(operators[5], ContentOperation::EndPath);
2981        }
2982
2983        #[test]
2984        fn test_parser_painting_operations() {
2985            let content = b"S s f f* B B* b b*";
2986            let operators = ContentParser::parse(content).unwrap();
2987
2988            assert_eq!(operators.len(), 8);
2989            assert_eq!(operators[0], ContentOperation::Stroke);
2990            assert_eq!(operators[1], ContentOperation::CloseStroke);
2991            assert_eq!(operators[2], ContentOperation::Fill);
2992            assert_eq!(operators[3], ContentOperation::FillEvenOdd);
2993            assert_eq!(operators[4], ContentOperation::FillStroke);
2994            assert_eq!(operators[5], ContentOperation::FillStrokeEvenOdd);
2995            assert_eq!(operators[6], ContentOperation::CloseFillStroke);
2996            assert_eq!(operators[7], ContentOperation::CloseFillStrokeEvenOdd);
2997        }
2998
2999        #[test]
3000        fn test_parser_line_style_operations() {
3001            let content = b"5 w 1 J 2 j 10 M [ 3 2 ] 0 d";
3002            let operators = ContentParser::parse(content).unwrap();
3003
3004            assert_eq!(operators.len(), 5);
3005            assert_eq!(operators[0], ContentOperation::SetLineWidth(5.0));
3006            assert_eq!(operators[1], ContentOperation::SetLineCap(1));
3007            assert_eq!(operators[2], ContentOperation::SetLineJoin(2));
3008            assert_eq!(operators[3], ContentOperation::SetMiterLimit(10.0));
3009            // Dash pattern test would need array support
3010        }
3011
3012        #[test]
3013        fn test_parser_text_state_operations() {
3014            let content = b"12 Tc 3 Tw 100 Tz 1 Tr 2 Ts";
3015            let operators = ContentParser::parse(content).unwrap();
3016
3017            assert_eq!(operators.len(), 5);
3018            assert_eq!(operators[0], ContentOperation::SetCharSpacing(12.0));
3019            assert_eq!(operators[1], ContentOperation::SetWordSpacing(3.0));
3020            assert_eq!(operators[2], ContentOperation::SetHorizontalScaling(100.0));
3021            assert_eq!(operators[3], ContentOperation::SetTextRenderMode(1));
3022            assert_eq!(operators[4], ContentOperation::SetTextRise(2.0));
3023        }
3024
3025        #[test]
3026        fn test_parser_unicode_text() {
3027            let content = b"BT (Hello \xC2\xA9 World \xE2\x9C\x93) Tj ET";
3028            let operators = ContentParser::parse(content).unwrap();
3029
3030            assert_eq!(operators.len(), 3);
3031            assert_eq!(operators[0], ContentOperation::BeginText);
3032            match &operators[1] {
3033                ContentOperation::ShowText(text) => {
3034                    assert!(text.len() > 5); // Should contain Unicode bytes
3035                }
3036                _ => panic!("Expected ShowText operation"),
3037            }
3038            assert_eq!(operators[2], ContentOperation::EndText);
3039        }
3040
3041        #[test]
3042        fn test_parser_stress_test_large_coordinates() {
3043            let content = b"999999.999 -999999.999 999999.999 -999999.999 999999.999 -999999.999 c";
3044            let operators = ContentParser::parse(content).unwrap();
3045
3046            assert_eq!(operators.len(), 1);
3047            match &operators[0] {
3048                ContentOperation::CurveTo(_x1, _y1, _x2, _y2, _x3, _y3) => {
3049                    assert!((*_x1 - 999999.999).abs() < 0.1);
3050                    assert!((*_y1 - (-999999.999)).abs() < 0.1);
3051                    assert!((*_x3 - 999999.999).abs() < 0.1);
3052                }
3053                _ => panic!("Expected CurveTo operation"),
3054            }
3055        }
3056
3057        #[test]
3058        fn test_parser_empty_content_stream() {
3059            let content = b"";
3060            let operators = ContentParser::parse(content).unwrap();
3061            assert!(operators.is_empty());
3062
3063            let content = b"   \n\t\r   ";
3064            let operators = ContentParser::parse(content).unwrap();
3065            assert!(operators.is_empty());
3066        }
3067
3068        #[test]
3069        fn test_tokenizer_error_recovery() {
3070            // A comment carrying a stray binary byte sits between two valid
3071            // path operators. The comment (and its binary) is skipped and
3072            // BOTH operators are recovered (issue #319 recovery contract).
3073            let content = b"100 200 m % Comment with\xFFbinary\n150 250 l";
3074            let ops = ContentParser::parse(content).expect("recovers around binary comment");
3075            assert!(
3076                ops.iter()
3077                    .any(|op| matches!(op, ContentOperation::MoveTo(_, _))),
3078                "MoveTo before the comment must survive: {ops:?}"
3079            );
3080            assert!(
3081                ops.iter()
3082                    .any(|op| matches!(op, ContentOperation::LineTo(_, _))),
3083                "LineTo after the comment must survive: {ops:?}"
3084            );
3085        }
3086
3087        #[test]
3088        fn malformed_operator_does_not_discard_surrounding_text() {
3089            // Issue #319: a single malformed operator must NOT drop the whole
3090            // page's content. Here a bare `Td` (missing its two operands)
3091            // sits between two valid text-show operators. Before the fix,
3092            // `parse_operators` propagated the operand error with `?`, so the
3093            // entire stream returned Err and BOTH show-text ops were lost
3094            // (the extractor then dropped the page). The parser must recover:
3095            // skip the bad operator, keep the valid ones.
3096            let content = b"BT /F1 12 Tf 72 700 Td (First line) Tj Td (Second line) Tj ET";
3097            let ops = ContentParser::parse_content(content)
3098                .expect("malformed operator must not fail the whole stream");
3099            let shown: Vec<&Vec<u8>> = ops
3100                .iter()
3101                .filter_map(|op| match op {
3102                    ContentOperation::ShowText(t) => Some(t),
3103                    _ => None,
3104                })
3105                .collect();
3106            assert_eq!(
3107                shown.len(),
3108                2,
3109                "both valid Tj operators must survive the malformed Td"
3110            );
3111            assert_eq!(shown[0], b"First line");
3112            assert_eq!(shown[1], b"Second line");
3113        }
3114
3115        #[test]
3116        fn test_parser_optimization_repeated_operations() {
3117            // Test performance with many repeated operations
3118            let mut content = Vec::new();
3119            for i in 0..1000 {
3120                content.extend_from_slice(format!("{} {} m ", i, i * 2).as_bytes());
3121            }
3122
3123            let start = std::time::Instant::now();
3124            let operators = ContentParser::parse(&content).unwrap();
3125            let duration = start.elapsed();
3126
3127            assert_eq!(operators.len(), 1000);
3128            assert!(duration.as_millis() < 200); // Should be fast
3129        }
3130
3131        #[test]
3132        fn test_parser_memory_efficiency_large_strings() {
3133            // Test with large text content
3134            let large_text = "A".repeat(10000);
3135            let content = format!("BT ({}) Tj ET", large_text);
3136            let operators = ContentParser::parse(content.as_bytes()).unwrap();
3137
3138            assert_eq!(operators.len(), 3);
3139            match &operators[1] {
3140                ContentOperation::ShowText(text) => {
3141                    assert_eq!(text.len(), 10000);
3142                }
3143                _ => panic!("Expected ShowText operation"),
3144            }
3145        }
3146    }
3147
3148    #[test]
3149    fn test_content_stream_too_large() {
3150        // Test handling of very large content streams (covering potential size limits)
3151        let mut large_content = Vec::new();
3152
3153        // Create a content stream with many operations
3154        for i in 0..10000 {
3155            large_content.extend_from_slice(format!("{} {} m ", i, i).as_bytes());
3156        }
3157        large_content.extend_from_slice(b"S");
3158
3159        // Should handle large content without panic
3160        let result = ContentParser::parse_content(&large_content);
3161        assert!(result.is_ok());
3162
3163        let operations = result.unwrap();
3164        // Should have many MoveTo operations plus one Stroke
3165        assert!(operations.len() > 10000);
3166    }
3167
3168    #[test]
3169    fn test_invalid_operator_handling() {
3170        // Test parsing with invalid operators
3171        let content = b"100 200 INVALID_OP 300 400 m";
3172        let result = ContentParser::parse_content(content);
3173
3174        // Should either handle gracefully or return error
3175        if let Ok(operations) = result {
3176            // If it succeeds, should have at least the valid MoveTo
3177            assert!(operations
3178                .iter()
3179                .any(|op| matches!(op, ContentOperation::MoveTo(_, _))));
3180        }
3181    }
3182
3183    #[test]
3184    fn test_nested_arrays_malformed() {
3185        // Test malformed nested arrays in TJ operator
3186        let content = b"[[(Hello] [World)]] TJ";
3187        let result = ContentParser::parse_content(content);
3188
3189        // Should handle malformed arrays gracefully
3190        assert!(result.is_ok() || result.is_err());
3191    }
3192
3193    #[test]
3194    fn test_escape_sequences_in_strings() {
3195        // Test various escape sequences in strings
3196        let test_cases = vec![
3197            (b"(\\n\\r\\t)".as_slice(), b"\n\r\t".as_slice()),
3198            (b"(\\\\)".as_slice(), b"\\".as_slice()),
3199            (b"(\\(\\))".as_slice(), b"()".as_slice()),
3200            (b"(\\123)".as_slice(), b"S".as_slice()), // Octal 123 = 83 = 'S'
3201            (b"(\\0)".as_slice(), b"\0".as_slice()),
3202        ];
3203
3204        for (input, expected) in test_cases {
3205            let mut content = Vec::new();
3206            content.extend_from_slice(input);
3207            content.extend_from_slice(b" Tj");
3208
3209            let result = ContentParser::parse_content(&content);
3210            assert!(result.is_ok());
3211
3212            let operations = result.unwrap();
3213            if let ContentOperation::ShowText(text) = &operations[0] {
3214                assert_eq!(text, expected, "Failed for input: {:?}", input);
3215            } else {
3216                panic!("Expected ShowText operation");
3217            }
3218        }
3219    }
3220
3221    #[test]
3222    fn test_content_with_inline_images() {
3223        // Test handling of inline images in content stream
3224        let content = b"BI /W 10 /H 10 /CS /RGB ID \x00\x01\x02\x03 EI";
3225        let result = ContentParser::parse_content(content);
3226
3227        // Should handle inline images (even if not fully implemented)
3228        assert!(result.is_ok() || result.is_err());
3229    }
3230
3231    #[test]
3232    fn test_operator_with_missing_operands() {
3233        // Test operators with insufficient operands
3234        let test_cases = vec![
3235            b"Tj" as &[u8], // ShowText without string
3236            b"m",           // MoveTo without coordinates
3237            b"rg",          // SetRGBColor without values
3238            b"Tf",          // SetFont without name and size
3239        ];
3240
3241        for content in test_cases {
3242            let result = ContentParser::parse_content(content);
3243            // Should handle gracefully (error or skip)
3244            assert!(result.is_ok() || result.is_err());
3245        }
3246    }
3247
3248    // --- Tests for infinite loop fix (curly braces, stray parens, inline images) ---
3249
3250    #[test]
3251    fn test_tokenizer_handles_curly_braces() {
3252        // Curly braces { } are not valid PDF content operators but appear in
3253        // binary inline image data. The tokenizer must skip them without hanging.
3254        let input = b"q { } Q";
3255        let mut tokenizer = ContentTokenizer::new(input);
3256
3257        let mut tokens = Vec::new();
3258        while let Some(token) = tokenizer.next_token().unwrap() {
3259            tokens.push(token);
3260        }
3261
3262        // Should produce tokens for q and Q, skipping { and }
3263        assert!(tokens.contains(&Token::Operator("q".to_string())));
3264        assert!(tokens.contains(&Token::Operator("Q".to_string())));
3265    }
3266
3267    #[test]
3268    fn test_tokenizer_handles_closing_paren() {
3269        // A stray ) outside a string literal should be skipped, not cause a hang
3270        let input = b"q ) Q";
3271        let mut tokenizer = ContentTokenizer::new(input);
3272
3273        let mut tokens = Vec::new();
3274        while let Some(token) = tokenizer.next_token().unwrap() {
3275            tokens.push(token);
3276        }
3277
3278        assert!(tokens.contains(&Token::Operator("q".to_string())));
3279        assert!(tokens.contains(&Token::Operator("Q".to_string())));
3280    }
3281
3282    #[test]
3283    fn test_inline_image_binary_with_curly_braces() {
3284        // Inline image binary data containing { and } bytes must be handled
3285        // correctly — the tokenizer should capture them as raw image data
3286        let content = b"BI /W 2 /H 2 /BPC 8 /CS /G ID \x7B\x7D\x00\xFF EI Q";
3287        let result = ContentParser::parse_content(content);
3288        assert!(
3289            result.is_ok(),
3290            "Parsing inline image with curly braces failed: {:?}",
3291            result.err()
3292        );
3293
3294        let ops = result.unwrap();
3295        // Should have InlineImage + RestoreGraphicsState
3296        let has_inline = ops
3297            .iter()
3298            .any(|op| matches!(op, ContentOperation::InlineImage { .. }));
3299        let has_q = ops
3300            .iter()
3301            .any(|op| matches!(op, ContentOperation::RestoreGraphicsState));
3302        assert!(has_inline, "Expected InlineImage operation");
3303        assert!(has_q, "Expected RestoreGraphicsState after EI");
3304    }
3305
3306    #[test]
3307    fn test_inline_image_binary_with_all_byte_values() {
3308        // Inline image with bytes 0x00-0xFF to ensure no byte causes a hang
3309        let mut content = Vec::new();
3310        content.extend_from_slice(b"BI /W 16 /H 16 /BPC 8 /CS /G ID ");
3311        // Add all 256 byte values as image data
3312        for b in 0u8..=255 {
3313            content.push(b);
3314        }
3315        content.extend_from_slice(b" EI Q");
3316
3317        let result = ContentParser::parse_content(&content);
3318        assert!(
3319            result.is_ok(),
3320            "Parsing inline image with all byte values failed: {:?}",
3321            result.err()
3322        );
3323    }
3324
3325    #[test]
3326    fn test_inline_image_ei_detection() {
3327        // EI must be preceded by whitespace to be recognized as end marker
3328        // "EI" within binary data (not preceded by whitespace) should NOT end the image
3329        let content = b"BI /W 2 /H 1 /BPC 8 /CS /G ID \x45\x49\x00\n EI Q";
3330        //                                               ^E  ^I  (within data)  ^real EI
3331        let result = ContentParser::parse_content(content);
3332        assert!(result.is_ok(), "EI detection failed: {:?}", result.err());
3333
3334        let ops = result.unwrap();
3335        let has_inline = ops
3336            .iter()
3337            .any(|op| matches!(op, ContentOperation::InlineImage { .. }));
3338        assert!(has_inline, "Expected InlineImage operation");
3339    }
3340
3341    #[test]
3342    fn test_tokenizer_no_infinite_loop_on_consecutive_delimiters() {
3343        // Multiple consecutive unhandled delimiters must not cause a hang
3344        let input = b"q {{{}}})))) Q";
3345        let mut tokenizer = ContentTokenizer::new(input);
3346
3347        let mut tokens = Vec::new();
3348        while let Some(token) = tokenizer.next_token().unwrap() {
3349            tokens.push(token);
3350            if tokens.len() > 100 {
3351                panic!("Tokenizer produced too many tokens — possible infinite loop");
3352            }
3353        }
3354
3355        assert!(tokens.contains(&Token::Operator("q".to_string())));
3356        assert!(tokens.contains(&Token::Operator("Q".to_string())));
3357    }
3358
3359    #[test]
3360    fn test_content_parser_inline_image_produces_correct_operation() {
3361        // Full parse of a simple inline image should produce correct params
3362        let content = b"BI /W 4 /H 4 /BPC 8 /CS /G ID \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F EI";
3363        let result = ContentParser::parse_content(content);
3364        assert!(result.is_ok(), "Parse failed: {:?}", result.err());
3365
3366        let ops = result.unwrap();
3367        assert_eq!(
3368            ops.len(),
3369            1,
3370            "Expected exactly 1 operation, got {}",
3371            ops.len()
3372        );
3373
3374        if let ContentOperation::InlineImage { params, data } = &ops[0] {
3375            assert_eq!(params.get("Width"), Some(&Object::Integer(4)));
3376            assert_eq!(params.get("Height"), Some(&Object::Integer(4)));
3377            assert_eq!(params.get("BitsPerComponent"), Some(&Object::Integer(8)));
3378            assert!(!data.is_empty(), "Image data should not be empty");
3379        } else {
3380            panic!("Expected InlineImage operation, got {:?}", ops[0]);
3381        }
3382    }
3383
3384    #[test]
3385    fn test_octal_escape_overflow_777() {
3386        // \777 = octal 777 = 511 decimal, overflows u8.
3387        // Per ISO 32000-1:2008 §7.3.4.2: "high-order overflow shall be ignored"
3388        // 511 as u8 = 255 (0x1FF truncated to 0xFF)
3389        let mut tokenizer = ContentTokenizer::new(b"(\\777)");
3390        let token = tokenizer.next_token().unwrap().unwrap();
3391        match token {
3392            Token::String(data) => assert_eq!(data, vec![0xFF]),
3393            _ => panic!("Expected string token"),
3394        }
3395    }
3396
3397    #[test]
3398    fn test_octal_escape_overflow_400() {
3399        // \400 = octal 400 = 256 decimal, just overflows u8.
3400        // 256 as u8 = 0
3401        let mut tokenizer = ContentTokenizer::new(b"(\\400)");
3402        let token = tokenizer.next_token().unwrap().unwrap();
3403        match token {
3404            Token::String(data) => assert_eq!(data, vec![0x00]),
3405            _ => panic!("Expected string token"),
3406        }
3407    }
3408
3409    #[test]
3410    fn test_octal_escape_overflow_577() {
3411        // \577 = octal 577 = 383 decimal.
3412        // 383 as u8 = 127 (0x17F truncated to 0x7F)
3413        let mut tokenizer = ContentTokenizer::new(b"(\\577)");
3414        let token = tokenizer.next_token().unwrap().unwrap();
3415        match token {
3416            Token::String(data) => assert_eq!(data, vec![0x7F]),
3417            _ => panic!("Expected string token"),
3418        }
3419    }
3420
3421    #[test]
3422    fn test_octal_escape_max_valid_377() {
3423        // \377 = 255, max valid octal for u8 - should still work correctly
3424        let mut tokenizer = ContentTokenizer::new(b"(\\377)");
3425        let token = tokenizer.next_token().unwrap().unwrap();
3426        match token {
3427            Token::String(data) => assert_eq!(data, vec![0xFF]),
3428            _ => panic!("Expected string token"),
3429        }
3430    }
3431
3432    #[test]
3433    fn test_octal_escape_overflow_mixed_with_valid() {
3434        // Mix of overflow octal and normal text
3435        let mut tokenizer = ContentTokenizer::new(b"(A\\777B\\101C)");
3436        let token = tokenizer.next_token().unwrap().unwrap();
3437        match token {
3438            Token::String(data) => {
3439                assert_eq!(data, vec![b'A', 0xFF, b'B', b'A', b'C']);
3440            }
3441            _ => panic!("Expected string token"),
3442        }
3443    }
3444}