Skip to main content

alpha2_extensions_web/
lib.rs

1//! The extension providers for the web: the same code the native host runs,
2//! as a wasm module of its own so a page that opens no such source never
3//! loads it.
4
5use std::collections::HashMap;
6use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
7
8use alpha2_extensions::{
9    Diagnostic, ExtensionError, FramePixels, Registry, ResourceResolver, SharedResolver,
10    VisualSource,
11};
12use alpha2_model::ProviderSource;
13use alpha2_time::MediaTime;
14use wasm_bindgen::prelude::*;
15
16fn js_error(error: impl std::fmt::Display) -> JsValue {
17    JsValue::from_str(&error.to_string())
18}
19
20/// Resources registered by the page: image sequence frames and SVG images
21/// by locator, and fonts.
22#[derive(Default)]
23struct Files {
24    files: HashMap<String, Arc<[u8]>>,
25    fonts: Vec<Arc<[u8]>>,
26}
27
28/// The page's resources, shared with the sources that read them as they
29/// render: an image sequence reads a frame registered when it shows it.
30#[derive(Clone, Default)]
31struct Resources(Arc<Mutex<Files>>);
32
33impl Resources {
34    fn lock(&self) -> MutexGuard<'_, Files> {
35        self.0.lock().unwrap_or_else(PoisonError::into_inner)
36    }
37}
38
39impl ResourceResolver for Resources {
40    fn bytes(&self, locator: &str) -> Result<Arc<[u8]>, ExtensionError> {
41        self.lock()
42            .files
43            .get(locator)
44            .cloned()
45            .ok_or_else(|| ExtensionError::MissingResource(locator.to_owned()))
46    }
47
48    fn fonts(&self) -> Vec<Arc<[u8]>> {
49        self.lock().fonts.clone()
50    }
51}
52
53/// The providers this module was built with, and the sources a page opened.
54///
55/// Failures are thrown as plain string messages, not `Error` objects.
56#[wasm_bindgen]
57pub struct ExtensionRegistry {
58    registry: Registry,
59    resources: Resources,
60    sources: HashMap<u32, Box<dyn VisualSource>>,
61    next: u32,
62}
63
64/// One frame a source rendered.
65#[wasm_bindgen]
66pub struct RenderedFrame {
67    width: u32,
68    height: u32,
69    pixels: FramePixels,
70    index: usize,
71    start: i64,
72    end: Option<i64>,
73    diagnostics: Vec<Diagnostic>,
74}
75
76#[wasm_bindgen]
77impl RenderedFrame {
78    /// Width in pixels.
79    #[wasm_bindgen(getter)]
80    #[must_use]
81    pub fn width(&self) -> u32 {
82        self.width
83    }
84
85    /// Height in pixels.
86    #[wasm_bindgen(getter)]
87    #[must_use]
88    pub fn height(&self) -> u32 {
89        self.height
90    }
91
92    /// The pixels' precision: `rgba8` for [`Self::pixels`], `linear_f16`
93    /// for [`Self::half_pixels`].
94    #[wasm_bindgen(getter)]
95    #[must_use]
96    pub fn precision(&self) -> String {
97        match self.pixels {
98            FramePixels::LinearF16(_) => "linear_f16",
99            _ => "rgba8",
100        }
101        .to_owned()
102    }
103
104    /// Premultiplied, sRGB-encoded RGBA8 rows, packed; empty for another
105    /// precision. Copied once, from the pixels the source shares, straight
106    /// into a new JavaScript array.
107    #[wasm_bindgen(getter)]
108    #[must_use]
109    pub fn pixels(&self) -> js_sys::Uint8Array {
110        js_sys::Uint8Array::from(self.pixels.rgba8().unwrap_or_default())
111    }
112
113    /// Premultiplied linear-light RGBA half floats (their bits) in BT.709
114    /// primaries, rows packed; empty for another precision. Copied once,
115    /// as [`Self::pixels`] is.
116    #[wasm_bindgen(getter, js_name = halfPixels)]
117    #[must_use]
118    pub fn half_pixels(&self) -> js_sys::Uint16Array {
119        js_sys::Uint16Array::from(self.pixels.linear_f16().unwrap_or_default())
120    }
121
122    /// What producing the frame found, as a JSON array of diagnostics.
123    #[wasm_bindgen(getter)]
124    #[must_use]
125    pub fn diagnostics(&self) -> String {
126        serde_json::to_string(&self.diagnostics).unwrap_or_default()
127    }
128
129    /// Which of the source's frames this is.
130    #[wasm_bindgen(getter)]
131    #[must_use]
132    pub fn index(&self) -> usize {
133        self.index
134    }
135
136    /// The first source tick it shows at; 0 for the first frame, which also
137    /// answers every earlier time.
138    #[wasm_bindgen(getter, js_name = startTicks)]
139    #[must_use]
140    pub fn start_ticks(&self) -> i64 {
141        self.start.max(0)
142    }
143
144    /// The first source tick it no longer shows at, or `undefined` for the
145    /// last frame.
146    #[wasm_bindgen(getter, js_name = endTicks)]
147    #[must_use]
148    pub fn end_ticks(&self) -> Option<i64> {
149        self.end
150    }
151}
152
153impl Default for ExtensionRegistry {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159#[wasm_bindgen]
160impl ExtensionRegistry {
161    /// Every provider this module was built with.
162    #[wasm_bindgen(constructor)]
163    #[must_use]
164    pub fn new() -> Self {
165        Self {
166            registry: Registry::with_defaults(),
167            resources: Resources::default(),
168            sources: HashMap::new(),
169            next: 1,
170        }
171    }
172
173    /// The providers' names, in priority order, as a JSON array.
174    #[must_use]
175    pub fn providers(&self) -> String {
176        serde_json::to_string(&self.registry.names()).unwrap_or_default()
177    }
178
179    /// Whether this module opens a project's provider source (JSON).
180    #[must_use]
181    pub fn supports(&self, provider: &str) -> bool {
182        serde_json::from_str::<ProviderSource>(provider)
183            .is_ok_and(|source| self.registry.supports(&source))
184    }
185
186    /// What the first provider that claims `bytes` finds, as JSON
187    /// `{ provider, report }`, or `undefined` when none does.
188    ///
189    /// # Errors
190    /// Why bytes a provider claims cannot be read.
191    pub fn probe(&self, bytes: &[u8]) -> Result<Option<String>, JsValue> {
192        match self.registry.probe(bytes) {
193            None => Ok(None),
194            Some(Err(error)) => Err(js_error(error)),
195            Some(Ok((provider, report))) => serde_json::to_string(&serde_json::json!({
196                "provider": provider,
197                "report": report,
198            }))
199            .map(Some)
200            .map_err(js_error),
201        }
202    }
203
204    /// Registers bytes under a locator: an image sequence's frame, or an
205    /// image an SVG names. A source reads them when it opens or renders, so
206    /// a page may register them only for that call (see [`Self::needs`]).
207    #[wasm_bindgen(js_name = registerResource)]
208    pub fn register_resource(&mut self, locator: String, bytes: Vec<u8>) {
209        self.resources.lock().files.insert(locator, bytes.into());
210    }
211
212    /// Forgets the bytes registered under a locator.
213    #[wasm_bindgen(js_name = releaseResource)]
214    pub fn release_resource(&mut self, locator: &str) {
215        self.resources.lock().files.remove(locator);
216    }
217
218    /// The bytes registered under locators, all together.
219    #[wasm_bindgen(js_name = resourceBytes)]
220    #[must_use]
221    pub fn resource_bytes(&self) -> usize {
222        self.resources
223            .lock()
224            .files
225            .values()
226            .map(|file| file.len())
227            .sum()
228    }
229
230    /// Registers a font an SVG's text may draw with, after those registered
231    /// before. A source reads the fonts when it opens.
232    #[wasm_bindgen(js_name = registerFont)]
233    pub fn register_font(&mut self, bytes: Vec<u8>) {
234        self.resources.lock().fonts.push(bytes.into());
235    }
236
237    /// Forgets every registered font. Sources opened before keep the fonts
238    /// they opened with.
239    #[wasm_bindgen(js_name = clearFonts)]
240    pub fn clear_fonts(&mut self) {
241        self.resources.lock().fonts.clear();
242    }
243
244    /// The resources opening a project's provider source (JSON) reads, as a
245    /// JSON array of locators: an image sequence's first frame.
246    ///
247    /// # Errors
248    /// When the provider source is not valid JSON.
249    #[wasm_bindgen(js_name = openNeeds)]
250    pub fn open_needs(&self, provider: &str) -> Result<String, JsValue> {
251        let provider: ProviderSource = serde_json::from_str(provider).map_err(js_error)?;
252        serde_json::to_string(&self.registry.open_needs(&provider)).map_err(js_error)
253    }
254
255    /// The resources rendering a source at a source tick reads, as a JSON
256    /// array of locators: those it does not already hold.
257    ///
258    /// # Errors
259    /// When the handle is not open.
260    pub fn needs(&self, handle: u32, time_ticks: i64) -> Result<String, JsValue> {
261        let source = self
262            .sources
263            .get(&handle)
264            .ok_or_else(|| js_error("the source is not open"))?;
265        serde_json::to_string(&source.needs(MediaTime::from_ticks(time_ticks))).map_err(js_error)
266    }
267
268    /// Opens a project's provider source (JSON) with its own bytes (none for
269    /// an image sequence), returning a handle.
270    ///
271    /// # Errors
272    /// Why the source cannot be opened.
273    pub fn open(&mut self, provider: &str, bytes: Option<Vec<u8>>) -> Result<u32, JsValue> {
274        let provider: ProviderSource = serde_json::from_str(provider).map_err(js_error)?;
275        let source = self
276            .registry
277            .open(
278                &provider,
279                bytes.map(Arc::from),
280                Arc::new(self.resources.clone()) as SharedResolver,
281            )
282            .map_err(js_error)?;
283        let handle = self.next;
284        self.next = self.next.wrapping_add(1).max(1);
285        self.sources.insert(handle, source);
286        Ok(handle)
287    }
288
289    /// What an opened source is, as JSON.
290    ///
291    /// # Errors
292    /// When the handle is not open.
293    pub fn report(&self, handle: u32) -> Result<String, JsValue> {
294        let source = self
295            .sources
296            .get(&handle)
297            .ok_or_else(|| js_error("the source is not open"))?;
298        serde_json::to_string(source.report()).map_err(js_error)
299    }
300
301    /// The frame a source shows at a source tick, rasterized to fit
302    /// `fit_width` by `fit_height` when it is a vector source (0 by 0 for its
303    /// own size).
304    ///
305    /// # Errors
306    /// When the handle is not open or the frame cannot be produced.
307    pub fn render(
308        &mut self,
309        handle: u32,
310        time_ticks: i64,
311        fit_width: u32,
312        fit_height: u32,
313    ) -> Result<RenderedFrame, JsValue> {
314        let source = self
315            .sources
316            .get_mut(&handle)
317            .ok_or_else(|| js_error("the source is not open"))?;
318        let frame = source
319            .render(MediaTime::from_ticks(time_ticks), (fit_width, fit_height))
320            .map_err(js_error)?;
321        Ok(RenderedFrame {
322            width: frame.width,
323            height: frame.height,
324            index: frame.index,
325            start: frame.start.ticks(),
326            end: frame.end.map(MediaTime::ticks),
327            pixels: frame.pixels,
328            diagnostics: frame.diagnostics,
329        })
330    }
331
332    /// The bytes an opened source holds between renders: its decoded frames
333    /// and images, its last rendered frame, its file's bytes when it keeps
334    /// them. A page that bounds what its open sources hold counts this.
335    ///
336    /// # Errors
337    /// When the handle is not open.
338    #[wasm_bindgen(js_name = residentBytes)]
339    pub fn resident_bytes(&self, handle: u32) -> Result<usize, JsValue> {
340        self.sources
341            .get(&handle)
342            .map(|source| source.resident_bytes())
343            .ok_or_else(|| js_error("the source is not open"))
344    }
345
346    /// Another reader of an opened source, positioned on its own, returning
347    /// its handle: it shares what the source decoded when it opened (an
348    /// animation's file, checked frames, and kept frames) with the source
349    /// and every other reader made from either, and decodes on its own. The
350    /// readers stay valid when the source they were made from is released.
351    /// `undefined` for a source that draws any time as cheaply, which a
352    /// page shares instead.
353    ///
354    /// # Errors
355    /// When the handle is not open.
356    pub fn reader(&mut self, handle: u32) -> Result<Option<u32>, JsValue> {
357        let source = self
358            .sources
359            .get(&handle)
360            .ok_or_else(|| js_error("the source is not open"))?;
361        let Some(reader) = source.reader() else {
362            return Ok(None);
363        };
364        let handle = self.next;
365        self.next = self.next.wrapping_add(1).max(1);
366        self.sources.insert(handle, reader);
367        Ok(Some(handle))
368    }
369
370    /// Of [`Self::resident_bytes`], what the source shares with the readers
371    /// made from it ([`Self::reader`]): the same bytes for each, which a page
372    /// counts once.
373    ///
374    /// # Errors
375    /// When the handle is not open.
376    #[wasm_bindgen(js_name = sharedBytes)]
377    pub fn shared_bytes(&self, handle: u32) -> Result<usize, JsValue> {
378        self.sources
379            .get(&handle)
380            .map(|source| source.shared_bytes())
381            .ok_or_else(|| js_error("the source is not open"))
382    }
383
384    /// Closes a source.
385    pub fn release(&mut self, handle: u32) {
386        self.sources.remove(&handle);
387    }
388}