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