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>> {
209 let mut ops = Vec::with_capacity(ops_estimate(data.len()));
210 parse_content_ops(data, |op, _| ops.push(op))?;
211 Ok(ops)
212}
213
214pub fn parse_content_spanned(data: &[u8]) -> Result<Vec<(Op, Span)>> {
218 let mut ops = Vec::with_capacity(ops_estimate(data.len()));
219 parse_content_ops(data, |op, span| ops.push((op, span)))?;
220 Ok(ops)
221}
222
223fn ops_estimate(len: usize) -> usize {
229 (len / 16).min(1 << 17)
230}
231
232fn parse_content_ops(data: &[u8], mut emit: impl FnMut(Op, Span)) -> Result<()> {
236 let mut ops = ContentOps::new(data);
237 while let Some((op, span)) = ops.next_op()? {
238 emit(op, span);
239 }
240 Ok(())
241}
242
243pub struct ContentOps<'a> {
248 lexer: Lexer<'a>,
249 stack: Vec<Object>,
250 run_start: Option<usize>,
254}
255
256impl<'a> ContentOps<'a> {
257 pub fn new(data: &'a [u8]) -> ContentOps<'a> {
259 ContentOps::at(data, 0)
260 }
261
262 pub fn at(data: &'a [u8], pos: usize) -> ContentOps<'a> {
268 ContentOps {
269 lexer: Lexer::at(data, pos),
270 stack: Vec::new(),
271 run_start: None,
272 }
273 }
274
275 pub fn pos(&self) -> usize {
277 self.lexer.pos()
278 }
279
280 pub fn next_op(&mut self) -> Result<Option<(Op, Span)>> {
283 loop {
284 let (token_start, raw) = self.lexer.next_raw_token_spanned()?;
285 let kw = match raw {
286 RawToken::Keyword(kw) => {
287 if self.run_start.is_none() {
288 self.run_start = Some(token_start);
289 }
290 kw
291 }
292 RawToken::Hex(span) => {
293 if self.run_start.is_none() {
294 self.run_start = Some(token_start);
295 }
296 self.stack.push(Object::String(decode_hex(span)));
297 continue;
298 }
299 RawToken::Owned(token) => {
300 if !matches!(token, Token::Eof) && self.run_start.is_none() {
301 self.run_start = Some(token_start);
302 }
303 match token {
304 Token::Eof => return Ok(None),
305 Token::Int(i) => self.stack.push(Object::Int(i)),
306 Token::Real(r) => self.stack.push(Object::Real(r)),
307 Token::Name(n) => self.stack.push(Object::Name(n)),
308 Token::LitString(s) | Token::HexString(s) => {
309 self.stack.push(Object::String(s))
310 }
311 Token::ArrayOpen => {
312 let a = parse_array(&mut self.lexer, 0)?;
313 self.stack.push(a);
314 }
315 Token::DictOpen => {
316 let d = parse_dict(&mut self.lexer, 0)?;
317 self.stack.push(Object::Dict(d));
318 }
319 Token::ArrayClose | Token::DictClose => {
321 self.stack.clear();
322 self.run_start = None;
323 }
324 Token::Keyword(_) => {
325 unreachable!("next_raw_token yields keywords borrowed")
326 }
327 }
328 continue;
329 }
330 };
331 match kw {
332 b"true" => self.stack.push(Object::Bool(true)),
333 b"false" => self.stack.push(Object::Bool(false)),
334 b"null" => self.stack.push(Object::Null),
335 b"BI" => {
336 let start = self.run_start.take().unwrap_or(token_start);
337 let image = parse_inline_image(&mut self.lexer);
338 self.stack.clear();
339 if let Some(op) = image {
340 let span = Span::new(start as u64, self.lexer.pos() as u64);
341 return Ok(Some((op, span)));
342 }
343 }
344 _ => {
345 let start = self.run_start.take().unwrap_or(token_start);
346 let op = dispatch(kw, &mut self.stack);
347 self.stack.clear();
348 if let Some(op) = op {
349 let span = Span::new(start as u64, self.lexer.pos() as u64);
350 return Ok(Some((op, span)));
351 }
352 }
353 }
354 }
355 }
356}
357
358fn parse_array(lexer: &mut Lexer, depth: usize) -> Result<Object> {
362 if depth >= MAX_NESTING_DEPTH {
363 return Err(too_deep(lexer));
364 }
365 let mut items = Vec::new();
366 loop {
367 match lexer.next_raw_token()? {
368 RawToken::Owned(Token::ArrayClose | Token::Eof) => break,
369 raw => {
370 if let Some(v) = compose_raw_value(lexer, raw, depth)? {
371 items.push(v);
372 }
373 }
374 }
375 }
376 Ok(Object::Array(items))
377}
378
379fn num(o: &Object) -> Option<f32> {
381 match o {
382 Object::Int(i) => Some(*i as f32),
383 Object::Real(r) => Some(*r as f32),
384 _ => None,
385 }
386}
387
388fn nums<const N: usize>(stack: &[Object]) -> Option<[f32; N]> {
390 if stack.len() < N {
391 return None;
392 }
393 let tail = &stack[stack.len() - N..];
394 let mut out = [0.0f32; N];
395 for (slot, o) in out.iter_mut().zip(tail) {
396 *slot = num(o)?;
397 }
398 Some(out)
399}
400
401fn int1(stack: &[Object]) -> Option<i32> {
403 match stack.last()? {
404 Object::Int(i) => Some(*i as i32),
405 Object::Real(r) => Some(*r as i32),
406 _ => None,
407 }
408}
409
410fn name1(stack: &mut [Object]) -> Option<Name> {
413 match stack.last_mut()? {
414 Object::Name(n) => Some(Name(std::mem::take(&mut n.0))),
415 _ => None,
416 }
417}
418
419fn str1(stack: &mut [Object]) -> Option<Vec<u8>> {
421 match stack.last_mut()? {
422 Object::String(s) => Some(std::mem::take(s)),
423 _ => None,
424 }
425}
426
427fn all_nums(stack: &[Object]) -> Option<Vec<f32>> {
429 stack.iter().map(num).collect()
430}
431
432fn matrix(stack: &[Object]) -> Option<Matrix> {
434 let [a, b, c, d, e, f] = nums::<6>(stack)?;
435 Some(Matrix { a, b, c, d, e, f })
436}
437
438fn parse_dict(lexer: &mut Lexer, depth: usize) -> Result<Dict> {
441 if depth >= MAX_NESTING_DEPTH {
442 return Err(too_deep(lexer));
443 }
444 let mut dict = Dict::new();
445 loop {
446 match lexer.next_raw_token()? {
447 RawToken::Owned(Token::DictClose | Token::Eof) => break,
448 RawToken::Owned(Token::Name(key)) => {
449 let raw = lexer.next_raw_token()?;
450 if matches!(raw, RawToken::Owned(Token::DictClose | Token::Eof)) {
451 break;
453 }
454 if let Some(v) = compose_raw_value(lexer, raw, depth)? {
455 dict.insert(key, v);
456 }
457 }
458 _ => {}
460 }
461 }
462 Ok(dict)
463}
464
465fn dispatch(kw: &[u8], stack: &mut [Object]) -> Option<Op> {
474 Some(match kw {
475 b"q" => Op::Save,
477 b"Q" => Op::Restore,
478 b"cm" => Op::Concat(matrix(stack)?),
479 b"w" => Op::SetLineWidth(nums::<1>(stack)?[0]),
480 b"J" => Op::SetLineCap(int1(stack)?),
481 b"j" => Op::SetLineJoin(int1(stack)?),
482 b"M" => Op::SetMiterLimit(nums::<1>(stack)?[0]),
483 b"d" => {
484 if stack.len() < 2 {
485 return None;
486 }
487 let phase = num(&stack[stack.len() - 1])?;
488 let Object::Array(items) = &stack[stack.len() - 2] else {
489 return None;
490 };
491 let dashes: Vec<f32> = items.iter().map(num).collect::<Option<_>>()?;
492 Op::SetDash(dashes, phase)
493 }
494 b"ri" => Op::SetRenderingIntent(name1(stack)?),
495 b"i" => Op::SetFlatness(nums::<1>(stack)?[0]),
496 b"gs" => Op::SetExtGState(name1(stack)?),
497
498 b"m" => {
500 let [x, y] = nums(stack)?;
501 Op::MoveTo(x, y)
502 }
503 b"l" => {
504 let [x, y] = nums(stack)?;
505 Op::LineTo(x, y)
506 }
507 b"c" => {
508 let [x1, y1, x2, y2, x3, y3] = nums(stack)?;
509 Op::CurveTo(x1, y1, x2, y2, x3, y3)
510 }
511 b"v" => {
512 let [x2, y2, x3, y3] = nums(stack)?;
513 Op::CurveToV(x2, y2, x3, y3)
514 }
515 b"y" => {
516 let [x1, y1, x3, y3] = nums(stack)?;
517 Op::CurveToY(x1, y1, x3, y3)
518 }
519 b"h" => Op::ClosePath,
520 b"re" => {
521 let [x, y, w, h] = nums(stack)?;
522 Op::Rect(x, y, w, h)
523 }
524
525 b"S" => Op::Stroke,
527 b"s" => Op::CloseStroke,
528 b"f" | b"F" => Op::Fill,
529 b"f*" => Op::FillEvenOdd,
530 b"B" => Op::FillStroke,
531 b"B*" => Op::FillStrokeEvenOdd,
532 b"b" => Op::CloseFillStroke,
533 b"b*" => Op::CloseFillStrokeEvenOdd,
534 b"n" => Op::EndPath,
535 b"W" => Op::ClipNonZero,
536 b"W*" => Op::ClipEvenOdd,
537
538 _ => return dispatch_color_text(kw, stack),
539 })
540}
541
542fn dispatch_color_text(kw: &[u8], stack: &mut [Object]) -> Option<Op> {
544 Some(match kw {
545 b"CS" => Op::SetStrokeColorSpace(name1(stack)?),
547 b"cs" => Op::SetFillColorSpace(name1(stack)?),
548 b"SC" => Op::SetStrokeColor(all_nums(stack)?),
549 b"sc" => Op::SetFillColor(all_nums(stack)?),
550 b"SCN" => {
551 let (comps, pattern) = color_n(stack)?;
552 Op::SetStrokeColorN(comps, pattern)
553 }
554 b"scn" => {
555 let (comps, pattern) = color_n(stack)?;
556 Op::SetFillColorN(comps, pattern)
557 }
558 b"G" => Op::SetStrokeGray(nums::<1>(stack)?[0]),
559 b"g" => Op::SetFillGray(nums::<1>(stack)?[0]),
560 b"RG" => {
561 let [r, g, b] = nums(stack)?;
562 Op::SetStrokeRGB(r, g, b)
563 }
564 b"rg" => {
565 let [r, g, b] = nums(stack)?;
566 Op::SetFillRGB(r, g, b)
567 }
568 b"K" => {
569 let [c, m, y, k] = nums(stack)?;
570 Op::SetStrokeCMYK(c, m, y, k)
571 }
572 b"k" => {
573 let [c, m, y, k] = nums(stack)?;
574 Op::SetFillCMYK(c, m, y, k)
575 }
576
577 b"BT" => Op::BeginText,
579 b"ET" => Op::EndText,
580 b"Tc" => Op::SetCharSpacing(nums::<1>(stack)?[0]),
581 b"Tw" => Op::SetWordSpacing(nums::<1>(stack)?[0]),
582 b"Tz" => Op::SetHorizScaling(nums::<1>(stack)?[0]),
583 b"TL" => Op::SetLeading(nums::<1>(stack)?[0]),
584 b"Tf" => {
585 if stack.len() < 2 {
586 return None;
587 }
588 let size = num(&stack[stack.len() - 1])?;
589 let at = stack.len() - 2;
590 let Object::Name(font) = &mut stack[at] else {
591 return None;
592 };
593 Op::SetFont(Name(std::mem::take(&mut font.0)), size)
594 }
595 b"d0" => {
596 let [wx, wy] = nums::<2>(stack)?;
597 Op::SetGlyphWidth(wx, wy)
598 }
599 b"d1" => {
600 let [wx, wy, llx, lly, urx, ury] = nums::<6>(stack)?;
601 Op::SetGlyphWidthBBox(wx, wy, llx, lly, urx, ury)
602 }
603 b"Tr" => Op::SetTextRender(int1(stack)?),
604 b"Ts" => Op::SetTextRise(nums::<1>(stack)?[0]),
605 b"Td" => {
606 let [tx, ty] = nums(stack)?;
607 Op::TextMove(tx, ty)
608 }
609 b"TD" => {
610 let [tx, ty] = nums(stack)?;
611 Op::TextMoveSetLeading(tx, ty)
612 }
613 b"Tm" => Op::SetTextMatrix(matrix(stack)?),
614 b"T*" => Op::TextNextLine,
615 b"Tj" => Op::ShowText(str1(stack)?),
616 b"TJ" => {
617 let Object::Array(items) = stack.last_mut()? else {
618 return None;
619 };
620 let mut adjusted = Vec::with_capacity(items.len());
621 adjusted.extend(items.iter_mut().filter_map(|o| match o {
622 Object::String(s) => Some(TextItem::Str(std::mem::take(s))),
623 other => num(other).map(TextItem::Offset),
624 }));
625 Op::ShowTextAdjusted(adjusted)
626 }
627 b"'" => Op::NextLineShowText(str1(stack)?),
628 b"\"" => {
629 if stack.len() < 3 {
630 return None;
631 }
632 let s = str1(stack)?;
633 let [aw, ac] = nums_at::<2>(stack, stack.len() - 3)?;
634 Op::NextLineShowTextSpaced(aw, ac, s)
635 }
636
637 _ => return dispatch_misc(kw, stack),
638 })
639}
640
641fn dispatch_misc(kw: &[u8], stack: &mut [Object]) -> Option<Op> {
644 Some(match kw {
645 b"Do" => Op::XObject(name1(stack)?),
646 b"sh" => Op::Shading(name1(stack)?),
647 b"MP" => Op::MarkedContentPoint(name1(stack)?),
648 b"DP" => {
649 let (tag, props) = tag_props(stack)?;
650 Op::MarkedContentPointProps(tag, props)
651 }
652 b"BMC" => Op::BeginMarkedContent(name1(stack)?),
653 b"BDC" => {
654 let (tag, props) = tag_props(stack)?;
655 Op::BeginMarkedContentProps(tag, props)
656 }
657 b"EMC" => Op::EndMarkedContent,
658 b"BX" => Op::BeginCompat,
659 b"EX" => Op::EndCompat,
660 _ => return None,
661 })
662}
663
664fn nums_at<const N: usize>(stack: &[Object], start: usize) -> Option<[f32; N]> {
667 let slice = stack.get(start..start + N)?;
668 let mut out = [0.0f32; N];
669 for (slot, o) in out.iter_mut().zip(slice) {
670 *slot = num(o)?;
671 }
672 Some(out)
673}
674
675fn color_n(stack: &mut [Object]) -> Option<(Vec<f32>, Option<Name>)> {
678 match stack.last_mut() {
679 Some(Object::Name(n)) => {
680 let pattern = Name(std::mem::take(&mut n.0));
681 let comps = all_nums(&stack[..stack.len() - 1])?;
682 Some((comps, Some(pattern)))
683 }
684 _ => Some((all_nums(stack)?, None)),
685 }
686}
687
688fn tag_props(stack: &mut [Object]) -> Option<(Name, Object)> {
691 if stack.len() < 2 {
692 return None;
693 }
694 let last = stack.len() - 1;
695 let props = match &stack[last] {
696 Object::Dict(_) | Object::Name(_) => std::mem::replace(&mut stack[last], Object::Null),
697 _ => return None,
698 };
699 let Object::Name(tag) = &mut stack[last - 1] else {
700 return None;
701 };
702 Some((Name(std::mem::take(&mut tag.0)), props))
703}
704
705fn parse_inline_image(lexer: &mut Lexer) -> Option<Op> {
709 let mut dict = Dict::new();
710 loop {
711 match lexer.next_token().ok()? {
712 Token::Keyword(kw) if kw == b"ID" => break,
713 Token::Eof => return None,
714 Token::Name(key) => {
715 let tok = lexer.next_token().ok()?;
716 if matches!(tok, Token::Eof) {
717 return None;
718 }
719 let Some(value) = compose_value(lexer, tok, 0).ok()? else {
720 continue;
721 };
722 let canon = expand_image_key(&key.0);
723 let value = if canon == "ColorSpace" {
724 expand_colorspace(value)
725 } else {
726 value
727 };
728 dict.insert(Name(canon.to_string()), value);
729 }
730 _ => {}
732 }
733 }
734 let bytes = lexer.data();
735 let mut start = lexer.pos();
736 if bytes.get(start).is_some_and(|&b| is_whitespace(b)) {
738 start += 1;
739 }
740 let declared = dict.get_int("Length").or_else(|| dict.get_int("L"));
741 let data = if let Some(n) = declared.and_then(|n| usize::try_from(n).ok()) {
742 let end = (start + n).min(bytes.len());
744 lexer.seek(end);
745 bytes[start..end].to_vec()
746 } else {
747 let Some(ei) = find_ei(bytes, start) else {
748 lexer.seek(bytes.len());
751 return None;
752 };
753 let mut end = ei;
755 if end > start && is_whitespace(bytes[end - 1]) {
756 end -= 1;
757 }
758 lexer.seek(ei);
759 bytes[start..end].to_vec()
760 };
761 consume_ei(lexer);
762 Some(Op::InlineImage(ImageParams { dict, data }))
763}
764
765fn find_ei(bytes: &[u8], start: usize) -> Option<usize> {
769 let mut from = start;
770 while let Some(off) = memchr::memchr(b'E', &bytes[from..]) {
771 let p = from + off;
772 from = p + 1;
773 if bytes.get(p + 1) != Some(&b'I') {
774 continue;
775 }
776 let before_ok = p == start || is_whitespace(bytes[p - 1]);
777 let after_ok = match bytes.get(p + 2) {
778 None => true,
779 Some(&b) => !crate::lexer::is_regular(b),
780 };
781 if before_ok && after_ok {
782 return Some(p);
783 }
784 }
785 None
786}
787
788fn consume_ei(lexer: &mut Lexer) {
791 let save = lexer.pos();
792 if let Ok(Token::Keyword(kw)) = lexer.next_token() {
793 if kw == b"EI" {
794 return;
795 }
796 }
797 match find_ei(lexer.data(), save) {
798 Some(p) => lexer.seek(p + 2),
799 None => lexer.seek(lexer.data().len()),
800 }
801}
802
803fn expand_image_key(key: &str) -> &str {
806 match key {
807 "BPC" => "BitsPerComponent",
808 "CS" => "ColorSpace",
809 "D" => "Decode",
810 "DP" => "DecodeParms",
811 "F" => "Filter",
812 "H" => "Height",
813 "W" => "Width",
814 "IM" => "ImageMask",
815 "I" => "Interpolate",
816 other => other,
817 }
818}
819
820fn expand_colorspace(value: Object) -> Object {
823 match value {
824 Object::Name(Name(n)) => Object::Name(Name(match n.as_str() {
825 "G" => "DeviceGray".to_string(),
826 "RGB" => "DeviceRGB".to_string(),
827 "CMYK" => "DeviceCMYK".to_string(),
828 "I" => "Indexed".to_string(),
829 _ => n,
830 })),
831 Object::Array(items) => Object::Array(items.into_iter().map(expand_colorspace).collect()),
832 other => other,
833 }
834}
835
836fn compose_raw_value(lexer: &mut Lexer, raw: RawToken, depth: usize) -> Result<Option<Object>> {
840 match raw {
841 RawToken::Owned(tok) => compose_value(lexer, tok, depth),
842 RawToken::Keyword(kw) => Ok(match kw {
843 b"true" => Some(Object::Bool(true)),
844 b"false" => Some(Object::Bool(false)),
845 b"null" => Some(Object::Null),
846 _ => None,
847 }),
848 RawToken::Hex(span) => Ok(Some(Object::String(decode_hex(span)))),
849 }
850}
851
852fn compose_value(lexer: &mut Lexer, tok: Token, depth: usize) -> Result<Option<Object>> {
856 Ok(match tok {
857 Token::Int(i) => Some(Object::Int(i)),
858 Token::Real(r) => Some(Object::Real(r)),
859 Token::Name(n) => Some(Object::Name(n)),
860 Token::LitString(s) | Token::HexString(s) => Some(Object::String(s)),
861 Token::ArrayOpen => Some(parse_array(lexer, depth + 1)?),
862 Token::DictOpen => Some(Object::Dict(parse_dict(lexer, depth + 1)?)),
863 Token::Keyword(kw) => match kw.as_slice() {
864 b"true" => Some(Object::Bool(true)),
865 b"false" => Some(Object::Bool(false)),
866 b"null" => Some(Object::Null),
867 _ => None,
868 },
869 _ => None,
870 })
871}
872
873#[cfg(test)]
874mod tests {
875 use super::*;
876
877 fn ops(src: &[u8]) -> Vec<Op> {
878 parse_content(src).expect("content parses")
879 }
880
881 fn name(s: &str) -> Name {
882 Name(s.to_string())
883 }
884
885 #[test]
886 fn shapes_fixture_parses_to_expected_ops() {
887 let path = concat!(
888 env!("CARGO_MANIFEST_DIR"),
889 "/../../tests/fixtures/shapes.pdf"
890 );
891 let doc = crate::document::Document::open(path).expect("open shapes.pdf");
892 let page = doc.page(0).expect("page 0");
893 let content = page.content(&doc).expect("page content");
894 let got = ops(&content);
895 let m = |a, b, c, d, e, f| Matrix { a, b, c, d, e, f };
896 assert_eq!(
897 got,
898 vec![
899 Op::SetFillRGB(1.0, 0.0, 0.0),
900 Op::Rect(72.0, 600.0, 100.0, 80.0),
901 Op::Fill,
902 Op::SetFillRGB(0.0, 0.5, 1.0),
903 Op::Rect(200.0, 600.0, 120.0, 60.0),
904 Op::Fill,
905 Op::SetFillRGB(0.2, 0.8, 0.2),
906 Op::Rect(340.0, 590.0, 90.0, 90.0),
907 Op::Fill,
908 Op::SetStrokeRGB(0.0, 0.0, 0.0),
909 Op::SetLineWidth(2.0),
910 Op::MoveTo(100.0, 300.0),
911 Op::CurveTo(150.0, 400.0, 250.0, 400.0, 300.0, 300.0),
912 Op::Stroke,
913 Op::Save,
914 Op::Concat(m(0.5, 0.0, 0.0, 0.5, 300.0, 100.0)),
915 Op::SetFillRGB(0.8, 0.0, 0.8),
916 Op::Rect(0.0, 0.0, 200.0, 200.0),
917 Op::Fill,
918 Op::Restore,
919 ]
920 );
921 }
922
923 #[test]
924 fn graphics_state_ops_round_trip() {
925 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 \
926 /Perceptual ri 1 i /GS1 gs");
927 assert_eq!(
928 got,
929 vec![
930 Op::Save,
931 Op::Restore,
932 Op::Concat(Matrix {
933 a: 1.0,
934 b: 0.0,
935 c: 0.0,
936 d: 1.0,
937 e: 10.0,
938 f: 20.0
939 }),
940 Op::SetLineWidth(2.0),
941 Op::SetLineCap(1),
942 Op::SetLineJoin(2),
943 Op::SetMiterLimit(3.5),
944 Op::SetDash(vec![3.0, 1.0], 0.5),
945 Op::SetRenderingIntent(name("Perceptual")),
946 Op::SetFlatness(1.0),
947 Op::SetExtGState(name("GS1")),
948 ]
949 );
950 }
951
952 #[test]
953 fn empty_dash_array_round_trips() {
954 assert_eq!(ops(b"[] 0 d"), vec![Op::SetDash(vec![], 0.0)]);
955 }
956
957 #[test]
958 fn path_ops_round_trip() {
959 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 \
960 72 600 100 80 re");
961 assert_eq!(
962 got,
963 vec![
964 Op::MoveTo(10.0, 20.0),
965 Op::LineTo(30.0, 40.0),
966 Op::CurveTo(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
967 Op::CurveToV(1.0, 2.0, 3.0, 4.0),
968 Op::CurveToY(5.0, 6.0, 7.0, 8.0),
969 Op::ClosePath,
970 Op::Rect(72.0, 600.0, 100.0, 80.0),
971 ]
972 );
973 }
974
975 #[test]
976 fn negative_and_real_coordinates_coerce() {
977 assert_eq!(
978 ops(b"-1.5 +2 m .5 -3 l"),
979 vec![Op::MoveTo(-1.5, 2.0), Op::LineTo(0.5, -3.0)]
980 );
981 }
982
983 #[test]
984 fn xobject_shading_marked_content_round_trip() {
985 let got = ops(b"/Im1 Do /Sh1 sh /Tag MP /Tag /P DP /Span BMC \
986 /Span << /MCID 3 >> BDC EMC BX EX");
987 let mut props = Dict::new();
988 props.insert(name("MCID"), Object::Int(3));
989 assert_eq!(
990 got,
991 vec![
992 Op::XObject(name("Im1")),
993 Op::Shading(name("Sh1")),
994 Op::MarkedContentPoint(name("Tag")),
995 Op::MarkedContentPointProps(name("Tag"), Object::Name(name("P"))),
996 Op::BeginMarkedContent(name("Span")),
997 Op::BeginMarkedContentProps(name("Span"), Object::Dict(props)),
998 Op::EndMarkedContent,
999 Op::BeginCompat,
1000 Op::EndCompat,
1001 ]
1002 );
1003 }
1004
1005 #[test]
1006 fn unknown_operator_is_skipped_without_breaking_following_ops() {
1007 assert_eq!(
1008 ops(b"zz 1 2 10 20 m 30 40 l"),
1009 vec![Op::MoveTo(10.0, 20.0), Op::LineTo(30.0, 40.0)]
1010 );
1011 assert_eq!(ops(b"1 2 zz 3 4 l"), vec![Op::LineTo(3.0, 4.0)]);
1013 }
1014
1015 #[test]
1016 fn malformed_operands_are_skipped() {
1017 assert_eq!(ops(b"1 m 5 6 l"), vec![Op::LineTo(5.0, 6.0)]);
1019 assert_eq!(ops(b"(a) (b) m S"), vec![Op::Stroke]);
1021 assert_eq!(ops(b"/N w"), Vec::<Op>::new());
1022 assert_eq!(ops(b"5 Tf"), Vec::<Op>::new());
1023 assert_eq!(ops(b"(x) TJ q"), vec![Op::Save]);
1025 assert_eq!(
1027 ops(b"[(y) /Bad 5] TJ"),
1028 vec![Op::ShowTextAdjusted(vec![
1029 TextItem::Str(b"y".to_vec()),
1030 TextItem::Offset(5.0),
1031 ])]
1032 );
1033 assert_eq!(ops(b"3 0 d"), Vec::<Op>::new());
1035 assert_eq!(ops(b"9 9 1 2 m"), vec![Op::MoveTo(1.0, 2.0)]);
1037 }
1038
1039 #[test]
1040 fn inline_image_with_hex_data_ending_in_ei() {
1041 let got = ops(b"q BI /W 2 /H 2 /BPC 8 /CS /G /F /AHx ID 00FF80FF> EI Q");
1042 assert_eq!(got.len(), 3);
1043 assert_eq!(got[0], Op::Save);
1044 assert_eq!(got[2], Op::Restore);
1045 let Op::InlineImage(img) = &got[1] else {
1046 panic!("expected inline image, got {:?}", got[1]);
1047 };
1048 assert_eq!(img.data, b"00FF80FF>");
1049 assert_eq!(img.dict.get_int("Width"), Some(2));
1050 assert_eq!(img.dict.get_int("Height"), Some(2));
1051 assert_eq!(img.dict.get_int("BitsPerComponent"), Some(8));
1052 assert_eq!(img.dict.get_name("ColorSpace"), Some(&name("DeviceGray")));
1053 assert_eq!(img.dict.get_name("Filter"), Some(&name("AHx")));
1054 }
1055
1056 #[test]
1057 fn inline_image_trusts_declared_length() {
1058 let mut src = b"BI /W 3 /H 1 /BPC 8 /CS /RGB /L 9 ID ".to_vec();
1060 src.extend_from_slice(b"ab EI wxy");
1061 src.extend_from_slice(b" EI 1 2 m");
1062 let got = ops(&src);
1063 assert_eq!(got.len(), 2);
1064 let Op::InlineImage(img) = &got[0] else {
1065 panic!("expected inline image, got {:?}", got[0]);
1066 };
1067 assert_eq!(img.data, b"ab EI wxy");
1068 assert_eq!(img.dict.get_name("ColorSpace"), Some(&name("DeviceRGB")));
1069 assert_eq!(got[1], Op::MoveTo(1.0, 2.0));
1070 }
1071
1072 #[test]
1073 fn inline_image_expands_indexed_colorspace_array() {
1074 let got = ops(
1075 b"BI /W 1 /H 1 /BPC 8 /CS [/I /RGB 1 <FF0000>] /IM false /I true \
1076 ID \xde\xad EI",
1077 );
1078 let Op::InlineImage(img) = &got[0] else {
1079 panic!("expected inline image, got {:?}", got[0]);
1080 };
1081 assert_eq!(img.data, b"\xde\xad");
1082 assert_eq!(img.dict.get("ImageMask"), Some(&Object::Bool(false)));
1083 assert_eq!(img.dict.get("Interpolate"), Some(&Object::Bool(true)));
1084 let cs = img.dict.get_array("ColorSpace").expect("CS array");
1085 assert_eq!(cs[0], Object::Name(name("Indexed")));
1086 assert_eq!(cs[1], Object::Name(name("DeviceRGB")));
1087 assert_eq!(cs[2], Object::Int(1));
1088 assert_eq!(cs[3], Object::String(b"\xff\x00\x00".to_vec()));
1089 }
1090
1091 #[test]
1092 fn inline_image_without_terminator_is_skipped() {
1093 assert_eq!(ops(b"BI /W 1 ID \x01\x02\x03"), Vec::<Op>::new());
1094 assert_eq!(ops(b"BI /W 1"), Vec::<Op>::new());
1095 }
1096
1097 #[test]
1098 fn inline_image_with_empty_data() {
1099 let got = ops(b"BI /W 0 /H 0 ID EI n");
1100 assert_eq!(got.len(), 2);
1101 let Op::InlineImage(img) = &got[0] else {
1102 panic!("expected inline image, got {:?}", got[0]);
1103 };
1104 assert!(img.data.is_empty());
1105 assert_eq!(got[1], Op::EndPath);
1106 }
1107
1108 #[test]
1109 fn stray_delimiters_and_comments_are_tolerated() {
1110 assert_eq!(
1111 ops(b"% comment\n1 2 m ] >> 3 4 l"),
1112 vec![Op::MoveTo(1.0, 2.0), Op::LineTo(3.0, 4.0)]
1113 );
1114 assert_eq!(ops(b""), Vec::<Op>::new());
1115 assert_eq!(ops(b" \n "), Vec::<Op>::new());
1116 }
1117
1118 #[test]
1119 fn text_state_ops_round_trip() {
1120 let got = ops(b"BT 0.5 Tc 1 Tw 90 Tz 14 TL /F1 12 Tf 3 Tr 4.5 Ts ET");
1121 assert_eq!(
1122 got,
1123 vec![
1124 Op::BeginText,
1125 Op::SetCharSpacing(0.5),
1126 Op::SetWordSpacing(1.0),
1127 Op::SetHorizScaling(90.0),
1128 Op::SetLeading(14.0),
1129 Op::SetFont(name("F1"), 12.0),
1130 Op::SetTextRender(3),
1131 Op::SetTextRise(4.5),
1132 Op::EndText,
1133 ]
1134 );
1135 }
1136
1137 #[test]
1138 fn text_positioning_and_showing_round_trip() {
1139 let got = ops(b"BT 72 720 Td 0 -14 TD 1 0 0 1 50 60 Tm T* \
1140 (Hi) Tj (there) ' 2 3 (spaced) \" ET");
1141 assert_eq!(
1142 got,
1143 vec![
1144 Op::BeginText,
1145 Op::TextMove(72.0, 720.0),
1146 Op::TextMoveSetLeading(0.0, -14.0),
1147 Op::SetTextMatrix(Matrix {
1148 a: 1.0,
1149 b: 0.0,
1150 c: 0.0,
1151 d: 1.0,
1152 e: 50.0,
1153 f: 60.0
1154 }),
1155 Op::TextNextLine,
1156 Op::ShowText(b"Hi".to_vec()),
1157 Op::NextLineShowText(b"there".to_vec()),
1158 Op::NextLineShowTextSpaced(2.0, 3.0, b"spaced".to_vec()),
1159 Op::EndText,
1160 ]
1161 );
1162 }
1163
1164 #[test]
1165 fn parses_d0_glyph_width() {
1166 let ops = parse_content(b"1000 0 d0").expect("parse");
1167 assert_eq!(ops, vec![Op::SetGlyphWidth(1000.0, 0.0)]);
1168 }
1169
1170 #[test]
1171 fn parses_d1_glyph_width_bbox() {
1172 let ops = parse_content(b"1000 0 0 0 750 700 d1").expect("parse");
1173 assert_eq!(
1174 ops,
1175 vec![Op::SetGlyphWidthBBox(1000.0, 0.0, 0.0, 0.0, 750.0, 700.0)]
1176 );
1177 }
1178
1179 #[test]
1180 fn d0_with_wrong_arity_is_skipped() {
1181 let ops = parse_content(b"1000 d0").expect("parse");
1183 assert!(ops.is_empty());
1184 }
1185
1186 #[test]
1187 fn tj_array_mixes_strings_and_numbers() {
1188 let got = ops(b"[(He) -120 (llo) 33.5 <20>] TJ");
1189 assert_eq!(
1190 got,
1191 vec![Op::ShowTextAdjusted(vec![
1192 TextItem::Str(b"He".to_vec()),
1193 TextItem::Offset(-120.0),
1194 TextItem::Str(b"llo".to_vec()),
1195 TextItem::Offset(33.5),
1196 TextItem::Str(b" ".to_vec()),
1197 ])]
1198 );
1199 }
1200
1201 #[test]
1202 fn color_ops_round_trip() {
1203 let got = ops(b"/DeviceRGB CS /DeviceGray cs 1 0 0 SC 0.5 sc \
1204 0.3 G 0.7 g 1 0 0 RG 0 1 0 rg 0 0 0 1 K 1 0 0 0 k");
1205 assert_eq!(
1206 got,
1207 vec![
1208 Op::SetStrokeColorSpace(name("DeviceRGB")),
1209 Op::SetFillColorSpace(name("DeviceGray")),
1210 Op::SetStrokeColor(vec![1.0, 0.0, 0.0]),
1211 Op::SetFillColor(vec![0.5]),
1212 Op::SetStrokeGray(0.3),
1213 Op::SetFillGray(0.7),
1214 Op::SetStrokeRGB(1.0, 0.0, 0.0),
1215 Op::SetFillRGB(0.0, 1.0, 0.0),
1216 Op::SetStrokeCMYK(0.0, 0.0, 0.0, 1.0),
1217 Op::SetFillCMYK(1.0, 0.0, 0.0, 0.0),
1218 ]
1219 );
1220 }
1221
1222 #[test]
1223 fn scn_with_and_without_pattern_name() {
1224 let got = ops(b"0.2 0.4 0.6 scn /P1 scn 0.1 0.2 /P2 SCN 1 SCN");
1225 assert_eq!(
1226 got,
1227 vec![
1228 Op::SetFillColorN(vec![0.2, 0.4, 0.6], None),
1229 Op::SetFillColorN(vec![], Some(name("P1"))),
1230 Op::SetStrokeColorN(vec![0.1, 0.2], Some(name("P2"))),
1231 Op::SetStrokeColorN(vec![1.0], None),
1232 ]
1233 );
1234 }
1235
1236 #[test]
1237 fn painting_and_clipping_ops_round_trip() {
1238 let got = ops(b"S s f F f* B B* b b* n W W*");
1239 assert_eq!(
1240 got,
1241 vec![
1242 Op::Stroke,
1243 Op::CloseStroke,
1244 Op::Fill,
1245 Op::Fill,
1246 Op::FillEvenOdd,
1247 Op::FillStroke,
1248 Op::FillStrokeEvenOdd,
1249 Op::CloseFillStroke,
1250 Op::CloseFillStrokeEvenOdd,
1251 Op::EndPath,
1252 Op::ClipNonZero,
1253 Op::ClipEvenOdd,
1254 ]
1255 );
1256 }
1257
1258 fn on_small_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
1262 std::thread::Builder::new()
1263 .stack_size(512 * 1024)
1264 .spawn(f)
1265 .expect("spawn test thread")
1266 .join()
1267 .expect("content parser must not overflow the stack")
1268 }
1269
1270 #[test]
1271 fn deeply_nested_array_is_rejected_not_stack_overflow() {
1272 let data = vec![b'['; 50_000];
1273 let result = on_small_stack(move || parse_content(&data));
1274 assert!(matches!(result, Err(Error::Syntax { .. })));
1275 }
1276
1277 #[test]
1278 fn deeply_nested_dict_is_rejected_not_stack_overflow() {
1279 let mut data = Vec::new();
1280 for _ in 0..50_000 {
1281 data.extend_from_slice(b"<</K");
1282 }
1283 let result = on_small_stack(move || parse_content(&data));
1284 assert!(matches!(result, Err(Error::Syntax { .. })));
1285 }
1286
1287 #[test]
1288 fn nesting_within_the_limit_still_parses() {
1289 let mut data: Vec<u8> = b"/Tag <</Deep ".to_vec();
1291 data.extend(std::iter::repeat_n(b'[', 50));
1292 data.extend(std::iter::repeat_n(b']', 50));
1293 data.extend_from_slice(b" >> BDC");
1294 let got = ops(&data);
1295 assert_eq!(got.len(), 1);
1296 assert!(matches!(got[0], Op::BeginMarkedContentProps(_, _)));
1297 }
1298
1299 #[test]
1300 fn spanned_ops_cover_their_source_bytes() {
1301 let data = b"q 1 0 0 1 5 5 cm BT /F1 12 Tf (Hi) Tj ET Q";
1302 let spanned = parse_content_spanned(data).unwrap();
1303 let plain = parse_content(data).unwrap();
1304 assert_eq!(
1305 spanned
1306 .iter()
1307 .map(|pair| pair.0.clone())
1308 .collect::<Vec<_>>(),
1309 plain
1310 );
1311 for (op, span) in &spanned {
1312 assert!(span.start < span.end && span.end as usize <= data.len());
1313 let slice = &data[span.start as usize..span.end as usize];
1315 let reparsed = parse_content(slice).unwrap();
1316 assert_eq!(reparsed.len(), 1, "span of {op:?} reparses to one op");
1317 assert_eq!(&reparsed[0], op);
1318 }
1319 for pair in spanned.windows(2) {
1321 assert!(pair[0].1.end <= pair[1].1.start);
1322 }
1323 }
1324
1325 #[test]
1326 fn unknown_operator_does_not_stretch_the_next_span() {
1327 let data = b"7 8 zz q";
1330 let spanned = parse_content_spanned(data).unwrap();
1331 assert_eq!(spanned.len(), 1);
1332 assert_eq!(spanned[0].0, Op::Save);
1333 assert_eq!(
1334 &data[spanned[0].1.start as usize..spanned[0].1.end as usize],
1335 b"q"
1336 );
1337 }
1338}