Skip to main content

stet_core/
device.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Output device trait — abstraction boundary for rendering backends.
6
7use crate::display_list::{DisplayElement, DisplayList};
8use crate::graphics_state::PsPath;
9
10// ── Re-exports from stet-graphics ───────────────────────────────────────────
11// These types used to be defined here. Re-export for backward compatibility.
12pub use stet_graphics::device::{
13    AxialShadingParams, BgUcrState, CMYK_ALL, CMYK_C, CMYK_K, CMYK_M, CMYK_Y, ClipParams,
14    ColorStop, FillParams, HalftoneScreen, HalftoneState, ImageColorSpace, ImageParams,
15    MeshShadingParams, PatchShadingParams, PatternFillParams, RadialShadingParams,
16    ShadingColorSpace, ShadingPatch, ShadingTriangle, ShadingVertex, SimpleColorSpace, SpotColor,
17    SpotColorSpace, StrokeParams, TextParams, TintLookupTable, TransferState, TransferTable,
18    cmyk_channel_for_name,
19};
20pub use stet_graphics::device::{PageSink, PageSinkFactory};
21
22/// Trait for raster rendering devices.
23///
24/// Operators never see the concrete implementation — they call trait methods.
25/// This enables backend swaps (tiny-skia, cairo, etc.) without changing operator code.
26pub trait OutputDevice {
27    /// Fill a path with the given color.
28    fn fill_path(&mut self, path: &PsPath, params: &FillParams);
29
30    /// Stroke a path with the given parameters.
31    fn stroke_path(&mut self, path: &PsPath, params: &StrokeParams);
32
33    /// Intersect the current clip region with the given path.
34    fn clip_path(&mut self, path: &PsPath, params: &ClipParams);
35
36    /// Reset clipping to the full page.
37    fn init_clip(&mut self);
38
39    /// Erase the page (fill with white).
40    fn erase_page(&mut self);
41
42    /// Output the current page (e.g., save PNG) and return Ok/Err.
43    fn show_page(&mut self, output_path: &str) -> Result<(), String>;
44
45    /// Draw an image from raw sample data.
46    fn draw_image(&mut self, sample_data: &[u8], params: &ImageParams);
47
48    /// Paint an axial (linear) gradient shading.
49    fn paint_axial_shading(&mut self, _params: &AxialShadingParams) {}
50
51    /// Paint a radial gradient shading.
52    fn paint_radial_shading(&mut self, _params: &RadialShadingParams) {}
53
54    /// Paint a Gouraud-shaded triangle mesh.
55    fn paint_mesh_shading(&mut self, _params: &MeshShadingParams) {}
56
57    /// Paint a Coons/tensor-product patch mesh.
58    fn paint_patch_shading(&mut self, _params: &PatchShadingParams) {}
59
60    /// Paint a tiled pattern fill.
61    fn paint_pattern_fill(&mut self, _params: &PatternFillParams) {}
62
63    /// Set the trim box for the next page (PDF points, lower-left origin).
64    /// Only meaningful for PDF output; other devices ignore this.
65    fn set_trim_box(&mut self, _llx: f64, _lly: f64, _urx: f64, _ury: f64) {}
66
67    /// Page dimensions in device pixels.
68    fn page_size(&self) -> (u32, u32);
69
70    /// Replay a display list and write output in one step.
71    fn replay_and_show(&mut self, list: DisplayList, output_path: &str) -> Result<(), String> {
72        for element in list.elements() {
73            match element {
74                DisplayElement::Fill { path, params } => self.fill_path(path, params),
75                DisplayElement::Stroke { path, params } => self.stroke_path(path, params),
76                DisplayElement::Clip { path, params } => self.clip_path(path, params),
77                DisplayElement::InitClip => self.init_clip(),
78                DisplayElement::Image {
79                    sample_data,
80                    params,
81                } => self.draw_image(sample_data, params),
82                DisplayElement::ErasePage => self.erase_page(),
83                DisplayElement::AxialShading { params } => self.paint_axial_shading(params),
84                DisplayElement::RadialShading { params } => self.paint_radial_shading(params),
85                DisplayElement::MeshShading { params } => self.paint_mesh_shading(params),
86                DisplayElement::PatchShading { params } => self.paint_patch_shading(params),
87                DisplayElement::PatternFill { params } => self.paint_pattern_fill(params),
88                DisplayElement::Text { .. } => {} // PDF-only, ignored by rasterizer
89                DisplayElement::Group { .. } | DisplayElement::SoftMasked { .. } => {
90                    // Groups/SoftMasked are handled by the banded renderer (SkiaDevice).
91                }
92                DisplayElement::OcgGroup {
93                    elements,
94                    visibility,
95                } => {
96                    // Clip ops always apply so subsequent top-level elements
97                    // inherit the right clip region, even when the layer is
98                    // hidden; paint ops are gated on visibility.
99                    let visible = visibility.default_visible();
100                    for elem in elements.elements() {
101                        match elem {
102                            DisplayElement::Clip { path, params } => self.clip_path(path, params),
103                            DisplayElement::InitClip => self.init_clip(),
104                            _ if !visible => {}
105                            DisplayElement::Fill { path, params } => self.fill_path(path, params),
106                            DisplayElement::Stroke { path, params } => {
107                                self.stroke_path(path, params)
108                            }
109                            DisplayElement::Image {
110                                sample_data,
111                                params,
112                            } => self.draw_image(sample_data, params),
113                            DisplayElement::ErasePage => self.erase_page(),
114                            DisplayElement::AxialShading { params } => {
115                                self.paint_axial_shading(params)
116                            }
117                            DisplayElement::RadialShading { params } => {
118                                self.paint_radial_shading(params)
119                            }
120                            DisplayElement::MeshShading { params } => {
121                                self.paint_mesh_shading(params)
122                            }
123                            DisplayElement::PatchShading { params } => {
124                                self.paint_patch_shading(params)
125                            }
126                            DisplayElement::PatternFill { params } => {
127                                self.paint_pattern_fill(params)
128                            }
129                            _ => {}
130                        }
131                    }
132                }
133                _ => {}
134            }
135        }
136        self.show_page(output_path)
137    }
138
139    /// Wait for any pending background render to complete.
140    fn finish(&mut self) -> Result<(), String> {
141        Ok(())
142    }
143
144    /// Called after interpretation finishes, with access to the interpreter context.
145    fn finish_with_context(&mut self, _ctx: &crate::context::Context) -> Result<(), String> {
146        self.finish()
147    }
148
149    /// Downcast to a concrete type. Override in implementations that need
150    /// to be accessed after rendering (e.g., PdfDevice for in-memory output).
151    fn as_any(&self) -> &dyn std::any::Any {
152        // Default: return a unit reference (downcasts will fail gracefully)
153        &()
154    }
155}
156
157/// A null rendering device that discards all output.
158pub struct NullDevice {
159    width: u32,
160    height: u32,
161}
162
163impl NullDevice {
164    pub fn new(width: u32, height: u32) -> Self {
165        Self { width, height }
166    }
167}
168
169impl OutputDevice for NullDevice {
170    fn fill_path(&mut self, _path: &PsPath, _params: &FillParams) {}
171    fn stroke_path(&mut self, _path: &PsPath, _params: &StrokeParams) {}
172    fn clip_path(&mut self, _path: &PsPath, _params: &ClipParams) {}
173    fn init_clip(&mut self) {}
174    fn erase_page(&mut self) {}
175    fn show_page(&mut self, _output_path: &str) -> Result<(), String> {
176        Ok(())
177    }
178    fn draw_image(&mut self, _sample_data: &[u8], _params: &ImageParams) {}
179    fn page_size(&self) -> (u32, u32) {
180        (self.width, self.height)
181    }
182}
183
184/// Replay a display list to any raster device.
185pub fn replay_to_device(list: &DisplayList, device: &mut dyn OutputDevice) {
186    for element in list.elements() {
187        match element {
188            DisplayElement::Fill { path, params } => {
189                device.fill_path(path, params);
190            }
191            DisplayElement::Stroke { path, params } => {
192                device.stroke_path(path, params);
193            }
194            DisplayElement::Clip { path, params } => {
195                device.clip_path(path, params);
196            }
197            DisplayElement::InitClip => {
198                device.init_clip();
199            }
200            DisplayElement::Image {
201                sample_data,
202                params,
203            } => {
204                device.draw_image(sample_data, params);
205            }
206            DisplayElement::ErasePage => {
207                device.erase_page();
208            }
209            DisplayElement::AxialShading { params } => {
210                device.paint_axial_shading(params);
211            }
212            DisplayElement::RadialShading { params } => {
213                device.paint_radial_shading(params);
214            }
215            DisplayElement::MeshShading { params } => {
216                device.paint_mesh_shading(params);
217            }
218            DisplayElement::PatchShading { params } => {
219                device.paint_patch_shading(params);
220            }
221            DisplayElement::PatternFill { params } => {
222                device.paint_pattern_fill(params);
223            }
224            DisplayElement::Text { .. } => {}
225            DisplayElement::Group { elements, .. } => {
226                replay_to_device(elements, device);
227            }
228            DisplayElement::SoftMasked { content, .. } => {
229                replay_to_device(content, device);
230            }
231            DisplayElement::OcgGroup {
232                elements,
233                visibility,
234            } => {
235                if visibility.default_visible() {
236                    replay_to_device(elements, device);
237                } else {
238                    // Still replay Clip/InitClip so they affect subsequent
239                    // top-level elements (see OcgGroup render path for the
240                    // rationale).
241                    for elem in elements.elements() {
242                        match elem {
243                            DisplayElement::Clip { path, params } => {
244                                device.clip_path(path, params);
245                            }
246                            DisplayElement::InitClip => {
247                                device.init_clip();
248                            }
249                            _ => {}
250                        }
251                    }
252                }
253            }
254            _ => {}
255        }
256    }
257}