1use 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
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 Ok(parse_content_spanned(data)?
210 .into_iter()
211 .map(|pair| pair.0)
212 .collect())
213}
214
215pub 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 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 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
276fn 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
297fn 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
306fn 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
319fn 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
328fn name1(stack: &[Object]) -> Option<Name> {
330 match stack.last()? {
331 Object::Name(n) => Some(n.clone()),
332 _ => None,
333 }
334}
335
336fn str1(stack: &[Object]) -> Option<Vec<u8>> {
338 match stack.last()? {
339 Object::String(s) => Some(s.clone()),
340 _ => None,
341 }
342}
343
344fn all_nums(stack: &[Object]) -> Option<Vec<f32>> {
346 stack.iter().map(num).collect()
347}
348
349fn 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
355fn 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 break;
370 }
371 if let Some(v) = compose_value(lexer, tok, depth)? {
372 dict.insert(key, v);
373 }
374 }
375 _ => {}
377 }
378 }
379 Ok(dict)
380}
381
382fn dispatch(kw: &[u8], stack: &[Object]) -> Option<Op> {
386 Some(match kw {
387 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 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 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
454fn dispatch_color_text(kw: &[u8], stack: &[Object]) -> Option<Op> {
456 Some(match kw {
457 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 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
554fn 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
577fn 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
588fn 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
600fn 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
616fn 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 _ => {}
643 }
644 }
645 let bytes = lexer.data();
646 let mut start = lexer.pos();
647 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 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 lexer.seek(bytes.len());
662 return None;
663 };
664 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
676fn 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
699fn 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
714fn 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
731fn 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
747fn 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 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 assert_eq!(ops(b"1 m 5 6 l"), vec![Op::LineTo(5.0, 6.0)]);
914 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 assert_eq!(ops(b"(x) TJ q"), vec![Op::Save]);
920 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 assert_eq!(ops(b"3 0 d"), Vec::<Op>::new());
930 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 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 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 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 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 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 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 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}