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::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}