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}