Skip to main content

alpha2_model/
provider.rs

1//! Sources whose media an extension provider supplies.
2
3use alpha2_time::FrameRate;
4use serde::{Deserialize, Serialize};
5
6use crate::{MAX_TEXT_CONTENT_BYTES, MediaKind, Rgba};
7
8/// Most frames an image sequence lists.
9pub const MAX_SEQUENCE_FRAMES: usize = 100_000;
10
11/// Largest dotLottie package a provider reads, in bytes, refusing larger files unread. Only the
12/// shown animation (within [`MAX_LOTTIE_DOCUMENT_BYTES`]) and its images are unpacked.
13pub const MAX_PROVIDER_DOCUMENT_BYTES: usize = 256 * 1024 * 1024;
14
15/// Largest Lottie animation a provider reads, in bytes (a JSON file or an unpacked dotLottie
16/// animation). Parsing can take about 45 times the document size, so this caps it near
17/// 750 MiB; larger documents are refused before parsing.
18pub const MAX_LOTTIE_DOCUMENT_BYTES: usize = 16 * 1024 * 1024;
19
20/// Largest SVG document a provider reads, in bytes. Parsing can take about 30 times the
21/// document size, so this caps it near 960 MiB; larger documents are refused before parsing.
22pub const MAX_SVG_DOCUMENT_BYTES: usize = 32 * 1024 * 1024;
23
24/// Longest LaTeX formula a source holds, in bytes.
25pub const MAX_LATEX_BYTES: usize = MAX_TEXT_CONTENT_BYTES;
26
27/// Largest LaTeX font size, in pixels per em.
28pub const MAX_LATEX_FONT_SIZE: f32 = 1_024.0;
29
30/// Largest PDF document a provider reads, in bytes. Larger files are refused before reading.
31pub const MAX_PDF_DOCUMENT_BYTES: usize = 256 * 1024 * 1024;
32
33/// Most pages a PDF document a provider reads may have.
34pub const MAX_PDF_PAGES: u32 = 10_000;
35
36/// A source whose media an extension provider supplies, not the host's video and image decoders.
37///
38/// A host picks the provider by probing the bytes at import, never by file name, and records
39/// it here so every host opens the source the same way. The engine does not read it.
40#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
41#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
42#[serde(tag = "format", rename_all = "snake_case", from = "ProviderSourceWire")]
43#[non_exhaustive]
44pub enum ProviderSource {
45    /// A static SVG document, rasterized at its shown size. The source's kind is `image`.
46    Svg,
47    /// A GIF, animated WebP, or APNG: one cycle of frames, each with its own duration.
48    /// Repetition is the clip's. The source's kind is `video`.
49    AnimatedImage,
50    /// A Lottie animation (JSON file or dotLottie package), evaluated at its own frames. The
51    /// source's kind is `video`.
52    Lottie {
53        /// The dotLottie animation shown, by ID. Absent means the package's first; always absent
54        /// for a Lottie JSON file.
55        #[serde(default, skip_serializing_if = "Option::is_none")]
56        animation: Option<String>,
57    },
58    /// Still images played in order at a fixed rate. The source's kind is `video`.
59    ImageSequence {
60        /// Each frame's locator, in play order. `null` marks a missing number, allowed only when
61        /// `missing` says how to show it.
62        frames: Vec<Option<String>>,
63        /// The rate the frames play at.
64        frame_rate: FrameRate,
65        /// The number of the first frame, as its file was numbered.
66        #[serde(default)]
67        start_number: u64,
68        /// How a missing frame is shown.
69        #[serde(default)]
70        missing: MissingFrames,
71        /// The primaries of OpenEXR frames' linear RGB, overriding the first frame's
72        /// chromaticities. Only for OpenEXR frames.
73        #[serde(default, skip_serializing_if = "Option::is_none")]
74        primaries: Option<LinearPrimaries>,
75    },
76    /// A still OpenEXR image: linear-light RGB(A), read at half precision. The source's kind is
77    /// `image`.
78    Exr {
79        /// The primaries of its linear RGB, overriding the file's chromaticities (BT.709 when the
80        /// file has none, as OpenEXR defines).
81        #[serde(default, skip_serializing_if = "Option::is_none")]
82        primaries: Option<LinearPrimaries>,
83    },
84    /// A LaTeX math formula typeset with the bundled STIX Two Math font. The source's kind is
85    /// `image`, and it has no locator or fingerprint since it reads no file. A formula that uses
86    /// a character the font lacks, such as Hangul in `\text`, does not open.
87    Latex {
88        /// Math-mode LaTeX without `$` delimiters. Non-empty, at most [`MAX_LATEX_BYTES`] bytes.
89        latex: String,
90        /// Pixels per em, above `0` and at most [`MAX_LATEX_FONT_SIZE`].
91        font_size: f32,
92        /// The ink where the formula sets no `\color` of its own. White when absent.
93        #[serde(default = "white")]
94        color: Rgba,
95        /// Display style, with large operators and limits above and below, or text style when
96        /// `false`. Display when absent.
97        #[serde(default = "display_style")]
98        display: bool,
99    },
100    /// One page of a PDF document. The source's kind is `image`.
101    Pdf {
102        /// The page shown, counted from `1`, at most [`MAX_PDF_PAGES`]. The first when absent.
103        #[serde(default = "first_page")]
104        page: u32,
105        /// Painted under the page, which is itself transparent. White paper when absent.
106        #[serde(default = "white")]
107        background: Rgba,
108    },
109}
110
111/// The wire form of [`ProviderSource`].
112///
113/// serde lets an internally tagged unit variant ignore extra fields even with
114/// `deny_unknown_fields`; empty struct variants reject them, as the published schema does.
115#[derive(Deserialize)]
116#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
117#[cfg_attr(feature = "schema", schemars(rename = "ProviderSource"))]
118#[serde(tag = "format", rename_all = "snake_case", deny_unknown_fields)]
119pub(crate) enum ProviderSourceWire {
120    /// A static SVG document, rasterized at its shown size. The source's kind is `image`.
121    Svg {},
122    /// A GIF, animated WebP, or APNG: one cycle of frames, each with its own duration.
123    /// Repetition is the clip's. The source's kind is `video`.
124    AnimatedImage {},
125    /// A Lottie animation (JSON file or dotLottie package), evaluated at its own frames. The
126    /// source's kind is `video`.
127    Lottie {
128        /// The dotLottie animation shown, by ID. Absent means the package's first; always absent
129        /// for a Lottie JSON file.
130        #[serde(default, skip_serializing_if = "Option::is_none")]
131        animation: Option<String>,
132    },
133    /// Still images played in order at a fixed rate. The source's kind is `video`.
134    ImageSequence {
135        /// Each frame's locator, in play order. `null` marks a missing number, allowed only when
136        /// `missing` says how to show it.
137        frames: Vec<Option<String>>,
138        /// The rate the frames play at.
139        frame_rate: FrameRate,
140        /// The number of the first frame, as its file was numbered.
141        #[serde(default)]
142        start_number: u64,
143        /// How a missing frame is shown.
144        #[serde(default)]
145        missing: MissingFrames,
146        /// The primaries of OpenEXR frames' linear RGB, overriding the first frame's
147        /// chromaticities. Only for OpenEXR frames.
148        #[serde(default, skip_serializing_if = "Option::is_none")]
149        primaries: Option<LinearPrimaries>,
150    },
151    /// A still OpenEXR image: linear-light RGB(A), read at half precision. The source's kind is
152    /// `image`.
153    Exr {
154        /// The primaries of its linear RGB, overriding the file's chromaticities (BT.709 when the
155        /// file has none, as OpenEXR defines).
156        #[serde(default, skip_serializing_if = "Option::is_none")]
157        primaries: Option<LinearPrimaries>,
158    },
159    /// A LaTeX math formula typeset with the bundled STIX Two Math font. The source's kind is
160    /// `image`, and it has no locator or fingerprint since it reads no file. A formula that uses
161    /// a character the font lacks, such as Hangul in `\text`, does not open.
162    Latex {
163        /// Math-mode LaTeX without `$` delimiters. Non-empty, at most [`MAX_LATEX_BYTES`] bytes.
164        latex: String,
165        /// Pixels per em, above `0` and at most [`MAX_LATEX_FONT_SIZE`].
166        font_size: f32,
167        /// The ink where the formula sets no `\color` of its own. White when absent.
168        #[serde(default = "white")]
169        color: Rgba,
170        /// Display style, with large operators and limits above and below, or text style when
171        /// `false`. Display when absent.
172        #[serde(default = "display_style")]
173        display: bool,
174    },
175    /// One page of a PDF document. The source's kind is `image`.
176    Pdf {
177        /// The page shown, counted from `1`, at most [`MAX_PDF_PAGES`]. The first when absent.
178        #[serde(default = "first_page")]
179        page: u32,
180        /// Painted under the page, which is itself transparent. White paper when absent.
181        #[serde(default = "white")]
182        background: Rgba,
183    },
184}
185
186impl From<ProviderSourceWire> for ProviderSource {
187    fn from(wire: ProviderSourceWire) -> Self {
188        match wire {
189            ProviderSourceWire::Svg {} => Self::Svg,
190            ProviderSourceWire::AnimatedImage {} => Self::AnimatedImage,
191            ProviderSourceWire::Lottie { animation } => Self::Lottie { animation },
192            ProviderSourceWire::ImageSequence {
193                frames,
194                frame_rate,
195                start_number,
196                missing,
197                primaries,
198            } => Self::ImageSequence {
199                frames,
200                frame_rate,
201                start_number,
202                missing,
203                primaries,
204            },
205            ProviderSourceWire::Exr { primaries } => Self::Exr { primaries },
206            ProviderSourceWire::Latex {
207                latex,
208                font_size,
209                color,
210                display,
211            } => Self::Latex {
212                latex,
213                font_size,
214                color,
215                display,
216            },
217            ProviderSourceWire::Pdf { page, background } => Self::Pdf { page, background },
218        }
219    }
220}
221
222const fn white() -> Rgba {
223    Rgba::new(1.0, 1.0, 1.0, 1.0)
224}
225
226const fn display_style() -> bool {
227    true
228}
229
230const fn first_page() -> u32 {
231    1
232}
233
234/// Named RGB primaries linear-light media may be in, each with its own white point.
235#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
236#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
237#[serde(rename_all = "snake_case")]
238pub enum LinearPrimaries {
239    /// ITU-R BT.709 and sRGB, D65 white.
240    Bt709,
241    /// ITU-R BT.2020, D65 white.
242    Bt2020,
243    /// DCI-P3 primaries with a D65 white, as Display P3.
244    P3D65,
245    /// ACES AP0 (SMPTE ST 2065-1), the ACES white.
246    AcesAp0,
247    /// ACES AP1 (ACEScg), the ACES white.
248    AcesAp1,
249}
250
251/// How an image sequence shows a frame missing from its numbering.
252#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
253#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
254#[serde(rename_all = "snake_case")]
255pub enum MissingFrames {
256    /// A sequence with a missing frame is refused.
257    #[default]
258    Reject,
259    /// The frame before it is shown.
260    Hold,
261    /// Nothing is shown: a transparent frame.
262    Blank,
263}
264
265impl ProviderSource {
266    /// The kind of source the provider's media is.
267    #[must_use]
268    pub const fn kind(&self) -> MediaKind {
269        match self {
270            Self::Svg | Self::Exr { .. } | Self::Latex { .. } | Self::Pdf { .. } => {
271                MediaKind::Image
272            }
273            Self::AnimatedImage | Self::Lottie { .. } | Self::ImageSequence { .. } => {
274                MediaKind::Video
275            }
276        }
277    }
278
279    /// Whether the provider reads a file of the source's own. One that does not, such as LaTeX,
280    /// holds its content in these settings, so its source has no locator or fingerprint.
281    #[must_use]
282    pub const fn reads_file(&self) -> bool {
283        !matches!(self, Self::Latex { .. })
284    }
285
286    /// Whether the settings are coherent and within the limits each variant's fields state.
287    #[must_use]
288    pub fn is_valid(&self) -> bool {
289        match self {
290            Self::Lottie {
291                animation: Some(animation),
292            } => return !animation.is_empty(),
293            Self::Latex {
294                latex,
295                font_size,
296                color,
297                ..
298            } => {
299                return !latex.is_empty()
300                    && latex.len() <= MAX_LATEX_BYTES
301                    && *font_size > 0.0
302                    && *font_size <= MAX_LATEX_FONT_SIZE
303                    && color.is_valid();
304            }
305            Self::Pdf { page, background } => {
306                return (1..=MAX_PDF_PAGES).contains(page) && background.is_valid();
307            }
308            _ => {}
309        }
310        let Self::ImageSequence {
311            frames, missing, ..
312        } = self
313        else {
314            return true;
315        };
316        let present = frames.iter().flatten().collect::<Vec<_>>();
317        let unique = present
318            .iter()
319            .collect::<std::collections::HashSet<_>>()
320            .len()
321            == present.len();
322        !frames.is_empty()
323            && frames.len() <= MAX_SEQUENCE_FRAMES
324            && frames[0].is_some()
325            && unique
326            && present.iter().all(|locator| !locator.is_empty())
327            && (present.len() == frames.len() || *missing != MissingFrames::Reject)
328    }
329}