1use 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
11const MAX_NESTING_DEPTH: usize = 128;
17
18fn too_deep(lexer: &Lexer) -> Error {
20 Error::Syntax {
21 offset: lexer.pos(),
22 msg: "operand nesting too deep".into(),
23 }
24}
25
26#[derive(Debug, Clone, PartialEq)]
30pub struct ImageParams {
31 pub dict: Dict,
32 pub data: Vec<u8>,
33}
34
35#[derive(Debug, Clone, PartialEq)]
37pub enum TextItem {
38 Str(Vec<u8>),
40 Offset(f32),
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub enum Op {
47 Save,
50 Restore,
52 Concat(Matrix),
54 SetLineWidth(f32),
56 SetLineCap(i32),
58 SetLineJoin(i32),
60 SetMiterLimit(f32),
62 SetDash(Vec<f32>, f32),
64 SetRenderingIntent(Name),
66 SetFlatness(f32),
68 SetExtGState(Name),
70
71 MoveTo(f32, f32),
74 LineTo(f32, f32),
76 CurveTo(f32, f32, f32, f32, f32, f32),
78 CurveToV(f32, f32, f32, f32),
80 CurveToY(f32, f32, f32, f32),
82 ClosePath,
84 Rect(f32, f32, f32, f32),
86
87 Stroke,
90 CloseStroke,
92 Fill,
94 FillEvenOdd,
96 FillStroke,
98 FillStrokeEvenOdd,
100 CloseFillStroke,
102 CloseFillStrokeEvenOdd,
104 EndPath,
106 ClipNonZero,
108 ClipEvenOdd,
110
111 SetStrokeColorSpace(Name),
114 SetFillColorSpace(Name),
116 SetStrokeColor(Vec<f32>),
118 SetStrokeColorN(Vec<f32>, Option<Name>),
120 SetFillColor(Vec<f32>),
122 SetFillColorN(Vec<f32>, Option<Name>),
124 SetStrokeGray(f32),
126 SetFillGray(f32),
128 SetStrokeRGB(f32, f32, f32),
130 SetFillRGB(f32, f32, f32),
132 SetStrokeCMYK(f32, f32, f32, f32),
134 SetFillCMYK(f32, f32, f32, f32),
136
137 BeginText,
140 EndText,
142 SetCharSpacing(f32),
144 SetWordSpacing(f32),
146 SetHorizScaling(f32),
148 SetLeading(f32),
150 SetFont(Name, f32),
152 SetGlyphWidth(f32, f32),
155 SetGlyphWidthBBox(f32, f32, f32, f32, f32, f32),
160 SetTextRender(i32),
162 SetTextRise(f32),
164 TextMove(f32, f32),
166 TextMoveSetLeading(f32, f32),
168 SetTextMatrix(Matrix),
170 TextNextLine,
172 ShowText(Vec<u8>),
174 ShowTextAdjusted(Vec<TextItem>),
176 NextLineShowText(Vec<u8>),
178 NextLineShowTextSpaced(f32, f32, Vec<u8>),
180
181 XObject(Name),
184 InlineImage(ImageParams),
186 Shading(Name),
188 MarkedContentPoint(Name),
190 MarkedContentPointProps(Name, Object),
192 BeginMarkedContent(Name),
194 BeginMarkedContentProps(Name, Object),
196 EndMarkedContent,
198 BeginCompat,
200 EndCompat,
202}
203
204pub 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
216pub 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
225fn ops_estimate(len: usize) -> usize {
231 (len / 16).min(1 << 17)
232}
233
234fn 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
245pub struct ContentOps<'a> {
250 lexer: Lexer<'a>,
251 stack: Vec<Object>,
252 run_start: Option<usize>,
256}
257
258impl<'a> ContentOps<'a> {
259 pub fn new(data: &'a [u8]) -> ContentOps<'a> {
261 ContentOps::at(data, 0)
262 }
263
264 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 pub fn pos(&self) -> usize {
279 self.lexer.pos()
280 }
281
282 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 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
362fn 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
383fn 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
392fn 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
405fn 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
414fn 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
423fn 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
431fn all_nums(stack: &[Object]) -> Option<Vec<f32>> {
433 stack.iter().map(num).collect()
434}
435
436fn 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
442fn 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 break;
457 }
458 if let Some(v) = compose_raw_value(lexer, raw, depth)? {
459 dict.insert(key, v);
460 }
461 }
462 _ => {}
464 }
465 }
466 Ok(dict)
467}
468
469fn dispatch(kw: &[u8], stack: &mut [Object]) -> Option<Op> {
480 Some(match kw {
481 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 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 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
548fn dispatch_color_text(kw: &[u8], stack: &mut [Object]) -> Option<Op> {
552 Some(match kw {
553 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 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
649fn 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
674fn 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
685fn 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
698fn 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
715fn 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 _ => {}
744 }
745 }
746 let bytes = lexer.data();
747 let mut start = lexer.pos();
748 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 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 lexer.seek(bytes.len());
763 return None;
764 };
765 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
777fn 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
800fn 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
815fn 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
834fn 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
850fn 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
866fn 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 #[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 #[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 #[test]
970 fn empty_dash_array_round_trips() {
971 assert_eq!(ops(b"[] 0 d"), vec![Op::SetDash(vec![], 0.0)]);
972 }
973
974 #[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 #[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 #[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 assert_eq!(ops(b"1 2 zz 3 4 l"), vec![Op::LineTo(3.0, 4.0)]);
1033 }
1034
1035 #[test]
1037 fn malformed_operands_are_skipped() {
1038 assert_eq!(ops(b"1 m 5 6 l"), vec![Op::LineTo(5.0, 6.0)]);
1040 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 assert_eq!(ops(b"(x) TJ q"), vec![Op::Save]);
1046 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 assert_eq!(ops(b"3 0 d"), Vec::<Op>::new());
1056 assert_eq!(ops(b"9 9 1 2 m"), vec![Op::MoveTo(1.0, 2.0)]);
1058 }
1059
1060 #[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 #[test]
1080 fn inline_image_trusts_declared_length() {
1081 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 #[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 #[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 #[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 #[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 #[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 let ops = parse_content(b"1000 d0").expect("parse");
1211 assert!(ops.is_empty());
1212 }
1213
1214 #[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 #[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 #[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 #[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 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 #[test]
1321 fn nesting_within_the_limit_still_parses() {
1322 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 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 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 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}