Skip to main content

pdfboss_core/
content.rs

1//! Content-stream operator parsing (ISO 32000 §8/§9), including inline
2//! images. Lenient: unknown operators and arity mismatches are skipped
3//! (the operand stack is cleared), never an error.
4
5use crate::elements::Span;
6use crate::error::{Error, Result};
7use crate::geom::Matrix;
8use crate::lexer::{decode_hex, is_whitespace, Lexer, RawToken, Token};
9use crate::object::{Dict, Name, Object};
10
11/// Maximum operand container (array/dictionary) nesting depth. Composing
12/// values recurses per nesting level, so without a bound a content stream
13/// of e.g. 50k `[`s overflows the stack and aborts the process. Genuine
14/// operands (dash arrays, `TJ` arrays, property dicts) nest one or two
15/// levels deep.
16const MAX_NESTING_DEPTH: usize = 128;
17
18/// The syntax error reported when `MAX_NESTING_DEPTH` is exceeded.
19fn too_deep(lexer: &Lexer) -> Error {
20    Error::Syntax {
21        offset: lexer.pos(),
22        msg: "operand nesting too deep".into(),
23    }
24}
25
26/// An inline image (`BI ... ID ... EI`) with its dictionary keys and
27/// colorspace abbreviations expanded to their canonical names
28/// (e.g. `/BPC` -> `/BitsPerComponent`, `/RGB` -> `/DeviceRGB`).
29#[derive(Debug, Clone, PartialEq)]
30pub struct ImageParams {
31    pub dict: Dict,
32    pub data: Vec<u8>,
33}
34
35/// One element of a `TJ` (show text with adjustments) array.
36#[derive(Debug, Clone, PartialEq)]
37pub enum TextItem {
38    /// A string to show.
39    Str(Vec<u8>),
40    /// A position adjustment in thousandths of text-space units.
41    Offset(f32),
42}
43
44/// A parsed content-stream operator with its operands.
45#[derive(Debug, Clone, PartialEq)]
46pub enum Op {
47    // Graphics state.
48    /// `q`
49    Save,
50    /// `Q`
51    Restore,
52    /// `cm`
53    Concat(Matrix),
54    /// `w`
55    SetLineWidth(f32),
56    /// `J`
57    SetLineCap(i32),
58    /// `j`
59    SetLineJoin(i32),
60    /// `M`
61    SetMiterLimit(f32),
62    /// `d` (dash array, phase)
63    SetDash(Vec<f32>, f32),
64    /// `ri`
65    SetRenderingIntent(Name),
66    /// `i`
67    SetFlatness(f32),
68    /// `gs`
69    SetExtGState(Name),
70
71    // Path construction.
72    /// `m`
73    MoveTo(f32, f32),
74    /// `l`
75    LineTo(f32, f32),
76    /// `c`
77    CurveTo(f32, f32, f32, f32, f32, f32),
78    /// `v` (first control point = current point)
79    CurveToV(f32, f32, f32, f32),
80    /// `y` (second control point = end point)
81    CurveToY(f32, f32, f32, f32),
82    /// `h`
83    ClosePath,
84    /// `re` (x, y, width, height)
85    Rect(f32, f32, f32, f32),
86
87    // Path painting.
88    /// `S`
89    Stroke,
90    /// `s`
91    CloseStroke,
92    /// `f` / `F`
93    Fill,
94    /// `f*`
95    FillEvenOdd,
96    /// `B`
97    FillStroke,
98    /// `B*`
99    FillStrokeEvenOdd,
100    /// `b`
101    CloseFillStroke,
102    /// `b*`
103    CloseFillStrokeEvenOdd,
104    /// `n`
105    EndPath,
106    /// `W`
107    ClipNonZero,
108    /// `W*`
109    ClipEvenOdd,
110
111    // Color.
112    /// `CS`
113    SetStrokeColorSpace(Name),
114    /// `cs`
115    SetFillColorSpace(Name),
116    /// `SC`
117    SetStrokeColor(Vec<f32>),
118    /// `SCN` (components, optional pattern name)
119    SetStrokeColorN(Vec<f32>, Option<Name>),
120    /// `sc`
121    SetFillColor(Vec<f32>),
122    /// `scn` (components, optional pattern name)
123    SetFillColorN(Vec<f32>, Option<Name>),
124    /// `G`
125    SetStrokeGray(f32),
126    /// `g`
127    SetFillGray(f32),
128    /// `RG`
129    SetStrokeRGB(f32, f32, f32),
130    /// `rg`
131    SetFillRGB(f32, f32, f32),
132    /// `K`
133    SetStrokeCMYK(f32, f32, f32, f32),
134    /// `k`
135    SetFillCMYK(f32, f32, f32, f32),
136
137    // Text.
138    /// `BT`
139    BeginText,
140    /// `ET`
141    EndText,
142    /// `Tc`
143    SetCharSpacing(f32),
144    /// `Tw`
145    SetWordSpacing(f32),
146    /// `Tz`
147    SetHorizScaling(f32),
148    /// `TL`
149    SetLeading(f32),
150    /// `Tf` (font resource name, size)
151    SetFont(Name, f32),
152    /// `d0` (wx, wy): sets the glyph width for a colored Type3 glyph,
153    /// whose content sets its own color (ISO 32000-1 Table 113).
154    SetGlyphWidth(f32, f32),
155    /// `d1` (wx, wy, llx, lly, urx, ury): sets the glyph width and bounding
156    /// box for an uncolored Type3 glyph description; color comes from the
157    /// text state and color operators in the proc are ignored
158    /// (ISO 32000-1 Table 113).
159    SetGlyphWidthBBox(f32, f32, f32, f32, f32, f32),
160    /// `Tr`
161    SetTextRender(i32),
162    /// `Ts`
163    SetTextRise(f32),
164    /// `Td`
165    TextMove(f32, f32),
166    /// `TD`
167    TextMoveSetLeading(f32, f32),
168    /// `Tm`
169    SetTextMatrix(Matrix),
170    /// `T*`
171    TextNextLine,
172    /// `Tj`
173    ShowText(Vec<u8>),
174    /// `TJ`
175    ShowTextAdjusted(Vec<TextItem>),
176    /// `'`
177    NextLineShowText(Vec<u8>),
178    /// `"` (word spacing, char spacing, string)
179    NextLineShowTextSpaced(f32, f32, Vec<u8>),
180
181    // XObjects, images, shading, marked content.
182    /// `Do`
183    XObject(Name),
184    /// `BI ... ID ... EI`
185    InlineImage(ImageParams),
186    /// `sh`
187    Shading(Name),
188    /// `MP`
189    MarkedContentPoint(Name),
190    /// `DP` (tag, properties: inline dict or resource name)
191    MarkedContentPointProps(Name, Object),
192    /// `BMC`
193    BeginMarkedContent(Name),
194    /// `BDC` (tag, properties: inline dict or resource name)
195    BeginMarkedContentProps(Name, Object),
196    /// `EMC`
197    EndMarkedContent,
198    /// `BX`
199    BeginCompat,
200    /// `EX`
201    EndCompat,
202}
203
204/// Parses a decoded content stream into a sequence of operators. Inline
205/// image data runs from after `ID` plus one whitespace byte to `EI` at a
206/// token boundary (or the declared `/L`ength when present, which is
207/// trusted).
208///
209/// Covers ISO 32000-1 §7.8.2.
210pub fn parse_content(data: &[u8]) -> Result<Vec<Op>> {
211    let mut ops = Vec::with_capacity(ops_estimate(data.len()));
212    parse_content_ops(data, |op, _| ops.push(op))?;
213    Ok(ops)
214}
215
216/// Like [`parse_content`], but also reports each operator's byte range —
217/// from the first token of its operand run through the operator keyword —
218/// within `data`.
219pub fn parse_content_spanned(data: &[u8]) -> Result<Vec<(Op, Span)>> {
220    let mut ops = Vec::with_capacity(ops_estimate(data.len()));
221    parse_content_ops(data, |op, span| ops.push((op, span)))?;
222    Ok(ops)
223}
224
225/// Initial operator-vector capacity for a content stream of `len` bytes.
226/// An operator run (operands, keyword, separators) averages well above 16
227/// bytes, so `len / 16` reserves slightly high instead of growing; the cap
228/// keeps a huge stream — usually mostly inline-image data — from
229/// pre-claiming space for operators it may never produce.
230fn ops_estimate(len: usize) -> usize {
231    (len / 16).min(1 << 17)
232}
233
234/// The parse loop behind both public entry points, as a wrapper over the
235/// pull parser: each finished operator is handed to `emit` — so the
236/// span-less caller never materializes spans it will throw away.
237fn parse_content_ops(data: &[u8], mut emit: impl FnMut(Op, Span)) -> Result<()> {
238    let mut ops = ContentOps::new(data);
239    while let Some((op, span)) = ops.next_op()? {
240        emit(op, span);
241    }
242    Ok(())
243}
244
245/// A pull parser over one content stream: each [`ContentOps::next_op`]
246/// yields the next operator with its byte range, in stream order — an
247/// executor consumes operators one at a time without materializing the
248/// whole vector.
249pub struct ContentOps<'a> {
250    lexer: Lexer<'a>,
251    stack: Vec<Object>,
252    /// Start of the current operand run; cleared whenever the run dies
253    /// (an operator was emitted, an unknown operator dropped it, or a
254    /// stray closer flushed it).
255    run_start: Option<usize>,
256}
257
258impl<'a> ContentOps<'a> {
259    /// Starts a pull parse at the beginning of `data`.
260    pub fn new(data: &'a [u8]) -> ContentOps<'a> {
261        ContentOps::at(data, 0)
262    }
263
264    /// Resumes a pull parse at byte offset `pos`. The operand stack is
265    /// empty at every operator boundary, so the offset [`ContentOps::pos`]
266    /// reported between two `next_op` calls is the parser's whole state —
267    /// an executor that suspends a stream mid-walk (a form invocation)
268    /// rebuilds from it exactly.
269    pub fn at(data: &'a [u8], pos: usize) -> ContentOps<'a> {
270        ContentOps {
271            lexer: Lexer::at(data, pos),
272            stack: Vec::new(),
273            run_start: None,
274        }
275    }
276
277    /// The cursor's byte offset — a resume point between `next_op` calls.
278    pub fn pos(&self) -> usize {
279        self.lexer.pos()
280    }
281
282    /// Parses forward to the next complete operator and returns it with
283    /// its span, or `None` at the end of the stream.
284    ///
285    /// Covers ISO 32000-1 §7.8.2.
286    pub fn next_op(&mut self) -> Result<Option<(Op, Span)>> {
287        loop {
288            let (token_start, raw) = self.lexer.next_raw_token_spanned()?;
289            let kw = match raw {
290                RawToken::Keyword(kw) => {
291                    if self.run_start.is_none() {
292                        self.run_start = Some(token_start);
293                    }
294                    kw
295                }
296                RawToken::Hex(span) => {
297                    if self.run_start.is_none() {
298                        self.run_start = Some(token_start);
299                    }
300                    self.stack.push(Object::String(decode_hex(span)));
301                    continue;
302                }
303                RawToken::Owned(token) => {
304                    if !matches!(token, Token::Eof) && self.run_start.is_none() {
305                        self.run_start = Some(token_start);
306                    }
307                    match token {
308                        Token::Eof => return Ok(None),
309                        Token::Int(i) => self.stack.push(Object::Int(i)),
310                        Token::Real(r) => self.stack.push(Object::Real(r)),
311                        Token::Name(n) => self.stack.push(Object::Name(n)),
312                        Token::LitString(s) | Token::HexString(s) => {
313                            self.stack.push(Object::String(s))
314                        }
315                        Token::ArrayOpen => {
316                            let a = parse_array(&mut self.lexer, 0)?;
317                            self.stack.push(a);
318                        }
319                        Token::DictOpen => {
320                            let d = parse_dict(&mut self.lexer, 0)?;
321                            self.stack.push(Object::Dict(d));
322                        }
323                        // Stray closers: malformed input, drop pending operands.
324                        Token::ArrayClose | Token::DictClose => {
325                            self.stack.clear();
326                            self.run_start = None;
327                        }
328                        Token::Keyword(_) => {
329                            unreachable!("next_raw_token yields keywords borrowed")
330                        }
331                    }
332                    continue;
333                }
334            };
335            match kw {
336                b"true" => self.stack.push(Object::Bool(true)),
337                b"false" => self.stack.push(Object::Bool(false)),
338                b"null" => self.stack.push(Object::Null),
339                b"BI" => {
340                    let start = self.run_start.take().unwrap_or(token_start);
341                    let image = parse_inline_image(&mut self.lexer);
342                    self.stack.clear();
343                    if let Some(op) = image {
344                        let span = Span::new(start as u64, self.lexer.pos() as u64);
345                        return Ok(Some((op, span)));
346                    }
347                }
348                _ => {
349                    let start = self.run_start.take().unwrap_or(token_start);
350                    let op = dispatch(kw, &mut self.stack);
351                    self.stack.clear();
352                    if let Some(op) = op {
353                        let span = Span::new(start as u64, self.lexer.pos() as u64);
354                        return Ok(Some((op, span)));
355                    }
356                }
357            }
358        }
359    }
360}
361
362/// Composes an array value; the opening `[` has already been consumed.
363/// Unexpected tokens inside are skipped leniently; nesting deeper than
364/// `MAX_NESTING_DEPTH` is a syntax error.
365fn parse_array(lexer: &mut Lexer, depth: usize) -> Result<Object> {
366    if depth >= MAX_NESTING_DEPTH {
367        return Err(too_deep(lexer));
368    }
369    let mut items = Vec::new();
370    loop {
371        match lexer.next_raw_token()? {
372            RawToken::Owned(Token::ArrayClose | Token::Eof) => break,
373            raw => {
374                if let Some(v) = compose_raw_value(lexer, raw, depth)? {
375                    items.push(v);
376                }
377            }
378        }
379    }
380    Ok(Object::Array(items))
381}
382
383/// Numeric coercion for operands: `Int` and `Real` both become `f32`.
384fn num(o: &Object) -> Option<f32> {
385    match o {
386        Object::Int(i) => Some(*i as f32),
387        Object::Real(r) => Some(*r as f32),
388        _ => None,
389    }
390}
391
392/// The last `N` operands as numbers, or `None` on arity/type mismatch.
393fn nums<const N: usize>(stack: &[Object]) -> Option<[f32; N]> {
394    if stack.len() < N {
395        return None;
396    }
397    let tail = &stack[stack.len() - N..];
398    let mut out = [0.0f32; N];
399    for (slot, o) in out.iter_mut().zip(tail) {
400        *slot = num(o)?;
401    }
402    Some(out)
403}
404
405/// The last operand as an integer (`Real` truncated leniently).
406fn int1(stack: &[Object]) -> Option<i32> {
407    match stack.last()? {
408        Object::Int(i) => Some(*i as i32),
409        Object::Real(r) => Some(*r as i32),
410        _ => None,
411    }
412}
413
414/// The last operand as a name, taken off the stack: the caller clears the
415/// stack after every dispatch, so operands are moved out, never cloned.
416fn name1(stack: &mut [Object]) -> Option<Name> {
417    match stack.last_mut()? {
418        Object::Name(n) => Some(Name(std::mem::take(&mut n.0))),
419        _ => None,
420    }
421}
422
423/// The last operand as a string's bytes, taken off the stack like [`name1`].
424fn str1(stack: &mut [Object]) -> Option<Vec<u8>> {
425    match stack.last_mut()? {
426        Object::String(s) => Some(std::mem::take(s)),
427        _ => None,
428    }
429}
430
431/// Every operand on the stack as a number (for `SC`/`sc`).
432fn all_nums(stack: &[Object]) -> Option<Vec<f32>> {
433    stack.iter().map(num).collect()
434}
435
436/// A six-number tail as a [`Matrix`] (for `cm`/`Tm`).
437fn matrix(stack: &[Object]) -> Option<Matrix> {
438    let [a, b, c, d, e, f] = nums::<6>(stack)?;
439    Some(Matrix { a, b, c, d, e, f })
440}
441
442/// Composes a dictionary; the opening `<<` has already been consumed.
443/// Nesting deeper than `MAX_NESTING_DEPTH` is a syntax error.
444fn parse_dict(lexer: &mut Lexer, depth: usize) -> Result<Dict> {
445    if depth >= MAX_NESTING_DEPTH {
446        return Err(too_deep(lexer));
447    }
448    let mut dict = Dict::new();
449    loop {
450        match lexer.next_raw_token()? {
451            RawToken::Owned(Token::DictClose | Token::Eof) => break,
452            RawToken::Owned(Token::Name(key)) => {
453                let raw = lexer.next_raw_token()?;
454                if matches!(raw, RawToken::Owned(Token::DictClose | Token::Eof)) {
455                    // Key with no value: drop it.
456                    break;
457                }
458                if let Some(v) = compose_raw_value(lexer, raw, depth)? {
459                    dict.insert(key, v);
460                }
461            }
462            // Non-name key: malformed, skip the token.
463            _ => {}
464        }
465    }
466    Ok(dict)
467}
468
469/// Maps an operator keyword plus its operand stack to a typed [`Op`].
470/// Returns `None` (operator skipped) for unknown keywords or operand
471/// arity/type mismatches.
472///
473/// The stack is dispatch's to consume — the caller clears it after every
474/// keyword — so heap operands (strings, names) are moved into the `Op`
475/// rather than cloned, and a mismatch may leave the stack partially
476/// emptied.
477///
478/// Covers ISO 32000-1 §8.2, §8.4.4, §8.5.2.1, §8.6.5.8 and Annex A.2.
479fn dispatch(kw: &[u8], stack: &mut [Object]) -> Option<Op> {
480    Some(match kw {
481        // Graphics state.
482        b"q" => Op::Save,
483        b"Q" => Op::Restore,
484        b"cm" => Op::Concat(matrix(stack)?),
485        b"w" => Op::SetLineWidth(nums::<1>(stack)?[0]),
486        b"J" => Op::SetLineCap(int1(stack)?),
487        b"j" => Op::SetLineJoin(int1(stack)?),
488        b"M" => Op::SetMiterLimit(nums::<1>(stack)?[0]),
489        b"d" => {
490            if stack.len() < 2 {
491                return None;
492            }
493            let phase = num(&stack[stack.len() - 1])?;
494            let Object::Array(items) = &stack[stack.len() - 2] else {
495                return None;
496            };
497            let dashes: Vec<f32> = items.iter().map(num).collect::<Option<_>>()?;
498            Op::SetDash(dashes, phase)
499        }
500        b"ri" => Op::SetRenderingIntent(name1(stack)?),
501        b"i" => Op::SetFlatness(nums::<1>(stack)?[0]),
502        b"gs" => Op::SetExtGState(name1(stack)?),
503
504        // Path construction.
505        b"m" => {
506            let [x, y] = nums(stack)?;
507            Op::MoveTo(x, y)
508        }
509        b"l" => {
510            let [x, y] = nums(stack)?;
511            Op::LineTo(x, y)
512        }
513        b"c" => {
514            let [x1, y1, x2, y2, x3, y3] = nums(stack)?;
515            Op::CurveTo(x1, y1, x2, y2, x3, y3)
516        }
517        b"v" => {
518            let [x2, y2, x3, y3] = nums(stack)?;
519            Op::CurveToV(x2, y2, x3, y3)
520        }
521        b"y" => {
522            let [x1, y1, x3, y3] = nums(stack)?;
523            Op::CurveToY(x1, y1, x3, y3)
524        }
525        b"h" => Op::ClosePath,
526        b"re" => {
527            let [x, y, w, h] = nums(stack)?;
528            Op::Rect(x, y, w, h)
529        }
530
531        // Path painting and clipping.
532        b"S" => Op::Stroke,
533        b"s" => Op::CloseStroke,
534        b"f" | b"F" => Op::Fill,
535        b"f*" => Op::FillEvenOdd,
536        b"B" => Op::FillStroke,
537        b"B*" => Op::FillStrokeEvenOdd,
538        b"b" => Op::CloseFillStroke,
539        b"b*" => Op::CloseFillStrokeEvenOdd,
540        b"n" => Op::EndPath,
541        b"W" => Op::ClipNonZero,
542        b"W*" => Op::ClipEvenOdd,
543
544        _ => return dispatch_color_text(kw, stack),
545    })
546}
547
548/// Continuation of [`dispatch`]: color and text operators.
549///
550/// Covers ISO 32000-1 §8.6.8, §9.2.2, §9.3.2, §9.4.2, §9.4.3 and Annex A.2.
551fn dispatch_color_text(kw: &[u8], stack: &mut [Object]) -> Option<Op> {
552    Some(match kw {
553        // Color.
554        b"CS" => Op::SetStrokeColorSpace(name1(stack)?),
555        b"cs" => Op::SetFillColorSpace(name1(stack)?),
556        b"SC" => Op::SetStrokeColor(all_nums(stack)?),
557        b"sc" => Op::SetFillColor(all_nums(stack)?),
558        b"SCN" => {
559            let (comps, pattern) = color_n(stack)?;
560            Op::SetStrokeColorN(comps, pattern)
561        }
562        b"scn" => {
563            let (comps, pattern) = color_n(stack)?;
564            Op::SetFillColorN(comps, pattern)
565        }
566        b"G" => Op::SetStrokeGray(nums::<1>(stack)?[0]),
567        b"g" => Op::SetFillGray(nums::<1>(stack)?[0]),
568        b"RG" => {
569            let [r, g, b] = nums(stack)?;
570            Op::SetStrokeRGB(r, g, b)
571        }
572        b"rg" => {
573            let [r, g, b] = nums(stack)?;
574            Op::SetFillRGB(r, g, b)
575        }
576        b"K" => {
577            let [c, m, y, k] = nums(stack)?;
578            Op::SetStrokeCMYK(c, m, y, k)
579        }
580        b"k" => {
581            let [c, m, y, k] = nums(stack)?;
582            Op::SetFillCMYK(c, m, y, k)
583        }
584
585        // Text.
586        b"BT" => Op::BeginText,
587        b"ET" => Op::EndText,
588        b"Tc" => Op::SetCharSpacing(nums::<1>(stack)?[0]),
589        b"Tw" => Op::SetWordSpacing(nums::<1>(stack)?[0]),
590        b"Tz" => Op::SetHorizScaling(nums::<1>(stack)?[0]),
591        b"TL" => Op::SetLeading(nums::<1>(stack)?[0]),
592        b"Tf" => {
593            if stack.len() < 2 {
594                return None;
595            }
596            let size = num(&stack[stack.len() - 1])?;
597            let at = stack.len() - 2;
598            let Object::Name(font) = &mut stack[at] else {
599                return None;
600            };
601            Op::SetFont(Name(std::mem::take(&mut font.0)), size)
602        }
603        b"d0" => {
604            let [wx, wy] = nums::<2>(stack)?;
605            Op::SetGlyphWidth(wx, wy)
606        }
607        b"d1" => {
608            let [wx, wy, llx, lly, urx, ury] = nums::<6>(stack)?;
609            Op::SetGlyphWidthBBox(wx, wy, llx, lly, urx, ury)
610        }
611        b"Tr" => Op::SetTextRender(int1(stack)?),
612        b"Ts" => Op::SetTextRise(nums::<1>(stack)?[0]),
613        b"Td" => {
614            let [tx, ty] = nums(stack)?;
615            Op::TextMove(tx, ty)
616        }
617        b"TD" => {
618            let [tx, ty] = nums(stack)?;
619            Op::TextMoveSetLeading(tx, ty)
620        }
621        b"Tm" => Op::SetTextMatrix(matrix(stack)?),
622        b"T*" => Op::TextNextLine,
623        b"Tj" => Op::ShowText(str1(stack)?),
624        b"TJ" => {
625            let Object::Array(items) = stack.last_mut()? else {
626                return None;
627            };
628            let mut adjusted = Vec::with_capacity(items.len());
629            adjusted.extend(items.iter_mut().filter_map(|o| match o {
630                Object::String(s) => Some(TextItem::Str(std::mem::take(s))),
631                other => num(other).map(TextItem::Offset),
632            }));
633            Op::ShowTextAdjusted(adjusted)
634        }
635        b"'" => Op::NextLineShowText(str1(stack)?),
636        b"\"" => {
637            if stack.len() < 3 {
638                return None;
639            }
640            let s = str1(stack)?;
641            let [aw, ac] = nums_at::<2>(stack, stack.len() - 3)?;
642            Op::NextLineShowTextSpaced(aw, ac, s)
643        }
644
645        _ => return dispatch_misc(kw, stack),
646    })
647}
648
649/// Continuation of [`dispatch`]: XObject, shading, marked-content, and
650/// compatibility operators.
651///
652/// Covers ISO 32000-1 §14.6.1 and §8.8.1.
653fn dispatch_misc(kw: &[u8], stack: &mut [Object]) -> Option<Op> {
654    Some(match kw {
655        b"Do" => Op::XObject(name1(stack)?),
656        b"sh" => Op::Shading(name1(stack)?),
657        b"MP" => Op::MarkedContentPoint(name1(stack)?),
658        b"DP" => {
659            let (tag, props) = tag_props(stack)?;
660            Op::MarkedContentPointProps(tag, props)
661        }
662        b"BMC" => Op::BeginMarkedContent(name1(stack)?),
663        b"BDC" => {
664            let (tag, props) = tag_props(stack)?;
665            Op::BeginMarkedContentProps(tag, props)
666        }
667        b"EMC" => Op::EndMarkedContent,
668        b"BX" => Op::BeginCompat,
669        b"EX" => Op::EndCompat,
670        _ => return None,
671    })
672}
673
674/// `N` numbers starting at `start` (for operators whose numeric operands
675/// are not last on the stack).
676fn nums_at<const N: usize>(stack: &[Object], start: usize) -> Option<[f32; N]> {
677    let slice = stack.get(start..start + N)?;
678    let mut out = [0.0f32; N];
679    for (slot, o) in out.iter_mut().zip(slice) {
680        *slot = num(o)?;
681    }
682    Some(out)
683}
684
685/// Operands of `SCN`/`scn`: numeric components optionally followed by a
686/// pattern name (taken off the stack like [`name1`]).
687fn color_n(stack: &mut [Object]) -> Option<(Vec<f32>, Option<Name>)> {
688    match stack.last_mut() {
689        Some(Object::Name(n)) => {
690            let pattern = Name(std::mem::take(&mut n.0));
691            let comps = all_nums(&stack[..stack.len() - 1])?;
692            Some((comps, Some(pattern)))
693        }
694        _ => Some((all_nums(stack)?, None)),
695    }
696}
697
698/// Operands of `DP`/`BDC`: a tag name and a properties value (an inline
699/// dictionary or a resource name), both taken off the stack like [`name1`].
700fn tag_props(stack: &mut [Object]) -> Option<(Name, Object)> {
701    if stack.len() < 2 {
702        return None;
703    }
704    let last = stack.len() - 1;
705    let props = match &stack[last] {
706        Object::Dict(_) | Object::Name(_) => std::mem::replace(&mut stack[last], Object::Null),
707        _ => return None,
708    };
709    let Object::Name(tag) = &mut stack[last - 1] else {
710        return None;
711    };
712    Some((Name(std::mem::take(&mut tag.0)), props))
713}
714
715/// Parses an inline image; the `BI` keyword has already been consumed.
716/// Returns `None` (image skipped) when the stream ends before `ID` or no
717/// `EI` terminator can be located.
718///
719/// Covers ISO 32000-1 §8.9.7.
720fn parse_inline_image(lexer: &mut Lexer) -> Option<Op> {
721    let mut dict = Dict::new();
722    loop {
723        match lexer.next_token().ok()? {
724            Token::Keyword(kw) if kw == b"ID" => break,
725            Token::Eof => return None,
726            Token::Name(key) => {
727                let tok = lexer.next_token().ok()?;
728                if matches!(tok, Token::Eof) {
729                    return None;
730                }
731                let Some(value) = compose_value(lexer, tok, 0).ok()? else {
732                    continue;
733                };
734                let canon = expand_image_key(&key.0);
735                let value = if canon == "ColorSpace" {
736                    expand_colorspace(value)
737                } else {
738                    value
739                };
740                dict.insert(Name(canon.to_string()), value);
741            }
742            // Malformed entry: skip the stray token.
743            _ => {}
744        }
745    }
746    let bytes = lexer.data();
747    let mut start = lexer.pos();
748    // Exactly one whitespace byte separates `ID` from the image data.
749    if bytes.get(start).is_some_and(|&b| is_whitespace(b)) {
750        start += 1;
751    }
752    let declared = dict.get_int("Length").or_else(|| dict.get_int("L"));
753    let data = if let Some(n) = declared.and_then(|n| usize::try_from(n).ok()) {
754        // A declared length is trusted.
755        let end = (start + n).min(bytes.len());
756        lexer.seek(end);
757        bytes[start..end].to_vec()
758    } else {
759        let Some(ei) = find_ei(bytes, start) else {
760            // No terminator anywhere: drop the image and stop lexing what
761            // can only be binary garbage.
762            lexer.seek(bytes.len());
763            return None;
764        };
765        // The whitespace byte before `EI` is a separator, not data.
766        let mut end = ei;
767        if end > start && is_whitespace(bytes[end - 1]) {
768            end -= 1;
769        }
770        lexer.seek(ei);
771        bytes[start..end].to_vec()
772    };
773    consume_ei(lexer);
774    Some(Op::InlineImage(ImageParams { dict, data }))
775}
776
777/// Finds the offset of an `EI` terminator at a token boundary: preceded
778/// by whitespace (or the data start) and followed by a non-regular byte
779/// or end of input.
780fn find_ei(bytes: &[u8], start: usize) -> Option<usize> {
781    let mut from = start;
782    while let Some(off) = memchr::memchr(b'E', &bytes[from..]) {
783        let p = from + off;
784        from = p + 1;
785        if bytes.get(p + 1) != Some(&b'I') {
786            continue;
787        }
788        let before_ok = p == start || is_whitespace(bytes[p - 1]);
789        let after_ok = match bytes.get(p + 2) {
790            None => true,
791            Some(&b) => !crate::lexer::is_regular(b),
792        };
793        if before_ok && after_ok {
794            return Some(p);
795        }
796    }
797    None
798}
799
800/// Consumes the `EI` keyword after inline image data, scanning forward
801/// leniently if the very next token is something else.
802fn consume_ei(lexer: &mut Lexer) {
803    let save = lexer.pos();
804    if let Ok(Token::Keyword(kw)) = lexer.next_token() {
805        if kw == b"EI" {
806            return;
807        }
808    }
809    match find_ei(lexer.data(), save) {
810        Some(p) => lexer.seek(p + 2),
811        None => lexer.seek(lexer.data().len()),
812    }
813}
814
815/// Canonical spelling of an inline image dictionary key (ISO 32000
816/// §8.9.7, Table 91).
817///
818/// Covers ISO 32000-1 §8.9.7.
819fn expand_image_key(key: &str) -> &str {
820    match key {
821        "BPC" => "BitsPerComponent",
822        "CS" => "ColorSpace",
823        "D" => "Decode",
824        "DP" => "DecodeParms",
825        "F" => "Filter",
826        "H" => "Height",
827        "W" => "Width",
828        "IM" => "ImageMask",
829        "I" => "Interpolate",
830        other => other,
831    }
832}
833
834/// Expands colorspace name abbreviations inside a `/CS` value, including
835/// names nested in an `Indexed` array.
836fn expand_colorspace(value: Object) -> Object {
837    match value {
838        Object::Name(Name(n)) => Object::Name(Name(match n.as_str() {
839            "G" => "DeviceGray".to_string(),
840            "RGB" => "DeviceRGB".to_string(),
841            "CMYK" => "DeviceCMYK".to_string(),
842            "I" => "Indexed".to_string(),
843            _ => n,
844        })),
845        Object::Array(items) => Object::Array(items.into_iter().map(expand_colorspace).collect()),
846        other => other,
847    }
848}
849
850/// [`compose_value`] over a raw token: a borrowed keyword matches the
851/// three value keywords without the copy [`Lexer::next_token`] would
852/// make, and a hex span decodes straight to its string bytes.
853fn compose_raw_value(lexer: &mut Lexer, raw: RawToken, depth: usize) -> Result<Option<Object>> {
854    match raw {
855        RawToken::Owned(tok) => compose_value(lexer, tok, depth),
856        RawToken::Keyword(kw) => Ok(match kw {
857            b"true" => Some(Object::Bool(true)),
858            b"false" => Some(Object::Bool(false)),
859            b"null" => Some(Object::Null),
860            _ => None,
861        }),
862        RawToken::Hex(span) => Ok(Some(Object::String(decode_hex(span)))),
863    }
864}
865
866/// Turns a leading token into an [`Object`], recursing for containers
867/// (bounded by `MAX_NESTING_DEPTH`). Returns `None` for tokens that
868/// carry no value (stray delimiters, unexpected keywords).
869fn compose_value(lexer: &mut Lexer, tok: Token, depth: usize) -> Result<Option<Object>> {
870    Ok(match tok {
871        Token::Int(i) => Some(Object::Int(i)),
872        Token::Real(r) => Some(Object::Real(r)),
873        Token::Name(n) => Some(Object::Name(n)),
874        Token::LitString(s) | Token::HexString(s) => Some(Object::String(s)),
875        Token::ArrayOpen => Some(parse_array(lexer, depth + 1)?),
876        Token::DictOpen => Some(Object::Dict(parse_dict(lexer, depth + 1)?)),
877        Token::Keyword(kw) => match kw.as_slice() {
878            b"true" => Some(Object::Bool(true)),
879            b"false" => Some(Object::Bool(false)),
880            b"null" => Some(Object::Null),
881            _ => None,
882        },
883        _ => None,
884    })
885}
886
887#[cfg(test)]
888mod tests {
889    use super::*;
890
891    fn ops(src: &[u8]) -> Vec<Op> {
892        parse_content(src).expect("content parses")
893    }
894
895    fn name(s: &str) -> Name {
896        Name(s.to_string())
897    }
898
899    // Covers ISO 32000-1 §7.8.2, §8.2 and Annex A.2.
900    #[test]
901    fn shapes_fixture_parses_to_expected_ops() {
902        let path = concat!(
903            env!("CARGO_MANIFEST_DIR"),
904            "/../../tests/fixtures/shapes.pdf"
905        );
906        let doc = crate::document::Document::open(path).expect("open shapes.pdf");
907        let page = doc.page(0).expect("page 0");
908        let content = page.content(&doc).expect("page content");
909        let got = ops(&content);
910        let m = |a, b, c, d, e, f| Matrix { a, b, c, d, e, f };
911        assert_eq!(
912            got,
913            vec![
914                Op::SetFillRGB(1.0, 0.0, 0.0),
915                Op::Rect(72.0, 600.0, 100.0, 80.0),
916                Op::Fill,
917                Op::SetFillRGB(0.0, 0.5, 1.0),
918                Op::Rect(200.0, 600.0, 120.0, 60.0),
919                Op::Fill,
920                Op::SetFillRGB(0.2, 0.8, 0.2),
921                Op::Rect(340.0, 590.0, 90.0, 90.0),
922                Op::Fill,
923                Op::SetStrokeRGB(0.0, 0.0, 0.0),
924                Op::SetLineWidth(2.0),
925                Op::MoveTo(100.0, 300.0),
926                Op::CurveTo(150.0, 400.0, 250.0, 400.0, 300.0, 300.0),
927                Op::Stroke,
928                Op::Save,
929                Op::Concat(m(0.5, 0.0, 0.0, 0.5, 300.0, 100.0)),
930                Op::SetFillRGB(0.8, 0.0, 0.8),
931                Op::Rect(0.0, 0.0, 200.0, 200.0),
932                Op::Fill,
933                Op::Restore,
934            ]
935        );
936    }
937
938    // Covers ISO 32000-1 §8.3.2.3, §8.4.3.3, §8.4.3.4, §8.4.3.5, §8.4.4, §8.6.5.8 and Annex A.2.
939    #[test]
940    fn graphics_state_ops_round_trip() {
941        let got = ops(b"q Q 1 0 0 1 10 20 cm 2 w 1 J 2 j 3.5 M [3 1] 0.5 d \
942                        /Perceptual ri 1 i /GS1 gs");
943        assert_eq!(
944            got,
945            vec![
946                Op::Save,
947                Op::Restore,
948                Op::Concat(Matrix {
949                    a: 1.0,
950                    b: 0.0,
951                    c: 0.0,
952                    d: 1.0,
953                    e: 10.0,
954                    f: 20.0
955                }),
956                Op::SetLineWidth(2.0),
957                Op::SetLineCap(1),
958                Op::SetLineJoin(2),
959                Op::SetMiterLimit(3.5),
960                Op::SetDash(vec![3.0, 1.0], 0.5),
961                Op::SetRenderingIntent(name("Perceptual")),
962                Op::SetFlatness(1.0),
963                Op::SetExtGState(name("GS1")),
964            ]
965        );
966    }
967
968    // Covers ISO 32000-1 §8.4.3.6.
969    #[test]
970    fn empty_dash_array_round_trips() {
971        assert_eq!(ops(b"[] 0 d"), vec![Op::SetDash(vec![], 0.0)]);
972    }
973
974    // Covers ISO 32000-1 §8.5.2.1 and Annex A.2.
975    #[test]
976    fn path_ops_round_trip() {
977        let got = ops(b"10 20 m 30 40 l 1 2 3 4 5 6 c 1 2 3 4 v 5 6 7 8 y h \
978                        72 600 100 80 re");
979        assert_eq!(
980            got,
981            vec![
982                Op::MoveTo(10.0, 20.0),
983                Op::LineTo(30.0, 40.0),
984                Op::CurveTo(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
985                Op::CurveToV(1.0, 2.0, 3.0, 4.0),
986                Op::CurveToY(5.0, 6.0, 7.0, 8.0),
987                Op::ClosePath,
988                Op::Rect(72.0, 600.0, 100.0, 80.0),
989            ]
990        );
991    }
992
993    #[test]
994    fn negative_and_real_coordinates_coerce() {
995        assert_eq!(
996            ops(b"-1.5 +2 m .5 -3 l"),
997            vec![Op::MoveTo(-1.5, 2.0), Op::LineTo(0.5, -3.0)]
998        );
999    }
1000
1001    // Covers ISO 32000-1 §14.6.1, §14.6.2, §8.2 and Annex A.2.
1002    #[test]
1003    fn xobject_shading_marked_content_round_trip() {
1004        let got = ops(b"/Im1 Do /Sh1 sh /Tag MP /Tag /P DP /Span BMC \
1005                        /Span << /MCID 3 >> BDC EMC BX EX");
1006        let mut props = Dict::new();
1007        props.insert(name("MCID"), Object::Int(3));
1008        assert_eq!(
1009            got,
1010            vec![
1011                Op::XObject(name("Im1")),
1012                Op::Shading(name("Sh1")),
1013                Op::MarkedContentPoint(name("Tag")),
1014                Op::MarkedContentPointProps(name("Tag"), Object::Name(name("P"))),
1015                Op::BeginMarkedContent(name("Span")),
1016                Op::BeginMarkedContentProps(name("Span"), Object::Dict(props)),
1017                Op::EndMarkedContent,
1018                Op::BeginCompat,
1019                Op::EndCompat,
1020            ]
1021        );
1022    }
1023
1024    // Covers ISO 32000-1 §7.8.2, §8.2, Annex A.2 and Annex I.
1025    #[test]
1026    fn unknown_operator_is_skipped_without_breaking_following_ops() {
1027        assert_eq!(
1028            ops(b"zz 1 2 10 20 m 30 40 l"),
1029            vec![Op::MoveTo(10.0, 20.0), Op::LineTo(30.0, 40.0)]
1030        );
1031        // Unknown operator with operands before it: operands are dropped.
1032        assert_eq!(ops(b"1 2 zz 3 4 l"), vec![Op::LineTo(3.0, 4.0)]);
1033    }
1034
1035    // Covers ISO 32000-1 §7.8.2.
1036    #[test]
1037    fn malformed_operands_are_skipped() {
1038        // Too few operands.
1039        assert_eq!(ops(b"1 m 5 6 l"), vec![Op::LineTo(5.0, 6.0)]);
1040        // Wrong operand types.
1041        assert_eq!(ops(b"(a) (b) m S"), vec![Op::Stroke]);
1042        assert_eq!(ops(b"/N w"), Vec::<Op>::new());
1043        assert_eq!(ops(b"5 Tf"), Vec::<Op>::new());
1044        // `TJ` whose operand is not an array.
1045        assert_eq!(ops(b"(x) TJ q"), vec![Op::Save]);
1046        // Non-string, non-number entries inside a `TJ` array are dropped.
1047        assert_eq!(
1048            ops(b"[(y) /Bad 5] TJ"),
1049            vec![Op::ShowTextAdjusted(vec![
1050                TextItem::Str(b"y".to_vec()),
1051                TextItem::Offset(5.0),
1052            ])]
1053        );
1054        // Dash without an array operand.
1055        assert_eq!(ops(b"3 0 d"), Vec::<Op>::new());
1056        // Extra operands: the trailing ones are used.
1057        assert_eq!(ops(b"9 9 1 2 m"), vec![Op::MoveTo(1.0, 2.0)]);
1058    }
1059
1060    // Covers ISO 32000-1 §7.8.2, §8.9.7 and Annex A.2.
1061    #[test]
1062    fn inline_image_with_hex_data_ending_in_ei() {
1063        let got = ops(b"q BI /W 2 /H 2 /BPC 8 /CS /G /F /AHx ID 00FF80FF> EI Q");
1064        assert_eq!(got.len(), 3);
1065        assert_eq!(got[0], Op::Save);
1066        assert_eq!(got[2], Op::Restore);
1067        let Op::InlineImage(img) = &got[1] else {
1068            panic!("expected inline image, got {:?}", got[1]);
1069        };
1070        assert_eq!(img.data, b"00FF80FF>");
1071        assert_eq!(img.dict.get_int("Width"), Some(2));
1072        assert_eq!(img.dict.get_int("Height"), Some(2));
1073        assert_eq!(img.dict.get_int("BitsPerComponent"), Some(8));
1074        assert_eq!(img.dict.get_name("ColorSpace"), Some(&name("DeviceGray")));
1075        assert_eq!(img.dict.get_name("Filter"), Some(&name("AHx")));
1076    }
1077
1078    // Covers ISO 32000-1 §8.9.7.
1079    #[test]
1080    fn inline_image_trusts_declared_length() {
1081        // Binary payload contains a spurious ` EI ` sequence; /L must win.
1082        let mut src = b"BI /W 3 /H 1 /BPC 8 /CS /RGB /L 9 ID ".to_vec();
1083        src.extend_from_slice(b"ab EI wxy");
1084        src.extend_from_slice(b" EI 1 2 m");
1085        let got = ops(&src);
1086        assert_eq!(got.len(), 2);
1087        let Op::InlineImage(img) = &got[0] else {
1088            panic!("expected inline image, got {:?}", got[0]);
1089        };
1090        assert_eq!(img.data, b"ab EI wxy");
1091        assert_eq!(img.dict.get_name("ColorSpace"), Some(&name("DeviceRGB")));
1092        assert_eq!(got[1], Op::MoveTo(1.0, 2.0));
1093    }
1094
1095    // Covers ISO 32000-1 §8.9.7.
1096    #[test]
1097    fn inline_image_expands_indexed_colorspace_array() {
1098        let got = ops(
1099            b"BI /W 1 /H 1 /BPC 8 /CS [/I /RGB 1 <FF0000>] /IM false /I true \
1100                        ID \xde\xad EI",
1101        );
1102        let Op::InlineImage(img) = &got[0] else {
1103            panic!("expected inline image, got {:?}", got[0]);
1104        };
1105        assert_eq!(img.data, b"\xde\xad");
1106        assert_eq!(img.dict.get("ImageMask"), Some(&Object::Bool(false)));
1107        assert_eq!(img.dict.get("Interpolate"), Some(&Object::Bool(true)));
1108        let cs = img.dict.get_array("ColorSpace").expect("CS array");
1109        assert_eq!(cs[0], Object::Name(name("Indexed")));
1110        assert_eq!(cs[1], Object::Name(name("DeviceRGB")));
1111        assert_eq!(cs[2], Object::Int(1));
1112        assert_eq!(cs[3], Object::String(b"\xff\x00\x00".to_vec()));
1113    }
1114
1115    #[test]
1116    fn inline_image_without_terminator_is_skipped() {
1117        assert_eq!(ops(b"BI /W 1 ID \x01\x02\x03"), Vec::<Op>::new());
1118        assert_eq!(ops(b"BI /W 1"), Vec::<Op>::new());
1119    }
1120
1121    #[test]
1122    fn inline_image_with_empty_data() {
1123        let got = ops(b"BI /W 0 /H 0 ID  EI n");
1124        assert_eq!(got.len(), 2);
1125        let Op::InlineImage(img) = &got[0] else {
1126            panic!("expected inline image, got {:?}", got[0]);
1127        };
1128        assert!(img.data.is_empty());
1129        assert_eq!(got[1], Op::EndPath);
1130    }
1131
1132    #[test]
1133    fn stray_delimiters_and_comments_are_tolerated() {
1134        assert_eq!(
1135            ops(b"% comment\n1 2 m ] >> 3 4 l"),
1136            vec![Op::MoveTo(1.0, 2.0), Op::LineTo(3.0, 4.0)]
1137        );
1138        assert_eq!(ops(b""), Vec::<Op>::new());
1139        assert_eq!(ops(b"   \n  "), Vec::<Op>::new());
1140    }
1141
1142    // Covers ISO 32000-1 §9.3.1, §9.3.2, §9.3.4 and Annex A.2.
1143    #[test]
1144    fn text_state_ops_round_trip() {
1145        let got = ops(b"BT 0.5 Tc 1 Tw 90 Tz 14 TL /F1 12 Tf 3 Tr 4.5 Ts ET");
1146        assert_eq!(
1147            got,
1148            vec![
1149                Op::BeginText,
1150                Op::SetCharSpacing(0.5),
1151                Op::SetWordSpacing(1.0),
1152                Op::SetHorizScaling(90.0),
1153                Op::SetLeading(14.0),
1154                Op::SetFont(name("F1"), 12.0),
1155                Op::SetTextRender(3),
1156                Op::SetTextRise(4.5),
1157                Op::EndText,
1158            ]
1159        );
1160    }
1161
1162    // Covers ISO 32000-1 §9.3.5, §9.4.2, §9.4.3 and Annex A.2.
1163    #[test]
1164    fn text_positioning_and_showing_round_trip() {
1165        let got = ops(b"BT 72 720 Td 0 -14 TD 1 0 0 1 50 60 Tm T* \
1166                        (Hi) Tj (there) ' 2 3 (spaced) \" ET");
1167        assert_eq!(
1168            got,
1169            vec![
1170                Op::BeginText,
1171                Op::TextMove(72.0, 720.0),
1172                Op::TextMoveSetLeading(0.0, -14.0),
1173                Op::SetTextMatrix(Matrix {
1174                    a: 1.0,
1175                    b: 0.0,
1176                    c: 0.0,
1177                    d: 1.0,
1178                    e: 50.0,
1179                    f: 60.0
1180                }),
1181                Op::TextNextLine,
1182                Op::ShowText(b"Hi".to_vec()),
1183                Op::NextLineShowText(b"there".to_vec()),
1184                Op::NextLineShowTextSpaced(2.0, 3.0, b"spaced".to_vec()),
1185                Op::EndText,
1186            ]
1187        );
1188    }
1189
1190    // Covers ISO 32000-1 Annex A.2.
1191    #[test]
1192    fn parses_d0_glyph_width() {
1193        let ops = parse_content(b"1000 0 d0").expect("parse");
1194        assert_eq!(ops, vec![Op::SetGlyphWidth(1000.0, 0.0)]);
1195    }
1196
1197    // Covers ISO 32000-1 §9.6.5 and Annex A.2.
1198    #[test]
1199    fn parses_d1_glyph_width_bbox() {
1200        let ops = parse_content(b"1000 0 0 0 750 700 d1").expect("parse");
1201        assert_eq!(
1202            ops,
1203            vec![Op::SetGlyphWidthBBox(1000.0, 0.0, 0.0, 0.0, 750.0, 700.0)]
1204        );
1205    }
1206
1207    #[test]
1208    fn d0_with_wrong_arity_is_skipped() {
1209        // Too few operands: leniently skipped (like any arity mismatch), not a panic.
1210        let ops = parse_content(b"1000 d0").expect("parse");
1211        assert!(ops.is_empty());
1212    }
1213
1214    // Covers ISO 32000-1 §9.4.3.
1215    #[test]
1216    fn tj_array_mixes_strings_and_numbers() {
1217        let got = ops(b"[(He) -120 (llo) 33.5 <20>] TJ");
1218        assert_eq!(
1219            got,
1220            vec![Op::ShowTextAdjusted(vec![
1221                TextItem::Str(b"He".to_vec()),
1222                TextItem::Offset(-120.0),
1223                TextItem::Str(b"llo".to_vec()),
1224                TextItem::Offset(33.5),
1225                TextItem::Str(b" ".to_vec()),
1226            ])]
1227        );
1228    }
1229
1230    // Covers ISO 32000-1 §8.6.2, §8.6.8 and Annex A.2.
1231    #[test]
1232    fn color_ops_round_trip() {
1233        let got = ops(b"/DeviceRGB CS /DeviceGray cs 1 0 0 SC 0.5 sc \
1234                        0.3 G 0.7 g 1 0 0 RG 0 1 0 rg 0 0 0 1 K 1 0 0 0 k");
1235        assert_eq!(
1236            got,
1237            vec![
1238                Op::SetStrokeColorSpace(name("DeviceRGB")),
1239                Op::SetFillColorSpace(name("DeviceGray")),
1240                Op::SetStrokeColor(vec![1.0, 0.0, 0.0]),
1241                Op::SetFillColor(vec![0.5]),
1242                Op::SetStrokeGray(0.3),
1243                Op::SetFillGray(0.7),
1244                Op::SetStrokeRGB(1.0, 0.0, 0.0),
1245                Op::SetFillRGB(0.0, 1.0, 0.0),
1246                Op::SetStrokeCMYK(0.0, 0.0, 0.0, 1.0),
1247                Op::SetFillCMYK(1.0, 0.0, 0.0, 0.0),
1248            ]
1249        );
1250    }
1251
1252    // Covers ISO 32000-1 §8.6.6.2, §8.6.8 and Annex A.2.
1253    #[test]
1254    fn scn_with_and_without_pattern_name() {
1255        let got = ops(b"0.2 0.4 0.6 scn /P1 scn 0.1 0.2 /P2 SCN 1 SCN");
1256        assert_eq!(
1257            got,
1258            vec![
1259                Op::SetFillColorN(vec![0.2, 0.4, 0.6], None),
1260                Op::SetFillColorN(vec![], Some(name("P1"))),
1261                Op::SetStrokeColorN(vec![0.1, 0.2], Some(name("P2"))),
1262                Op::SetStrokeColorN(vec![1.0], None),
1263            ]
1264        );
1265    }
1266
1267    // Covers ISO 32000-1 §8.5.3.1, §8.5.3.3.3 and Annex A.2.
1268    #[test]
1269    fn painting_and_clipping_ops_round_trip() {
1270        let got = ops(b"S s f F f* B B* b b* n W W*");
1271        assert_eq!(
1272            got,
1273            vec![
1274                Op::Stroke,
1275                Op::CloseStroke,
1276                Op::Fill,
1277                Op::Fill,
1278                Op::FillEvenOdd,
1279                Op::FillStroke,
1280                Op::FillStrokeEvenOdd,
1281                Op::CloseFillStroke,
1282                Op::CloseFillStrokeEvenOdd,
1283                Op::EndPath,
1284                Op::ClipNonZero,
1285                Op::ClipEvenOdd,
1286            ]
1287        );
1288    }
1289
1290    /// Runs `f` on a deliberately small stack so that unbounded recursion
1291    /// would abort the test binary instead of silently passing on a large
1292    /// main-thread stack.
1293    fn on_small_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
1294        std::thread::Builder::new()
1295            .stack_size(512 * 1024)
1296            .spawn(f)
1297            .expect("spawn test thread")
1298            .join()
1299            .expect("content parser must not overflow the stack")
1300    }
1301
1302    #[test]
1303    fn deeply_nested_array_is_rejected_not_stack_overflow() {
1304        let data = vec![b'['; 50_000];
1305        let result = on_small_stack(move || parse_content(&data));
1306        assert!(matches!(result, Err(Error::Syntax { .. })));
1307    }
1308
1309    #[test]
1310    fn deeply_nested_dict_is_rejected_not_stack_overflow() {
1311        let mut data = Vec::new();
1312        for _ in 0..50_000 {
1313            data.extend_from_slice(b"<</K");
1314        }
1315        let result = on_small_stack(move || parse_content(&data));
1316        assert!(matches!(result, Err(Error::Syntax { .. })));
1317    }
1318
1319    // Covers ISO 32000-1 §14.6.1 and Annex C.2.
1320    #[test]
1321    fn nesting_within_the_limit_still_parses() {
1322        // A BDC property dict holding a modestly nested array operand.
1323        let mut data: Vec<u8> = b"/Tag <</Deep ".to_vec();
1324        data.extend(std::iter::repeat_n(b'[', 50));
1325        data.extend(std::iter::repeat_n(b']', 50));
1326        data.extend_from_slice(b" >> BDC");
1327        let got = ops(&data);
1328        assert_eq!(got.len(), 1);
1329        assert!(matches!(got[0], Op::BeginMarkedContentProps(_, _)));
1330    }
1331
1332    #[test]
1333    fn spanned_ops_cover_their_source_bytes() {
1334        let data = b"q 1 0 0 1 5 5 cm BT /F1 12 Tf (Hi) Tj ET Q";
1335        let spanned = parse_content_spanned(data).unwrap();
1336        let plain = parse_content(data).unwrap();
1337        assert_eq!(
1338            spanned
1339                .iter()
1340                .map(|pair| pair.0.clone())
1341                .collect::<Vec<_>>(),
1342            plain
1343        );
1344        for (op, span) in &spanned {
1345            assert!(span.start < span.end && span.end as usize <= data.len());
1346            // Re-parsing exactly the spanned bytes yields exactly this op.
1347            let slice = &data[span.start as usize..span.end as usize];
1348            let reparsed = parse_content(slice).unwrap();
1349            assert_eq!(reparsed.len(), 1, "span of {op:?} reparses to one op");
1350            assert_eq!(&reparsed[0], op);
1351        }
1352        // Spans are ordered and non-overlapping.
1353        for pair in spanned.windows(2) {
1354            assert!(pair[0].1.end <= pair[1].1.start);
1355        }
1356    }
1357
1358    #[test]
1359    fn unknown_operator_does_not_stretch_the_next_span() {
1360        // `zz` is unknown: its operands are dropped, and the following op's
1361        // span must start at `q`, not at `7`.
1362        let data = b"7 8 zz q";
1363        let spanned = parse_content_spanned(data).unwrap();
1364        assert_eq!(spanned.len(), 1);
1365        assert_eq!(spanned[0].0, Op::Save);
1366        assert_eq!(
1367            &data[spanned[0].1.start as usize..spanned[0].1.end as usize],
1368            b"q"
1369        );
1370    }
1371}