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 core::fmt::{Display, Formatter};
11use smallvec::{SmallVec, smallvec};
12
13use crate::content::macros::{op_all, op_impl, op0, op1, op2, op3, op4, op6};
14
15include!("ops_generated.rs");
16
17// Need to special-case those because they have variable arguments.
18
19fn parse_named_color<'b, 'a>(
20    objects: &'b [Object<'a>],
21) -> Option<(SmallVec<[Number; OPERANDS_THRESHOLD]>, Option<&'b Name<'a>>)> {
22    let mut nums = smallvec![];
23    let mut name = None;
24
25    for o in objects {
26        match o {
27            Object::Number(n) => nums.push(*n),
28            Object::Name(n) => name = Some(n),
29            _ => {
30                warn!("encountered unknown object {o:?} when parsing scn/SCN");
31
32                return None;
33            }
34        }
35    }
36
37    Some((nums, name))
38}
39
40fn fmt_named_color(
41    f: &mut Formatter<'_>,
42    operator: &str,
43    numbers: &[Number],
44    name: Option<&Name<'_>>,
45) -> core::fmt::Result {
46    f.write_str(operator)?;
47    if !numbers.is_empty() || name.is_some() {
48        f.write_str("(")?;
49        for (index, number) in numbers.iter().enumerate() {
50            if index > 0 {
51                f.write_str(" ")?;
52            }
53            Display::fmt(number, f)?;
54        }
55        if let Some(name) = name {
56            if !numbers.is_empty() {
57                f.write_str(" ")?;
58            }
59            Display::fmt(name, f)?;
60        }
61        f.write_str(")")?;
62    }
63    Ok(())
64}
65
66#[derive(Debug, Clone, PartialEq)]
67pub struct StrokeColorNamed<'b, 'a>(
68    pub SmallVec<[Number; OPERANDS_THRESHOLD]>,
69    pub Option<&'b Name<'a>>,
70);
71
72impl Display for StrokeColorNamed<'_, '_> {
73    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
74        fmt_named_color(f, "StrokeColorNamed", &self.0, self.1)
75    }
76}
77
78op_impl!(
79    StrokeColorNamed<'b, 'a>,
80    "SCN",
81    u8::MAX as usize,
82    |stack: &'b Stack<'a>| {
83        let (nums, name) = parse_named_color(stack.as_slice())?;
84        Some(StrokeColorNamed(nums, name))
85    }
86);
87
88#[derive(Debug, PartialEq, Clone)]
89pub struct NonStrokeColorNamed<'b, 'a>(
90    pub SmallVec<[Number; OPERANDS_THRESHOLD]>,
91    pub Option<&'b Name<'a>>,
92);
93
94impl Display for NonStrokeColorNamed<'_, '_> {
95    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
96        fmt_named_color(f, "NonStrokeColorNamed", &self.0, self.1)
97    }
98}
99
100op_impl!(
101    NonStrokeColorNamed<'b, 'a>,
102    "scn",
103    u8::MAX as usize,
104    |stack: &'b Stack<'a>| {
105        let (nums, name) = parse_named_color(stack.as_slice())?;
106        Some(NonStrokeColorNamed(nums, name))
107    }
108);
109
110#[cfg(test)]
111mod tests {
112    use crate::content::TypedIter;
113    use crate::content::ops::{
114        BeginMarkedContentWithProperties, ClosePath, EndMarkedContent, FillPathNonZero, LineTo,
115        MarkedContentPointWithProperties, MoveTo, NonStrokeColorDeviceRgb, NonStrokeColorNamed,
116        SetGraphicsState, StrokeColorNamed, Transform, TypedInstruction,
117    };
118    use crate::object::Name;
119    use crate::object::Number;
120    use crate::object::Object;
121    use crate::object::{Dict, FromBytes};
122    fn n(num: i32) -> Number {
123        Number::from_i32(num)
124    }
125
126    #[test]
127    fn basic_ops_1() {
128        let input = b"
1291 0 0 -1 0 200 cm
130/g0 gs
1311 0 0 rg
132";
133
134        let mut iter = TypedIter::new(input);
135
136        assert!(matches!(
137            iter.next(),
138            Some(TypedInstruction::Transform(Transform(a, b, c, d, e, f)))
139                if [a, b, c, d, e, f] == [n(1), n(0), n(0), n(-1), n(0), n(200)]
140        ));
141        assert!(matches!(
142            iter.next(),
143            Some(TypedInstruction::SetGraphicsState(SetGraphicsState(name)))
144                if name.as_ref() == b"g0"
145        ));
146        assert!(matches!(
147            iter.next(),
148            Some(TypedInstruction::NonStrokeColorDeviceRgb(NonStrokeColorDeviceRgb(r, g, b)))
149                if [r, g, b] == [n(1), n(0), n(0)]
150        ));
151        assert!(iter.next().is_none());
152    }
153
154    #[test]
155    fn basic_ops_2() {
156        let input = b"
15720 20 m
158180 20 l
159180 180 l
16020 180 l
161h
162f
163";
164
165        let mut iter = TypedIter::new(input);
166
167        assert!(matches!(
168            iter.next(),
169            Some(TypedInstruction::MoveTo(MoveTo(x, y))) if [x, y] == [n(20), n(20)]
170        ));
171        assert!(matches!(
172            iter.next(),
173            Some(TypedInstruction::LineTo(LineTo(x, y))) if [x, y] == [n(180), n(20)]
174        ));
175        assert!(matches!(
176            iter.next(),
177            Some(TypedInstruction::LineTo(LineTo(x, y))) if [x, y] == [n(180), n(180)]
178        ));
179        assert!(matches!(
180            iter.next(),
181            Some(TypedInstruction::LineTo(LineTo(x, y))) if [x, y] == [n(20), n(180)]
182        ));
183        assert!(matches!(
184            iter.next(),
185            Some(TypedInstruction::ClosePath(ClosePath))
186        ));
187        assert!(matches!(
188            iter.next(),
189            Some(TypedInstruction::FillPathNonZero(FillPathNonZero))
190        ));
191        assert!(iter.next().is_none());
192    }
193
194    #[test]
195    fn display_ops() {
196        let mut iter = TypedIter::new(
197            b"h 2 w 1 0 0 RG 0 0 10 20 re 10 20 m 30 20 60 40 90 80 c \
198              0.1 0.2 SC 0.0 scn 1.0 /Spot SCN (Hello) Tj <00FF> Tj foo",
199        );
200
201        assert_eq!(format!("{}", iter.next().unwrap()), "ClosePath");
202        assert_eq!(format!("{}", iter.next().unwrap()), "LineWidth(2)");
203        assert_eq!(
204            format!("{}", iter.next().unwrap()),
205            "StrokeColorDeviceRgb(1 0 0)"
206        );
207        assert_eq!(format!("{}", iter.next().unwrap()), "RectPath(0 0 10 20)");
208        assert_eq!(format!("{}", iter.next().unwrap()), "MoveTo(10 20)");
209        assert_eq!(
210            format!("{}", iter.next().unwrap()),
211            "CubicTo(30 20 60 40 90 80)"
212        );
213        assert_eq!(format!("{}", iter.next().unwrap()), "StrokeColor(0.1 0.2)");
214        assert_eq!(
215            format!("{}", iter.next().unwrap()),
216            "NonStrokeColorNamed(0.0)"
217        );
218        assert_eq!(
219            format!("{}", iter.next().unwrap()),
220            "StrokeColorNamed(1.0 /Spot)"
221        );
222        assert_eq!(format!("{}", iter.next().unwrap()), "ShowText(\"Hello\")");
223        assert_eq!(format!("{}", iter.next().unwrap()), "ShowText([0, 255])");
224        assert_eq!(format!("{}", iter.next().unwrap()), "Fallback(foo)");
225        assert!(iter.next().is_none());
226    }
227
228    #[test]
229    fn scn() {
230        let input = b"
2310.0 scn
2321.0 1.0 1.0 /DeviceRgb SCN
233";
234
235        let mut iter = TypedIter::new(input);
236
237        match iter.next() {
238            Some(TypedInstruction::NonStrokeColorNamed(NonStrokeColorNamed(nums, None))) => {
239                assert_eq!(nums.as_slice(), &[Number::from_f32(0.0)]);
240            }
241            other => panic!("unexpected instruction: {other:?}"),
242        }
243
244        match iter.next() {
245            Some(TypedInstruction::StrokeColorNamed(StrokeColorNamed(nums, Some(name)))) => {
246                assert_eq!(
247                    nums.as_slice(),
248                    &[
249                        Number::from_f32(1.0),
250                        Number::from_f32(1.0),
251                        Number::from_f32(1.0)
252                    ]
253                );
254                assert_eq!(name.as_ref(), b"DeviceRgb");
255            }
256            other => panic!("unexpected instruction: {other:?}"),
257        }
258
259        assert!(iter.next().is_none());
260    }
261
262    #[test]
263    fn dp() {
264        let input = b"/Attribute<</ShowCenterPoint false >> DP";
265
266        let mut iter = TypedIter::new(input);
267
268        match iter.next() {
269            Some(TypedInstruction::MarkedContentPointWithProperties(
270                MarkedContentPointWithProperties(name, object),
271            )) => {
272                assert_eq!(name.as_ref(), b"Attribute");
273                assert_eq!(
274                    object,
275                    &Object::Dict(Dict::from_bytes(b"<</ShowCenterPoint false >>").unwrap())
276                );
277            }
278            other => panic!("unexpected instruction: {other:?}"),
279        }
280
281        assert!(iter.next().is_none());
282    }
283
284    #[test]
285    fn bdc_with_dict() {
286        let input = b"/Span << /MCID 0 /Alt (Alt)>> BDC EMC";
287
288        let mut iter = TypedIter::new(input);
289
290        match iter.next() {
291            Some(TypedInstruction::BeginMarkedContentWithProperties(
292                BeginMarkedContentWithProperties(name, object),
293            )) => {
294                assert_eq!(name.as_ref(), b"Span");
295                assert_eq!(
296                    object,
297                    &Object::Dict(Dict::from_bytes(b"<< /MCID 0 /Alt (Alt)>>").unwrap())
298                );
299            }
300            other => panic!("unexpected instruction: {other:?}"),
301        }
302
303        assert!(matches!(
304            iter.next(),
305            Some(TypedInstruction::EndMarkedContent(EndMarkedContent))
306        ));
307        assert!(iter.next().is_none());
308    }
309
310    #[test]
311    fn bdc_with_name() {
312        let input = b"/Span /Name BDC EMC";
313
314        let mut iter = TypedIter::new(input);
315
316        match iter.next() {
317            Some(TypedInstruction::BeginMarkedContentWithProperties(
318                BeginMarkedContentWithProperties(name, object),
319            )) => {
320                assert_eq!(name.as_ref(), b"Span");
321                assert_eq!(object, &Object::Name(Name::new_unescaped(b"Name")));
322            }
323            other => panic!("unexpected instruction: {other:?}"),
324        }
325
326        assert!(matches!(
327            iter.next(),
328            Some(TypedInstruction::EndMarkedContent(EndMarkedContent))
329        ));
330        assert!(iter.next().is_none());
331    }
332}