Skip to main content

justpdf_render/
device.rs

1use tiny_skia::{
2    BlendMode, Color, FillRule, LineCap as SkiaLineCap, LineJoin as SkiaLineJoin, Mask,
3    Paint, Pixmap, Stroke, Transform,
4};
5
6use crate::error::{RenderError, Result};
7use crate::graphics_state;
8
9/// A rendering device that draws PDF operations onto a pixel buffer.
10pub struct PixmapDevice {
11    pub pixmap: Pixmap,
12    pub(crate) clip_mask: Option<Mask>,
13}
14
15impl PixmapDevice {
16    pub fn new(width: u32, height: u32) -> Result<Self> {
17        let pixmap = Pixmap::new(width, height).ok_or_else(|| RenderError::InvalidDimensions {
18            detail: format!("cannot create {width}x{height} pixmap"),
19        })?;
20        Ok(Self {
21            pixmap,
22            clip_mask: None,
23        })
24    }
25
26    /// Set a clipping path (replaces existing clip).
27    pub fn set_clip_path(
28        &mut self,
29        path: &tiny_skia::Path,
30        fill_rule: FillRule,
31        transform: Transform,
32    ) {
33        let w = self.pixmap.width();
34        let h = self.pixmap.height();
35        if let Some(mut mask) = Mask::new(w, h) {
36            mask.fill_path(path, fill_rule, true, transform);
37            self.clip_mask = Some(mask);
38        }
39    }
40
41    /// Intersect the current clip with another path.
42    pub fn intersect_clip_path(
43        &mut self,
44        path: &tiny_skia::Path,
45        fill_rule: FillRule,
46        transform: Transform,
47    ) {
48        if let Some(mask) = &mut self.clip_mask {
49            mask.intersect_path(path, fill_rule, true, transform);
50        } else {
51            self.set_clip_path(path, fill_rule, transform);
52        }
53    }
54
55    /// Clear the clip mask.
56    pub fn clear_clip(&mut self) {
57        self.clip_mask = None;
58    }
59
60    /// Fill a path.
61    pub fn fill_path(
62        &mut self,
63        path: &tiny_skia::Path,
64        fill_rule: FillRule,
65        transform: Transform,
66        color: [u8; 4],
67        blend_mode: BlendMode,
68    ) {
69        let mut paint = Paint::default();
70        paint.set_color(Color::from_rgba8(color[0], color[1], color[2], color[3]));
71        paint.anti_alias = true;
72        paint.blend_mode = blend_mode;
73
74        let clip = self.clip_mask.as_ref();
75        self.pixmap
76            .fill_path(path, &paint, fill_rule, transform, clip);
77    }
78
79    /// Stroke a path.
80    pub fn stroke_path(
81        &mut self,
82        path: &tiny_skia::Path,
83        transform: Transform,
84        color: [u8; 4],
85        gs: &graphics_state::GraphicsState,
86        blend_mode: BlendMode,
87    ) {
88        let mut paint = Paint::default();
89        paint.set_color(Color::from_rgba8(color[0], color[1], color[2], color[3]));
90        paint.anti_alias = true;
91        paint.blend_mode = blend_mode;
92
93        let mut stroke = Stroke::default();
94        stroke.width = gs.line_width as f32;
95        stroke.line_cap = match gs.line_cap {
96            graphics_state::LineCap::Butt => SkiaLineCap::Butt,
97            graphics_state::LineCap::Round => SkiaLineCap::Round,
98            graphics_state::LineCap::Square => SkiaLineCap::Square,
99        };
100        stroke.line_join = match gs.line_join {
101            graphics_state::LineJoin::Miter => SkiaLineJoin::Miter,
102            graphics_state::LineJoin::Round => SkiaLineJoin::Round,
103            graphics_state::LineJoin::Bevel => SkiaLineJoin::Bevel,
104        };
105        stroke.miter_limit = gs.miter_limit as f32;
106
107        if !gs.dash_pattern.is_empty() {
108            let dashes: Vec<f32> = gs.dash_pattern.iter().map(|d| *d as f32).collect();
109            if let Some(dash) = tiny_skia::StrokeDash::new(dashes, gs.dash_phase as f32) {
110                stroke.dash = Some(dash);
111            }
112        }
113
114        let clip = self.clip_mask.as_ref();
115        self.pixmap
116            .stroke_path(path, &paint, &stroke, transform, clip);
117    }
118
119    /// Draw an RGBA image at the given transform.
120    pub fn draw_image(
121        &mut self,
122        image_pixmap: &tiny_skia::PixmapRef,
123        transform: Transform,
124        alpha: f32,
125        blend_mode: BlendMode,
126    ) {
127        let mut paint = tiny_skia::PixmapPaint::default();
128        paint.opacity = alpha;
129        paint.blend_mode = blend_mode;
130        paint.quality = tiny_skia::FilterQuality::Bilinear;
131
132        let clip = self.clip_mask.as_ref();
133        self.pixmap
134            .draw_pixmap(0, 0, *image_pixmap, &paint, transform, clip);
135    }
136
137    /// Draw a pixmap onto this device (for transparency group compositing).
138    pub fn draw_pixmap(
139        &mut self,
140        src: &tiny_skia::PixmapRef,
141        transform: Transform,
142        alpha: f32,
143        blend_mode: BlendMode,
144    ) {
145        let mut paint = tiny_skia::PixmapPaint::default();
146        paint.opacity = alpha;
147        paint.blend_mode = blend_mode;
148        paint.quality = tiny_skia::FilterQuality::Bilinear;
149
150        let clip = self.clip_mask.as_ref();
151        self.pixmap
152            .draw_pixmap(0, 0, *src, &paint, transform, clip);
153    }
154
155    /// Fill a path with a pattern (tiled pixmap).
156    pub fn fill_path_with_pattern(
157        &mut self,
158        path: &tiny_skia::Path,
159        fill_rule: FillRule,
160        transform: Transform,
161        pattern_pixmap: &tiny_skia::PixmapRef,
162        pattern_transform: Transform,
163        blend_mode: BlendMode,
164    ) {
165        let mut paint = Paint::default();
166        paint.anti_alias = true;
167        paint.blend_mode = blend_mode;
168
169        paint.shader = tiny_skia::Pattern::new(
170            *pattern_pixmap,
171            tiny_skia::SpreadMode::Repeat,
172            tiny_skia::FilterQuality::Bilinear,
173            1.0,
174            pattern_transform,
175        );
176
177        let clip = self.clip_mask.as_ref();
178        self.pixmap
179            .fill_path(path, &paint, fill_rule, transform, clip);
180    }
181
182    /// Stroke a path with a pattern (tiled pixmap).
183    pub fn stroke_path_with_pattern(
184        &mut self,
185        path: &tiny_skia::Path,
186        transform: Transform,
187        gs: &graphics_state::GraphicsState,
188        pattern_pixmap: &tiny_skia::PixmapRef,
189        pattern_transform: Transform,
190        blend_mode: BlendMode,
191    ) {
192        let mut paint = Paint::default();
193        paint.anti_alias = true;
194        paint.blend_mode = blend_mode;
195
196        paint.shader = tiny_skia::Pattern::new(
197            *pattern_pixmap,
198            tiny_skia::SpreadMode::Repeat,
199            tiny_skia::FilterQuality::Bilinear,
200            1.0,
201            pattern_transform,
202        );
203
204        let mut stroke = Stroke::default();
205        stroke.width = gs.line_width as f32;
206        stroke.line_cap = match gs.line_cap {
207            graphics_state::LineCap::Butt => SkiaLineCap::Butt,
208            graphics_state::LineCap::Round => SkiaLineCap::Round,
209            graphics_state::LineCap::Square => SkiaLineCap::Square,
210        };
211        stroke.line_join = match gs.line_join {
212            graphics_state::LineJoin::Miter => SkiaLineJoin::Miter,
213            graphics_state::LineJoin::Round => SkiaLineJoin::Round,
214            graphics_state::LineJoin::Bevel => SkiaLineJoin::Bevel,
215        };
216        stroke.miter_limit = gs.miter_limit as f32;
217
218        if !gs.dash_pattern.is_empty() {
219            let dashes: Vec<f32> = gs.dash_pattern.iter().map(|d| *d as f32).collect();
220            if let Some(dash) = tiny_skia::StrokeDash::new(dashes, gs.dash_phase as f32) {
221                stroke.dash = Some(dash);
222            }
223        }
224
225        let clip = self.clip_mask.as_ref();
226        self.pixmap
227            .stroke_path(path, &paint, &stroke, transform, clip);
228    }
229
230    /// Fill the entire pixmap with a color.
231    pub fn clear(&mut self, color: Color) {
232        self.pixmap.fill(color);
233    }
234
235    /// Encode the pixmap as PNG bytes.
236    pub fn encode_png(&self) -> Result<Vec<u8>> {
237        self.pixmap
238            .encode_png()
239            .map_err(|e| RenderError::Encode {
240                detail: e.to_string(),
241            })
242    }
243
244    /// Get the raw RGBA pixel data.
245    pub fn raw_rgba(&self) -> &[u8] {
246        self.pixmap.data()
247    }
248
249    /// Get the pixmap dimensions (width, height).
250    pub fn dimensions(&self) -> (u32, u32) {
251        (self.pixmap.width(), self.pixmap.height())
252    }
253
254    /// Encode the pixmap as JPEG bytes.
255    pub fn encode_jpeg(&self, quality: u8) -> Result<Vec<u8>> {
256        let width = self.pixmap.width();
257        let height = self.pixmap.height();
258        let rgba_data = self.pixmap.data();
259
260        // Convert RGBA to RGB for JPEG
261        let mut rgb_data = Vec::with_capacity((width * height * 3) as usize);
262        for pixel in rgba_data.chunks(4) {
263            rgb_data.push(pixel[0]);
264            rgb_data.push(pixel[1]);
265            rgb_data.push(pixel[2]);
266        }
267
268        let mut buf = std::io::Cursor::new(Vec::new());
269        let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, quality);
270        image::ImageEncoder::write_image(
271            encoder,
272            &rgb_data,
273            width,
274            height,
275            image::ColorType::Rgb8.into(),
276        )
277        .map_err(|e| RenderError::Encode {
278            detail: e.to_string(),
279        })?;
280
281        Ok(buf.into_inner())
282    }
283}