Skip to main content

rpdfium_render/
cfx_psrenderer.rs

1// Derived from PDFium's core/fxge/cfx_psrenderer.h + cfx_psrenderer.cpp
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! PostScript Level 2/3 rendering backend.
7//!
8//! Implements [`RenderBackend`] to emit PostScript operators instead of pixels.
9//! Use [`PsRenderBackend::new`] for standalone PS files (with a preamble that
10//! defines short operator aliases), or [`PsRenderBackend::new_raw`] to embed
11//! the output in a larger PS document.
12//!
13//! The PS content is accumulated in a [`PsSurface`]; call
14//! [`PsSurface::into_bytes`] to retrieve the complete byte sequence,
15//! including a `%%EOF` trailer.
16
17use 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
27// ---------------------------------------------------------------------------
28// Preamble — matches upstream cfx_psrenderer.cpp abbreviation definitions
29// ---------------------------------------------------------------------------
30
31const 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
54// ---------------------------------------------------------------------------
55// PsSurface
56// ---------------------------------------------------------------------------
57
58/// PS output surface — accumulates PostScript operator bytes.
59pub struct PsSurface {
60    pub(crate) buf: String,
61    width: u32,
62    height: u32,
63}
64
65impl PsSurface {
66    /// Consume the surface and return the accumulated PS bytes, including a
67    /// trailing `%%EOF` marker.
68    pub fn into_bytes(mut self) -> Vec<u8> {
69        self.buf.push_str("%%EOF\n");
70        self.buf.into_bytes()
71    }
72}
73
74// ---------------------------------------------------------------------------
75// PsRenderBackend
76// ---------------------------------------------------------------------------
77
78/// PostScript Level 2/3 rendering backend.
79///
80/// Emits PS operators instead of pixels.  Transparency (opacity, blend modes)
81/// is not supported — this matches upstream `cfx_psrenderer` behavior for PS
82/// Level 2, which has no native transparency model.
83pub struct PsRenderBackend {
84    include_preamble: bool,
85}
86
87impl PsRenderBackend {
88    /// Create a backend that emits a standalone PS file with operator
89    /// abbreviation preamble and `%%Page:` markers.
90    pub fn new() -> Self {
91        Self {
92            include_preamble: true,
93        }
94    }
95
96    /// Create a backend that emits raw PS operators without a preamble,
97    /// suitable for embedding in a larger PS document.
98    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
111// ---------------------------------------------------------------------------
112// Internal helpers
113// ---------------------------------------------------------------------------
114
115/// Format a floating-point number in PS-friendly form (drop trailing zeros).
116fn 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
125/// Emit `r g b rg\n` for the given colour.
126fn 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
133/// Emit path ops transformed by `transform`, using PS abbreviations.
134fn 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
174// ---------------------------------------------------------------------------
175// RenderBackend implementation
176// ---------------------------------------------------------------------------
177
178impl 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        // Line width
217        let _ = writeln!(surface.buf, "{} w", fmt_f(style.width as f64));
218        // Line cap (u8 repr: 0=butt, 1=round, 2=square)
219        let _ = writeln!(surface.buf, "{} J", style.line_cap as u8);
220        // Line join (u8 repr: 0=miter, 1=round, 2=bevel)
221        let _ = writeln!(surface.buf, "{} j", style.line_join as u8);
222        // Miter limit
223        let _ = writeln!(surface.buf, "{} M", fmt_f(style.miter_limit as f64));
224        // Dash pattern
225        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        // Emit gsave + concat to apply transform
255        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        // Determine color space and bytes-per-pixel for source data
268        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        // PS Level 2 image operator with ASCIIHex encoding
280        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        // Emit hex-encoded pixels (strip alpha for RGBA)
287        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        // PS Level 2 has no native transparency; emit gsave as best effort.
324        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        // No-op: PS has no concept of surface-level compositing.
337    }
338
339    fn surface_pixels(&self, _surface: &PsSurface) -> Vec<u8> {
340        // PS backend does not produce a pixel buffer.
341        Vec::new()
342    }
343
344    fn finish(self, surface: PsSurface) -> Bitmap {
345        // PS backend does not produce a pixel bitmap.
346        // Retrieve PS content via PsSurface::into_bytes() before calling finish().
347        Bitmap::new(surface.width, surface.height, BitmapFormat::Rgba32)
348    }
349}
350
351// ---------------------------------------------------------------------------
352// Tests
353// ---------------------------------------------------------------------------
354
355#[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        // Draw something so the surface is not trivially empty
389        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        // Color should appear as "rg"
431        assert!(
432            output.contains("rg"),
433            "fill_path must emit rg (setrgbcolor)"
434        );
435        // Fill operator
436        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        // Line width operator
463        assert!(
464            output.contains(" w\n"),
465            "stroke_path must emit w (setlinewidth)"
466        );
467        // Stroke operator
468        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        // But rg and f should still be present
519        assert!(output.contains("rg"), "raw output must contain rg");
520    }
521
522    #[test]
523    fn test_ps_output_ends_with_eof_marker() {
524        // into_bytes() must always append %%EOF regardless of content.
525        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        // The %%PageBoundingBox comment must reflect the exact width and height
549        // passed to create_surface, confirming dimensions are preserved.
550        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        // surface_dimensions() must return the exact values given to create_surface.
563        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        // fill_path with EvenOdd must emit "F" (eofill abbreviation), not "f".
573        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        // fill_path must emit "m" (moveto) and "l" (lineto) PS abbreviations
593        // for the path operations, confirming drawing commands produce PS syntax.
594        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}