1#![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
43pub trait ExportFeature {
45 const ENABLE_TRACING: bool;
47}
48
49pub struct DefaultExportFeature;
51pub 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 }
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 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 }
94 }
95
96 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 }
105 }
106}
107
108#[derive(Clone)]
110pub struct CanvasPage {
111 pub elem: CanvasNode,
113 pub content: Fingerprint,
115 pub size: Size,
117}
118
119pub struct CanvasTask<Feat: ExportFeature> {
122 _feat_phantom: std::marker::PhantomData<Feat>,
123}
124
125impl<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 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
154pub struct CanvasRenderTask<'m, 't, Feat: ExportFeature> {
159 pub module: &'m Module,
161
162 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 = 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 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 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
238pub struct CanvasStack {
242 pub kind: GroupKind,
244 pub ts: sk::Transform,
246 pub clipper: Option<ir::PathItem>,
248 pub fill: Option<ImmutStr>,
250 pub inner: EcoVec<(ir::Point, CanvasNode)>,
252 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
276impl<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
316impl<'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 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
367pub 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
389unsafe impl<T> Send for UnsafeMemorize<T> {}
391unsafe 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 &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 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 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 web_sys::console::log_1(
517 &format!(
518 "err image loading in {:?} {:?} {:?} {}",
519 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}