1use pdfboss_core::content::{ImageParams, Op, TextItem};
24use pdfboss_core::{Name, Object};
25
26use crate::ser::{serialize_object, write_name, write_real_f32, write_string};
27
28pub fn serialize_ops(ops: &[Op]) -> Vec<u8> {
35 let mut out = Vec::new();
36 for op in ops {
37 push_op(op, &mut out);
38 }
39 out
40}
41
42fn push_op(op: &Op, out: &mut Vec<u8>) {
45 match op {
46 Op::Save => push_kw(b"q", out),
47 Op::Restore => push_kw(b"Q", out),
48 Op::Concat(m) => push_nums(&[m.a, m.b, m.c, m.d, m.e, m.f], b"cm", out),
49 Op::SetLineWidth(w) => push_nums(&[*w], b"w", out),
50 Op::SetLineCap(cap) => push_int(*cap, b"J", out),
51 Op::SetLineJoin(join) => push_int(*join, b"j", out),
52 Op::SetMiterLimit(limit) => push_nums(&[*limit], b"M", out),
53 Op::SetDash(dashes, phase) => {
54 push_f32_array(dashes, out);
55 out.push(b' ');
56 push_nums(&[*phase], b"d", out);
57 }
58 Op::SetRenderingIntent(n) => push_name_op(n, b"ri", out),
59 Op::SetFlatness(f) => push_nums(&[*f], b"i", out),
60 Op::SetExtGState(n) => push_name_op(n, b"gs", out),
61 Op::MoveTo(x, y) => push_nums(&[*x, *y], b"m", out),
62 Op::LineTo(x, y) => push_nums(&[*x, *y], b"l", out),
63 Op::CurveTo(x1, y1, x2, y2, x3, y3) => {
64 push_nums(&[*x1, *y1, *x2, *y2, *x3, *y3], b"c", out);
65 }
66 Op::CurveToV(x2, y2, x3, y3) => push_nums(&[*x2, *y2, *x3, *y3], b"v", out),
67 Op::CurveToY(x1, y1, x3, y3) => push_nums(&[*x1, *y1, *x3, *y3], b"y", out),
68 Op::ClosePath => push_kw(b"h", out),
69 Op::Rect(x, y, w, h) => push_nums(&[*x, *y, *w, *h], b"re", out),
70 Op::Stroke => push_kw(b"S", out),
71 Op::CloseStroke => push_kw(b"s", out),
72 Op::Fill => push_kw(b"f", out),
73 Op::FillEvenOdd => push_kw(b"f*", out),
74 Op::FillStroke => push_kw(b"B", out),
75 Op::FillStrokeEvenOdd => push_kw(b"B*", out),
76 Op::CloseFillStroke => push_kw(b"b", out),
77 Op::CloseFillStrokeEvenOdd => push_kw(b"b*", out),
78 Op::EndPath => push_kw(b"n", out),
79 Op::ClipNonZero => push_kw(b"W", out),
80 Op::ClipEvenOdd => push_kw(b"W*", out),
81 Op::SetStrokeColorSpace(n) => push_name_op(n, b"CS", out),
82 Op::SetFillColorSpace(n) => push_name_op(n, b"cs", out),
83 Op::SetStrokeColor(comps) => push_nums(comps, b"SC", out),
84 Op::SetStrokeColorN(comps, pattern) => {
85 push_color_n(comps, pattern.as_ref(), b"SCN", out);
86 }
87 Op::SetFillColor(comps) => push_nums(comps, b"sc", out),
88 Op::SetFillColorN(comps, pattern) => push_color_n(comps, pattern.as_ref(), b"scn", out),
89 Op::SetStrokeGray(g) => push_nums(&[*g], b"G", out),
90 Op::SetFillGray(g) => push_nums(&[*g], b"g", out),
91 Op::SetStrokeRGB(r, g, b) => push_nums(&[*r, *g, *b], b"RG", out),
92 Op::SetFillRGB(r, g, b) => push_nums(&[*r, *g, *b], b"rg", out),
93 Op::SetStrokeCMYK(c, m, y, k) => push_nums(&[*c, *m, *y, *k], b"K", out),
94 Op::SetFillCMYK(c, m, y, k) => push_nums(&[*c, *m, *y, *k], b"k", out),
95 Op::BeginText => push_kw(b"BT", out),
96 Op::EndText => push_kw(b"ET", out),
97 Op::SetCharSpacing(v) => push_nums(&[*v], b"Tc", out),
98 Op::SetWordSpacing(v) => push_nums(&[*v], b"Tw", out),
99 Op::SetHorizScaling(v) => push_nums(&[*v], b"Tz", out),
100 Op::SetLeading(v) => push_nums(&[*v], b"TL", out),
101 Op::SetFont(n, size) => {
102 write_name(&n.0, out);
103 out.push(b' ');
104 push_nums(&[*size], b"Tf", out);
105 }
106 Op::SetGlyphWidth(wx, wy) => push_nums(&[*wx, *wy], b"d0", out),
107 Op::SetGlyphWidthBBox(wx, wy, llx, lly, urx, ury) => {
108 push_nums(&[*wx, *wy, *llx, *lly, *urx, *ury], b"d1", out);
109 }
110 Op::SetTextRender(mode) => push_int(*mode, b"Tr", out),
111 Op::SetTextRise(v) => push_nums(&[*v], b"Ts", out),
112 Op::TextMove(tx, ty) => push_nums(&[*tx, *ty], b"Td", out),
113 Op::TextMoveSetLeading(tx, ty) => push_nums(&[*tx, *ty], b"TD", out),
114 Op::SetTextMatrix(m) => push_nums(&[m.a, m.b, m.c, m.d, m.e, m.f], b"Tm", out),
115 Op::TextNextLine => push_kw(b"T*", out),
116 Op::ShowText(s) => push_string_op(s, b"Tj", out),
117 Op::ShowTextAdjusted(items) => push_text_adjusted(items, out),
118 Op::NextLineShowText(s) => push_string_op(s, b"'", out),
119 Op::NextLineShowTextSpaced(aw, ac, s) => {
120 write_real_f32(*aw, out);
121 out.push(b' ');
122 write_real_f32(*ac, out);
123 out.push(b' ');
124 push_string_op(s, b"\"", out);
125 }
126 Op::XObject(n) => push_name_op(n, b"Do", out),
127 Op::InlineImage(img) => push_inline_image(img, out),
128 Op::Shading(n) => push_name_op(n, b"sh", out),
129 Op::MarkedContentPoint(n) => push_name_op(n, b"MP", out),
130 Op::MarkedContentPointProps(tag, props) => push_tag_props(tag, props, b"DP", out),
131 Op::BeginMarkedContent(n) => push_name_op(n, b"BMC", out),
132 Op::BeginMarkedContentProps(tag, props) => push_tag_props(tag, props, b"BDC", out),
133 Op::EndMarkedContent => push_kw(b"EMC", out),
134 Op::BeginCompat => push_kw(b"BX", out),
135 Op::EndCompat => push_kw(b"EX", out),
136 }
137}
138
139fn push_kw(kw: &[u8], out: &mut Vec<u8>) {
141 out.extend_from_slice(kw);
142 out.push(b'\n');
143}
144
145fn push_nums(vals: &[f32], kw: &[u8], out: &mut Vec<u8>) {
147 for v in vals {
148 write_real_f32(*v, out);
149 out.push(b' ');
150 }
151 push_kw(kw, out);
152}
153
154fn push_int(value: i32, kw: &[u8], out: &mut Vec<u8>) {
156 out.extend_from_slice(value.to_string().as_bytes());
157 out.push(b' ');
158 push_kw(kw, out);
159}
160
161fn push_name_op(n: &Name, kw: &[u8], out: &mut Vec<u8>) {
163 write_name(&n.0, out);
164 out.push(b' ');
165 push_kw(kw, out);
166}
167
168fn push_string_op(s: &[u8], kw: &[u8], out: &mut Vec<u8>) {
170 write_string(s, out);
171 out.push(b' ');
172 push_kw(kw, out);
173}
174
175fn push_f32_array(vals: &[f32], out: &mut Vec<u8>) {
177 out.push(b'[');
178 for (i, v) in vals.iter().enumerate() {
179 if i > 0 {
180 out.push(b' ');
181 }
182 write_real_f32(*v, out);
183 }
184 out.push(b']');
185}
186
187fn push_color_n(comps: &[f32], pattern: Option<&Name>, kw: &[u8], out: &mut Vec<u8>) {
189 for v in comps {
190 write_real_f32(*v, out);
191 out.push(b' ');
192 }
193 if let Some(n) = pattern {
194 write_name(&n.0, out);
195 out.push(b' ');
196 }
197 push_kw(kw, out);
198}
199
200fn push_text_adjusted(items: &[TextItem], out: &mut Vec<u8>) {
202 out.push(b'[');
203 for (i, item) in items.iter().enumerate() {
204 if i > 0 {
205 out.push(b' ');
206 }
207 match item {
208 TextItem::Str(s) => write_string(s, out),
209 TextItem::Offset(v) => write_real_f32(*v, out),
210 }
211 }
212 out.extend_from_slice(b"] ");
213 push_kw(b"TJ", out);
214}
215
216fn push_tag_props(tag: &Name, props: &Object, kw: &[u8], out: &mut Vec<u8>) {
218 write_name(&tag.0, out);
219 out.push(b' ');
220 push_object_or_null(props, out);
221 out.push(b' ');
222 push_kw(kw, out);
223}
224
225fn push_object_or_null(obj: &Object, out: &mut Vec<u8>) {
228 let mark = out.len();
229 if serialize_object(obj, out).is_err() {
230 out.truncate(mark);
231 out.extend_from_slice(b"null");
232 }
233}
234
235fn push_inline_image(img: &ImageParams, out: &mut Vec<u8>) {
238 out.extend_from_slice(b"BI");
239 let mut entries: Vec<(&Name, &Object)> = img.dict.iter().collect();
240 entries.sort_unstable_by(|a, b| a.0.cmp(b.0));
241 for (key, value) in entries {
242 out.push(b' ');
243 write_name(&key.0, out);
244 out.push(b' ');
245 push_object_or_null(value, out);
246 }
247 out.extend_from_slice(b" ID ");
248 out.extend_from_slice(&img.data);
249 out.extend_from_slice(b" EI\n");
250}
251
252#[cfg(test)]
253mod tests {
254 use pdfboss_core::content::{parse_content, ImageParams, TextItem};
255 use pdfboss_core::geom::Matrix;
256 use pdfboss_core::{Dict, Name, Object};
257
258 use super::*;
259
260 const VARIANT_COUNT: usize = 70;
263
264 fn name(s: &str) -> Name {
265 Name(s.to_string())
266 }
267
268 fn m(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Matrix {
269 Matrix { a, b, c, d, e, f }
270 }
271
272 fn image(entries: &[(&str, Object)], data: &[u8]) -> Op {
273 let mut dict = Dict::new();
274 for (key, value) in entries {
275 dict.insert(name(key), value.clone());
276 }
277 Op::InlineImage(ImageParams {
278 dict,
279 data: data.to_vec(),
280 })
281 }
282
283 fn round_trip(ops: &[Op]) {
284 let bytes = serialize_ops(ops);
285 let parsed = parse_content(&bytes).unwrap_or_else(|e| {
286 panic!(
287 "serialized ops parse: {e:?}\n{}",
288 String::from_utf8_lossy(&bytes)
289 )
290 });
291 assert_eq!(
292 parsed,
293 ops,
294 "round-trip of {}",
295 String::from_utf8_lossy(&bytes)
296 );
297 }
298
299 fn variant_index(op: &Op) -> usize {
300 match op {
301 Op::Save => 0,
302 Op::Restore => 1,
303 Op::Concat(..) => 2,
304 Op::SetLineWidth(..) => 3,
305 Op::SetLineCap(..) => 4,
306 Op::SetLineJoin(..) => 5,
307 Op::SetMiterLimit(..) => 6,
308 Op::SetDash(..) => 7,
309 Op::SetRenderingIntent(..) => 8,
310 Op::SetFlatness(..) => 9,
311 Op::SetExtGState(..) => 10,
312 Op::MoveTo(..) => 11,
313 Op::LineTo(..) => 12,
314 Op::CurveTo(..) => 13,
315 Op::CurveToV(..) => 14,
316 Op::CurveToY(..) => 15,
317 Op::ClosePath => 16,
318 Op::Rect(..) => 17,
319 Op::Stroke => 18,
320 Op::CloseStroke => 19,
321 Op::Fill => 20,
322 Op::FillEvenOdd => 21,
323 Op::FillStroke => 22,
324 Op::FillStrokeEvenOdd => 23,
325 Op::CloseFillStroke => 24,
326 Op::CloseFillStrokeEvenOdd => 25,
327 Op::EndPath => 26,
328 Op::ClipNonZero => 27,
329 Op::ClipEvenOdd => 28,
330 Op::SetStrokeColorSpace(..) => 29,
331 Op::SetFillColorSpace(..) => 30,
332 Op::SetStrokeColor(..) => 31,
333 Op::SetStrokeColorN(..) => 32,
334 Op::SetFillColor(..) => 33,
335 Op::SetFillColorN(..) => 34,
336 Op::SetStrokeGray(..) => 35,
337 Op::SetFillGray(..) => 36,
338 Op::SetStrokeRGB(..) => 37,
339 Op::SetFillRGB(..) => 38,
340 Op::SetStrokeCMYK(..) => 39,
341 Op::SetFillCMYK(..) => 40,
342 Op::BeginText => 41,
343 Op::EndText => 42,
344 Op::SetCharSpacing(..) => 43,
345 Op::SetWordSpacing(..) => 44,
346 Op::SetHorizScaling(..) => 45,
347 Op::SetLeading(..) => 46,
348 Op::SetFont(..) => 47,
349 Op::SetGlyphWidth(..) => 48,
350 Op::SetGlyphWidthBBox(..) => 49,
351 Op::SetTextRender(..) => 50,
352 Op::SetTextRise(..) => 51,
353 Op::TextMove(..) => 52,
354 Op::TextMoveSetLeading(..) => 53,
355 Op::SetTextMatrix(..) => 54,
356 Op::TextNextLine => 55,
357 Op::ShowText(..) => 56,
358 Op::ShowTextAdjusted(..) => 57,
359 Op::NextLineShowText(..) => 58,
360 Op::NextLineShowTextSpaced(..) => 59,
361 Op::XObject(..) => 60,
362 Op::InlineImage(..) => 61,
363 Op::Shading(..) => 62,
364 Op::MarkedContentPoint(..) => 63,
365 Op::MarkedContentPointProps(..) => 64,
366 Op::BeginMarkedContent(..) => 65,
367 Op::BeginMarkedContentProps(..) => 66,
368 Op::EndMarkedContent => 67,
369 Op::BeginCompat => 68,
370 Op::EndCompat => 69,
371 }
372 }
373
374 fn sample_ops() -> Vec<Op> {
375 let mut props = Dict::new();
376 props.insert(name("MCID"), Object::Int(3));
377 vec![
378 Op::Save,
379 Op::Restore,
380 Op::Concat(m(1.0, 0.0, 0.0, 1.0, 10.5, 20.0)),
381 Op::SetLineWidth(2.5),
382 Op::SetLineCap(1),
383 Op::SetLineJoin(2),
384 Op::SetMiterLimit(3.5),
385 Op::SetDash(vec![3.0, 1.5], 0.5),
386 Op::SetRenderingIntent(name("Perceptual")),
387 Op::SetFlatness(1.5),
388 Op::SetExtGState(name("GS1")),
389 Op::MoveTo(10.0, 20.0),
390 Op::LineTo(-30.5, 40.0),
391 Op::CurveTo(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
392 Op::CurveToV(1.5, 2.5, 3.5, 4.5),
393 Op::CurveToY(5.0, 6.0, 7.0, 8.0),
394 Op::ClosePath,
395 Op::Rect(72.0, 600.0, 100.0, 80.0),
396 Op::Stroke,
397 Op::CloseStroke,
398 Op::Fill,
399 Op::FillEvenOdd,
400 Op::FillStroke,
401 Op::FillStrokeEvenOdd,
402 Op::CloseFillStroke,
403 Op::CloseFillStrokeEvenOdd,
404 Op::EndPath,
405 Op::ClipNonZero,
406 Op::ClipEvenOdd,
407 Op::SetStrokeColorSpace(name("DeviceRGB")),
408 Op::SetFillColorSpace(name("Pattern")),
409 Op::SetStrokeColor(vec![1.0, 0.5, 0.25]),
410 Op::SetStrokeColorN(vec![0.1, 0.2], Some(name("P2"))),
411 Op::SetFillColor(vec![0.5]),
412 Op::SetFillColorN(vec![0.2, 0.4, 0.6], None),
413 Op::SetStrokeGray(0.3),
414 Op::SetFillGray(0.7),
415 Op::SetStrokeRGB(1.0, 0.0, 0.5),
416 Op::SetFillRGB(0.0, 1.0, 0.0),
417 Op::SetStrokeCMYK(0.0, 0.25, 0.5, 1.0),
418 Op::SetFillCMYK(1.0, 0.0, 0.0, 0.125),
419 Op::BeginText,
420 Op::EndText,
421 Op::SetCharSpacing(0.5),
422 Op::SetWordSpacing(1.5),
423 Op::SetHorizScaling(90.0),
424 Op::SetLeading(14.5),
425 Op::SetFont(name("F1"), 12.0),
426 Op::SetGlyphWidth(1000.0, 0.0),
427 Op::SetGlyphWidthBBox(1000.0, 0.0, 0.0, 0.0, 750.0, 700.0),
428 Op::SetTextRender(3),
429 Op::SetTextRise(4.5),
430 Op::TextMove(72.0, 720.0),
431 Op::TextMoveSetLeading(0.0, -14.0),
432 Op::SetTextMatrix(m(2.0, 0.0, 0.0, 2.0, 50.5, 60.0)),
433 Op::TextNextLine,
434 Op::ShowText(b"Hi (there)\\".to_vec()),
435 Op::ShowTextAdjusted(vec![
436 TextItem::Str(b"He".to_vec()),
437 TextItem::Offset(-120.0),
438 TextItem::Str(b"llo".to_vec()),
439 TextItem::Offset(33.5),
440 ]),
441 Op::NextLineShowText(b"next line".to_vec()),
442 Op::NextLineShowTextSpaced(2.0, 3.0, b"spaced".to_vec()),
443 Op::XObject(name("Im1")),
444 image(
445 &[
446 ("ColorSpace", Object::Name(name("DeviceGray"))),
447 ("Width", Object::Int(2)),
448 ("BitsPerComponent", Object::Int(8)),
449 ("Height", Object::Int(2)),
450 ],
451 &[0, 255, 128, 127],
452 ),
453 Op::Shading(name("Sh1")),
454 Op::MarkedContentPoint(name("Tag")),
455 Op::MarkedContentPointProps(name("Tag"), Object::Name(name("P"))),
456 Op::BeginMarkedContent(name("Span")),
457 Op::BeginMarkedContentProps(name("Span"), Object::Dict(props)),
458 Op::EndMarkedContent,
459 Op::BeginCompat,
460 Op::EndCompat,
461 ]
462 }
463
464 #[test]
465 fn every_variant_round_trips() {
466 let samples = sample_ops();
467 let covered: std::collections::BTreeSet<usize> =
468 samples.iter().map(variant_index).collect();
469 assert_eq!(covered, (0..VARIANT_COUNT).collect());
470 for op in &samples {
471 round_trip(std::slice::from_ref(op));
472 }
473 round_trip(&samples);
474 }
475
476 #[test]
477 fn mixed_sequence_round_trips() {
478 let mut props = Dict::new();
479 props.insert(name("MCID"), Object::Int(0));
480 let ops = vec![
481 Op::Save,
482 Op::Concat(m(0.5, 0.0, 0.0, 0.5, 300.0, 100.0)),
483 Op::Rect(0.0, 0.0, 200.0, 200.0),
484 Op::Fill,
485 Op::BeginMarkedContentProps(name("P"), Object::Dict(props)),
486 Op::BeginText,
487 Op::SetFont(name("F1"), 12.0),
488 Op::TextMove(72.0, 720.0),
489 Op::ShowText(b"Hello, world".to_vec()),
490 Op::ShowTextAdjusted(vec![
491 TextItem::Str(b"kern".to_vec()),
492 TextItem::Offset(-15.5),
493 TextItem::Str(b"ed".to_vec()),
494 ]),
495 Op::EndText,
496 Op::EndMarkedContent,
497 Op::XObject(name("Im1")),
498 Op::Restore,
499 ];
500 round_trip(&ops);
501 }
502
503 #[test]
504 fn inline_image_hazardous_data_round_trips_via_declared_length() {
505 let op = image(
506 &[
507 ("Width", Object::Int(3)),
508 ("Height", Object::Int(1)),
509 ("BitsPerComponent", Object::Int(8)),
510 ("ColorSpace", Object::Name(name("DeviceRGB"))),
511 ("L", Object::Int(9)),
512 ],
513 b"ab EI wxy",
514 );
515 round_trip(std::slice::from_ref(&op));
516 round_trip(&[op, Op::MoveTo(1.0, 2.0)]);
517 }
518
519 #[test]
520 fn parser_produced_length_image_round_trips() {
521 let mut src = b"BI /W 3 /H 1 /BPC 8 /CS /RGB /L 9 ID ".to_vec();
522 src.extend_from_slice(b"ab EI wxy");
523 src.extend_from_slice(b" EI 1 2 m");
524 let parsed = parse_content(&src).expect("source parses");
525 assert_eq!(parsed.len(), 2);
526 round_trip(&parsed);
527 }
528
529 #[test]
530 fn inline_image_ei_boundary_data_round_trips_without_length() {
531 let base = [("Width", Object::Int(1)), ("Height", Object::Int(1))];
532 for data in [
533 b"noEIhazard".as_slice(),
534 b"EIx\x01".as_slice(),
535 b"zxEI".as_slice(),
536 b"tail ".as_slice(),
537 ] {
538 round_trip(std::slice::from_ref(&image(&base, data)));
539 }
540 }
541
542 #[test]
543 fn f32_hard_cases_survive_round_trip() {
544 let ops = vec![
545 Op::SetLineWidth(0.1),
546 Op::SetLineWidth(1e-7),
547 Op::SetLineWidth(-1e-7),
548 Op::MoveTo(-0.25, -123.456),
549 Op::SetDash(vec![0.1, 1e-7, -0.3], 16_777_216.0),
550 Op::ShowTextAdjusted(vec![TextItem::Offset(-120.25), TextItem::Offset(1e-7)]),
551 Op::SetTextRise(f32::MIN_POSITIVE),
552 Op::SetCharSpacing(3.4e38),
553 ];
554 for op in &ops {
555 round_trip(std::slice::from_ref(op));
556 }
557 round_trip(&ops);
558 }
559
560 #[test]
561 fn empty_operand_containers_round_trip() {
562 let ops = vec![
563 Op::SetDash(vec![], 0.0),
564 Op::ShowTextAdjusted(vec![]),
565 Op::SetStrokeColor(vec![]),
566 Op::SetStrokeColorN(vec![], None),
567 Op::SetFillColorN(vec![], Some(name("P1"))),
568 Op::InlineImage(ImageParams {
569 dict: Dict::new(),
570 data: Vec::new(),
571 }),
572 ];
573 for op in &ops {
574 round_trip(std::slice::from_ref(op));
575 }
576 round_trip(&ops);
577 }
578
579 #[test]
580 fn layout_pins_exact_bytes() {
581 assert_eq!(serialize_ops(&[Op::Save]), b"q\n");
582 assert_eq!(
583 serialize_ops(&[Op::Concat(m(1.0, 0.0, 0.0, 1.0, 10.0, 20.0))]),
584 b"1 0 0 1 10 20 cm\n"
585 );
586 assert_eq!(
587 serialize_ops(&[Op::SetDash(vec![3.0, 1.0], 0.5)]),
588 b"[3 1] 0.5 d\n"
589 );
590 assert_eq!(
591 serialize_ops(&[Op::ShowTextAdjusted(vec![
592 TextItem::Str(b"He".to_vec()),
593 TextItem::Offset(-120.0),
594 TextItem::Str(b"llo".to_vec()),
595 ])]),
596 b"[(He) -120 (llo)] TJ\n"
597 );
598 assert_eq!(
599 serialize_ops(&[Op::SetStrokeColorN(vec![0.1, 0.2], Some(name("P2")))]),
600 b"0.1 0.2 /P2 SCN\n"
601 );
602 assert_eq!(
603 serialize_ops(&[Op::NextLineShowTextSpaced(2.0, 3.0, b"spaced".to_vec())]),
604 b"2 3 (spaced) \"\n"
605 );
606 assert_eq!(
607 serialize_ops(&[Op::SetFont(name("F1"), 12.0)]),
608 b"/F1 12 Tf\n"
609 );
610 assert_eq!(serialize_ops(&[Op::SetTextRender(2)]), b"2 Tr\n");
611 let mut want = b"BI /Width 2 ID ".to_vec();
612 want.extend_from_slice(&[0, 255]);
613 want.extend_from_slice(b" EI\n");
614 assert_eq!(
615 serialize_ops(&[image(&[("Width", Object::Int(2))], &[0, 255])]),
616 want
617 );
618 }
619}