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::{is_whitespace, Lexer, 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).
208pub fn parse_content(data: &[u8]) -> Result<Vec<Op>> {
209    Ok(parse_content_spanned(data)?
210        .into_iter()
211        .map(|pair| pair.0)
212        .collect())
213}
214
215/// Like [`parse_content`], but also reports each operator's byte range —
216/// from the first token of its operand run through the operator keyword —
217/// within `data`.
218pub fn parse_content_spanned(data: &[u8]) -> Result<Vec<(Op, Span)>> {
219    let mut lexer = Lexer::new(data);
220    let mut ops = Vec::new();
221    let mut stack: Vec<Object> = Vec::new();
222    // Start of the current operand run; cleared whenever the run dies
223    // (an operator was emitted, an unknown operator dropped it, or a
224    // stray closer flushed it).
225    let mut run_start: Option<usize> = None;
226    loop {
227        lexer.skip_whitespace_and_comments();
228        let token_start = lexer.pos();
229        let token = lexer.next_token()?;
230        if !matches!(token, Token::Eof) && run_start.is_none() {
231            run_start = Some(token_start);
232        }
233        match token {
234            Token::Eof => break,
235            Token::Int(i) => stack.push(Object::Int(i)),
236            Token::Real(r) => stack.push(Object::Real(r)),
237            Token::Name(n) => stack.push(Object::Name(n)),
238            Token::LitString(s) | Token::HexString(s) => stack.push(Object::String(s)),
239            Token::ArrayOpen => {
240                let a = parse_array(&mut lexer, 0)?;
241                stack.push(a);
242            }
243            Token::DictOpen => {
244                let d = parse_dict(&mut lexer, 0)?;
245                stack.push(Object::Dict(d));
246            }
247            // Stray closers: malformed input, drop pending operands.
248            Token::ArrayClose | Token::DictClose => {
249                stack.clear();
250                run_start = None;
251            }
252            Token::Keyword(kw) => match kw.as_slice() {
253                b"true" => stack.push(Object::Bool(true)),
254                b"false" => stack.push(Object::Bool(false)),
255                b"null" => stack.push(Object::Null),
256                b"BI" => {
257                    let start = run_start.take().unwrap_or(token_start);
258                    if let Some(op) = parse_inline_image(&mut lexer) {
259                        ops.push((op, Span::new(start as u64, lexer.pos() as u64)));
260                    }
261                    stack.clear();
262                }
263                _ => {
264                    let start = run_start.take().unwrap_or(token_start);
265                    if let Some(op) = dispatch(&kw, &stack) {
266                        ops.push((op, Span::new(start as u64, lexer.pos() as u64)));
267                    }
268                    stack.clear();
269                }
270            },
271        }
272    }
273    Ok(ops)
274}
275
276/// Composes an array value; the opening `[` has already been consumed.
277/// Unexpected tokens inside are skipped leniently; nesting deeper than
278/// `MAX_NESTING_DEPTH` is a syntax error.
279fn parse_array(lexer: &mut Lexer, depth: usize) -> Result<Object> {
280    if depth >= MAX_NESTING_DEPTH {
281        return Err(too_deep(lexer));
282    }
283    let mut items = Vec::new();
284    loop {
285        match lexer.next_token()? {
286            Token::ArrayClose | Token::Eof => break,
287            tok => {
288                if let Some(v) = compose_value(lexer, tok, depth)? {
289                    items.push(v);
290                }
291            }
292        }
293    }
294    Ok(Object::Array(items))
295}
296
297/// Numeric coercion for operands: `Int` and `Real` both become `f32`.
298fn num(o: &Object) -> Option<f32> {
299    match o {
300        Object::Int(i) => Some(*i as f32),
301        Object::Real(r) => Some(*r as f32),
302        _ => None,
303    }
304}
305
306/// The last `N` operands as numbers, or `None` on arity/type mismatch.
307fn nums<const N: usize>(stack: &[Object]) -> Option<[f32; N]> {
308    if stack.len() < N {
309        return None;
310    }
311    let tail = &stack[stack.len() - N..];
312    let mut out = [0.0f32; N];
313    for (slot, o) in out.iter_mut().zip(tail) {
314        *slot = num(o)?;
315    }
316    Some(out)
317}
318
319/// The last operand as an integer (`Real` truncated leniently).
320fn int1(stack: &[Object]) -> Option<i32> {
321    match stack.last()? {
322        Object::Int(i) => Some(*i as i32),
323        Object::Real(r) => Some(*r as i32),
324        _ => None,
325    }
326}
327
328/// The last operand as a name.
329fn name1(stack: &[Object]) -> Option<Name> {
330    match stack.last()? {
331        Object::Name(n) => Some(n.clone()),
332        _ => None,
333    }
334}
335
336/// The last operand as a string's bytes.
337fn str1(stack: &[Object]) -> Option<Vec<u8>> {
338    match stack.last()? {
339        Object::String(s) => Some(s.clone()),
340        _ => None,
341    }
342}
343
344/// Every operand on the stack as a number (for `SC`/`sc`).
345fn all_nums(stack: &[Object]) -> Option<Vec<f32>> {
346    stack.iter().map(num).collect()
347}
348
349/// A six-number tail as a [`Matrix`] (for `cm`/`Tm`).
350fn matrix(stack: &[Object]) -> Option<Matrix> {
351    let [a, b, c, d, e, f] = nums::<6>(stack)?;
352    Some(Matrix { a, b, c, d, e, f })
353}
354
355/// Composes a dictionary; the opening `<<` has already been consumed.
356/// Nesting deeper than `MAX_NESTING_DEPTH` is a syntax error.
357fn parse_dict(lexer: &mut Lexer, depth: usize) -> Result<Dict> {
358    if depth >= MAX_NESTING_DEPTH {
359        return Err(too_deep(lexer));
360    }
361    let mut dict = Dict::new();
362    loop {
363        match lexer.next_token()? {
364            Token::DictClose | Token::Eof => break,
365            Token::Name(key) => {
366                let tok = lexer.next_token()?;
367                if matches!(tok, Token::DictClose | Token::Eof) {
368                    // Key with no value: drop it.
369                    break;
370                }
371                if let Some(v) = compose_value(lexer, tok, depth)? {
372                    dict.insert(key, v);
373                }
374            }
375            // Non-name key: malformed, skip the token.
376            _ => {}
377        }
378    }
379    Ok(dict)
380}
381
382/// Maps an operator keyword plus its operand stack to a typed [`Op`].
383/// Returns `None` (operator skipped) for unknown keywords or operand
384/// arity/type mismatches.
385fn dispatch(kw: &[u8], stack: &[Object]) -> Option<Op> {
386    Some(match kw {
387        // Graphics state.
388        b"q" => Op::Save,
389        b"Q" => Op::Restore,
390        b"cm" => Op::Concat(matrix(stack)?),
391        b"w" => Op::SetLineWidth(nums::<1>(stack)?[0]),
392        b"J" => Op::SetLineCap(int1(stack)?),
393        b"j" => Op::SetLineJoin(int1(stack)?),
394        b"M" => Op::SetMiterLimit(nums::<1>(stack)?[0]),
395        b"d" => {
396            if stack.len() < 2 {
397                return None;
398            }
399            let phase = num(&stack[stack.len() - 1])?;
400            let Object::Array(items) = &stack[stack.len() - 2] else {
401                return None;
402            };
403            let dashes: Vec<f32> = items.iter().map(num).collect::<Option<_>>()?;
404            Op::SetDash(dashes, phase)
405        }
406        b"ri" => Op::SetRenderingIntent(name1(stack)?),
407        b"i" => Op::SetFlatness(nums::<1>(stack)?[0]),
408        b"gs" => Op::SetExtGState(name1(stack)?),
409
410        // Path construction.
411        b"m" => {
412            let [x, y] = nums(stack)?;
413            Op::MoveTo(x, y)
414        }
415        b"l" => {
416            let [x, y] = nums(stack)?;
417            Op::LineTo(x, y)
418        }
419        b"c" => {
420            let [x1, y1, x2, y2, x3, y3] = nums(stack)?;
421            Op::CurveTo(x1, y1, x2, y2, x3, y3)
422        }
423        b"v" => {
424            let [x2, y2, x3, y3] = nums(stack)?;
425            Op::CurveToV(x2, y2, x3, y3)
426        }
427        b"y" => {
428            let [x1, y1, x3, y3] = nums(stack)?;
429            Op::CurveToY(x1, y1, x3, y3)
430        }
431        b"h" => Op::ClosePath,
432        b"re" => {
433            let [x, y, w, h] = nums(stack)?;
434            Op::Rect(x, y, w, h)
435        }
436
437        // Path painting and clipping.
438        b"S" => Op::Stroke,
439        b"s" => Op::CloseStroke,
440        b"f" | b"F" => Op::Fill,
441        b"f*" => Op::FillEvenOdd,
442        b"B" => Op::FillStroke,
443        b"B*" => Op::FillStrokeEvenOdd,
444        b"b" => Op::CloseFillStroke,
445        b"b*" => Op::CloseFillStrokeEvenOdd,
446        b"n" => Op::EndPath,
447        b"W" => Op::ClipNonZero,
448        b"W*" => Op::ClipEvenOdd,
449
450        _ => return dispatch_color_text(kw, stack),
451    })
452}
453
454/// Continuation of [`dispatch`]: color and text operators.
455fn dispatch_color_text(kw: &[u8], stack: &[Object]) -> Option<Op> {
456    Some(match kw {
457        // Color.
458        b"CS" => Op::SetStrokeColorSpace(name1(stack)?),
459        b"cs" => Op::SetFillColorSpace(name1(stack)?),
460        b"SC" => Op::SetStrokeColor(all_nums(stack)?),
461        b"sc" => Op::SetFillColor(all_nums(stack)?),
462        b"SCN" => {
463            let (comps, pattern) = color_n(stack)?;
464            Op::SetStrokeColorN(comps, pattern)
465        }
466        b"scn" => {
467            let (comps, pattern) = color_n(stack)?;
468            Op::SetFillColorN(comps, pattern)
469        }
470        b"G" => Op::SetStrokeGray(nums::<1>(stack)?[0]),
471        b"g" => Op::SetFillGray(nums::<1>(stack)?[0]),
472        b"RG" => {
473            let [r, g, b] = nums(stack)?;
474            Op::SetStrokeRGB(r, g, b)
475        }
476        b"rg" => {
477            let [r, g, b] = nums(stack)?;
478            Op::SetFillRGB(r, g, b)
479        }
480        b"K" => {
481            let [c, m, y, k] = nums(stack)?;
482            Op::SetStrokeCMYK(c, m, y, k)
483        }
484        b"k" => {
485            let [c, m, y, k] = nums(stack)?;
486            Op::SetFillCMYK(c, m, y, k)
487        }
488
489        // Text.
490        b"BT" => Op::BeginText,
491        b"ET" => Op::EndText,
492        b"Tc" => Op::SetCharSpacing(nums::<1>(stack)?[0]),
493        b"Tw" => Op::SetWordSpacing(nums::<1>(stack)?[0]),
494        b"Tz" => Op::SetHorizScaling(nums::<1>(stack)?[0]),
495        b"TL" => Op::SetLeading(nums::<1>(stack)?[0]),
496        b"Tf" => {
497            if stack.len() < 2 {
498                return None;
499            }
500            let size = num(&stack[stack.len() - 1])?;
501            let Object::Name(font) = &stack[stack.len() - 2] else {
502                return None;
503            };
504            Op::SetFont(font.clone(), size)
505        }
506        b"d0" => {
507            let [wx, wy] = nums::<2>(stack)?;
508            Op::SetGlyphWidth(wx, wy)
509        }
510        b"d1" => {
511            let [wx, wy, llx, lly, urx, ury] = nums::<6>(stack)?;
512            Op::SetGlyphWidthBBox(wx, wy, llx, lly, urx, ury)
513        }
514        b"Tr" => Op::SetTextRender(int1(stack)?),
515        b"Ts" => Op::SetTextRise(nums::<1>(stack)?[0]),
516        b"Td" => {
517            let [tx, ty] = nums(stack)?;
518            Op::TextMove(tx, ty)
519        }
520        b"TD" => {
521            let [tx, ty] = nums(stack)?;
522            Op::TextMoveSetLeading(tx, ty)
523        }
524        b"Tm" => Op::SetTextMatrix(matrix(stack)?),
525        b"T*" => Op::TextNextLine,
526        b"Tj" => Op::ShowText(str1(stack)?),
527        b"TJ" => {
528            let Object::Array(items) = stack.last()? else {
529                return None;
530            };
531            let adjusted = items
532                .iter()
533                .filter_map(|o| match o {
534                    Object::String(s) => Some(TextItem::Str(s.clone())),
535                    other => num(other).map(TextItem::Offset),
536                })
537                .collect();
538            Op::ShowTextAdjusted(adjusted)
539        }
540        b"'" => Op::NextLineShowText(str1(stack)?),
541        b"\"" => {
542            if stack.len() < 3 {
543                return None;
544            }
545            let s = str1(stack)?;
546            let [aw, ac] = nums_at::<2>(stack, stack.len() - 3)?;
547            Op::NextLineShowTextSpaced(aw, ac, s)
548        }
549
550        _ => return dispatch_misc(kw, stack),
551    })
552}
553
554/// Continuation of [`dispatch`]: XObject, shading, marked-content, and
555/// compatibility operators.
556fn dispatch_misc(kw: &[u8], stack: &[Object]) -> Option<Op> {
557    Some(match kw {
558        b"Do" => Op::XObject(name1(stack)?),
559        b"sh" => Op::Shading(name1(stack)?),
560        b"MP" => Op::MarkedContentPoint(name1(stack)?),
561        b"DP" => {
562            let (tag, props) = tag_props(stack)?;
563            Op::MarkedContentPointProps(tag, props)
564        }
565        b"BMC" => Op::BeginMarkedContent(name1(stack)?),
566        b"BDC" => {
567            let (tag, props) = tag_props(stack)?;
568            Op::BeginMarkedContentProps(tag, props)
569        }
570        b"EMC" => Op::EndMarkedContent,
571        b"BX" => Op::BeginCompat,
572        b"EX" => Op::EndCompat,
573        _ => return None,
574    })
575}
576
577/// `N` numbers starting at `start` (for operators whose numeric operands
578/// are not last on the stack).
579fn nums_at<const N: usize>(stack: &[Object], start: usize) -> Option<[f32; N]> {
580    let slice = stack.get(start..start + N)?;
581    let mut out = [0.0f32; N];
582    for (slot, o) in out.iter_mut().zip(slice) {
583        *slot = num(o)?;
584    }
585    Some(out)
586}
587
588/// Operands of `SCN`/`scn`: numeric components optionally followed by a
589/// pattern name.
590fn color_n(stack: &[Object]) -> Option<(Vec<f32>, Option<Name>)> {
591    match stack.last() {
592        Some(Object::Name(n)) => {
593            let comps = all_nums(&stack[..stack.len() - 1])?;
594            Some((comps, Some(n.clone())))
595        }
596        _ => Some((all_nums(stack)?, None)),
597    }
598}
599
600/// Operands of `DP`/`BDC`: a tag name and a properties value (an inline
601/// dictionary or a resource name).
602fn tag_props(stack: &[Object]) -> Option<(Name, Object)> {
603    if stack.len() < 2 {
604        return None;
605    }
606    let props = match &stack[stack.len() - 1] {
607        o @ (Object::Dict(_) | Object::Name(_)) => o.clone(),
608        _ => return None,
609    };
610    let Object::Name(tag) = &stack[stack.len() - 2] else {
611        return None;
612    };
613    Some((tag.clone(), props))
614}
615
616/// Parses an inline image; the `BI` keyword has already been consumed.
617/// Returns `None` (image skipped) when the stream ends before `ID` or no
618/// `EI` terminator can be located.
619fn parse_inline_image(lexer: &mut Lexer) -> Option<Op> {
620    let mut dict = Dict::new();
621    loop {
622        match lexer.next_token().ok()? {
623            Token::Keyword(kw) if kw == b"ID" => break,
624            Token::Eof => return None,
625            Token::Name(key) => {
626                let tok = lexer.next_token().ok()?;
627                if matches!(tok, Token::Eof) {
628                    return None;
629                }
630                let Some(value) = compose_value(lexer, tok, 0).ok()? else {
631                    continue;
632                };
633                let canon = expand_image_key(&key.0);
634                let value = if canon == "ColorSpace" {
635                    expand_colorspace(value)
636                } else {
637                    value
638                };
639                dict.insert(Name(canon.to_string()), value);
640            }
641            // Malformed entry: skip the stray token.
642            _ => {}
643        }
644    }
645    let bytes = lexer.data();
646    let mut start = lexer.pos();
647    // Exactly one whitespace byte separates `ID` from the image data.
648    if bytes.get(start).is_some_and(|&b| is_whitespace(b)) {
649        start += 1;
650    }
651    let declared = dict.get_int("Length").or_else(|| dict.get_int("L"));
652    let data = if let Some(n) = declared.and_then(|n| usize::try_from(n).ok()) {
653        // A declared length is trusted.
654        let end = (start + n).min(bytes.len());
655        lexer.seek(end);
656        bytes[start..end].to_vec()
657    } else {
658        let Some(ei) = find_ei(bytes, start) else {
659            // No terminator anywhere: drop the image and stop lexing what
660            // can only be binary garbage.
661            lexer.seek(bytes.len());
662            return None;
663        };
664        // The whitespace byte before `EI` is a separator, not data.
665        let mut end = ei;
666        if end > start && is_whitespace(bytes[end - 1]) {
667            end -= 1;
668        }
669        lexer.seek(ei);
670        bytes[start..end].to_vec()
671    };
672    consume_ei(lexer);
673    Some(Op::InlineImage(ImageParams { dict, data }))
674}
675
676/// Finds the offset of an `EI` terminator at a token boundary: preceded
677/// by whitespace (or the data start) and followed by a non-regular byte
678/// or end of input.
679fn find_ei(bytes: &[u8], start: usize) -> Option<usize> {
680    let mut from = start;
681    while let Some(off) = memchr::memchr(b'E', &bytes[from..]) {
682        let p = from + off;
683        from = p + 1;
684        if bytes.get(p + 1) != Some(&b'I') {
685            continue;
686        }
687        let before_ok = p == start || is_whitespace(bytes[p - 1]);
688        let after_ok = match bytes.get(p + 2) {
689            None => true,
690            Some(&b) => !crate::lexer::is_regular(b),
691        };
692        if before_ok && after_ok {
693            return Some(p);
694        }
695    }
696    None
697}
698
699/// Consumes the `EI` keyword after inline image data, scanning forward
700/// leniently if the very next token is something else.
701fn consume_ei(lexer: &mut Lexer) {
702    let save = lexer.pos();
703    if let Ok(Token::Keyword(kw)) = lexer.next_token() {
704        if kw == b"EI" {
705            return;
706        }
707    }
708    match find_ei(lexer.data(), save) {
709        Some(p) => lexer.seek(p + 2),
710        None => lexer.seek(lexer.data().len()),
711    }
712}
713
714/// Canonical spelling of an inline image dictionary key (ISO 32000
715/// §8.9.7, Table 91).
716fn expand_image_key(key: &str) -> &str {
717    match key {
718        "BPC" => "BitsPerComponent",
719        "CS" => "ColorSpace",
720        "D" => "Decode",
721        "DP" => "DecodeParms",
722        "F" => "Filter",
723        "H" => "Height",
724        "W" => "Width",
725        "IM" => "ImageMask",
726        "I" => "Interpolate",
727        other => other,
728    }
729}
730
731/// Expands colorspace name abbreviations inside a `/CS` value, including
732/// names nested in an `Indexed` array.
733fn expand_colorspace(value: Object) -> Object {
734    match value {
735        Object::Name(Name(n)) => Object::Name(Name(match n.as_str() {
736            "G" => "DeviceGray".to_string(),
737            "RGB" => "DeviceRGB".to_string(),
738            "CMYK" => "DeviceCMYK".to_string(),
739            "I" => "Indexed".to_string(),
740            _ => n,
741        })),
742        Object::Array(items) => Object::Array(items.into_iter().map(expand_colorspace).collect()),
743        other => other,
744    }
745}
746
747/// Turns a leading token into an [`Object`], recursing for containers
748/// (bounded by `MAX_NESTING_DEPTH`). Returns `None` for tokens that
749/// carry no value (stray delimiters, unexpected keywords).
750fn compose_value(lexer: &mut Lexer, tok: Token, depth: usize) -> Result<Option<Object>> {
751    Ok(match tok {
752        Token::Int(i) => Some(Object::Int(i)),
753        Token::Real(r) => Some(Object::Real(r)),
754        Token::Name(n) => Some(Object::Name(n)),
755        Token::LitString(s) | Token::HexString(s) => Some(Object::String(s)),
756        Token::ArrayOpen => Some(parse_array(lexer, depth + 1)?),
757        Token::DictOpen => Some(Object::Dict(parse_dict(lexer, depth + 1)?)),
758        Token::Keyword(kw) => match kw.as_slice() {
759            b"true" => Some(Object::Bool(true)),
760            b"false" => Some(Object::Bool(false)),
761            b"null" => Some(Object::Null),
762            _ => None,
763        },
764        _ => None,
765    })
766}
767
768#[cfg(test)]
769mod tests {
770    use super::*;
771
772    fn ops(src: &[u8]) -> Vec<Op> {
773        parse_content(src).expect("content parses")
774    }
775
776    fn name(s: &str) -> Name {
777        Name(s.to_string())
778    }
779
780    #[test]
781    fn shapes_fixture_parses_to_expected_ops() {
782        let path = concat!(
783            env!("CARGO_MANIFEST_DIR"),
784            "/../../tests/fixtures/shapes.pdf"
785        );
786        let doc = crate::document::Document::open(path).expect("open shapes.pdf");
787        let page = doc.page(0).expect("page 0");
788        let content = page.content(&doc).expect("page content");
789        let got = ops(&content);
790        let m = |a, b, c, d, e, f| Matrix { a, b, c, d, e, f };
791        assert_eq!(
792            got,
793            vec![
794                Op::SetFillRGB(1.0, 0.0, 0.0),
795                Op::Rect(72.0, 600.0, 100.0, 80.0),
796                Op::Fill,
797                Op::SetFillRGB(0.0, 0.5, 1.0),
798                Op::Rect(200.0, 600.0, 120.0, 60.0),
799                Op::Fill,
800                Op::SetFillRGB(0.2, 0.8, 0.2),
801                Op::Rect(340.0, 590.0, 90.0, 90.0),
802                Op::Fill,
803                Op::SetStrokeRGB(0.0, 0.0, 0.0),
804                Op::SetLineWidth(2.0),
805                Op::MoveTo(100.0, 300.0),
806                Op::CurveTo(150.0, 400.0, 250.0, 400.0, 300.0, 300.0),
807                Op::Stroke,
808                Op::Save,
809                Op::Concat(m(0.5, 0.0, 0.0, 0.5, 300.0, 100.0)),
810                Op::SetFillRGB(0.8, 0.0, 0.8),
811                Op::Rect(0.0, 0.0, 200.0, 200.0),
812                Op::Fill,
813                Op::Restore,
814            ]
815        );
816    }
817
818    #[test]
819    fn graphics_state_ops_round_trip() {
820        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 \
821                        /Perceptual ri 1 i /GS1 gs");
822        assert_eq!(
823            got,
824            vec![
825                Op::Save,
826                Op::Restore,
827                Op::Concat(Matrix {
828                    a: 1.0,
829                    b: 0.0,
830                    c: 0.0,
831                    d: 1.0,
832                    e: 10.0,
833                    f: 20.0
834                }),
835                Op::SetLineWidth(2.0),
836                Op::SetLineCap(1),
837                Op::SetLineJoin(2),
838                Op::SetMiterLimit(3.5),
839                Op::SetDash(vec![3.0, 1.0], 0.5),
840                Op::SetRenderingIntent(name("Perceptual")),
841                Op::SetFlatness(1.0),
842                Op::SetExtGState(name("GS1")),
843            ]
844        );
845    }
846
847    #[test]
848    fn empty_dash_array_round_trips() {
849        assert_eq!(ops(b"[] 0 d"), vec![Op::SetDash(vec![], 0.0)]);
850    }
851
852    #[test]
853    fn path_ops_round_trip() {
854        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 \
855                        72 600 100 80 re");
856        assert_eq!(
857            got,
858            vec![
859                Op::MoveTo(10.0, 20.0),
860                Op::LineTo(30.0, 40.0),
861                Op::CurveTo(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
862                Op::CurveToV(1.0, 2.0, 3.0, 4.0),
863                Op::CurveToY(5.0, 6.0, 7.0, 8.0),
864                Op::ClosePath,
865                Op::Rect(72.0, 600.0, 100.0, 80.0),
866            ]
867        );
868    }
869
870    #[test]
871    fn negative_and_real_coordinates_coerce() {
872        assert_eq!(
873            ops(b"-1.5 +2 m .5 -3 l"),
874            vec![Op::MoveTo(-1.5, 2.0), Op::LineTo(0.5, -3.0)]
875        );
876    }
877
878    #[test]
879    fn xobject_shading_marked_content_round_trip() {
880        let got = ops(b"/Im1 Do /Sh1 sh /Tag MP /Tag /P DP /Span BMC \
881                        /Span << /MCID 3 >> BDC EMC BX EX");
882        let mut props = Dict::new();
883        props.insert(name("MCID"), Object::Int(3));
884        assert_eq!(
885            got,
886            vec![
887                Op::XObject(name("Im1")),
888                Op::Shading(name("Sh1")),
889                Op::MarkedContentPoint(name("Tag")),
890                Op::MarkedContentPointProps(name("Tag"), Object::Name(name("P"))),
891                Op::BeginMarkedContent(name("Span")),
892                Op::BeginMarkedContentProps(name("Span"), Object::Dict(props)),
893                Op::EndMarkedContent,
894                Op::BeginCompat,
895                Op::EndCompat,
896            ]
897        );
898    }
899
900    #[test]
901    fn unknown_operator_is_skipped_without_breaking_following_ops() {
902        assert_eq!(
903            ops(b"zz 1 2 10 20 m 30 40 l"),
904            vec![Op::MoveTo(10.0, 20.0), Op::LineTo(30.0, 40.0)]
905        );
906        // Unknown operator with operands before it: operands are dropped.
907        assert_eq!(ops(b"1 2 zz 3 4 l"), vec![Op::LineTo(3.0, 4.0)]);
908    }
909
910    #[test]
911    fn malformed_operands_are_skipped() {
912        // Too few operands.
913        assert_eq!(ops(b"1 m 5 6 l"), vec![Op::LineTo(5.0, 6.0)]);
914        // Wrong operand types.
915        assert_eq!(ops(b"(a) (b) m S"), vec![Op::Stroke]);
916        assert_eq!(ops(b"/N w"), Vec::<Op>::new());
917        assert_eq!(ops(b"5 Tf"), Vec::<Op>::new());
918        // `TJ` whose operand is not an array.
919        assert_eq!(ops(b"(x) TJ q"), vec![Op::Save]);
920        // Non-string, non-number entries inside a `TJ` array are dropped.
921        assert_eq!(
922            ops(b"[(y) /Bad 5] TJ"),
923            vec![Op::ShowTextAdjusted(vec![
924                TextItem::Str(b"y".to_vec()),
925                TextItem::Offset(5.0),
926            ])]
927        );
928        // Dash without an array operand.
929        assert_eq!(ops(b"3 0 d"), Vec::<Op>::new());
930        // Extra operands: the trailing ones are used.
931        assert_eq!(ops(b"9 9 1 2 m"), vec![Op::MoveTo(1.0, 2.0)]);
932    }
933
934    #[test]
935    fn inline_image_with_hex_data_ending_in_ei() {
936        let got = ops(b"q BI /W 2 /H 2 /BPC 8 /CS /G /F /AHx ID 00FF80FF> EI Q");
937        assert_eq!(got.len(), 3);
938        assert_eq!(got[0], Op::Save);
939        assert_eq!(got[2], Op::Restore);
940        let Op::InlineImage(img) = &got[1] else {
941            panic!("expected inline image, got {:?}", got[1]);
942        };
943        assert_eq!(img.data, b"00FF80FF>");
944        assert_eq!(img.dict.get_int("Width"), Some(2));
945        assert_eq!(img.dict.get_int("Height"), Some(2));
946        assert_eq!(img.dict.get_int("BitsPerComponent"), Some(8));
947        assert_eq!(img.dict.get_name("ColorSpace"), Some(&name("DeviceGray")));
948        assert_eq!(img.dict.get_name("Filter"), Some(&name("AHx")));
949    }
950
951    #[test]
952    fn inline_image_trusts_declared_length() {
953        // Binary payload contains a spurious ` EI ` sequence; /L must win.
954        let mut src = b"BI /W 3 /H 1 /BPC 8 /CS /RGB /L 9 ID ".to_vec();
955        src.extend_from_slice(b"ab EI wxy");
956        src.extend_from_slice(b" EI 1 2 m");
957        let got = ops(&src);
958        assert_eq!(got.len(), 2);
959        let Op::InlineImage(img) = &got[0] else {
960            panic!("expected inline image, got {:?}", got[0]);
961        };
962        assert_eq!(img.data, b"ab EI wxy");
963        assert_eq!(img.dict.get_name("ColorSpace"), Some(&name("DeviceRGB")));
964        assert_eq!(got[1], Op::MoveTo(1.0, 2.0));
965    }
966
967    #[test]
968    fn inline_image_expands_indexed_colorspace_array() {
969        let got = ops(
970            b"BI /W 1 /H 1 /BPC 8 /CS [/I /RGB 1 <FF0000>] /IM false /I true \
971                        ID \xde\xad EI",
972        );
973        let Op::InlineImage(img) = &got[0] else {
974            panic!("expected inline image, got {:?}", got[0]);
975        };
976        assert_eq!(img.data, b"\xde\xad");
977        assert_eq!(img.dict.get("ImageMask"), Some(&Object::Bool(false)));
978        assert_eq!(img.dict.get("Interpolate"), Some(&Object::Bool(true)));
979        let cs = img.dict.get_array("ColorSpace").expect("CS array");
980        assert_eq!(cs[0], Object::Name(name("Indexed")));
981        assert_eq!(cs[1], Object::Name(name("DeviceRGB")));
982        assert_eq!(cs[2], Object::Int(1));
983        assert_eq!(cs[3], Object::String(b"\xff\x00\x00".to_vec()));
984    }
985
986    #[test]
987    fn inline_image_without_terminator_is_skipped() {
988        assert_eq!(ops(b"BI /W 1 ID \x01\x02\x03"), Vec::<Op>::new());
989        assert_eq!(ops(b"BI /W 1"), Vec::<Op>::new());
990    }
991
992    #[test]
993    fn inline_image_with_empty_data() {
994        let got = ops(b"BI /W 0 /H 0 ID  EI n");
995        assert_eq!(got.len(), 2);
996        let Op::InlineImage(img) = &got[0] else {
997            panic!("expected inline image, got {:?}", got[0]);
998        };
999        assert!(img.data.is_empty());
1000        assert_eq!(got[1], Op::EndPath);
1001    }
1002
1003    #[test]
1004    fn stray_delimiters_and_comments_are_tolerated() {
1005        assert_eq!(
1006            ops(b"% comment\n1 2 m ] >> 3 4 l"),
1007            vec![Op::MoveTo(1.0, 2.0), Op::LineTo(3.0, 4.0)]
1008        );
1009        assert_eq!(ops(b""), Vec::<Op>::new());
1010        assert_eq!(ops(b"   \n  "), Vec::<Op>::new());
1011    }
1012
1013    #[test]
1014    fn text_state_ops_round_trip() {
1015        let got = ops(b"BT 0.5 Tc 1 Tw 90 Tz 14 TL /F1 12 Tf 3 Tr 4.5 Ts ET");
1016        assert_eq!(
1017            got,
1018            vec![
1019                Op::BeginText,
1020                Op::SetCharSpacing(0.5),
1021                Op::SetWordSpacing(1.0),
1022                Op::SetHorizScaling(90.0),
1023                Op::SetLeading(14.0),
1024                Op::SetFont(name("F1"), 12.0),
1025                Op::SetTextRender(3),
1026                Op::SetTextRise(4.5),
1027                Op::EndText,
1028            ]
1029        );
1030    }
1031
1032    #[test]
1033    fn text_positioning_and_showing_round_trip() {
1034        let got = ops(b"BT 72 720 Td 0 -14 TD 1 0 0 1 50 60 Tm T* \
1035                        (Hi) Tj (there) ' 2 3 (spaced) \" ET");
1036        assert_eq!(
1037            got,
1038            vec![
1039                Op::BeginText,
1040                Op::TextMove(72.0, 720.0),
1041                Op::TextMoveSetLeading(0.0, -14.0),
1042                Op::SetTextMatrix(Matrix {
1043                    a: 1.0,
1044                    b: 0.0,
1045                    c: 0.0,
1046                    d: 1.0,
1047                    e: 50.0,
1048                    f: 60.0
1049                }),
1050                Op::TextNextLine,
1051                Op::ShowText(b"Hi".to_vec()),
1052                Op::NextLineShowText(b"there".to_vec()),
1053                Op::NextLineShowTextSpaced(2.0, 3.0, b"spaced".to_vec()),
1054                Op::EndText,
1055            ]
1056        );
1057    }
1058
1059    #[test]
1060    fn parses_d0_glyph_width() {
1061        let ops = parse_content(b"1000 0 d0").expect("parse");
1062        assert_eq!(ops, vec![Op::SetGlyphWidth(1000.0, 0.0)]);
1063    }
1064
1065    #[test]
1066    fn parses_d1_glyph_width_bbox() {
1067        let ops = parse_content(b"1000 0 0 0 750 700 d1").expect("parse");
1068        assert_eq!(
1069            ops,
1070            vec![Op::SetGlyphWidthBBox(1000.0, 0.0, 0.0, 0.0, 750.0, 700.0)]
1071        );
1072    }
1073
1074    #[test]
1075    fn d0_with_wrong_arity_is_skipped() {
1076        // Too few operands: leniently skipped (like any arity mismatch), not a panic.
1077        let ops = parse_content(b"1000 d0").expect("parse");
1078        assert!(ops.is_empty());
1079    }
1080
1081    #[test]
1082    fn tj_array_mixes_strings_and_numbers() {
1083        let got = ops(b"[(He) -120 (llo) 33.5 <20>] TJ");
1084        assert_eq!(
1085            got,
1086            vec![Op::ShowTextAdjusted(vec![
1087                TextItem::Str(b"He".to_vec()),
1088                TextItem::Offset(-120.0),
1089                TextItem::Str(b"llo".to_vec()),
1090                TextItem::Offset(33.5),
1091                TextItem::Str(b" ".to_vec()),
1092            ])]
1093        );
1094    }
1095
1096    #[test]
1097    fn color_ops_round_trip() {
1098        let got = ops(b"/DeviceRGB CS /DeviceGray cs 1 0 0 SC 0.5 sc \
1099                        0.3 G 0.7 g 1 0 0 RG 0 1 0 rg 0 0 0 1 K 1 0 0 0 k");
1100        assert_eq!(
1101            got,
1102            vec![
1103                Op::SetStrokeColorSpace(name("DeviceRGB")),
1104                Op::SetFillColorSpace(name("DeviceGray")),
1105                Op::SetStrokeColor(vec![1.0, 0.0, 0.0]),
1106                Op::SetFillColor(vec![0.5]),
1107                Op::SetStrokeGray(0.3),
1108                Op::SetFillGray(0.7),
1109                Op::SetStrokeRGB(1.0, 0.0, 0.0),
1110                Op::SetFillRGB(0.0, 1.0, 0.0),
1111                Op::SetStrokeCMYK(0.0, 0.0, 0.0, 1.0),
1112                Op::SetFillCMYK(1.0, 0.0, 0.0, 0.0),
1113            ]
1114        );
1115    }
1116
1117    #[test]
1118    fn scn_with_and_without_pattern_name() {
1119        let got = ops(b"0.2 0.4 0.6 scn /P1 scn 0.1 0.2 /P2 SCN 1 SCN");
1120        assert_eq!(
1121            got,
1122            vec![
1123                Op::SetFillColorN(vec![0.2, 0.4, 0.6], None),
1124                Op::SetFillColorN(vec![], Some(name("P1"))),
1125                Op::SetStrokeColorN(vec![0.1, 0.2], Some(name("P2"))),
1126                Op::SetStrokeColorN(vec![1.0], None),
1127            ]
1128        );
1129    }
1130
1131    #[test]
1132    fn painting_and_clipping_ops_round_trip() {
1133        let got = ops(b"S s f F f* B B* b b* n W W*");
1134        assert_eq!(
1135            got,
1136            vec![
1137                Op::Stroke,
1138                Op::CloseStroke,
1139                Op::Fill,
1140                Op::Fill,
1141                Op::FillEvenOdd,
1142                Op::FillStroke,
1143                Op::FillStrokeEvenOdd,
1144                Op::CloseFillStroke,
1145                Op::CloseFillStrokeEvenOdd,
1146                Op::EndPath,
1147                Op::ClipNonZero,
1148                Op::ClipEvenOdd,
1149            ]
1150        );
1151    }
1152
1153    /// Runs `f` on a deliberately small stack so that unbounded recursion
1154    /// would abort the test binary instead of silently passing on a large
1155    /// main-thread stack.
1156    fn on_small_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
1157        std::thread::Builder::new()
1158            .stack_size(512 * 1024)
1159            .spawn(f)
1160            .expect("spawn test thread")
1161            .join()
1162            .expect("content parser must not overflow the stack")
1163    }
1164
1165    #[test]
1166    fn deeply_nested_array_is_rejected_not_stack_overflow() {
1167        let data = vec![b'['; 50_000];
1168        let result = on_small_stack(move || parse_content(&data));
1169        assert!(matches!(result, Err(Error::Syntax { .. })));
1170    }
1171
1172    #[test]
1173    fn deeply_nested_dict_is_rejected_not_stack_overflow() {
1174        let mut data = Vec::new();
1175        for _ in 0..50_000 {
1176            data.extend_from_slice(b"<</K");
1177        }
1178        let result = on_small_stack(move || parse_content(&data));
1179        assert!(matches!(result, Err(Error::Syntax { .. })));
1180    }
1181
1182    #[test]
1183    fn nesting_within_the_limit_still_parses() {
1184        // A BDC property dict holding a modestly nested array operand.
1185        let mut data: Vec<u8> = b"/Tag <</Deep ".to_vec();
1186        data.extend(std::iter::repeat_n(b'[', 50));
1187        data.extend(std::iter::repeat_n(b']', 50));
1188        data.extend_from_slice(b" >> BDC");
1189        let got = ops(&data);
1190        assert_eq!(got.len(), 1);
1191        assert!(matches!(got[0], Op::BeginMarkedContentProps(_, _)));
1192    }
1193
1194    #[test]
1195    fn spanned_ops_cover_their_source_bytes() {
1196        let data = b"q 1 0 0 1 5 5 cm BT /F1 12 Tf (Hi) Tj ET Q";
1197        let spanned = parse_content_spanned(data).unwrap();
1198        let plain = parse_content(data).unwrap();
1199        assert_eq!(
1200            spanned
1201                .iter()
1202                .map(|pair| pair.0.clone())
1203                .collect::<Vec<_>>(),
1204            plain
1205        );
1206        for (op, span) in &spanned {
1207            assert!(span.start < span.end && span.end as usize <= data.len());
1208            // Re-parsing exactly the spanned bytes yields exactly this op.
1209            let slice = &data[span.start as usize..span.end as usize];
1210            let reparsed = parse_content(slice).unwrap();
1211            assert_eq!(reparsed.len(), 1, "span of {op:?} reparses to one op");
1212            assert_eq!(&reparsed[0], op);
1213        }
1214        // Spans are ordered and non-overlapping.
1215        for pair in spanned.windows(2) {
1216            assert!(pair[0].1.end <= pair[1].1.start);
1217        }
1218    }
1219
1220    #[test]
1221    fn unknown_operator_does_not_stretch_the_next_span() {
1222        // `zz` is unknown: its operands are dropped, and the following op's
1223        // span must start at `q`, not at `7`.
1224        let data = b"7 8 zz q";
1225        let spanned = parse_content_spanned(data).unwrap();
1226        assert_eq!(spanned.len(), 1);
1227        assert_eq!(spanned[0].0, Op::Save);
1228        assert_eq!(
1229            &data[spanned[0].1.start as usize..spanned[0].1.end as usize],
1230            b"q"
1231        );
1232    }
1233}