alpha2_model/provider.rs
1//! Sources whose media an extension provider supplies.
2
3use alpha2_time::FrameRate;
4use serde::{Deserialize, Serialize};
5
6use crate::MediaKind;
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. A package is read
12/// whole, and only the files a source shows are unpacked from it: its
13/// animation, within [`MAX_LOTTIE_DOCUMENT_BYTES`], and that animation's
14/// images. A larger file is refused before it is read.
15pub const MAX_PROVIDER_DOCUMENT_BYTES: usize = 256 * 1024 * 1024;
16
17/// Largest Lottie animation a provider reads, in bytes: a Lottie JSON file,
18/// or the animation a dotLottie package unpacks to. A Lottie document is
19/// parsed whole, to as much as about 45 times its size at its densest
20/// (numbers alone), so a larger one is refused before it is parsed, and a
21/// host refuses a larger file before reading it. Within it, a document
22/// parses to at most about 750 MiB.
23pub const MAX_LOTTIE_DOCUMENT_BYTES: usize = 16 * 1024 * 1024;
24
25/// Largest SVG document a provider reads, in bytes. An SVG document is
26/// parsed whole, to as much as about 30 times its size at its densest
27/// (elements alone), so a larger one is refused before it is parsed, and a
28/// host refuses a larger file before reading it. Within it, a document
29/// parses to at most about 960 MiB.
30pub const MAX_SVG_DOCUMENT_BYTES: usize = 32 * 1024 * 1024;
31
32/// A source whose media an extension provider supplies, rather than the
33/// host's video and image decoders.
34///
35/// A host picks the provider by probing the bytes at import, never by a file
36/// name, and records it here with the settings the source needs, so every
37/// host opens the source the same way. The engine does not read it: a
38/// provider source needs frames like any video or image source.
39#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
40#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
41#[serde(tag = "format", rename_all = "snake_case", from = "ProviderSourceWire")]
42#[non_exhaustive]
43pub enum ProviderSource {
44 /// A static SVG document, rasterized at the size it is shown. The
45 /// source's kind is `image`.
46 Svg,
47 /// A GIF, animated WebP, or APNG: one cycle of frames, each with its own
48 /// duration. Repetition is the clip's. The source's kind is `video`.
49 AnimatedImage,
50 /// A Lottie animation, from a Lottie JSON file or a dotLottie package,
51 /// evaluated at its own frames. The source's kind is `video`.
52 Lottie {
53 /// The dotLottie animation shown, by its ID; the package's first
54 /// when absent, and absent 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
59 /// `video`.
60 ImageSequence {
61 /// Each frame's locator, in play order. `null` is a number missing
62 /// from the sequence, allowed only when `missing` says how to show
63 /// it.
64 frames: Vec<Option<String>>,
65 /// The rate the frames play at.
66 frame_rate: FrameRate,
67 /// The number of the first frame, as its file was numbered.
68 #[serde(default)]
69 start_number: u64,
70 /// How a missing frame is shown.
71 #[serde(default)]
72 missing: MissingFrames,
73 /// The primaries OpenEXR frames' linear RGB is in, overriding what
74 /// the first frame's chromaticities say. Only for OpenEXR frames.
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 primaries: Option<LinearPrimaries>,
77 },
78 /// A still OpenEXR image: linear-light RGB(A), read at half precision.
79 /// The source's kind is `image`.
80 Exr {
81 /// The primaries its linear RGB is in, overriding what the file's
82 /// chromaticities say (BT.709 when it says nothing, as OpenEXR
83 /// defines).
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 primaries: Option<LinearPrimaries>,
86 },
87}
88
89/// The wire form of [`ProviderSource`].
90///
91/// serde lets an internally tagged unit variant ignore extra fields even with
92/// `deny_unknown_fields`, so `{"format": "svg", "frames": []}` would parse.
93/// Empty struct variants reject them, as the published schema does.
94#[derive(Deserialize)]
95#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
96#[cfg_attr(feature = "schema", schemars(rename = "ProviderSource"))]
97#[serde(tag = "format", rename_all = "snake_case", deny_unknown_fields)]
98pub(crate) enum ProviderSourceWire {
99 /// A static SVG document, rasterized at the size it is shown. The
100 /// source's kind is `image`.
101 Svg {},
102 /// A GIF, animated WebP, or APNG: one cycle of frames, each with its own
103 /// duration. Repetition is the clip's. The source's kind is `video`.
104 AnimatedImage {},
105 /// A Lottie animation, from a Lottie JSON file or a dotLottie package,
106 /// evaluated at its own frames. The source's kind is `video`.
107 Lottie {
108 /// The dotLottie animation shown, by its ID; the package's first
109 /// when absent, and absent for a Lottie JSON file.
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 animation: Option<String>,
112 },
113 /// Still images played in order at a fixed rate. The source's kind is
114 /// `video`.
115 ImageSequence {
116 /// Each frame's locator, in play order. `null` is a number missing
117 /// from the sequence, allowed only when `missing` says how to show
118 /// it.
119 frames: Vec<Option<String>>,
120 /// The rate the frames play at.
121 frame_rate: FrameRate,
122 /// The number of the first frame, as its file was numbered.
123 #[serde(default)]
124 start_number: u64,
125 /// How a missing frame is shown.
126 #[serde(default)]
127 missing: MissingFrames,
128 /// The primaries OpenEXR frames' linear RGB is in, overriding what
129 /// the first frame's chromaticities say. Only for OpenEXR frames.
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 primaries: Option<LinearPrimaries>,
132 },
133 /// A still OpenEXR image: linear-light RGB(A), read at half precision.
134 /// The source's kind is `image`.
135 Exr {
136 /// The primaries its linear RGB is in, overriding what the file's
137 /// chromaticities say (BT.709 when it says nothing, as OpenEXR
138 /// defines).
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 primaries: Option<LinearPrimaries>,
141 },
142}
143
144impl From<ProviderSourceWire> for ProviderSource {
145 fn from(wire: ProviderSourceWire) -> Self {
146 match wire {
147 ProviderSourceWire::Svg {} => Self::Svg,
148 ProviderSourceWire::AnimatedImage {} => Self::AnimatedImage,
149 ProviderSourceWire::Lottie { animation } => Self::Lottie { animation },
150 ProviderSourceWire::ImageSequence {
151 frames,
152 frame_rate,
153 start_number,
154 missing,
155 primaries,
156 } => Self::ImageSequence {
157 frames,
158 frame_rate,
159 start_number,
160 missing,
161 primaries,
162 },
163 ProviderSourceWire::Exr { primaries } => Self::Exr { primaries },
164 }
165 }
166}
167
168/// Named RGB primaries linear-light media may be in, each with its own white
169/// point.
170#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[serde(rename_all = "snake_case")]
173pub enum LinearPrimaries {
174 /// ITU-R BT.709 and sRGB, D65 white.
175 Bt709,
176 /// ITU-R BT.2020, D65 white.
177 Bt2020,
178 /// DCI-P3 primaries with a D65 white, as Display P3.
179 P3D65,
180 /// ACES AP0 (SMPTE ST 2065-1), the ACES white.
181 AcesAp0,
182 /// ACES AP1 (ACEScg), the ACES white.
183 AcesAp1,
184}
185
186/// How an image sequence shows a frame missing from its numbering.
187#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
188#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
189#[serde(rename_all = "snake_case")]
190pub enum MissingFrames {
191 /// A sequence with a missing frame is refused.
192 #[default]
193 Reject,
194 /// The frame before it is shown.
195 Hold,
196 /// Nothing is shown: a transparent frame.
197 Blank,
198}
199
200impl ProviderSource {
201 /// The kind of source the provider's media is.
202 #[must_use]
203 pub const fn kind(&self) -> MediaKind {
204 match self {
205 Self::Svg | Self::Exr { .. } => MediaKind::Image,
206 Self::AnimatedImage | Self::Lottie { .. } | Self::ImageSequence { .. } => {
207 MediaKind::Video
208 }
209 }
210 }
211
212 /// Whether the settings are coherent: a Lottie animation ID is not empty,
213 /// and an image sequence lists at most
214 /// [`MAX_SEQUENCE_FRAMES`] frames, the first present, no locator empty or
215 /// listed twice, and a missing frame only with a policy for it.
216 #[must_use]
217 pub fn is_valid(&self) -> bool {
218 if let Self::Lottie {
219 animation: Some(animation),
220 } = self
221 {
222 return !animation.is_empty();
223 }
224 let Self::ImageSequence {
225 frames, missing, ..
226 } = self
227 else {
228 return true;
229 };
230 let present = frames.iter().flatten().collect::<Vec<_>>();
231 let unique = present
232 .iter()
233 .collect::<std::collections::HashSet<_>>()
234 .len()
235 == present.len();
236 !frames.is_empty()
237 && frames.len() <= MAX_SEQUENCE_FRAMES
238 && frames[0].is_some()
239 && unique
240 && present.iter().all(|locator| !locator.is_empty())
241 && (present.len() == frames.len() || *missing != MissingFrames::Reject)
242 }
243}