reflexo_vec2canvas/
lib.rs

1// todo
2#![allow(clippy::arc_with_non_send_sync)]
3
4mod bounds;
5mod device;
6#[cfg(feature = "incremental")]
7mod incr;
8mod ops;
9#[cfg(feature = "rasterize_glyph")]
10mod pixglyph_canvas;
11mod utils;
12
13pub use bounds::BBoxAt;
14pub use device::CanvasDevice;
15#[cfg(feature = "incremental")]
16pub use incr::*;
17use js_sys::Promise;
18pub use ops::*;
19use web_sys::{Blob, HtmlImageElement, OffscreenCanvas, OffscreenCanvasRenderingContext2d};
20
21use std::{
22    cell::OnceCell,
23    fmt::Debug,
24    sync::{Arc, Mutex},
25};
26
27use ecow::EcoVec;
28use reflexo::{
29    hash::Fingerprint,
30    vector::{
31        ir::{
32            self, Abs, Axes, FontIndice, FontItem, FontRef, Image, ImmutStr, Module, Point, Ratio,
33            Rect, Scalar, Size,
34        },
35        vm::{GroupContext, RenderVm, TransformContext},
36    },
37};
38use tiny_skia as sk;
39use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
40
41use bounds::*;
42
43/// All the features that can be enabled or disabled.
44pub trait ExportFeature {
45    /// Whether to enable tracing.
46    const ENABLE_TRACING: bool;
47}
48
49/// The default feature set which is used for exporting full-fledged canvas.
50pub struct DefaultExportFeature;
51/// The default feature set which is used for exporting canvas for printing.
52pub type DefaultCanvasTask = CanvasTask<DefaultExportFeature>;
53
54impl ExportFeature for DefaultExportFeature {
55    const ENABLE_TRACING: bool = false;
56}
57
58#[derive(Clone, Copy)]
59pub struct BrowserFontMetric {
60    pub semi_char_width: f32,
61    pub full_char_width: f32,
62    pub emoji_width: f32,
63    // height: f32,
64}
65
66impl BrowserFontMetric {
67    pub fn from_env() -> Self {
68        let v = OffscreenCanvas::new(0, 0).expect("offscreen canvas is not supported");
69        let ctx = v
70            .get_context("2d")
71            .unwrap()
72            .unwrap()
73            .dyn_into::<OffscreenCanvasRenderingContext2d>()
74            .unwrap();
75
76        let _g = CanvasStateGuard::new(&ctx);
77        ctx.set_font("128px monospace");
78        let metrics = ctx.measure_text("A").unwrap();
79        let semi_char_width = metrics.width();
80        let metrics = ctx.measure_text("喵").unwrap();
81        let full_char_width = metrics.width();
82        let metrics = ctx.measure_text("🦄").unwrap();
83        let emoji_width = metrics.width();
84        // let a_height =
85        //     (metrics.font_bounding_box_descent() +
86        // metrics.font_bounding_box_ascent()).abs();
87
88        Self {
89            semi_char_width: (semi_char_width / 128.) as f32,
90            full_char_width: (full_char_width / 128.) as f32,
91            emoji_width: (emoji_width / 128.) as f32,
92            // height: (a_height / 128.) as f32,
93        }
94    }
95
96    /// Create a new instance for testing.
97    /// The width are prime numbers for helping test.
98    pub fn new_test() -> Self {
99        Self {
100            semi_char_width: 2.0,
101            full_char_width: 3.0,
102            emoji_width: 5.0,
103            // height: 7.0,
104        }
105    }
106}
107
108/// A rendered page of canvas.
109#[derive(Clone)]
110pub struct CanvasPage {
111    /// A rendered canvas element.
112    pub elem: CanvasNode,
113    /// The fingerprint of the content for identifying page difference.
114    pub content: Fingerprint,
115    /// The size of the page.
116    pub size: Size,
117}
118
119/// The task context for exporting canvas.
120/// It is also as a namespace for all the functions used in the task.
121pub struct CanvasTask<Feat: ExportFeature> {
122    _feat_phantom: std::marker::PhantomData<Feat>,
123}
124
125/// Unfortunately, `Default` derive does not work for generic structs.
126impl<Feat: ExportFeature> Default for CanvasTask<Feat> {
127    fn default() -> Self {
128        Self {
129            _feat_phantom: std::marker::PhantomData,
130        }
131    }
132}
133
134impl<Feat: ExportFeature> CanvasTask<Feat> {
135    /// fork a render task with module.
136    pub fn fork_canvas_render_task<'m, 't>(
137        &'t mut self,
138        module: &'m ir::Module,
139    ) -> CanvasRenderTask<'m, 't, Feat> {
140        CanvasRenderTask::<Feat> {
141            module,
142
143            use_stable_glyph_id: true,
144
145            _feat_phantom: Default::default(),
146        }
147    }
148}
149
150trait GlyphFactory {
151    fn get_glyph(&mut self, font: &FontItem, glyph: u32, fill: ImmutStr) -> Option<CanvasNode>;
152}
153
154/// Holds the data for rendering canvas.
155///
156/// The 'm lifetime is the lifetime of the module which stores the frame data.
157/// The 't lifetime is the lifetime of SVG task.
158pub struct CanvasRenderTask<'m, 't, Feat: ExportFeature> {
159    /// The module which stores the frame data.
160    pub module: &'m Module,
161
162    /// See [`ExportFeature`].
163    pub use_stable_glyph_id: bool,
164
165    _feat_phantom: std::marker::PhantomData<&'t Feat>,
166}
167
168impl<'m, Feat: ExportFeature> FontIndice<'m> for CanvasRenderTask<'m, '_, Feat> {
169    fn get_font(&self, value: &FontRef) -> Option<&'m ir::FontItem> {
170        self.module.fonts.get(value.idx as usize)
171    }
172}
173
174impl<Feat: ExportFeature> GlyphFactory for CanvasRenderTask<'_, '_, Feat> {
175    fn get_glyph(&mut self, font: &FontItem, glyph: u32, fill: ImmutStr) -> Option<CanvasNode> {
176        let glyph_data = font.get_glyph(glyph)?;
177        Some(Arc::new(CanvasElem::Glyph(CanvasGlyphElem {
178            fill,
179            upem: font.units_per_em,
180            glyph_data: glyph_data.clone(),
181        })))
182    }
183}
184
185impl<'m, Feat: ExportFeature> RenderVm<'m> for CanvasRenderTask<'m, '_, Feat> {
186    // type Resultant = String;
187    type Resultant = CanvasNode;
188    type Group = CanvasStack;
189
190    fn get_item(&self, value: &Fingerprint) -> Option<&'m ir::VecItem> {
191        self.module.get_item(value)
192    }
193
194    fn start_group(&mut self, _v: &Fingerprint) -> Self::Group {
195        Self::Group {
196            kind: GroupKind::General,
197            ts: sk::Transform::identity(),
198            clipper: None,
199            fill: None,
200            inner: EcoVec::new(),
201            rect: CanvasBBox::Dynamic(Box::new(OnceCell::new())),
202        }
203    }
204
205    fn start_text(&mut self, value: &Fingerprint, text: &ir::TextItem) -> Self::Group {
206        let mut g = self.start_group(value);
207        g.kind = GroupKind::Text;
208        g.rect = {
209            // upem is the unit per em defined in the font.
210            let font = self.get_font(&text.shape.font).unwrap();
211            let upem = Scalar(font.units_per_em.0);
212            let accender = Scalar(font.ascender.0) * upem;
213
214            // todo: glyphs like macron has zero width... why?
215            let w = text.width();
216
217            if text.shape.size.0 == 0. {
218                CanvasBBox::Static(Box::new(Rect {
219                    lo: Point::new(Scalar(0.), accender - upem),
220                    hi: Point::new(Scalar(0.), accender),
221                }))
222            } else {
223                CanvasBBox::Static(Box::new(Rect {
224                    lo: Point::new(Scalar(0.), accender - upem),
225                    hi: Point::new(w * upem / text.shape.size, accender),
226                }))
227            }
228        };
229        for style in &text.shape.styles {
230            if let ir::PathStyle::Fill(fill) = style {
231                g.fill = Some(fill.clone());
232            }
233        }
234        g
235    }
236}
237
238/// A stacked builder for [`CanvasNode`].
239///
240/// It holds state of the building process.
241pub struct CanvasStack {
242    /// The kind of the group.
243    pub kind: GroupKind,
244    /// The transform matrix.
245    pub ts: sk::Transform,
246    /// A unique clip path on stack
247    pub clipper: Option<ir::PathItem>,
248    /// The fill color.
249    pub fill: Option<ImmutStr>,
250    /// The inner elements.
251    pub inner: EcoVec<(ir::Point, CanvasNode)>,
252    /// The bounding box of the group.
253    pub rect: CanvasBBox,
254}
255
256impl From<CanvasStack> for CanvasNode {
257    fn from(s: CanvasStack) -> Self {
258        let inner: CanvasNode = Arc::new(CanvasElem::Group(CanvasGroupElem {
259            ts: Box::new(s.ts),
260            inner: s.inner,
261            kind: s.kind,
262            rect: s.rect,
263        }));
264        if let Some(clipper) = s.clipper {
265            Arc::new(CanvasElem::Clip(CanvasClipElem {
266                d: clipper.d,
267                inner,
268                clip_bbox: CanvasBBox::Dynamic(Box::new(OnceCell::new())),
269            }))
270        } else {
271            inner
272        }
273    }
274}
275
276/// See [`TransformContext`].
277impl<C> TransformContext<C> for CanvasStack {
278    fn transform_matrix(mut self, _ctx: &mut C, m: &ir::Transform) -> Self {
279        let sub_ts: sk::Transform = (*m).into();
280        self.ts = self.ts.post_concat(sub_ts);
281        self
282    }
283
284    fn transform_translate(mut self, _ctx: &mut C, matrix: Axes<Abs>) -> Self {
285        self.ts = self.ts.post_translate(matrix.x.0, matrix.y.0);
286        self
287    }
288
289    fn transform_scale(mut self, _ctx: &mut C, x: Ratio, y: Ratio) -> Self {
290        self.ts = self.ts.post_scale(x.0, y.0);
291        self
292    }
293
294    fn transform_rotate(self, _ctx: &mut C, _matrix: Scalar) -> Self {
295        todo!()
296    }
297
298    fn transform_skew(mut self, _ctx: &mut C, matrix: (Ratio, Ratio)) -> Self {
299        self.ts = self.ts.post_concat(sk::Transform {
300            sx: 1.,
301            sy: 1.,
302            kx: matrix.0 .0,
303            ky: matrix.1 .0,
304            tx: 0.,
305            ty: 0.,
306        });
307        self
308    }
309
310    fn transform_clip(mut self, _ctx: &mut C, matrix: &ir::PathItem) -> Self {
311        self.clipper = Some(matrix.clone());
312        self
313    }
314}
315
316/// See [`GroupContext`].
317impl<'m, C: RenderVm<'m, Resultant = CanvasNode> + GlyphFactory> GroupContext<C> for CanvasStack {
318    fn render_path(&mut self, _ctx: &mut C, path: &ir::PathItem, _abs_ref: &Fingerprint) {
319        self.inner.push((
320            ir::Point::default(),
321            Arc::new(CanvasElem::Path(CanvasPathElem {
322                path_data: Box::new(path.clone()),
323                rect: CanvasBBox::Dynamic(Box::new(OnceCell::new())),
324            })),
325        ))
326    }
327
328    fn render_image(&mut self, _ctx: &mut C, image_item: &ir::ImageItem) {
329        self.inner.push((
330            ir::Point::default(),
331            Arc::new(CanvasElem::Image(CanvasImageElem {
332                image_data: image_item.clone(),
333            })),
334        ))
335    }
336
337    fn render_item_at(&mut self, ctx: &mut C, pos: crate::ir::Point, item: &Fingerprint) {
338        self.inner.push((pos, ctx.render_item(item)));
339    }
340
341    fn render_glyph(&mut self, ctx: &mut C, pos: Scalar, font: &FontItem, glyph: u32) {
342        if let Some(glyph) = ctx.get_glyph(font, glyph, self.fill.clone().unwrap()) {
343            self.inner.push((ir::Point::new(pos, Scalar(0.)), glyph));
344        }
345    }
346}
347
348#[inline]
349#[must_use]
350fn set_transform(canvas: &dyn CanvasDevice, transform: sk::Transform) -> bool {
351    if transform.sx == 0. || transform.sy == 0. {
352        return false;
353    }
354
355    // see sync_transform
356    let a = transform.sx as f64;
357    let b = transform.ky as f64;
358    let c = transform.kx as f64;
359    let d = transform.sy as f64;
360    let e = transform.tx as f64;
361    let f = transform.ty as f64;
362
363    canvas.set_transform(a, b, c, d, e, f);
364    true
365}
366
367/// A guard for saving and restoring the canvas state.
368///
369/// When the guard is created, a cheap checkpoint of the canvas state is saved.
370/// When the guard is dropped, the canvas state is restored.
371pub struct CanvasStateGuard<'a>(&'a dyn CanvasDevice);
372
373impl<'a> CanvasStateGuard<'a> {
374    pub fn new(context: &'a dyn CanvasDevice) -> Self {
375        context.save();
376        Self(context)
377    }
378}
379
380impl Drop for CanvasStateGuard<'_> {
381    fn drop(&mut self) {
382        self.0.restore();
383    }
384}
385
386#[derive(Debug, Clone)]
387struct UnsafeMemorize<T>(T);
388
389// Safety: `UnsafeMemorize` is only used in wasm targets
390unsafe impl<T> Send for UnsafeMemorize<T> {}
391// Safety: `UnsafeMemorize` is only used in wasm targets
392unsafe impl<T> Sync for UnsafeMemorize<T> {}
393
394#[derive(Debug, Clone)]
395struct LazyImage {
396    elem: Promise,
397    loaded: Arc<Mutex<Option<JsValue>>>,
398}
399
400fn create_image(image: Arc<Image>) -> Option<LazyImage> {
401    let is_svg = image.format.contains("svg");
402
403    web_sys::console::log_1(&format!("image format: {:?}", image.format).into());
404
405    let u = js_sys::Uint8Array::new_with_length(image.data.len() as u32);
406    u.copy_from(&image.data);
407
408    let f = format!("image/{}", image.format);
409
410    let blob = || {
411        let parts = js_sys::Array::new();
412        parts.push(&u);
413
414        let tag = web_sys::BlobPropertyBag::new();
415        tag.set_type(&f);
416        web_sys::Blob::new_with_u8_array_sequence_and_options(
417            &parts,
418            // todo: security check
419            // https://security.stackexchange.com/questions/148507/how-to-prevent-xss-in-svg-file-upload
420            // todo: use our custom font
421            &tag,
422        )
423        .unwrap()
424    };
425
426    let res = match web_sys::window() {
427        Some(e) => {
428            if is_svg {
429                let blob = blob();
430                Some(wasm_bindgen_futures::future_to_promise(async move {
431                    // todo: image-rendering is not respected
432                    let img = HtmlImageElement::new().unwrap();
433                    let p = exception_create_image_blob(&blob, &img);
434                    p.await;
435                    Ok(html_image_to_bitmap(&img).into())
436                }))
437            } else {
438                e.create_image_bitmap_with_blob(&blob()).ok()
439            }
440        }
441        None => {
442            let this = js_sys::global()
443                .dyn_into::<web_sys::WorkerGlobalScope>()
444                .unwrap();
445            if is_svg {
446                js_sys::Reflect::get(&this, &JsValue::from_str("loadSvg"))
447                    .unwrap()
448                    .dyn_into::<js_sys::Function>()
449                    .unwrap()
450                    .call2(&JsValue::NULL, &u, &f.into())
451                    .unwrap()
452                    .dyn_into::<Promise>()
453                    .ok()
454            } else {
455                this.create_image_bitmap_with_blob(&blob()).ok()
456            }
457        }
458    };
459
460    let loaded = Arc::new(Mutex::new(None));
461
462    let elem = res.map(|elem| {
463        let loaded_that = loaded.clone();
464        wasm_bindgen_futures::future_to_promise(async move {
465            let elem = wasm_bindgen_futures::JsFuture::from(elem).await?;
466            *loaded_that.lock().unwrap() = Some(elem.clone());
467            Ok(elem)
468        })
469    });
470
471    elem.map(|elem| LazyImage { elem, loaded })
472}
473
474pub fn html_image_to_bitmap(img: &HtmlImageElement) -> web_sys::ImageBitmap {
475    let canvas = web_sys::OffscreenCanvas::new(img.width(), img.height()).unwrap();
476
477    let ctx = canvas
478        .get_context("2d")
479        .expect("get context 2d")
480        .expect("get context 2d");
481    let ctx = ctx
482        .dyn_into::<web_sys::OffscreenCanvasRenderingContext2d>()
483        .expect("must be OffscreenCanvasRenderingContext2d");
484    ctx.draw_image_with_html_image_element(img, 0., 0.)
485        .expect("must draw_image_with_html_image_element");
486
487    canvas
488        .transfer_to_image_bitmap()
489        .expect("transfer_to_image_bitmap")
490}
491
492pub async fn exception_create_image_blob(blob: &Blob, image_elem: &HtmlImageElement) {
493    let data_url = web_sys::Url::create_object_url_with_blob(blob).unwrap();
494
495    let img_load_promise = Promise::new(
496        &mut move |complete: js_sys::Function, _reject: js_sys::Function| {
497            let data_url = data_url.clone();
498            let data_url2 = data_url.clone();
499            let complete2 = complete.clone();
500
501            image_elem.set_src(&data_url);
502
503            // simulate async callback from another thread
504            let a = Closure::<dyn Fn()>::new(move || {
505                web_sys::Url::revoke_object_url(&data_url).unwrap();
506                complete.call0(&complete).unwrap();
507            });
508
509            image_elem.set_onload(Some(a.as_ref().unchecked_ref()));
510            a.forget();
511
512            let a = Closure::<dyn Fn(JsValue)>::new(move |e: JsValue| {
513                web_sys::Url::revoke_object_url(&data_url2).unwrap();
514                complete2.call0(&complete2).unwrap();
515                // let end = std::time::Instant::now();
516                web_sys::console::log_1(
517                    &format!(
518                        "err image loading in {:?} {:?} {:?} {}",
519                        // end - begin,
520                        0,
521                        js_sys::Reflect::get(&e, &"type".into()).unwrap(),
522                        js_sys::JSON::stringify(&e).unwrap(),
523                        data_url2,
524                    )
525                    .into(),
526                );
527            });
528
529            image_elem.set_onerror(Some(a.as_ref().unchecked_ref()));
530            a.forget();
531        },
532    );
533
534    wasm_bindgen_futures::JsFuture::from(img_load_promise)
535        .await
536        .unwrap();
537}
538
539#[comemo::memoize]
540fn rasterize_image(e: Arc<Image>) -> Option<UnsafeMemorize<LazyImage>> {
541    create_image(e).map(UnsafeMemorize)
542}