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