Skip to main content

hayro_syntax/content/
mod.rs

1/*!
2PDF content operators.
3
4This module provides facilities to read and interpret PDF content streams using
5high-level types.
6
7```
8use hayro_syntax::object::Number;
9use hayro_syntax::content::*;
10use hayro_syntax::content::ops::*;
11
12let content_stream = b"1 0 0 -1 0 200 cm
130 1.0 0 rg
140 0 m
15200 0 l
16200 200 l
170 200 l
18h
19f";
20
21let mut iter = TypedIter::new(content_stream);
22assert!(matches!(iter.next(), Some(TypedInstruction::Transform(_))));
23assert!(matches!(iter.next(), Some(TypedInstruction::NonStrokeColorDeviceRgb(_))));
24assert!(matches!(iter.next(), Some(TypedInstruction::MoveTo(_))));
25assert!(matches!(iter.next(), Some(TypedInstruction::LineTo(_))));
26assert!(matches!(iter.next(), Some(TypedInstruction::LineTo(_))));
27assert!(matches!(iter.next(), Some(TypedInstruction::LineTo(_))));
28assert!(matches!(iter.next(), Some(TypedInstruction::ClosePath(_))));
29assert!(matches!(iter.next(), Some(TypedInstruction::FillPathNonZero(_))));
30```
31*/
32
33#[allow(missing_docs)]
34pub mod ops;
35
36use crate::content::ops::TypedInstruction;
37use crate::object;
38use crate::object::dict::InlineImageDict;
39use crate::object::name::{Name, skip_name_like};
40use crate::object::{Array, Number, Object, Stream};
41use crate::reader::Reader;
42use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable};
43use crate::trivia::{is_regular_character, is_white_space_character};
44use crate::util::find_needle;
45use alloc::vec::Vec;
46use core::fmt::{Debug, Display, Formatter};
47use core::ops::Deref;
48use smallvec::SmallVec;
49
50// `scn` with 32 DeviceN components and a pattern name.
51const OPERANDS_THRESHOLD: usize = 33;
52const INLINE_COLOR_COMPONENTS: usize = 4;
53
54impl Debug for Operator<'_> {
55    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
56        write!(f, "{}", self.0.as_str())
57    }
58}
59
60impl Display for Operator<'_> {
61    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
62        write!(f, "{}", self.0.as_str())
63    }
64}
65
66/// A content stream operator.
67#[derive(Clone, PartialEq)]
68pub struct Operator<'a>(Name<'a>);
69
70impl Deref for Operator<'_> {
71    type Target = [u8];
72
73    fn deref(&self) -> &Self::Target {
74        self.0.as_ref()
75    }
76}
77
78impl Skippable for Operator<'_> {
79    fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> {
80        skip_name_like(r, false).map(|_| ())
81    }
82}
83
84impl<'a> Readable<'a> for Operator<'a> {
85    fn read(r: &mut Reader<'a>, _: &ReaderContext<'a>) -> Option<Self> {
86        let start = r.offset();
87        let mut escaped = false;
88        while let Some(b) = r.peek_byte().filter(|b| is_regular_character(*b)) {
89            escaped |= b == b'#';
90            r.forward();
91        }
92
93        let data = r.range(start..r.offset())?;
94        if data.is_empty() {
95            return None;
96        }
97
98        let name = if escaped {
99            Name::new_escaped(data)?
100        } else {
101            Name::new_unescaped(data)
102        };
103
104        Some(Self(name))
105    }
106}
107
108/// An iterator over operators in the PDF content streams, providing raw access to the instructions.
109#[derive(Debug, Clone)]
110pub struct UntypedIter<'a> {
111    reader: Reader<'a>,
112    stack: Stack<'a>,
113    operator: Option<Operator<'a>>,
114}
115
116impl<'a> UntypedIter<'a> {
117    /// Create a new untyped iterator.
118    pub fn new(data: &'a [u8]) -> Self {
119        Self {
120            reader: Reader::new(data),
121            stack: Stack::new(),
122            operator: None,
123        }
124    }
125
126    /// Create a new empty untyped iterator.
127    pub fn empty() -> Self {
128        Self {
129            reader: Reader::new(&[]),
130            stack: Stack::new(),
131            operator: None,
132        }
133    }
134
135    /// Return the next instruction.
136    #[allow(clippy::should_implement_trait)]
137    pub fn next(&mut self) -> Option<Instruction<'_, 'a>> {
138        self.stack.clear();
139
140        let operator = read_instruction(&mut self.reader, &mut self.stack)?;
141        self.operator = Some(operator);
142        Some(Instruction {
143            operands: &self.stack,
144            operator: self.operator.as_ref().unwrap(),
145        })
146    }
147}
148
149fn read_instruction<'a>(r: &mut Reader<'a>, stack: &mut Stack<'a>) -> Option<Operator<'a>> {
150    // Keeps the read position in a register; must be written back before calling out.
151    let mut cur = r.clone();
152
153    loop {
154        cur.skip_white_spaces_and_comments();
155
156        let Some(first) = cur.peek_byte() else {
157            *r = cur;
158            return None;
159        };
160
161        if matches!(first, b'.' | b'+' | b'-' | b'0'..=b'9') {
162            let start = cur.offset();
163            match Number::read_simple(&mut cur) {
164                Some(number) => {
165                    if stack.push(Object::Number(number)).is_none() {
166                        *r = cur;
167                        return None;
168                    }
169                    continue;
170                }
171                None => cur.jump(start),
172            }
173        }
174
175        match first {
176            // I believe booleans/null never appear as an operator?
177            b'.' | b'+' | b'-' | b'0'..=b'9' | b'/' | b'[' | b'<' | b'(' => {
178                *r = cur;
179                read_operand(r, stack)?;
180                cur = r.clone();
181            }
182            _ => {
183                *r = cur;
184                let Some(operator) = r.read_without_context::<Operator<'a>>() else {
185                    r.jump_to_end();
186                    return None;
187                };
188
189                // Inline images need special casing...
190                if operator.as_ref() == b"BI" {
191                    read_inline_image(r, stack)?;
192                }
193
194                return Some(operator);
195            }
196        }
197    }
198}
199
200fn read_operand<'a>(r: &mut Reader<'a>, stack: &mut Stack<'a>) -> Option<()> {
201    // See issue 994. In all sane scenarios, if the next byte is a number
202    // it has to be an operand (a number). However, it's possible that
203    // the number is followed by a regular character, in which case it
204    // should behave more like an operator (even though there exists
205    // no operator that starts with a number). In order to preserve
206    // similar behavior to Acrobat and Chromium, we try to consume
207    // such an operator and then simply skip it.
208    if let Some(object) = r.read_without_context::<Object<'a>>() {
209        stack.push(object)
210    } else if r.read_without_context::<Operator<'a>>().is_some() {
211        stack.clear();
212        Some(())
213    } else {
214        None
215    }
216}
217
218fn read_inline_image<'a>(r: &mut Reader<'a>, stack: &mut Stack<'a>) -> Option<()> {
219    // The ID operator will already be consumed by this.
220    let inline_dict = r.read_without_context::<InlineImageDict<'_>>()?;
221    let dict = inline_dict.get_dict().clone();
222
223    // One whitespace after "ID".
224    r.read_white_space()?;
225
226    let stream_data = r.tail()?;
227    let start_offset = r.offset();
228
229    'outer: while let Some(pos) = find_needle(r.tail()?, b"EI") {
230        r.read_bytes(pos)?;
231
232        if r.peek_bytes(2) == Some(b"EI") {
233            // If the following character is not a whitespace character, then we are in a ASCII-85 stream.
234            if r.peek_bytes(3)
235                .is_some_and(|b| !is_white_space_character(b[2]))
236            {
237                r.read_bytes(3)?;
238
239                continue;
240            }
241
242            let end_offset = r.offset() - start_offset;
243            let image_data = &stream_data[..end_offset];
244
245            let stream = Stream::new(image_data, dict.clone());
246
247            // Note that there is a possibility that the encoded stream data
248            // contains the "EI" operator as part of the data, in which case we
249            // cannot confidently know whether we have hit the actual end of the
250            // stream. See also <https://github.com/pdf-association/pdf-issues/issues/543>
251            // PDF 2.0 does have a `/Length` attribute we can read, but since it's relatively
252            // new we don't bother trying to read it.
253            let tail = &r.tail()?[2..];
254            let mut find_reader = Reader::new(tail);
255            let mut binary = BinaryTracker::default();
256            let mut is_content = None;
257            let mut next_bi = find_needle(tail, b"BI");
258
259            while !find_reader.at_end() {
260                let offset = find_reader.offset();
261
262                if next_bi.is_some_and(|bi| bi < offset) {
263                    next_bi = find_needle(&tail[offset..], b"BI").map(|bi| offset + bi);
264                }
265
266                let next_ei = find_needle(&tail[offset..], b"EI").map(|ei| offset + ei);
267
268                let (next_pos, is_ei) = match (next_ei, next_bi) {
269                    (Some(ei), Some(bi)) if ei <= bi => (ei, true),
270                    (Some(_), Some(bi)) => (bi, false),
271                    (Some(ei), None) => (ei, true),
272                    (None, Some(bi)) => (bi, false),
273                    (None, None) => break,
274                };
275
276                find_reader.read_bytes(next_pos - offset)?;
277
278                if is_ei {
279                    // If there is any binary data in-between, we for sure
280                    // have not reached the end.
281                    if binary.found_before(tail, next_pos) {
282                        r.read_bytes(2)?;
283                        continue 'outer;
284                    }
285
286                    // Otherwise, the only possibility that we reached an
287                    // "EI", even though the previous one was valid, is
288                    // that it's part of a string in the content
289                    // stream that follows the inline image. Therefore,
290                    // it should be valid to interpret `tail` as a content
291                    // stream.
292                    if !*is_content.get_or_insert_with(|| starts_with_content(tail)) {
293                        // Seems like the data in-between is not a valid content
294                        // stream, so we are likely still within the image data.
295                        r.read_bytes(2)?;
296                        continue 'outer;
297                    }
298                } else {
299                    // Possibly another inline image, if so, the previously found "EI"
300                    // is indeed the end of data.
301                    let mut cloned = find_reader.clone();
302                    cloned.read_bytes(2)?;
303                    if cloned
304                        .read_without_context::<InlineImageDict<'_>>()
305                        .is_some()
306                    {
307                        break;
308                    }
309                }
310
311                find_reader.read_byte()?;
312            }
313
314            stack.push(Object::Stream(stream))?;
315
316            r.read_bytes(2)?;
317            r.skip_white_spaces();
318
319            break;
320        }
321    }
322
323    Some(())
324}
325
326fn starts_with_content(data: &[u8]) -> bool {
327    let mut iter = TypedIter::new(data);
328    let mut counter = 0;
329
330    while let Some(op) = iter.next() {
331        // If we have read more than 20 valid operators, it should be
332        // safe to assume that we are in a content stream, so abort
333        // early. The only situation where this could reasonably
334        // be violated is if we have 20 subsequent instances of
335        // q/Q in the image data, which seems very unlikely.
336        if counter >= 20 {
337            return true;
338        }
339
340        if matches!(
341            op,
342            TypedInstruction::NextLineAndShowText(_)
343                | TypedInstruction::ShowText(_)
344                | TypedInstruction::ShowTexts(_)
345                | TypedInstruction::ShowTextWithParameters(_)
346        ) {
347            return true;
348        }
349
350        counter += 1;
351    }
352
353    false
354}
355
356// Content streams only hold non-ASCII bytes within strings, so one outside
357// of them means that we are still within binary image data.
358#[derive(Default)]
359struct BinaryTracker {
360    pos: usize,
361    string_depth: usize,
362    escaped: bool,
363    found: bool,
364}
365
366impl BinaryTracker {
367    fn found_before(&mut self, data: &[u8], end: usize) -> bool {
368        if self.found || end <= self.pos {
369            return self.found;
370        }
371
372        for (i, &b) in data[self.pos..end].iter().enumerate() {
373            if self.string_depth > 0 {
374                if self.escaped {
375                    self.escaped = false;
376                } else if b == b'\\' {
377                    self.escaped = true;
378                } else if b == b'(' {
379                    self.string_depth += 1;
380                } else if b == b')' {
381                    self.string_depth -= 1;
382                }
383            } else if !b.is_ascii() {
384                self.found = true;
385                self.pos += i;
386                return true;
387            } else if b == b'(' {
388                self.string_depth = 1;
389            }
390        }
391
392        self.pos = end;
393        false
394    }
395}
396
397/// An iterator over PDF content streams that provide access to the instructions
398/// in a typed fashion.
399#[derive(Clone)]
400pub struct TypedIter<'a> {
401    untyped: UntypedIter<'a>,
402}
403
404impl<'a> TypedIter<'a> {
405    /// Create a new typed iterator.
406    pub fn new(data: &'a [u8]) -> Self {
407        Self {
408            untyped: UntypedIter::new(data),
409        }
410    }
411
412    pub(crate) fn from_untyped(untyped: UntypedIter<'a>) -> Self {
413        Self { untyped }
414    }
415
416    /// Return the next typed instruction.
417    #[allow(clippy::should_implement_trait)]
418    pub fn next(&mut self) -> Option<TypedInstruction<'_, 'a>> {
419        let op = self.untyped.next()?;
420        // TODO: Explore whether dispatching can be made more efficient.
421        match TypedInstruction::dispatch(&op) {
422            Some(op) => Some(op),
423            // In case this returns `None`, the content stream is invalid. In case a path-drawing
424            // operator was used, let's abort completely, otherwise we might end up drawing random stuff.
425            // However, for other operators it could be worth it to just skip it but keep attempting
426            // to read other content operators.
427            None => {
428                if [
429                    &b"m"[..],
430                    &b"l"[..],
431                    &b"c"[..],
432                    &b"v"[..],
433                    &b"y"[..],
434                    &b"h"[..],
435                    &b"re"[..],
436                ]
437                .contains(&op.operator.0.deref())
438                {
439                    None
440                } else {
441                    Some(TypedInstruction::Fallback(op.operator))
442                }
443            }
444        }
445    }
446}
447
448/// An instruction (= operator and its operands) in a content stream.
449#[derive(Debug)]
450pub struct Instruction<'b, 'a> {
451    /// The stack containing the operands.
452    pub operands: &'b Stack<'a>,
453    /// The actual operator.
454    pub operator: &'b Operator<'a>,
455}
456
457impl<'b, 'a> Instruction<'b, 'a> {
458    /// An iterator over the operands of the instruction.
459    pub fn operands(&self) -> OperandIterator<'b, 'a> {
460        OperandIterator::new(self.operands)
461    }
462}
463
464/// A stack holding the arguments of an operator.
465#[derive(Clone, PartialEq)]
466pub struct Stack<'a> {
467    // TODO: Explore using an object pool to avoid repeatedly
468    // allocating/deallocating objects.
469    data: Vec<Object<'a>>,
470}
471
472impl<'a> Default for Stack<'a> {
473    fn default() -> Self {
474        Self::new()
475    }
476}
477
478impl<'a> Stack<'a> {
479    /// Create a new, empty stack.
480    pub fn new() -> Self {
481        Self { data: Vec::new() }
482    }
483
484    fn push(&mut self, operand: Object<'a>) -> Option<()> {
485        if self.data.len() >= OPERANDS_THRESHOLD {
486            return None;
487        }
488
489        self.data.push(operand);
490        Some(())
491    }
492
493    fn clear(&mut self) {
494        self.data.clear();
495    }
496
497    fn len(&self) -> usize {
498        self.data.len()
499    }
500
501    fn as_slice(&self) -> &[Object<'a>] {
502        &self.data
503    }
504
505    fn get<'b, T>(&'b self, index: usize) -> Option<T>
506    where
507        T: Operand<'b, 'a>,
508    {
509        self.as_slice().get(index).and_then(T::from_object)
510    }
511
512    fn get_all<'b, T>(&'b self) -> Option<SmallVec<[T; INLINE_COLOR_COMPONENTS]>>
513    where
514        T: Operand<'b, 'a>,
515    {
516        let mut operands = SmallVec::new();
517
518        for op in self.as_slice() {
519            let converted = T::from_object(op)?;
520            operands.push(converted);
521        }
522
523        Some(operands)
524    }
525}
526
527impl Debug for Stack<'_> {
528    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
529        f.debug_list().entries(self.as_slice()).finish()
530    }
531}
532
533trait Operand<'b, 'a>: Sized {
534    fn from_object(object: &'b Object<'a>) -> Option<Self>;
535}
536
537impl<'b, 'a> Operand<'b, 'a> for Number {
538    fn from_object(object: &'b Object<'a>) -> Option<Self> {
539        match object {
540            Object::Number(n) => Some(*n),
541            _ => None,
542        }
543    }
544}
545
546impl<'b, 'a> Operand<'b, 'a> for &'b object::String<'a> {
547    fn from_object(object: &'b Object<'a>) -> Option<Self> {
548        match object {
549            Object::String(s) => Some(s),
550            _ => None,
551        }
552    }
553}
554
555impl<'b, 'a> Operand<'b, 'a> for &'b Name<'a> {
556    fn from_object(object: &'b Object<'a>) -> Option<Self> {
557        match object {
558            Object::Name(n) => Some(n),
559            _ => None,
560        }
561    }
562}
563
564impl<'b, 'a> Operand<'b, 'a> for &'b Array<'a> {
565    fn from_object(object: &'b Object<'a>) -> Option<Self> {
566        match object {
567            Object::Array(a) => Some(a),
568            _ => None,
569        }
570    }
571}
572
573impl<'b, 'a> Operand<'b, 'a> for &'b Stream<'a> {
574    fn from_object(object: &'b Object<'a>) -> Option<Self> {
575        match object {
576            Object::Stream(s) => Some(s),
577            _ => None,
578        }
579    }
580}
581
582impl<'b, 'a> Operand<'b, 'a> for &'b Object<'a> {
583    fn from_object(object: &'b Object<'a>) -> Option<Self> {
584        Some(object)
585    }
586}
587
588/// An iterator over the operands of an operator.
589pub struct OperandIterator<'b, 'a> {
590    stack: &'b Stack<'a>,
591    cur_index: usize,
592}
593
594impl<'b, 'a> OperandIterator<'b, 'a> {
595    fn new(stack: &'b Stack<'a>) -> Self {
596        Self {
597            stack,
598            cur_index: 0,
599        }
600    }
601}
602
603impl<'b, 'a> Iterator for OperandIterator<'b, 'a> {
604    type Item = &'b Object<'a>;
605
606    fn next(&mut self) -> Option<Self::Item> {
607        if let Some(item) = self.stack.as_slice().get(self.cur_index) {
608            self.cur_index += 1;
609
610            Some(item)
611        } else {
612            None
613        }
614    }
615}
616
617pub(crate) trait OperatorTrait<'b, 'a>: Sized {
618    fn from_stack(stack: &'b Stack<'a>) -> Option<Self>;
619}
620
621mod macros {
622    macro_rules! display_op {
623        ($t:ident $(<$($l:lifetime),+>)?;) => {
624            impl$(<$($l),+>)? core::fmt::Display for $t$(<$($l),+>)? {
625                fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
626                    f.write_str(stringify!($t))
627                }
628            }
629        };
630        ($t:ident $(<$($l:lifetime),+>)?; $first:tt $(, $field:tt)*) => {
631            impl$(<$($l),+>)? core::fmt::Display for $t$(<$($l),+>)? {
632                fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
633                    f.write_str(concat!(stringify!($t), "("))?;
634                    core::fmt::Display::fmt(&self.$first, f)?;
635                    $(
636                        f.write_str(" ")?;
637                        core::fmt::Display::fmt(&self.$field, f)?;
638                    )*
639                    f.write_str(")")
640                }
641            }
642        };
643    }
644
645    macro_rules! display_variadic_op {
646        ($t:ident $(<$($l:lifetime),+>)?) => {
647            impl$(<$($l),+>)? core::fmt::Display for $t$(<$($l),+>)? {
648                fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
649                    f.write_str(stringify!($t))?;
650                    if let Some((first, rest)) = self.0.split_first() {
651                        f.write_str("(")?;
652                        core::fmt::Display::fmt(first, f)?;
653                        for value in rest {
654                            f.write_str(" ")?;
655                            core::fmt::Display::fmt(value, f)?;
656                        }
657                        f.write_str(")")?;
658                    }
659                    Ok(())
660                }
661            }
662        };
663    }
664
665    macro_rules! op_impl {
666        ($t:ident $(<$($l:lifetime),+>)?, $e:expr, $n:expr, |$stack:ident : $stack_ty:ty| $body:block) => {
667            impl<'b, 'a> OperatorTrait<'b, 'a> for $t$(<$($l),+>)? {
668                #[inline(always)]
669                fn from_stack($stack: $stack_ty) -> Option<Self> {
670                    $body
671                }
672            }
673
674            impl<'b, 'a> From<$t$(<$($l),+>)?> for TypedInstruction<'b, 'a> {
675                fn from(value: $t$(<$($l),+>)?) -> Self {
676                    TypedInstruction::$t(value)
677                }
678            }
679
680            impl<'b, 'a> TryFrom<TypedInstruction<'b, 'a>> for $t$(<$($l),+>)? {
681                type Error = ();
682
683                fn try_from(value: TypedInstruction<'b, 'a>) -> core::result::Result<Self, Self::Error> {
684                    match value {
685                        TypedInstruction::$t(e) => Ok(e),
686                        _ => Err(())
687                    }
688                }
689            }
690        };
691    }
692
693    // The `shift` parameter will always be 0 in valid PDFs. The purpose of the parameter is
694    // so that in case there are garbage operands in the content stream, we prefer to use
695    // the operands that are closer to the operator instead of the values at the bottom
696    // of the stack.
697
698    macro_rules! op0 {
699        ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => {
700            crate::content::macros::display_op!($t$(<$($l),+>)?;);
701            crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, 0, |_stack: &'b Stack<'a>| {
702                Some(Self)
703            });
704        }
705    }
706
707    macro_rules! op1 {
708        ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => {
709            crate::content::macros::display_op!($t$(<$($l),+>)?; 0);
710            crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, 1, |stack: &'b Stack<'a>| {
711                let shift = stack.len().saturating_sub(1);
712                Some(Self(stack.get(0 + shift)?))
713            });
714        }
715    }
716
717    macro_rules! op_all {
718        ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => {
719            crate::content::macros::display_variadic_op!($t$(<$($l),+>)?);
720            crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, u8::MAX as usize, |stack: &'b Stack<'a>| {
721                Some(Self(stack.get_all()?))
722            });
723        }
724    }
725
726    macro_rules! op2 {
727        ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => {
728            crate::content::macros::display_op!($t$(<$($l),+>)?; 0, 1);
729            crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, 2, |stack: &'b Stack<'a>| {
730                let shift = stack.len().saturating_sub(2);
731                Some(Self(stack.get(0 + shift)?, stack.get(1 + shift)?))
732            });
733        }
734    }
735
736    macro_rules! op3 {
737        ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => {
738            crate::content::macros::display_op!($t$(<$($l),+>)?; 0, 1, 2);
739            crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, 3, |stack: &'b Stack<'a>| {
740                let shift = stack.len().saturating_sub(3);
741                Some(Self(stack.get(0 + shift)?, stack.get(1 + shift)?,
742                stack.get(2 + shift)?))
743            });
744        }
745    }
746
747    macro_rules! op4 {
748        ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => {
749            crate::content::macros::display_op!($t$(<$($l),+>)?; 0, 1, 2, 3);
750            crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, 4, |stack: &'b Stack<'a>| {
751               let shift = stack.len().saturating_sub(4);
752            Some(Self(stack.get(0 + shift)?, stack.get(1 + shift)?,
753            stack.get(2 + shift)?, stack.get(3 + shift)?))
754            });
755        }
756    }
757
758    macro_rules! op6 {
759        ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => {
760            crate::content::macros::display_op!($t$(<$($l),+>)?; 0, 1, 2, 3, 4, 5);
761            crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, 6, |stack: &'b Stack<'a>| {
762                let shift = stack.len().saturating_sub(6);
763            Some(Self(stack.get(0 + shift)?, stack.get(1 + shift)?,
764            stack.get(2 + shift)?, stack.get(3 + shift)?,
765            stack.get(4 + shift)?, stack.get(5 + shift)?))
766            });
767        }
768    }
769
770    pub(crate) use display_op;
771    pub(crate) use display_variadic_op;
772    pub(crate) use op_all;
773    pub(crate) use op_impl;
774    pub(crate) use op0;
775    pub(crate) use op1;
776    pub(crate) use op2;
777    pub(crate) use op3;
778    pub(crate) use op4;
779    pub(crate) use op6;
780}