piet_common/
web_back.rs

1// Copyright 2019 the Piet Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Support for piet Web back-end.
5
6use std::fmt;
7use std::marker::PhantomData;
8use std::path::Path;
9
10#[cfg(feature = "png")]
11use std::fs::File;
12#[cfg(feature = "png")]
13use std::io::BufWriter;
14
15#[cfg(feature = "png")]
16use png::{ColorType, Encoder};
17use wasm_bindgen::JsCast;
18
19use piet::{ImageBuf, ImageFormat};
20#[doc(hidden)]
21pub use piet_web::*;
22
23pub type Piet<'a> = WebRenderContext<'a>;
24
25/// The associated brush type for this backend.
26///
27/// This type matches `RenderContext::Brush`
28pub type Brush = piet_web::Brush;
29
30/// The associated text factory for this backend.
31///
32/// This type matches `RenderContext::Text`
33pub type PietText = WebText;
34
35/// The associated text layout type for this backend.
36///
37/// This type matches `RenderContext::Text::TextLayout`
38pub type PietTextLayout = WebTextLayout;
39
40/// The associated text layout builder for this backend.
41///
42/// This type matches `RenderContext::Text::TextLayoutBuilder`
43pub type PietTextLayoutBuilder = WebTextLayoutBuilder;
44
45/// The associated image type for this backend.
46///
47/// This type matches `RenderContext::Image`
48pub type PietImage = WebImage;
49
50/// A struct that can be used to create bitmap render contexts.
51pub struct Device {
52    // Since not all backends can support `Device: Sync`, make it non-Sync here to, for fewer
53    // portability surprises.
54    marker: std::marker::PhantomData<*const ()>,
55}
56
57unsafe impl Send for Device {}
58
59/// A struct provides a `RenderContext` and then can have its bitmap extracted.
60pub struct BitmapTarget<'a> {
61    canvas: web_sys::HtmlCanvasElement,
62    context: web_sys::CanvasRenderingContext2d,
63    phantom: PhantomData<&'a ()>,
64}
65
66impl Device {
67    /// Create a new device.
68    pub fn new() -> Result<Device, piet::Error> {
69        Ok(Device {
70            marker: std::marker::PhantomData,
71        })
72    }
73
74    /// Create a new bitmap target.
75    pub fn bitmap_target(
76        &mut self,
77        width: usize,
78        height: usize,
79        pix_scale: f64,
80    ) -> Result<BitmapTarget<'_>, piet::Error> {
81        let document = web_sys::window().unwrap().document().unwrap();
82        let canvas = document
83            .create_element("canvas")
84            .unwrap()
85            .dyn_into::<web_sys::HtmlCanvasElement>()
86            .unwrap();
87        let context = canvas
88            .get_context("2d")
89            .unwrap()
90            .unwrap()
91            .dyn_into::<web_sys::CanvasRenderingContext2d>()
92            .unwrap();
93
94        canvas.set_width(width as u32);
95        canvas.set_height(height as u32);
96        let _ = context.scale(pix_scale, pix_scale);
97
98        Ok(BitmapTarget {
99            canvas,
100            context,
101            phantom: Default::default(),
102        })
103    }
104}
105
106impl<'a> BitmapTarget<'a> {
107    /// Get a piet `RenderContext` for the bitmap.
108    pub fn render_context(&mut self) -> WebRenderContext<'_> {
109        WebRenderContext::new(self.context.clone(), web_sys::window().unwrap())
110    }
111
112    /// Get raw RGBA pixels from the bitmap.
113    fn raw_pixels(&mut self, fmt: ImageFormat) -> Result<Vec<u8>, piet::Error> {
114        // TODO: This code is just a snippet. A thorough review and testing should be done before
115        // this is used. It is here for compatibility with druid.
116
117        if fmt != ImageFormat::RgbaPremul {
118            return Err(piet::Error::NotSupported);
119        }
120
121        let width = self.canvas.width() as usize;
122        let height = self.canvas.height() as usize;
123
124        let img_data = self
125            .context
126            .get_image_data(0.0, 0.0, width as f64, height as f64)
127            .map_err(|jsv| piet::Error::BackendError(Box::new(JsError::new(jsv))))?;
128
129        // ImageDate is in RGBA order. This should be the same as expected on the output.
130        Ok(img_data.data().0)
131    }
132
133    /// Get an in-memory pixel buffer from the bitmap.
134    // Clippy complains about a to_xxx method taking &mut self. Semantically speaking, this is not
135    // really a mutation, so we'll keep the name. Consider using interior mutability in the future.
136    #[allow(clippy::wrong_self_convention)]
137    pub fn to_image_buf(&mut self, fmt: ImageFormat) -> Result<ImageBuf, piet::Error> {
138        let data = self.raw_pixels(fmt)?;
139        let width = self.canvas.width() as usize;
140        let height = self.canvas.height() as usize;
141        Ok(ImageBuf::from_raw(data, fmt, width, height))
142    }
143
144    /// Get raw RGBA pixels from the bitmap by copying them into `buf`. If all the pixels were
145    /// copied, returns the number of bytes written. If `buf` wasn't big enough, returns an error
146    /// and doesn't write anything.
147    pub fn copy_raw_pixels(
148        &mut self,
149        fmt: ImageFormat,
150        buf: &mut [u8],
151    ) -> Result<usize, piet::Error> {
152        let data = self.raw_pixels(fmt)?;
153        if data.len() > buf.len() {
154            return Err(piet::Error::InvalidInput);
155        }
156        buf.copy_from_slice(&data[..]);
157        Ok(data.len())
158    }
159
160    /// Save bitmap to RGBA PNG file
161    #[cfg(feature = "png")]
162    pub fn save_to_file<P: AsRef<Path>>(mut self, path: P) -> Result<(), piet::Error> {
163        let height = self.canvas.height();
164        let width = self.canvas.width();
165        let image = self.raw_pixels(ImageFormat::RgbaPremul)?;
166        let file = BufWriter::new(File::create(path).map_err(Into::<Box<_>>::into)?);
167        let mut encoder = Encoder::new(file, width, height);
168        encoder.set_color(ColorType::Rgba);
169        encoder
170            .write_header()
171            .map_err(Into::<Box<_>>::into)?
172            .write_image_data(&image)
173            .map_err(Into::<Box<_>>::into)?;
174        Ok(())
175    }
176
177    /// Stub for feature is missing
178    #[cfg(not(feature = "png"))]
179    pub fn save_to_file<P: AsRef<Path>>(self, _path: P) -> Result<(), piet::Error> {
180        Err(piet::Error::MissingFeature("png"))
181    }
182}
183
184#[derive(Clone, Debug)]
185struct JsError {
186    jsv: wasm_bindgen::JsValue,
187}
188
189impl JsError {
190    fn new(jsv: wasm_bindgen::JsValue) -> Self {
191        JsError { jsv }
192    }
193}
194
195impl fmt::Display for JsError {
196    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
197        write!(f, "{:?}", self.jsv)
198    }
199}
200
201impl std::error::Error for JsError {}