1use std::fmt::Write as FmtWrite;
18
19use rpdfium_core::{Matrix, Point};
20use rpdfium_graphics::{Bitmap, BitmapFormat, BlendMode, ClipPath, FillRule, PathOp};
21
22use crate::color_convert::RgbaColor;
23use crate::image::{DecodedImage, DecodedImageFormat};
24use crate::renderdevicedriver_iface::RenderBackend;
25use crate::stroke::StrokeStyle;
26
27const PREAMBLE: &str = concat!(
32 "%!PS-Adobe-3.0\n",
33 "%%BeginProlog\n",
34 "/m {moveto} bind def\n",
35 "/l {lineto} bind def\n",
36 "/c {curveto} bind def\n",
37 "/h {closepath} bind def\n",
38 "/f {fill} bind def\n",
39 "/F {eofill} bind def\n",
40 "/s {stroke} bind def\n",
41 "/W {clip} bind def\n",
42 "/W* {eoclip} bind def\n",
43 "/q {gsave} bind def\n",
44 "/Q {grestore} bind def\n",
45 "/rg {setrgbcolor} bind def\n",
46 "/w {setlinewidth} bind def\n",
47 "/J {setlinecap} bind def\n",
48 "/j {setlinejoin} bind def\n",
49 "/M {setmiterlimit} bind def\n",
50 "/d {setdash} bind def\n",
51 "%%EndProlog\n",
52);
53
54pub struct PsSurface {
60 pub(crate) buf: String,
61 width: u32,
62 height: u32,
63}
64
65impl PsSurface {
66 pub fn into_bytes(mut self) -> Vec<u8> {
69 self.buf.push_str("%%EOF\n");
70 self.buf.into_bytes()
71 }
72}
73
74pub struct PsRenderBackend {
84 include_preamble: bool,
85}
86
87impl PsRenderBackend {
88 pub fn new() -> Self {
91 Self {
92 include_preamble: true,
93 }
94 }
95
96 pub fn new_raw() -> Self {
99 Self {
100 include_preamble: false,
101 }
102 }
103}
104
105impl Default for PsRenderBackend {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111fn fmt_f(v: f64) -> String {
117 if v.fract() == 0.0 {
118 format!("{}", v as i64)
119 } else {
120 let s = format!("{:.4}", v);
121 s.trim_end_matches('0').trim_end_matches('.').to_string()
122 }
123}
124
125fn emit_color(buf: &mut String, color: &RgbaColor) {
127 let r = color.r as f64 / 255.0;
128 let g = color.g as f64 / 255.0;
129 let b = color.b as f64 / 255.0;
130 let _ = writeln!(buf, "{} {} {} rg", fmt_f(r), fmt_f(g), fmt_f(b));
131}
132
133fn emit_path_ops(buf: &mut String, ops: &[PathOp], transform: &Matrix) {
135 for op in ops {
136 match op {
137 PathOp::MoveTo { x, y } => {
138 let p = transform.transform_point(Point::new(*x as f64, *y as f64));
139 let _ = writeln!(buf, "{} {} m", fmt_f(p.x), fmt_f(p.y));
140 }
141 PathOp::LineTo { x, y } => {
142 let p = transform.transform_point(Point::new(*x as f64, *y as f64));
143 let _ = writeln!(buf, "{} {} l", fmt_f(p.x), fmt_f(p.y));
144 }
145 PathOp::CurveTo {
146 x1,
147 y1,
148 x2,
149 y2,
150 x3,
151 y3,
152 } => {
153 let p1 = transform.transform_point(Point::new(*x1 as f64, *y1 as f64));
154 let p2 = transform.transform_point(Point::new(*x2 as f64, *y2 as f64));
155 let p3 = transform.transform_point(Point::new(*x3 as f64, *y3 as f64));
156 let _ = writeln!(
157 buf,
158 "{} {} {} {} {} {} c",
159 fmt_f(p1.x),
160 fmt_f(p1.y),
161 fmt_f(p2.x),
162 fmt_f(p2.y),
163 fmt_f(p3.x),
164 fmt_f(p3.y)
165 );
166 }
167 PathOp::Close => {
168 buf.push_str("h\n");
169 }
170 }
171 }
172}
173
174impl RenderBackend for PsRenderBackend {
179 type Surface = PsSurface;
180
181 fn create_surface(&self, width: u32, height: u32, _bg: &RgbaColor) -> PsSurface {
182 let mut buf = String::new();
183 if self.include_preamble {
184 buf.push_str(PREAMBLE);
185 let _ = writeln!(buf, "%%Page: 1 1");
186 let _ = writeln!(buf, "%%PageBoundingBox: 0 0 {} {}", width, height);
187 }
188 PsSurface { buf, width, height }
189 }
190
191 fn fill_path(
192 &mut self,
193 surface: &mut PsSurface,
194 ops: &[PathOp],
195 fill_rule: FillRule,
196 color: &RgbaColor,
197 transform: &Matrix,
198 ) {
199 emit_color(&mut surface.buf, color);
200 emit_path_ops(&mut surface.buf, ops, transform);
201 match fill_rule {
202 FillRule::NonZero => surface.buf.push_str("f\n"),
203 FillRule::EvenOdd => surface.buf.push_str("F\n"),
204 }
205 }
206
207 fn stroke_path(
208 &mut self,
209 surface: &mut PsSurface,
210 ops: &[PathOp],
211 style: &StrokeStyle,
212 color: &RgbaColor,
213 transform: &Matrix,
214 ) {
215 emit_color(&mut surface.buf, color);
216 let _ = writeln!(surface.buf, "{} w", fmt_f(style.width as f64));
218 let _ = writeln!(surface.buf, "{} J", style.line_cap as u8);
220 let _ = writeln!(surface.buf, "{} j", style.line_join as u8);
222 let _ = writeln!(surface.buf, "{} M", fmt_f(style.miter_limit as f64));
224 if let Some(dash) = &style.dash {
226 surface.buf.push('[');
227 for (i, v) in dash.array.iter().enumerate() {
228 if i > 0 {
229 surface.buf.push(' ');
230 }
231 surface.buf.push_str(&fmt_f(*v as f64));
232 }
233 let _ = writeln!(surface.buf, "] {} d", fmt_f(dash.phase as f64));
234 } else {
235 surface.buf.push_str("[] 0 d\n");
236 }
237 emit_path_ops(&mut surface.buf, ops, transform);
238 surface.buf.push_str("s\n");
239 }
240
241 fn draw_image(
242 &mut self,
243 surface: &mut PsSurface,
244 image: &DecodedImage,
245 transform: &Matrix,
246 _interpolate: bool,
247 ) {
248 let w = image.width;
249 let h = image.height;
250 if w == 0 || h == 0 {
251 return;
252 }
253
254 surface.buf.push_str("q\n");
256 let _ = writeln!(
257 surface.buf,
258 "[{} {} {} {} {} {}] concat",
259 fmt_f(transform.a),
260 fmt_f(transform.b),
261 fmt_f(transform.c),
262 fmt_f(transform.d),
263 fmt_f(transform.e),
264 fmt_f(transform.f),
265 );
266
267 let (components, cs_name) = match image.format {
269 DecodedImageFormat::Gray8 => (1usize, "DeviceGray"),
270 DecodedImageFormat::Rgb24 => (3usize, "DeviceRGB"),
271 DecodedImageFormat::Rgba32 => (3usize, "DeviceRGB"),
272 };
273 let src_stride = match image.format {
274 DecodedImageFormat::Gray8 => 1usize,
275 DecodedImageFormat::Rgb24 => 3usize,
276 DecodedImageFormat::Rgba32 => 4usize,
277 };
278
279 let _ = writeln!(
281 surface.buf,
282 "{} {} 8 [{} 0 0 -{} 0 {}]\n/{} setcolorspace\ncurrentfile /ASCIIHexDecode filter\nimage",
283 w, h, w, h, h, cs_name
284 );
285
286 for row in 0..h as usize {
288 for col in 0..w as usize {
289 let offset = row * w as usize * src_stride + col * src_stride;
290 for c in 0..components {
291 let byte = image.data.get(offset + c).copied().unwrap_or(0);
292 let _ = write!(surface.buf, "{:02X}", byte);
293 }
294 }
295 }
296 surface.buf.push_str(">\nQ\n");
297 }
298
299 fn push_clip(&mut self, surface: &mut PsSurface, clip: &ClipPath, transform: &Matrix) {
300 surface.buf.push_str("q\n");
301 for entry in &clip.paths {
302 emit_path_ops(&mut surface.buf, &entry.ops, transform);
303 match entry.fill_rule {
304 FillRule::NonZero => surface.buf.push_str("W\n"),
305 FillRule::EvenOdd => surface.buf.push_str("W*\n"),
306 }
307 surface.buf.push_str("newpath\n");
308 }
309 }
310
311 fn pop_clip(&mut self, surface: &mut PsSurface) {
312 surface.buf.push_str("Q\n");
313 }
314
315 fn push_group(
316 &mut self,
317 surface: &mut PsSurface,
318 _blend_mode: BlendMode,
319 _opacity: f32,
320 _isolated: bool,
321 _knockout: bool,
322 ) {
323 surface.buf.push_str("q\n");
325 }
326
327 fn pop_group(&mut self, surface: &mut PsSurface) {
328 surface.buf.push_str("Q\n");
329 }
330
331 fn surface_dimensions(&self, surface: &PsSurface) -> (u32, u32) {
332 (surface.width, surface.height)
333 }
334
335 fn composite_over(&mut self, _dst: &mut PsSurface, _src: &PsSurface) {
336 }
338
339 fn surface_pixels(&self, _surface: &PsSurface) -> Vec<u8> {
340 Vec::new()
342 }
343
344 fn finish(self, surface: PsSurface) -> Bitmap {
345 Bitmap::new(surface.width, surface.height, BitmapFormat::Rgba32)
348 }
349}
350
351#[cfg(test)]
356mod tests {
357 use rpdfium_core::Matrix;
358 use rpdfium_graphics::{ClipPath, FillRule, PathOp};
359
360 use super::*;
361 use crate::color_convert::RgbaColor;
362 use crate::renderdevicedriver_iface::RenderBackend;
363 use crate::stroke::StrokeStyle;
364
365 fn make_rect_ops() -> Vec<PathOp> {
366 vec![
367 PathOp::MoveTo { x: 0.0, y: 0.0 },
368 PathOp::LineTo { x: 100.0, y: 0.0 },
369 PathOp::LineTo { x: 100.0, y: 100.0 },
370 PathOp::LineTo { x: 0.0, y: 100.0 },
371 PathOp::Close,
372 ]
373 }
374
375 fn red() -> RgbaColor {
376 RgbaColor {
377 r: 255,
378 g: 0,
379 b: 0,
380 a: 255,
381 }
382 }
383
384 #[test]
385 fn test_ps_preamble_contains_abbreviations() {
386 let mut backend = PsRenderBackend::new();
387 let mut surface = backend.create_surface(200, 200, &RgbaColor::default());
388 backend.fill_path(
390 &mut surface,
391 &make_rect_ops(),
392 FillRule::NonZero,
393 &red(),
394 &Matrix::identity(),
395 );
396 let bytes = surface.into_bytes();
397 let output = String::from_utf8(bytes).unwrap();
398 assert!(
399 output.contains("/m {moveto} bind def"),
400 "missing /m abbreviation"
401 );
402 assert!(
403 output.contains("/rg {setrgbcolor} bind def"),
404 "missing /rg abbreviation"
405 );
406 assert!(
407 output.contains("/q {gsave} bind def"),
408 "missing /q abbreviation"
409 );
410 }
411
412 #[test]
413 fn test_ps_fill_rect_contains_setrgbcolor() {
414 let mut backend = PsRenderBackend::new();
415 let mut surface = backend.create_surface(100, 100, &RgbaColor::default());
416 backend.fill_path(
417 &mut surface,
418 &make_rect_ops(),
419 FillRule::NonZero,
420 &RgbaColor {
421 r: 51,
422 g: 102,
423 b: 153,
424 a: 255,
425 },
426 &Matrix::identity(),
427 );
428 let bytes = surface.into_bytes();
429 let output = String::from_utf8(bytes).unwrap();
430 assert!(
432 output.contains("rg"),
433 "fill_path must emit rg (setrgbcolor)"
434 );
435 assert!(
437 output.contains("\nf\n") || output.ends_with("\nf\n"),
438 "fill_path must emit f"
439 );
440 }
441
442 #[test]
443 fn test_ps_stroke_path_contains_operators() {
444 let mut backend = PsRenderBackend::new();
445 let mut surface = backend.create_surface(100, 100, &RgbaColor::default());
446 let style = StrokeStyle {
447 width: 2.0,
448 line_cap: rpdfium_graphics::LineCapStyle::Butt,
449 line_join: rpdfium_graphics::LineJoinStyle::Miter,
450 miter_limit: 10.0,
451 dash: None,
452 };
453 backend.stroke_path(
454 &mut surface,
455 &make_rect_ops(),
456 &style,
457 &red(),
458 &Matrix::identity(),
459 );
460 let bytes = surface.into_bytes();
461 let output = String::from_utf8(bytes).unwrap();
462 assert!(
464 output.contains(" w\n"),
465 "stroke_path must emit w (setlinewidth)"
466 );
467 assert!(
469 output.contains("\ns\n") || output.ends_with("s\n"),
470 "stroke_path must emit s"
471 );
472 }
473
474 #[test]
475 fn test_ps_clip_uses_gsave_grestore() {
476 let mut backend = PsRenderBackend::new();
477 let mut surface = backend.create_surface(100, 100, &RgbaColor::default());
478 let mut clip = ClipPath::new();
479 clip.push(make_rect_ops(), FillRule::NonZero);
480 backend.push_clip(&mut surface, &clip, &Matrix::identity());
481 backend.pop_clip(&mut surface);
482 let bytes = surface.into_bytes();
483 let output = String::from_utf8(bytes).unwrap();
484 assert!(output.contains("q\n"), "push_clip must emit q (gsave)");
485 assert!(output.contains("Q\n"), "pop_clip must emit Q (grestore)");
486 }
487
488 #[test]
489 fn test_ps_group_push_pop_uses_gsave_grestore() {
490 let mut backend = PsRenderBackend::new();
491 let mut surface = backend.create_surface(100, 100, &RgbaColor::default());
492 backend.push_group(&mut surface, BlendMode::Normal, 1.0, true, false);
493 backend.pop_group(&mut surface);
494 let bytes = surface.into_bytes();
495 let output = String::from_utf8(bytes).unwrap();
496 assert!(output.contains("q\n"), "push_group must emit q (gsave)");
497 assert!(output.contains("Q\n"), "pop_group must emit Q (grestore)");
498 }
499
500 #[test]
501 fn test_ps_new_raw_omits_preamble() {
502 let mut backend = PsRenderBackend::new_raw();
503 let mut surface = backend.create_surface(100, 100, &RgbaColor::default());
504 backend.fill_path(
505 &mut surface,
506 &make_rect_ops(),
507 FillRule::NonZero,
508 &red(),
509 &Matrix::identity(),
510 );
511 let bytes = surface.into_bytes();
512 let output = String::from_utf8(bytes).unwrap();
513 assert!(!output.contains("%!PS"), "new_raw must not emit PS header");
514 assert!(
515 !output.contains("/m {moveto}"),
516 "new_raw must not emit preamble"
517 );
518 assert!(output.contains("rg"), "raw output must contain rg");
520 }
521
522 #[test]
523 fn test_ps_output_ends_with_eof_marker() {
524 let mut backend = PsRenderBackend::new();
526 let mut surface = backend.create_surface(300, 200, &RgbaColor::default());
527 backend.fill_path(
528 &mut surface,
529 &make_rect_ops(),
530 FillRule::EvenOdd,
531 &red(),
532 &Matrix::identity(),
533 );
534 let bytes = surface.into_bytes();
535 let output = String::from_utf8(bytes).unwrap();
536 assert!(
537 output.contains("%%EOF"),
538 "PS output must contain %%EOF trailer"
539 );
540 assert!(
541 output.ends_with("%%EOF\n"),
542 "%%EOF must be the very last line"
543 );
544 }
545
546 #[test]
547 fn test_ps_surface_dimensions_appear_in_page_bounding_box() {
548 let backend = PsRenderBackend::new();
551 let surface = backend.create_surface(640, 480, &RgbaColor::default());
552 let bytes = surface.into_bytes();
553 let output = String::from_utf8(bytes).unwrap();
554 assert!(
555 output.contains("%%PageBoundingBox: 0 0 640 480"),
556 "PageBoundingBox must encode surface dimensions (640x480); got: {output}"
557 );
558 }
559
560 #[test]
561 fn test_ps_surface_dimensions_from_backend() {
562 let backend = PsRenderBackend::new();
564 let surface = backend.create_surface(320, 240, &RgbaColor::default());
565 let (w, h) = backend.surface_dimensions(&surface);
566 assert_eq!(w, 320, "width must be 320");
567 assert_eq!(h, 240, "height must be 240");
568 }
569
570 #[test]
571 fn test_ps_eofill_operator_for_even_odd_rule() {
572 let mut backend = PsRenderBackend::new_raw();
574 let mut surface = backend.create_surface(100, 100, &RgbaColor::default());
575 backend.fill_path(
576 &mut surface,
577 &make_rect_ops(),
578 FillRule::EvenOdd,
579 &red(),
580 &Matrix::identity(),
581 );
582 let bytes = surface.into_bytes();
583 let output = String::from_utf8(bytes).unwrap();
584 assert!(
585 output.contains("\nF\n") || output.ends_with("F\n"),
586 "EvenOdd fill must emit F (eofill)"
587 );
588 }
589
590 #[test]
591 fn test_ps_moveto_lineto_appear_in_fill_output() {
592 let mut backend = PsRenderBackend::new_raw();
595 let mut surface = backend.create_surface(100, 100, &RgbaColor::default());
596 backend.fill_path(
597 &mut surface,
598 &make_rect_ops(),
599 FillRule::NonZero,
600 &red(),
601 &Matrix::identity(),
602 );
603 let bytes = surface.into_bytes();
604 let output = String::from_utf8(bytes).unwrap();
605 assert!(output.contains(" m\n"), "fill_path must emit m (moveto)");
606 assert!(output.contains(" l\n"), "fill_path must emit l (lineto)");
607 assert!(output.contains("h\n"), "fill_path must emit h (closepath)");
608 }
609}