Skip to main content

hayro_syntax/content/
ops.rs

1//! Content stream operators.
2
3use crate::content::{Instruction, OPERANDS_THRESHOLD, OperatorTrait, Stack};
4use crate::object;
5use crate::object::Array;
6use crate::object::Name;
7use crate::object::Number;
8use crate::object::Object;
9use crate::object::Stream;
10use smallvec::{SmallVec, smallvec};
11
12use crate::content::macros::{op_all, op_impl, op0, op1, op2, op3, op4, op6};
13
14include!("ops_generated.rs");
15
16// Need to special-case those because they have variable arguments.
17
18fn parse_named_color<'b, 'a>(
19    objects: &'b [Object<'a>],
20) -> Option<(SmallVec<[Number; OPERANDS_THRESHOLD]>, Option<&'b Name<'a>>)> {
21    let mut nums = smallvec![];
22    let mut name = None;
23
24    for o in objects {
25        match o {
26            Object::Number(n) => nums.push(*n),
27            Object::Name(n) => name = Some(n),
28            _ => {
29                warn!("encountered unknown object {o:?} when parsing scn/SCN");
30
31                return None;
32            }
33        }
34    }
35
36    Some((nums, name))
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct StrokeColorNamed<'b, 'a>(
41    pub SmallVec<[Number; OPERANDS_THRESHOLD]>,
42    pub Option<&'b Name<'a>>,
43);
44
45op_impl!(
46    StrokeColorNamed<'b, 'a>,
47    "SCN",
48    u8::MAX as usize,
49    |stack: &'b Stack<'a>| {
50        let (nums, name) = parse_named_color(stack.as_slice())?;
51        Some(StrokeColorNamed(nums, name))
52    }
53);
54
55#[derive(Debug, PartialEq, Clone)]
56pub struct NonStrokeColorNamed<'b, 'a>(
57    pub SmallVec<[Number; OPERANDS_THRESHOLD]>,
58    pub Option<&'b Name<'a>>,
59);
60
61op_impl!(
62    NonStrokeColorNamed<'b, 'a>,
63    "scn",
64    u8::MAX as usize,
65    |stack: &'b Stack<'a>| {
66        let (nums, name) = parse_named_color(stack.as_slice())?;
67        Some(NonStrokeColorNamed(nums, name))
68    }
69);
70
71#[cfg(test)]
72mod tests {
73    use crate::content::TypedIter;
74    use crate::content::ops::{
75        BeginMarkedContentWithProperties, ClosePath, EndMarkedContent, FillPathNonZero, LineTo,
76        MarkedContentPointWithProperties, MoveTo, NonStrokeColorDeviceRgb, NonStrokeColorNamed,
77        SetGraphicsState, StrokeColorNamed, Transform, TypedInstruction,
78    };
79    use crate::object::Name;
80    use crate::object::Number;
81    use crate::object::Object;
82    use crate::object::{Dict, FromBytes};
83    fn n(num: i32) -> Number {
84        Number::from_i32(num)
85    }
86
87    #[test]
88    fn basic_ops_1() {
89        let input = b"
901 0 0 -1 0 200 cm
91/g0 gs
921 0 0 rg
93";
94
95        let mut iter = TypedIter::new(input);
96
97        assert!(matches!(
98            iter.next(),
99            Some(TypedInstruction::Transform(Transform(a, b, c, d, e, f)))
100                if [a, b, c, d, e, f] == [n(1), n(0), n(0), n(-1), n(0), n(200)]
101        ));
102        assert!(matches!(
103            iter.next(),
104            Some(TypedInstruction::SetGraphicsState(SetGraphicsState(name)))
105                if name.as_ref() == b"g0"
106        ));
107        assert!(matches!(
108            iter.next(),
109            Some(TypedInstruction::NonStrokeColorDeviceRgb(NonStrokeColorDeviceRgb(r, g, b)))
110                if [r, g, b] == [n(1), n(0), n(0)]
111        ));
112        assert!(iter.next().is_none());
113    }
114
115    #[test]
116    fn basic_ops_2() {
117        let input = b"
11820 20 m
119180 20 l
120180 180 l
12120 180 l
122h
123f
124";
125
126        let mut iter = TypedIter::new(input);
127
128        assert!(matches!(
129            iter.next(),
130            Some(TypedInstruction::MoveTo(MoveTo(x, y))) if [x, y] == [n(20), n(20)]
131        ));
132        assert!(matches!(
133            iter.next(),
134            Some(TypedInstruction::LineTo(LineTo(x, y))) if [x, y] == [n(180), n(20)]
135        ));
136        assert!(matches!(
137            iter.next(),
138            Some(TypedInstruction::LineTo(LineTo(x, y))) if [x, y] == [n(180), n(180)]
139        ));
140        assert!(matches!(
141            iter.next(),
142            Some(TypedInstruction::LineTo(LineTo(x, y))) if [x, y] == [n(20), n(180)]
143        ));
144        assert!(matches!(
145            iter.next(),
146            Some(TypedInstruction::ClosePath(ClosePath))
147        ));
148        assert!(matches!(
149            iter.next(),
150            Some(TypedInstruction::FillPathNonZero(FillPathNonZero))
151        ));
152        assert!(iter.next().is_none());
153    }
154
155    #[test]
156    fn scn() {
157        let input = b"
1580.0 scn
1591.0 1.0 1.0 /DeviceRgb SCN
160";
161
162        let mut iter = TypedIter::new(input);
163
164        match iter.next() {
165            Some(TypedInstruction::NonStrokeColorNamed(NonStrokeColorNamed(nums, None))) => {
166                assert_eq!(nums.as_slice(), &[Number::from_f32(0.0)]);
167            }
168            other => panic!("unexpected instruction: {other:?}"),
169        }
170
171        match iter.next() {
172            Some(TypedInstruction::StrokeColorNamed(StrokeColorNamed(nums, Some(name)))) => {
173                assert_eq!(
174                    nums.as_slice(),
175                    &[
176                        Number::from_f32(1.0),
177                        Number::from_f32(1.0),
178                        Number::from_f32(1.0)
179                    ]
180                );
181                assert_eq!(name.as_ref(), b"DeviceRgb");
182            }
183            other => panic!("unexpected instruction: {other:?}"),
184        }
185
186        assert!(iter.next().is_none());
187    }
188
189    #[test]
190    fn dp() {
191        let input = b"/Attribute<</ShowCenterPoint false >> DP";
192
193        let mut iter = TypedIter::new(input);
194
195        match iter.next() {
196            Some(TypedInstruction::MarkedContentPointWithProperties(
197                MarkedContentPointWithProperties(name, object),
198            )) => {
199                assert_eq!(name.as_ref(), b"Attribute");
200                assert_eq!(
201                    object,
202                    &Object::Dict(Dict::from_bytes(b"<</ShowCenterPoint false >>").unwrap())
203                );
204            }
205            other => panic!("unexpected instruction: {other:?}"),
206        }
207
208        assert!(iter.next().is_none());
209    }
210
211    #[test]
212    fn bdc_with_dict() {
213        let input = b"/Span << /MCID 0 /Alt (Alt)>> BDC EMC";
214
215        let mut iter = TypedIter::new(input);
216
217        match iter.next() {
218            Some(TypedInstruction::BeginMarkedContentWithProperties(
219                BeginMarkedContentWithProperties(name, object),
220            )) => {
221                assert_eq!(name.as_ref(), b"Span");
222                assert_eq!(
223                    object,
224                    &Object::Dict(Dict::from_bytes(b"<< /MCID 0 /Alt (Alt)>>").unwrap())
225                );
226            }
227            other => panic!("unexpected instruction: {other:?}"),
228        }
229
230        assert!(matches!(
231            iter.next(),
232            Some(TypedInstruction::EndMarkedContent(EndMarkedContent))
233        ));
234        assert!(iter.next().is_none());
235    }
236
237    #[test]
238    fn bdc_with_name() {
239        let input = b"/Span /Name BDC EMC";
240
241        let mut iter = TypedIter::new(input);
242
243        match iter.next() {
244            Some(TypedInstruction::BeginMarkedContentWithProperties(
245                BeginMarkedContentWithProperties(name, object),
246            )) => {
247                assert_eq!(name.as_ref(), b"Span");
248                assert_eq!(object, &Object::Name(Name::new_unescaped(b"Name")));
249            }
250            other => panic!("unexpected instruction: {other:?}"),
251        }
252
253        assert!(matches!(
254            iter.next(),
255            Some(TypedInstruction::EndMarkedContent(EndMarkedContent))
256        ));
257        assert!(iter.next().is_none());
258    }
259}