Skip to main content

day_core/
image.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! Raster images (docs/images.md): decoding encoded bytes into a native image, reading what it
5//! is, encoding it back, and releasing it again. Rendering a canvas drawing into a bitmap or a
6//! PDF (docs/canvas.md "Exporting") lives here too: a rendered bitmap is the same [`Bitmap`] a
7//! decode returns, held in the same toolkit table.
8//!
9//! The shape mirrors presentation (`present.rs`): a request id, a pending registry, and a
10//! completion that arrives as an [`day_spec::Event`] the backend raises. It is request-shaped rather than
11//! blocking because one backend can never answer synchronously (a browser decodes through
12//! `createImageBitmap`, which is a promise), and one async-shaped API everywhere beats a
13//! synchronous one that is a lie on the web (docs/async.md rule 3: a callback and a future,
14//! never a runtime).
15//!
16//! Everything here is thread-local and `!Send`, like the rest of day-core: Day has one UI thread.
17
18use std::cell::{Cell, RefCell};
19use std::collections::HashMap;
20use std::future::Future;
21use std::pin::Pin;
22use std::rc::Rc;
23use std::sync::Arc;
24use std::task::{Context, Poll, Waker};
25
26use day_spec::{
27    BitmapId, BitmapInfo, Cap, DrawOp, EncodeSpec, ExportError, ImageError, ImageFormat,
28    ImageProperties, PdfInfo, PdfPage, Size,
29};
30
31use crate::with_tree;
32
33day_reactive::tls_slots! {
34    image;
35    /// Keyed by request id, carrying the bitmap id day-core minted for it: the completion brings
36    /// only what the decoder read, never an id, so nothing a backend answers can name a handle
37    /// to something else.
38    static PENDING_DECODE: RefCell<HashMap<u64, (BitmapId, Pending<(BitmapId, BitmapInfo)>)>> =
39        RefCell::new(HashMap::new());
40    static PENDING_ENCODE: RefCell<HashMap<u64, Pending<Vec<u8>>>> = RefCell::new(HashMap::new());
41    /// Canvas exports, keyed like the decodes: a rendered bitmap's id is minted here too.
42    static PENDING_BITMAP: RefCell<HashMap<u64, (BitmapId, Waiting<(BitmapId, BitmapInfo), ExportError>)>> =
43        RefCell::new(HashMap::new());
44    static PENDING_PDF: RefCell<HashMap<u64, Waiting<Vec<u8>, ExportError>>> =
45        RefCell::new(HashMap::new());
46    /// Releases that arrived while the tree was borrowed (a handler dropping its last handle
47    /// inside `collect_and_release`), drained at the top of the next pump, once the borrow ends.
48    static DEFERRED_RELEASE: RefCell<Vec<BitmapId>> = RefCell::new(Vec::new());
49    static NEXT_REQ: Cell<u64> = const { Cell::new(1) };
50    /// [`canvas_exports`]'s answer for the installed tree, read once: the caps and the encode
51    /// list never change under a running toolkit, and a context menu built inside the tree's
52    /// own borrow must still be able to ask.
53    static EXPORTS: Cell<Option<CanvasExports>> = const { Cell::new(None) };
54    /// Ids are minted here, not by the backends: day-core hands the id to the backend with the
55    /// bytes, so the backend's side table is keyed by something the app can name afterwards.
56    /// Never reused, so a completion arriving after a release finds nothing and does nothing.
57    static NEXT_BITMAP: Cell<u64> = const { Cell::new(1) };
58}
59
60/// A caller waiting by CALLBACK rather than by future (docs/async.md rule 3): what
61/// `decode_async` / `encode_async` hand over in place of an `.await`.
62///
63/// Named rather than spelled out at each use so the three signatures below read as one idea,
64/// and not `Done<T>`, which is day-bridge's callback tier and a different thing.
65type ImageDone<T, E = ImageError> = Box<dyn FnOnce(Result<T, E>)>;
66
67/// One waiting caller: either a future parked on a `Waker`, or a plain callback. The error is
68/// [`ImageError`] for the image duties and [`ExportError`] for canvas exports.
69struct Waiting<T, E> {
70    shared: Rc<Shared<T, E>>,
71    callback: Option<ImageDone<T, E>>,
72}
73
74/// The image duties' waiter, named so their signatures read as before.
75type Pending<T> = Waiting<T, ImageError>;
76
77struct Shared<T, E = ImageError> {
78    result: RefCell<Option<Result<T, E>>>,
79    waker: RefCell<Option<Waker>>,
80    /// The future was dropped before its answer came. A decode that completes afterwards has
81    /// nobody to hand its image to, so the completion releases it instead of parking it.
82    cancelled: Cell<bool>,
83}
84
85impl<T, E> Default for Shared<T, E> {
86    fn default() -> Self {
87        Shared {
88            result: RefCell::new(None),
89            waker: RefCell::new(None),
90            cancelled: Cell::new(false),
91        }
92    }
93}
94
95fn next_req() -> u64 {
96    NEXT_REQ.with(|c| {
97        let v = c.get();
98        c.set(v + 1);
99        v
100    })
101}
102
103fn next_bitmap() -> BitmapId {
104    BitmapId(NEXT_BITMAP.with(|c| {
105        let v = c.get();
106        c.set(v + 1);
107        v
108    }))
109}
110
111// ---------------------------------------------------------------------------
112// The handle
113// ---------------------------------------------------------------------------
114
115/// A decoded image the toolkit holds, released when the last handle drops.
116///
117/// Cheap to clone (an `Rc`), so several nodes and the canvas can draw the same decode. `!Send`
118/// like everything else that talks to the toolkit.
119#[derive(Clone)]
120pub struct Bitmap(Rc<BitmapInner>);
121
122struct BitmapInner {
123    id: BitmapId,
124    info: BitmapInfo,
125}
126
127impl Drop for BitmapInner {
128    fn drop(&mut self) {
129        release(self.id);
130    }
131}
132
133/// Release a toolkit image now if the tree is free, and at the next pump if it is not.
134///
135/// A handle can die inside the tree's own borrow: `collect_and_release` drops a removed node's
136/// handlers, and a handler may own the last clone. A plain `with_tree` there would panic on the
137/// re-borrow, so the id is queued and [`flush_deferred_releases`] runs it once the borrow has
138/// ended. The tree being absent (teardown, or a test that never mounted one) is the other way
139/// to have nothing to release, and that is the no-op the duty promises.
140fn release(id: BitmapId) {
141    if crate::tree_absent() {
142        return;
143    }
144    if crate::with_tree_if_free(|t| t.release_image(id)).is_none() {
145        DEFERRED_RELEASE.with(|q| q.borrow_mut().push(id));
146        crate::request_pump();
147    }
148}
149
150/// Run the releases that could not run inline. Called at the top of every pump, which begins
151/// only once the tree borrow has ended; a pump nested inside a still-borrowed tree keeps them
152/// queued rather than dropping them.
153pub(crate) fn flush_deferred_releases() {
154    let ids: Vec<BitmapId> = DEFERRED_RELEASE.with(|q| std::mem::take(&mut *q.borrow_mut()));
155    for id in ids {
156        if crate::with_tree_if_free(|t| t.release_image(id)).is_none() {
157            DEFERRED_RELEASE.with(|q| q.borrow_mut().push(id));
158        }
159    }
160}
161
162impl Bitmap {
163    /// The id the toolkit knows this image by, which [`day_spec::ImageSource::Decoded`] carries.
164    pub fn id(&self) -> BitmapId {
165        self.0.id
166    }
167
168    /// Pixel size, scale, format and alpha, read once at decode (docs/images.md).
169    pub fn info(&self) -> BitmapInfo {
170        self.0.info
171    }
172
173    /// The metadata beyond the pixels: EXIF orientation, DPI, capture time. `None` where this
174    /// backend ships no metadata reader; probe [`Cap::ImageProperties`] before offering an
175    /// affordance that needs it.
176    pub fn properties(&self) -> Option<ImageProperties> {
177        let id = self.0.id;
178        with_tree(|t| t.image_properties(id))
179    }
180
181    /// Encode back to bytes, in `spec`'s format. Await it inside `day::task`.
182    pub fn encode(&self, spec: EncodeSpec) -> EncodeFuture {
183        EncodeFuture {
184            source: self.clone(),
185            spec,
186            shared: Rc::new(Shared::default()),
187            started: false,
188        }
189    }
190
191    /// [`Bitmap::encode`]'s callback half, for a call site that is not already async.
192    pub fn encode_async(
193        &self,
194        spec: EncodeSpec,
195        on_done: impl FnOnce(Result<Vec<u8>, ImageError>) + 'static,
196    ) {
197        if support(Cap::ImageEncode) == day_spec::Support::Unsupported {
198            on_done(Err(ImageError::Unsupported));
199            return;
200        }
201        // The clone rides in the callback so the toolkit's image outlives the encode however the
202        // app juggles its own handle; a browser encodes on a later turn.
203        let keep = self.clone();
204        start_encode(
205            self.0.id,
206            spec,
207            None,
208            Some(Box::new(move |r| {
209                drop(keep);
210                on_done(r)
211            })),
212        );
213    }
214}
215
216impl std::fmt::Debug for Bitmap {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        f.debug_struct("Bitmap")
219            .field("id", &self.0.id.0)
220            .field("info", &self.0.info)
221            .finish()
222    }
223}
224
225/// Two handles are the same image when they name the same decode.
226impl PartialEq for Bitmap {
227    fn eq(&self, other: &Self) -> bool {
228        self.0.id == other.0.id
229    }
230}
231
232// ---------------------------------------------------------------------------
233// Decoding
234// ---------------------------------------------------------------------------
235
236/// Decode encoded image bytes (PNG/JPEG/…) with the platform's own decoder.
237///
238/// ```ignore
239/// day::task(async move {
240///     match day::decode_image(bytes).await {
241///         Ok(bitmap) => shown.set(Some(bitmap)),
242///         Err(e) => log::warn!("not an image: {e}"),
243///     }
244/// });
245/// ```
246///
247/// `Err(ImageError::Unsupported)` where the backend has no byte decoder at all
248/// ([`Cap::ImageDecode`]); `Err(ImageError::Decode)` where it has one and the bytes are not an
249/// image it reads.
250pub fn decode(bytes: Arc<Vec<u8>>) -> DecodeFuture {
251    DecodeFuture {
252        bytes: Some(bytes),
253        shared: Rc::new(Shared::default()),
254        started: false,
255    }
256}
257
258/// [`decode`]'s callback half (docs/async.md rule 3), for a call site that is not already async.
259pub fn decode_async(
260    bytes: Arc<Vec<u8>>,
261    on_done: impl FnOnce(Result<Bitmap, ImageError>) + 'static,
262) {
263    if support(Cap::ImageDecode) == day_spec::Support::Unsupported {
264        on_done(Err(ImageError::Unsupported));
265        return;
266    }
267    start_decode(
268        &bytes,
269        None,
270        Some(Box::new(move |r| on_done(r.map(bitmap_from)))),
271    );
272}
273
274/// Whether this toolkit can decode image bytes at all ([`Cap::ImageDecode`]).
275pub fn image_decode_support() -> day_spec::Support {
276    support(Cap::ImageDecode)
277}
278
279/// Whether this toolkit can encode a decoded image back to bytes ([`Cap::ImageEncode`]).
280pub fn image_encode_support() -> day_spec::Support {
281    support(Cap::ImageEncode)
282}
283
284/// Which formats [`Bitmap::encode`] can write here: the [`Cap::ImageEncode`]
285/// counterpart of `font_families()`. Empty where encoding is unsupported.
286pub fn image_encode_formats() -> Vec<ImageFormat> {
287    with_tree(|t| t.image_encode_formats())
288}
289
290fn support(cap: Cap) -> day_spec::Support {
291    with_tree(|t| t.capability(cap))
292}
293
294fn bitmap_from((id, info): (BitmapId, BitmapInfo)) -> Bitmap {
295    Bitmap(Rc::new(BitmapInner { id, info }))
296}
297
298/// Shared by the future and the callback form: mint the id, park the waiter, ask the toolkit.
299fn start_decode(
300    bytes: &[u8],
301    shared: Option<Rc<Shared<(BitmapId, BitmapInfo)>>>,
302    callback: Option<ImageDone<(BitmapId, BitmapInfo)>>,
303) -> u64 {
304    let req = next_req();
305    let id = next_bitmap();
306    PENDING_DECODE.with(|p| {
307        p.borrow_mut().insert(
308            req,
309            (
310                id,
311                Pending {
312                    shared: shared.unwrap_or_default(),
313                    callback,
314                },
315            ),
316        )
317    });
318    with_tree(|t| t.decode_image(req, id, bytes));
319    req
320}
321
322fn start_encode(
323    id: BitmapId,
324    spec: EncodeSpec,
325    shared: Option<Rc<Shared<Vec<u8>>>>,
326    callback: Option<ImageDone<Vec<u8>>>,
327) -> u64 {
328    let req = next_req();
329    PENDING_ENCODE.with(|p| {
330        p.borrow_mut().insert(
331            req,
332            Pending {
333                shared: shared.unwrap_or_default(),
334                callback,
335            },
336        )
337    });
338    with_tree(|t| t.encode_image(req, id, &spec));
339    req
340}
341
342/// A decode in flight. Resolves with the decoded [`Bitmap`], or why it could not be.
343pub struct DecodeFuture {
344    bytes: Option<Arc<Vec<u8>>>,
345    shared: Rc<Shared<(BitmapId, BitmapInfo)>>,
346    started: bool,
347}
348
349impl Future for DecodeFuture {
350    type Output = Result<Bitmap, ImageError>;
351
352    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
353        if let Some(result) = self.shared.result.borrow_mut().take() {
354            return Poll::Ready(result.map(bitmap_from));
355        }
356        if !self.started {
357            self.started = true;
358            if support(Cap::ImageDecode) == day_spec::Support::Unsupported {
359                return Poll::Ready(Err(ImageError::Unsupported));
360            }
361            let bytes = self.bytes.take().expect("decode bytes");
362            let shared = self.shared.clone();
363            start_decode(&bytes, Some(shared), None);
364            // A backend that decodes inline has already answered by now; every backend but the
365            // browser does, since `with_tree` pumps the completion on its way out. The waker is
366            // not registered yet at that point, so `deliver`'s wake reaches nobody; take the
367            // result here instead of parking forever. (Presentation never needed this: a native
368            // modal cannot answer before it is shown.)
369            if let Some(result) = self.shared.result.borrow_mut().take() {
370                return Poll::Ready(result.map(bitmap_from));
371            }
372        }
373        *self.shared.waker.borrow_mut() = Some(cx.waker().clone());
374        Poll::Pending
375    }
376}
377
378impl Drop for DecodeFuture {
379    fn drop(&mut self) {
380        // Answered but never taken: no handle was built, so the toolkit's image would outlive
381        // every owner. Release it here.
382        if let Some(Ok((id, _))) = self.shared.result.borrow_mut().take() {
383            release(id);
384        }
385        // Still in flight: the completion, when it comes, releases rather than parking a result
386        // nobody will read (`resolve_image_decode`).
387        self.shared.cancelled.set(true);
388    }
389}
390
391/// An encode in flight. Resolves with the encoded bytes, or why they could not be produced.
392pub struct EncodeFuture {
393    /// Held for the encode's whole life: dropping the app's last handle mid-encode would
394    /// otherwise release the very image the toolkit is still reading.
395    source: Bitmap,
396    spec: EncodeSpec,
397    shared: Rc<Shared<Vec<u8>>>,
398    started: bool,
399}
400
401impl Future for EncodeFuture {
402    type Output = Result<Vec<u8>, ImageError>;
403
404    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
405        if let Some(result) = self.shared.result.borrow_mut().take() {
406            return Poll::Ready(result);
407        }
408        if !self.started {
409            self.started = true;
410            if support(Cap::ImageEncode) == day_spec::Support::Unsupported {
411                return Poll::Ready(Err(ImageError::Unsupported));
412            }
413            let shared = self.shared.clone();
414            start_encode(self.source.id(), self.spec, Some(shared), None);
415            // Answered inline; see `DecodeFuture::poll`.
416            if let Some(result) = self.shared.result.borrow_mut().take() {
417                return Poll::Ready(result);
418            }
419        }
420        *self.shared.waker.borrow_mut() = Some(cx.waker().clone());
421        Poll::Pending
422    }
423}
424
425// ---------------------------------------------------------------------------
426// Completions (pumped from `Event::ImageDecoded` / `Event::ImageEncoded`)
427// ---------------------------------------------------------------------------
428
429/// Deliver a decode answer. Called from `pump_events`; a request already answered, cancelled, or
430/// never issued finds nothing and does nothing, the same contract presentation has.
431pub fn resolve_image_decode(req: u64, result: Result<BitmapInfo, ImageError>) {
432    let Some((id, entry)) = PENDING_DECODE.with(|p| p.borrow_mut().remove(&req)) else {
433        return;
434    };
435    // The future is gone (a browser decode outliving the task that asked): a success has nobody
436    // to own it, so it is released here rather than parked as a result no one will read.
437    if entry.callback.is_none() && entry.shared.cancelled.get() {
438        if result.is_ok() {
439            release(id);
440        }
441        return;
442    }
443    deliver(entry, result.map(|info| (id, info)));
444}
445
446/// Deliver an encode answer (see [`resolve_image_decode`]).
447pub fn resolve_image_encode(req: u64, result: Result<Vec<u8>, ImageError>) {
448    let Some(entry) = PENDING_ENCODE.with(|p| p.borrow_mut().remove(&req)) else {
449        return;
450    };
451    deliver(entry, result);
452}
453
454fn deliver<T, E>(entry: Waiting<T, E>, value: Result<T, E>) {
455    if let Some(cb) = entry.callback {
456        cb(value);
457        return;
458    }
459    *entry.shared.result.borrow_mut() = Some(value);
460    if let Some(waker) = entry.shared.waker.borrow_mut().take() {
461        waker.wake();
462    }
463}
464
465/// Resolve every pending request (tests, and `uninstall_tree`): the tree that would have answered
466/// is going away, so each waiter gets `Unsupported` (there is no backend left to ask) rather
467/// than staying parked forever. Queued releases go too: their toolkit is the one being torn down.
468pub(crate) fn reset() {
469    let decodes: Vec<_> =
470        PENDING_DECODE.with(|p| p.borrow_mut().drain().map(|(_, (_, e))| e).collect());
471    for entry in decodes {
472        deliver(entry, Err(ImageError::Unsupported));
473    }
474    let encodes: Vec<_> = PENDING_ENCODE.with(|p| p.borrow_mut().drain().map(|(_, e)| e).collect());
475    for entry in encodes {
476        deliver(entry, Err(ImageError::Unsupported));
477    }
478    let bitmaps: Vec<_> =
479        PENDING_BITMAP.with(|p| p.borrow_mut().drain().map(|(_, (_, e))| e).collect());
480    for entry in bitmaps {
481        deliver(entry, Err(ExportError::Unsupported));
482    }
483    let pdfs: Vec<_> = PENDING_PDF.with(|p| p.borrow_mut().drain().map(|(_, e)| e).collect());
484    for entry in pdfs {
485        deliver(entry, Err(ExportError::Unsupported));
486    }
487    DEFERRED_RELEASE.with(|q| q.borrow_mut().clear());
488    EXPORTS.with(|c| c.set(None));
489}
490
491// ---------------------------------------------------------------------------
492// Canvas export (docs/canvas.md "Exporting")
493// ---------------------------------------------------------------------------
494
495/// What this toolkit can export a canvas drawing to ([`Cap::CanvasBitmap`],
496/// [`Cap::CanvasPdf`]). Read it to enable or hide export commands, the way
497/// [`image_encode_formats`] decides which formats to offer.
498#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
499pub struct CanvasExports {
500    /// [`render_bitmap`] works here, and with it PNG export wherever [`Bitmap::encode`] writes
501    /// PNG (see [`CanvasExports::png`]).
502    pub bitmap: bool,
503    /// [`render_pdf`] works here: the platform has a native PDF writer.
504    pub pdf: bool,
505    png: bool,
506}
507
508impl CanvasExports {
509    /// Whether a drawing can become PNG bytes here: a rendered bitmap plus a PNG encoder.
510    pub fn png(&self) -> bool {
511        self.png
512    }
513}
514
515/// What this toolkit can export a canvas drawing to.
516///
517/// Safe to call anywhere, including from a context menu that is built while the tree is busy
518/// summoning it: the answer is read once per installed tree and kept. Before a tree is
519/// installed, and in the rare case its first read lands inside a borrow, everything answers
520/// `false`.
521pub fn canvas_exports() -> CanvasExports {
522    if let Some(known) = EXPORTS.with(|c| c.get()) {
523        return known;
524    }
525    let Some(read) = crate::with_tree_if_free(|t| {
526        let yes = |cap| t.capability(cap) != day_spec::Support::Unsupported;
527        let bitmap = yes(Cap::CanvasBitmap);
528        CanvasExports {
529            bitmap,
530            pdf: yes(Cap::CanvasPdf),
531            png: bitmap && t.image_encode_formats().contains(&ImageFormat::Png),
532        }
533    }) else {
534        return CanvasExports::default();
535    };
536    EXPORTS.with(|c| c.set(Some(read)));
537    read
538}
539
540/// `size` points at `scale` is something a toolkit can be asked to render: positive and
541/// finite, and within [`day_spec::EXPORT_MAX_SIDE`] / [`day_spec::EXPORT_MAX_PIXELS`].
542fn check_bitmap(size: Size, scale: f64) -> Result<(), ExportError> {
543    let positive = |v: f64| v.is_finite() && v > 0.0;
544    if !positive(size.width) || !positive(size.height) || !positive(scale) {
545        return Err(ExportError::Invalid);
546    }
547    let (width, height) = day_spec::export_pixels(size, scale);
548    if width > day_spec::EXPORT_MAX_SIDE
549        || height > day_spec::EXPORT_MAX_SIDE
550        || u64::from(width) * u64::from(height) > day_spec::EXPORT_MAX_PIXELS
551    {
552        return Err(ExportError::TooLarge { width, height });
553    }
554    Ok(())
555}
556
557fn check_pdf(pages: &[PdfPage]) -> Result<(), ExportError> {
558    let positive = |v: f64| v.is_finite() && v > 0.0;
559    if pages.is_empty()
560        || pages
561            .iter()
562            .any(|p| !positive(p.size.width) || !positive(p.size.height))
563    {
564        return Err(ExportError::Invalid);
565    }
566    Ok(())
567}
568
569/// The check every bitmap export runs before asking the toolkit: the cap, then the size.
570fn admit_bitmap(size: Size, scale: f64) -> Result<(), ExportError> {
571    if support(Cap::CanvasBitmap) == day_spec::Support::Unsupported {
572        return Err(ExportError::Unsupported);
573    }
574    check_bitmap(size, scale)
575}
576
577fn admit_pdf(pages: &[PdfPage]) -> Result<(), ExportError> {
578    if support(Cap::CanvasPdf) == day_spec::Support::Unsupported {
579        return Err(ExportError::Unsupported);
580    }
581    check_pdf(pages)
582}
583
584/// Replay `ops` into an offscreen bitmap of `size` points at `scale` pixels per point, through
585/// the toolkit's own canvas painter. Resolves with a [`Bitmap`] like a decode does, so
586/// `image(&bitmap)`, `Draw::image` and [`Bitmap::encode`] all take it. Apps reach this through
587/// `Drawing::to_bitmap`.
588pub fn render_bitmap(ops: Vec<DrawOp>, size: Size, scale: f64) -> RenderBitmap {
589    RenderBitmap {
590        job: Some((ops, size, scale)),
591        shared: Rc::new(Shared::default()),
592        started: false,
593    }
594}
595
596/// [`render_bitmap`]'s callback half (docs/async.md rule 3).
597pub fn render_bitmap_async(
598    ops: Vec<DrawOp>,
599    size: Size,
600    scale: f64,
601    on_done: impl FnOnce(Result<Bitmap, ExportError>) + 'static,
602) {
603    if let Err(e) = admit_bitmap(size, scale) {
604        on_done(Err(e));
605        return;
606    }
607    start_bitmap(
608        &ops,
609        size,
610        scale,
611        None,
612        Some(Box::new(move |r| on_done(r.map(bitmap_from)))),
613    );
614}
615
616/// Replay each page into one PDF document through the platform's own PDF writer. Resolves with
617/// the document's bytes. Apps reach this through `Drawing::to_pdf` and `PdfDocument::render`.
618pub fn render_pdf(pages: Vec<PdfPage>, info: PdfInfo) -> RenderPdf {
619    RenderPdf {
620        job: Some((pages, info)),
621        shared: Rc::new(Shared::default()),
622        started: false,
623    }
624}
625
626/// [`render_pdf`]'s callback half (docs/async.md rule 3).
627pub fn render_pdf_async(
628    pages: Vec<PdfPage>,
629    info: PdfInfo,
630    on_done: impl FnOnce(Result<Vec<u8>, ExportError>) + 'static,
631) {
632    if let Err(e) = admit_pdf(&pages) {
633        on_done(Err(e));
634        return;
635    }
636    start_pdf(&pages, &info, None, Some(Box::new(on_done)));
637}
638
639fn start_bitmap(
640    ops: &[DrawOp],
641    size: Size,
642    scale: f64,
643    shared: Option<Rc<Shared<(BitmapId, BitmapInfo), ExportError>>>,
644    callback: Option<ImageDone<(BitmapId, BitmapInfo), ExportError>>,
645) {
646    let req = next_req();
647    let id = next_bitmap();
648    PENDING_BITMAP.with(|p| {
649        p.borrow_mut().insert(
650            req,
651            (
652                id,
653                Waiting {
654                    shared: shared.unwrap_or_default(),
655                    callback,
656                },
657            ),
658        )
659    });
660    with_tree(|t| t.render_bitmap(req, id, ops, size, scale));
661}
662
663fn start_pdf(
664    pages: &[PdfPage],
665    info: &PdfInfo,
666    shared: Option<Rc<Shared<Vec<u8>, ExportError>>>,
667    callback: Option<ImageDone<Vec<u8>, ExportError>>,
668) {
669    let req = next_req();
670    PENDING_PDF.with(|p| {
671        p.borrow_mut().insert(
672            req,
673            Waiting {
674                shared: shared.unwrap_or_default(),
675                callback,
676            },
677        )
678    });
679    with_tree(|t| t.render_pdf(req, pages, info));
680}
681
682/// A bitmap render in flight. Resolves with the rendered [`Bitmap`], or why it could not be.
683/// Nothing is asked of the toolkit until the first poll.
684pub struct RenderBitmap {
685    job: Option<(Vec<DrawOp>, Size, f64)>,
686    shared: Rc<Shared<(BitmapId, BitmapInfo), ExportError>>,
687    started: bool,
688}
689
690impl Future for RenderBitmap {
691    type Output = Result<Bitmap, ExportError>;
692
693    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
694        if let Some(result) = self.shared.result.borrow_mut().take() {
695            return Poll::Ready(result.map(bitmap_from));
696        }
697        if !self.started {
698            self.started = true;
699            let Some((ops, size, scale)) = self.job.take() else {
700                return Poll::Ready(Err(ExportError::Invalid));
701            };
702            if let Err(e) = admit_bitmap(size, scale) {
703                return Poll::Ready(Err(e));
704            }
705            start_bitmap(&ops, size, scale, Some(self.shared.clone()), None);
706            // Answered inline everywhere but the browser; see `DecodeFuture::poll`.
707            if let Some(result) = self.shared.result.borrow_mut().take() {
708                return Poll::Ready(result.map(bitmap_from));
709            }
710        }
711        *self.shared.waker.borrow_mut() = Some(cx.waker().clone());
712        Poll::Pending
713    }
714}
715
716impl Drop for RenderBitmap {
717    fn drop(&mut self) {
718        // The same two cases as `DecodeFuture`: answered but never taken, or still in flight.
719        if let Some(Ok((id, _))) = self.shared.result.borrow_mut().take() {
720            release(id);
721        }
722        self.shared.cancelled.set(true);
723    }
724}
725
726/// A PDF render in flight. Resolves with the document's bytes, or why they could not be
727/// produced. Nothing is asked of the toolkit until the first poll.
728pub struct RenderPdf {
729    job: Option<(Vec<PdfPage>, PdfInfo)>,
730    shared: Rc<Shared<Vec<u8>, ExportError>>,
731    started: bool,
732}
733
734impl Future for RenderPdf {
735    type Output = Result<Vec<u8>, ExportError>;
736
737    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
738        if let Some(result) = self.shared.result.borrow_mut().take() {
739            return Poll::Ready(result);
740        }
741        if !self.started {
742            self.started = true;
743            let Some((pages, info)) = self.job.take() else {
744                return Poll::Ready(Err(ExportError::Invalid));
745            };
746            if let Err(e) = admit_pdf(&pages) {
747                return Poll::Ready(Err(e));
748            }
749            start_pdf(&pages, &info, Some(self.shared.clone()), None);
750            if let Some(result) = self.shared.result.borrow_mut().take() {
751                return Poll::Ready(result);
752            }
753        }
754        *self.shared.waker.borrow_mut() = Some(cx.waker().clone());
755        Poll::Pending
756    }
757}
758
759/// Deliver a bitmap render's answer (`Event::BitmapRendered`). The same contract as
760/// [`resolve_image_decode`], including releasing a success nobody is waiting for.
761pub fn resolve_bitmap_render(req: u64, result: Result<BitmapInfo, ExportError>) {
762    let Some((id, entry)) = PENDING_BITMAP.with(|p| p.borrow_mut().remove(&req)) else {
763        return;
764    };
765    if entry.callback.is_none() && entry.shared.cancelled.get() {
766        if result.is_ok() {
767            release(id);
768        }
769        return;
770    }
771    deliver(entry, result.map(|info| (id, info)));
772}
773
774/// Deliver a PDF render's answer (`Event::PdfRendered`).
775pub fn resolve_pdf_render(req: u64, result: Result<Vec<u8>, ExportError>) {
776    let Some(entry) = PENDING_PDF.with(|p| p.borrow_mut().remove(&req)) else {
777        return;
778    };
779    deliver(entry, result);
780}
781
782#[cfg(test)]
783mod export_tests {
784    use super::*;
785
786    #[test]
787    fn bitmap_sizes_are_checked_before_the_toolkit() {
788        let size = Size::new(100.0, 50.0);
789        assert_eq!(check_bitmap(size, 2.0), Ok(()));
790        assert_eq!(
791            check_bitmap(Size::new(0.0, 50.0), 1.0),
792            Err(ExportError::Invalid)
793        );
794        assert_eq!(check_bitmap(size, f64::NAN), Err(ExportError::Invalid));
795        assert_eq!(check_bitmap(size, -1.0), Err(ExportError::Invalid));
796        assert_eq!(
797            check_bitmap(Size::new(16_385.0, 1.0), 1.0),
798            Err(ExportError::TooLarge {
799                width: 16_385,
800                height: 1
801            })
802        );
803        // Each side fits, the total does not: 10,000 × 10,000 is 100 megapixels.
804        assert_eq!(
805            check_bitmap(Size::new(5_000.0, 5_000.0), 2.0),
806            Err(ExportError::TooLarge {
807                width: 10_000,
808                height: 10_000
809            })
810        );
811        // Partial pixels round up, so the limit counts the bitmap actually allocated.
812        assert_eq!(day_spec::export_pixels(Size::new(10.2, 3.0), 2.0), (21, 6));
813    }
814
815    #[test]
816    fn a_pdf_needs_pages_with_a_size() {
817        assert_eq!(check_pdf(&[]), Err(ExportError::Invalid));
818        let page = |w, h| PdfPage {
819            size: Size::new(w, h),
820            ops: Vec::new(),
821        };
822        assert_eq!(check_pdf(&[page(612.0, 792.0)]), Ok(()));
823        assert_eq!(
824            check_pdf(&[page(612.0, 792.0), page(0.0, 10.0)]),
825            Err(ExportError::Invalid)
826        );
827    }
828}
829
830#[cfg(test)]
831mod teardown_tests {
832    use super::*;
833
834    #[test]
835    fn bitmap_drop_during_reactive_tls_teardown() {
836        const CHILD: &str = "DAY_CORE_BITMAP_REACTIVE_TEARDOWN_CHILD";
837        if let Some(mode) = std::env::var_os(CHILD) {
838            struct TeardownBitmap {
839                bitmap: Option<Bitmap>,
840                dropped: std::sync::mpsc::Sender<()>,
841            }
842            impl Drop for TeardownBitmap {
843                fn drop(&mut self) {
844                    drop(self.bitmap.take());
845                    self.dropped.send(()).unwrap();
846                }
847            }
848            let (dropped, observed) = std::sync::mpsc::channel();
849            std::thread::spawn(move || {
850                // The reactive arena owns the last bitmap until its TLS destructor runs.
851                // Construct the handle directly so the first case never initializes core TLS.
852                day_reactive::Signal::global(TeardownBitmap {
853                    bitmap: Some(bitmap_from((BitmapId(1), BitmapInfo::default()))),
854                    dropped,
855                });
856                if mode == "initialized" {
857                    // On Windows GNU, core TLS can be initialized again after its destructor
858                    // finishes. This is the canvas mock_e2e crash's teardown ordering.
859                    assert!(crate::try_tls_root(|_| ()).is_ok());
860                }
861            })
862            .join()
863            .unwrap();
864            observed
865                .recv_timeout(std::time::Duration::from_secs(5))
866                .expect("bitmap was not dropped during reactive TLS teardown");
867            return;
868        }
869        // A TLS destructor panic aborts, so isolate both the never-initialized core and the
870        // already-destroyed core cases. Neither may initialize signals while releasing a bitmap.
871        for mode in ["uninitialized", "initialized"] {
872            let output = std::process::Command::new(std::env::current_exe().unwrap())
873                .args([
874                    "--exact",
875                    "image::teardown_tests::bitmap_drop_during_reactive_tls_teardown",
876                    "--nocapture",
877                ])
878                .env(CHILD, mode)
879                .output()
880                .unwrap();
881            assert!(
882                output.status.success(),
883                "{mode}: {}",
884                String::from_utf8_lossy(&output.stderr)
885            );
886        }
887    }
888
889    #[test]
890    fn bitmap_drop_during_core_tls_teardown() {
891        const CHILD: &str = "DAY_CORE_BITMAP_TEARDOWN_CHILD";
892        if std::env::var_os(CHILD).is_some() {
893            struct TeardownBitmap {
894                bitmap: Option<Bitmap>,
895                dropped: std::sync::mpsc::Sender<()>,
896            }
897            impl Drop for TeardownBitmap {
898                fn drop(&mut self) {
899                    // LocalKey access must fail while its own destructor runs. This does not
900                    // depend on the platform's ordering of unrelated TLS destructors.
901                    assert!(crate::try_tls_root(|_| ()).is_err());
902                    drop(self.bitmap.take());
903                    self.dropped.send(()).unwrap();
904                }
905            }
906            let (dropped, observed) = std::sync::mpsc::channel();
907            std::thread::spawn(move || {
908                let held = TeardownBitmap {
909                    bitmap: Some(bitmap_from((BitmapId(1), BitmapInfo::default()))),
910                    dropped,
911                };
912                // A pending callback owns the bitmap until the core TLS root is destroyed.
913                // The callback is never invoked: dropping the registry drops its captures.
914                PENDING_ENCODE.with(|pending| {
915                    pending.borrow_mut().insert(
916                        1,
917                        Pending {
918                            shared: Rc::new(Shared::default()),
919                            callback: Some(Box::new(move |_| drop(held))),
920                        },
921                    );
922                });
923                assert!(crate::try_tls_root(|_| ()).is_ok());
924            })
925            .join()
926            .unwrap();
927            // Prove the destructor actually exercised the release path, rather than allowing
928            // a platform that skipped TLS cleanup to pass silently.
929            observed
930                .recv_timeout(std::time::Duration::from_secs(5))
931                .expect("bitmap was not dropped during core TLS teardown");
932            return;
933        }
934        let output = std::process::Command::new(std::env::current_exe().unwrap())
935            .args([
936                "--exact",
937                "image::teardown_tests::bitmap_drop_during_core_tls_teardown",
938                "--nocapture",
939            ])
940            .env(CHILD, "1")
941            .output()
942            .unwrap();
943        assert!(
944            output.status.success(),
945            "{}",
946            String::from_utf8_lossy(&output.stderr)
947        );
948    }
949}