Skip to main content

alpha2_model/
error.rs

1use crate::{ClipId, CompositionId, EffectResourceId, NodeId, TrackId};
2use alpha2_media::SourceId;
3use std::fmt;
4
5/// What holds a shape or a layout a [`ModelError::InvalidShape`] or
6/// [`ModelError::InvalidLayout`] names.
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub enum ShapeOwner {
9    /// A generated source's shapes or layout generator.
10    Source(SourceId),
11    /// A clip's masks.
12    Clip(ClipId),
13    /// A composition node's shapes or layout generator, or its masks.
14    Node {
15        /// The composition.
16        composition: CompositionId,
17        /// The node.
18        node: NodeId,
19    },
20}
21
22impl fmt::Display for ShapeOwner {
23    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::Source(source) => write!(formatter, "source {source}"),
26            Self::Clip(clip) => write!(formatter, "clip {clip}"),
27            Self::Node { composition, node } => {
28                write!(formatter, "composition {composition} node {node}")
29            }
30        }
31    }
32}
33
34/// A document invariant violation.
35#[derive(Clone, Debug, Eq, PartialEq)]
36#[non_exhaustive]
37pub enum ModelError {
38    /// This build cannot interpret the document version.
39    UnsupportedSchemaVersion {
40        /// Version found in the decoded document.
41        found: u32,
42    },
43    /// At least one ID was empty.
44    EmptyIdentifier,
45    /// Two sources share one ID.
46    DuplicateSourceId(SourceId),
47    /// Two effect resources share one ID.
48    DuplicateEffectResourceId(EffectResourceId),
49    /// Two tracks share one ID.
50    DuplicateTrackId(TrackId),
51    /// Two clips share one ID.
52    DuplicateClipId(ClipId),
53    /// A source locator was empty.
54    EmptyLocator {
55        /// Source whose locator was empty.
56        source: SourceId,
57    },
58    /// A still image source says it has audio, which no image holds.
59    AudioOnImage {
60        /// The image source.
61        source: SourceId,
62    },
63    /// A source's provider does not fit it: another kind of source than the
64    /// provider supplies, audio, or incoherent settings; or a generated
65    /// source has one.
66    InvalidProvider {
67        /// The source.
68        source: SourceId,
69    },
70    /// A source's fingerprint is malformed, or a generated source has one.
71    InvalidFingerprint {
72        /// The source.
73        source: SourceId,
74    },
75    /// A source declares a color whose dynamic range its transfer
76    /// contradicts: HDR is exactly a PQ or HLG transfer.
77    ContradictoryColor {
78        /// The source.
79        source: SourceId,
80    },
81    /// A generated source has no valid generator, or has a locator, color
82    /// metadata, or audio; or another source has a generator.
83    InvalidGenerator {
84        /// The source.
85        source: SourceId,
86    },
87    /// A shapes generator's content tree, or a mask's outline, breaks a
88    /// limit, has a value out of range, or has keyframes that cannot
89    /// interpolate.
90    InvalidShape {
91        /// What holds the shape.
92        owner: ShapeOwner,
93        /// Where in it the problem lies, like `items[2].items[0].path` or
94        /// `masks[1].shape.size`.
95        at: String,
96        /// What is wrong there.
97        problem: crate::ShapeProblem,
98    },
99    /// A layout generator's tree breaks a limit or its structure rules, has
100    /// a value out of range, or has keyframes that cannot interpolate.
101    InvalidLayout {
102        /// What holds the layout.
103        owner: ShapeOwner,
104        /// Where in it the problem lies, like `root.children[2].padding`.
105        at: String,
106        /// What is wrong there.
107        problem: crate::LayoutProblem,
108    },
109    /// An effect resource locator was empty.
110    EmptyEffectResourceLocator {
111        /// Resource whose locator was empty.
112        resource: EffectResourceId,
113    },
114    /// A clip effect references an absent effect resource.
115    UnknownEffectResource {
116        /// Clip containing the reference.
117        clip: ClipId,
118        /// Missing resource identity.
119        resource: EffectResourceId,
120    },
121    /// A clip has an empty source or timeline range.
122    EmptyClipRange {
123        /// Clip with an empty range.
124        clip: ClipId,
125    },
126    /// A clip starts before the project or source timeline origin.
127    NegativeClipRange {
128        /// Clip with a negative range start.
129        clip: ClipId,
130    },
131    /// Two clips overlap without one transition covering their intersection.
132    OverlappingClips {
133        /// Track containing the overlap.
134        track: TrackId,
135        /// First overlapping clip.
136        first: ClipId,
137        /// Second overlapping clip.
138        second: ClipId,
139    },
140    /// A clip refers to an absent source.
141    UnknownClipSource {
142        /// Clip containing the absent source reference.
143        clip: ClipId,
144        /// Referenced source that was not in the project.
145        source: SourceId,
146    },
147    /// A source cannot be placed on the selected track kind.
148    IncompatibleClipSource {
149        /// Clip containing the incompatible reference.
150        clip: ClipId,
151        /// Track that cannot accept the source kind.
152        track: TrackId,
153        /// Source with the incompatible kind.
154        source: SourceId,
155    },
156    /// A visual clip has non-finite values, invalid opacity, or unordered keyframes.
157    InvalidVisualProperties {
158        /// Clip containing the invalid presentation data.
159        clip: ClipId,
160    },
161    /// A clip has an invalid gain, pan, fade, or audio effect parameter.
162    InvalidAudioProperties {
163        /// Clip containing the invalid audio data.
164        clip: ClipId,
165    },
166    /// A track bus has an invalid gain, pan, effect, or ducking parameter.
167    InvalidTrackAudio {
168        /// Track containing the invalid bus data.
169        track: TrackId,
170    },
171    /// The project master bus has an invalid gain, effect, or limiter parameter.
172    InvalidMasterAudio,
173    /// A ducking sidechain names a missing track, itself, or forms a cycle.
174    InvalidDuckingSidechain {
175        /// Track whose ducking reference is unusable.
176        track: TrackId,
177        /// Referenced sidechain track.
178        sidechain: TrackId,
179    },
180    /// A clip names an absent composition.
181    UnknownComposition {
182        /// The clip.
183        clip: ClipId,
184        /// The composition it names.
185        composition: CompositionId,
186    },
187    /// A clip's composition has a non-positive scale, reaches a composition
188    /// time past [`alpha2_time::MAX_SERIALIZED_TICKS`] before the clip ends,
189    /// or is on an audio clip.
190    InvalidClipComposition {
191        /// The clip.
192        clip: ClipId,
193    },
194    /// Two compositions share one ID.
195    DuplicateCompositionId(CompositionId),
196    /// A node ID has a `/`, which joins the IDs of the group and media nodes
197    /// a drawn node was inlined through.
198    InvalidNodeId {
199        /// The composition.
200        composition: CompositionId,
201        /// The node ID.
202        node: NodeId,
203    },
204    /// Two nodes of one composition share an ID.
205    DuplicateNodeId {
206        /// The composition.
207        composition: CompositionId,
208        /// The repeated node ID.
209        node: NodeId,
210    },
211    /// A composition has more nodes than the renderer will schedule.
212    TooManyCompositionNodes {
213        /// The composition.
214        composition: CompositionId,
215        /// Number supplied by the document.
216        found: usize,
217        /// Largest supported count.
218        maximum: usize,
219    },
220    /// A node has an invalid setting: an effect identity, version,
221    /// parameter, or value; a transform, progress, opacity, or mask out of
222    /// range; too many merge layers or masks; or a bypass on a node without
223    /// inputs.
224    InvalidNode {
225        /// The composition.
226        composition: CompositionId,
227        /// The node.
228        node: NodeId,
229    },
230    /// A node input names a node its composition does not have.
231    UnknownNodeInput {
232        /// The composition.
233        composition: CompositionId,
234        /// The node whose input it is.
235        node: NodeId,
236        /// The input: `input`, `from`, `to`, `matte`, `layers[i]`, or
237        /// `inputs[i]`.
238        input: String,
239        /// The node named.
240        target: NodeId,
241    },
242    /// A node depends on its own output.
243    CompositionCycle {
244        /// The composition.
245        composition: CompositionId,
246        /// A node on the cycle.
247        node: NodeId,
248    },
249    /// A composition's output names a node it does not have.
250    UnknownCompositionOutput {
251        /// The composition.
252        composition: CompositionId,
253        /// The node named.
254        node: NodeId,
255    },
256    /// A media node names an absent source or one that is not visual.
257    InvalidNodeSource {
258        /// The composition.
259        composition: CompositionId,
260        /// The node.
261        node: NodeId,
262        /// The source named.
263        source: SourceId,
264    },
265    /// A composition's group inputs are unnamed, repeated, or too many, or
266    /// a control names no effect setting of its nodes or repeats one.
267    InvalidCompositionPorts {
268        /// The composition.
269        composition: CompositionId,
270    },
271    /// A group node names an absent composition.
272    UnknownGroupComposition {
273        /// The composition holding the group node.
274        composition: CompositionId,
275        /// The group node.
276        node: NodeId,
277        /// The composition it names.
278        target: CompositionId,
279    },
280    /// A composition uses itself through group nodes or composition
281    /// sources.
282    CompositionUsesItself {
283        /// A composition on the loop.
284        composition: CompositionId,
285    },
286    /// A composition draws compositions, through group nodes or
287    /// composition sources, nested more than
288    /// [`crate::MAX_COMPOSITION_DEPTH`] deep.
289    CompositionNestedTooDeep {
290        /// The first such composition, in document order.
291        composition: CompositionId,
292        /// Deepest supported nesting.
293        maximum: usize,
294    },
295    /// The compositions draw more nodes than
296    /// [`crate::MAX_INLINED_NODES`], each group node and composition-source
297    /// media node counted as the nodes of the composition it draws.
298    TooManyInlinedNodes {
299        /// Largest supported count.
300        maximum: usize,
301    },
302    /// The clips on the timeline at one instant draw more than
303    /// [`crate::MAX_FRAME_INLINED_NODES`] composition nodes together.
304    FrameDrawsTooManyNodes {
305        /// The first clip, in document order, starting at the first such
306        /// instant.
307        clip: ClipId,
308        /// The nodes the clips at that instant draw.
309        found: u64,
310        /// Largest supported count.
311        maximum: usize,
312    },
313    /// A composition source names an absent composition, or has what only a
314    /// host-supplied source has.
315    InvalidCompositionSource {
316        /// The source.
317        source: SourceId,
318    },
319    /// A node's effect references an absent effect resource.
320    UnknownNodeResource {
321        /// The composition.
322        composition: CompositionId,
323        /// The node.
324        node: NodeId,
325        /// The resource named.
326        resource: EffectResourceId,
327    },
328    /// A clip has more than [`crate::MAX_CLIP_MASKS`] masks, a mask with a
329    /// negative or non-finite value, or masks on an audio track.
330    InvalidClipMask {
331        /// Clip carrying the invalid mask.
332        clip: ClipId,
333    },
334    /// A caption track has no captions, or has clips or transitions, or its
335    /// cues or style are invalid; or another track has captions.
336    InvalidCaptions {
337        /// The track.
338        track: TrackId,
339    },
340    /// A clip freezes on an instant outside its source range.
341    InvalidTimeMap {
342        /// The clip.
343        clip: ClipId,
344    },
345    /// A clip's matte names no visual clip, itself, a clip with a matte of its
346    /// own, or a clip in a transition; or an audio clip has a matte.
347    InvalidMatte {
348        /// Clip whose matte is invalid.
349        clip: ClipId,
350    },
351    /// A transition is not contained in two distinct visual clips on its track.
352    InvalidClipTransition {
353        /// Track containing the invalid transition.
354        track: TrackId,
355    },
356    /// Transitions active at one instant lead from clip to clip in a circle,
357    /// which leaves no clip to show.
358    TransitionCycle {
359        /// Track containing the transitions.
360        track: TrackId,
361        /// An instant they are all active.
362        at: alpha2_time::MediaTime,
363    },
364    /// Two clips have more than one transition between them, in either
365    /// direction: each overlap shows exactly one.
366    DuplicateTransition {
367        /// Track containing the transitions.
368        track: TrackId,
369        /// The transition's first clip.
370        first: ClipId,
371        /// The transition's second clip.
372        second: ClipId,
373    },
374    /// The project contains too many compositions.
375    TooManyCompositions {
376        /// Number supplied by the document.
377        found: usize,
378        /// Largest supported count.
379        maximum: usize,
380    },
381    /// The source table exceeds the bounded project resource budget.
382    TooManySources {
383        /// Number supplied by the document.
384        found: usize,
385        /// Largest supported count.
386        maximum: usize,
387    },
388    /// The timeline contains too many tracks.
389    TooManyTracks {
390        /// Number supplied by the document.
391        found: usize,
392        /// Largest supported count.
393        maximum: usize,
394    },
395    /// The timeline contains too many clips in total.
396    TooManyClips {
397        /// Number supplied by the document.
398        found: usize,
399        /// Largest supported count.
400        maximum: usize,
401    },
402    /// More of the project's audio effects hold state at once than
403    /// [`crate::MAX_AUDIO_EFFECTS`] (see [`crate::Project::audio_effect_load`]).
404    TooManyAudioEffects {
405        /// The most effects holding state at once.
406        found: usize,
407        /// Largest supported count.
408        maximum: usize,
409        /// The earliest timeline position, in ticks, at which `found` hold
410        /// state.
411        at_ticks: i64,
412    },
413    /// The project's audio effects hold more than
414    /// [`crate::MAX_AUDIO_DELAY_MS`] of delay at once (see
415    /// [`crate::Project::audio_effect_load`]).
416    TooMuchAudioDelay {
417        /// The most milliseconds held at once, rounded up.
418        found_ms: u64,
419        /// Largest supported total.
420        maximum_ms: u32,
421        /// The earliest timeline position, in ticks, at which `found_ms` is
422        /// held.
423        at_ticks: i64,
424    },
425    /// One track contains too many clips.
426    TooManyClipsOnTrack {
427        /// Track containing the excessive clip list.
428        track: TrackId,
429        /// Number supplied by the document.
430        found: usize,
431        /// Largest supported count.
432        maximum: usize,
433    },
434    /// A caption track holds more than [`crate::MAX_CLIPS_PER_TRACK`] cues.
435    TooManyCaptionCues {
436        /// The caption track.
437        track: TrackId,
438        /// Number supplied by the document.
439        found: usize,
440        /// Largest supported count.
441        maximum: usize,
442    },
443    /// One track contains too many transitions.
444    TooManyTransitions {
445        /// Track containing the excessive transition list.
446        track: TrackId,
447        /// Number supplied by the document.
448        found: usize,
449        /// Largest supported count.
450        maximum: usize,
451    },
452    /// Width or height was zero.
453    ZeroCanvasDimension,
454    /// A canvas edge exceeds the portable GPU-independent limit.
455    CanvasDimensionTooLarge {
456        /// Supplied width.
457        width: u32,
458        /// Supplied height.
459        height: u32,
460        /// Largest supported edge.
461        maximum: u32,
462    },
463    /// The canvas area exceeds the bounded pixel budget.
464    CanvasPixelBudgetExceeded {
465        /// Supplied pixel area.
466        pixels: u64,
467        /// Largest supported pixel area.
468        maximum: u64,
469    },
470    /// A background component is non-finite or outside `0..=1`.
471    InvalidBackground,
472    /// The color pipeline's HDR peak is outside
473    /// [`crate::HDR_PEAK_NITS_RANGE`].
474    InvalidColorPipeline,
475    /// The project sample rate is outside the supported DSP interval.
476    UnsupportedSampleRate {
477        /// Supplied rate.
478        found: u32,
479        /// Smallest supported rate.
480        minimum: u32,
481        /// Largest supported rate.
482        maximum: u32,
483    },
484}
485
486impl fmt::Display for ModelError {
487    #[allow(clippy::too_many_lines)]
488    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
489        match self {
490            Self::UnsupportedSchemaVersion { found } => {
491                write!(formatter, "unsupported schema version {found}")
492            }
493            Self::EmptyIdentifier => formatter.write_str("document contains an empty identifier"),
494            Self::DuplicateSourceId(id) => write!(formatter, "duplicate source ID {id}"),
495            Self::DuplicateEffectResourceId(id) => {
496                write!(formatter, "duplicate effect resource ID {id}")
497            }
498            Self::DuplicateTrackId(id) => write!(formatter, "duplicate track ID {id}"),
499            Self::DuplicateClipId(id) => write!(formatter, "duplicate clip ID {id}"),
500            Self::EmptyLocator { source } => {
501                write!(formatter, "source {source} has an empty locator")
502            }
503            Self::AudioOnImage { source } => {
504                write!(
505                    formatter,
506                    "source {source} is a still image and has no audio"
507                )
508            }
509            Self::InvalidGenerator { source } => {
510                write!(formatter, "source {source} has an invalid generator")
511            }
512            Self::InvalidShape { owner, at, problem } => {
513                write!(formatter, "{owner} has an invalid shape: {at} {problem}")
514            }
515            Self::InvalidLayout { owner, at, problem } => {
516                write!(formatter, "{owner} has an invalid layout: {at} {problem}")
517            }
518            Self::ContradictoryColor { source } => write!(
519                formatter,
520                "source {source} declares a dynamic range its transfer contradicts"
521            ),
522            Self::InvalidFingerprint { source } => {
523                write!(formatter, "source {source} has an invalid fingerprint")
524            }
525            Self::InvalidProvider { source } => {
526                write!(formatter, "source {source} has an invalid provider")
527            }
528            Self::EmptyEffectResourceLocator { resource } => {
529                write!(formatter, "effect resource {resource} has an empty locator")
530            }
531            Self::UnknownEffectResource { clip, resource } => {
532                write!(
533                    formatter,
534                    "clip {clip} references unknown effect resource {resource}"
535                )
536            }
537            Self::EmptyClipRange { clip } => write!(formatter, "clip {clip} has an empty range"),
538            Self::NegativeClipRange { clip } => {
539                write!(formatter, "clip {clip} has a range before timeline origin")
540            }
541            Self::OverlappingClips {
542                track,
543                first,
544                second,
545            } => write!(
546                formatter,
547                "clips {first} and {second} overlap without a covering transition on track {track}"
548            ),
549            Self::UnknownClipSource { clip, source } => {
550                write!(formatter, "clip {clip} references unknown source {source}")
551            }
552            Self::IncompatibleClipSource {
553                clip,
554                track,
555                source,
556            } => {
557                write!(
558                    formatter,
559                    "clip {clip} cannot use source {source} on track {track}"
560                )
561            }
562            Self::InvalidVisualProperties { clip } => {
563                write!(formatter, "clip {clip} has invalid visual properties")
564            }
565            Self::InvalidAudioProperties { clip } => {
566                write!(formatter, "clip {clip} has invalid audio properties")
567            }
568            Self::InvalidTrackAudio { track } => {
569                write!(formatter, "track {track} has invalid audio bus settings")
570            }
571            Self::InvalidMasterAudio => {
572                write!(formatter, "the project master bus is invalid")
573            }
574            Self::InvalidDuckingSidechain { track, sidechain } => write!(
575                formatter,
576                "track {track} cannot duck from sidechain track {sidechain}"
577            ),
578            Self::UnknownComposition { clip, composition } => {
579                write!(
580                    formatter,
581                    "clip {clip} references unknown composition {composition}"
582                )
583            }
584            Self::InvalidClipComposition { clip } => {
585                write!(
586                    formatter,
587                    "clip {clip} has an invalid composition reference"
588                )
589            }
590            Self::DuplicateCompositionId(id) => write!(formatter, "duplicate composition ID {id}"),
591            Self::InvalidNodeId { composition, node } => {
592                write!(
593                    formatter,
594                    "composition {composition} has node ID {node}, which has a `/`"
595                )
596            }
597            Self::DuplicateNodeId { composition, node } => {
598                write!(
599                    formatter,
600                    "composition {composition} has duplicate node ID {node}"
601                )
602            }
603            Self::TooManyCompositionNodes {
604                composition,
605                found,
606                maximum,
607            } => write!(
608                formatter,
609                "composition {composition} has {found} nodes; the maximum is {maximum}"
610            ),
611            Self::InvalidNode { composition, node } => {
612                write!(
613                    formatter,
614                    "node {node} of composition {composition} has an invalid setting"
615                )
616            }
617            Self::UnknownNodeInput {
618                composition,
619                node,
620                input,
621                target,
622            } => write!(
623                formatter,
624                "input {input} of node {node} in composition {composition} names unknown node {target}"
625            ),
626            Self::CompositionCycle { composition, node } => write!(
627                formatter,
628                "node {node} of composition {composition} depends on its own output"
629            ),
630            Self::UnknownCompositionOutput { composition, node } => write!(
631                formatter,
632                "composition {composition} outputs unknown node {node}"
633            ),
634            Self::InvalidNodeSource {
635                composition,
636                node,
637                source,
638            } => write!(
639                formatter,
640                "node {node} of composition {composition} cannot show source {source}"
641            ),
642            Self::InvalidCompositionPorts { composition } => write!(
643                formatter,
644                "composition {composition} has invalid group inputs or controls"
645            ),
646            Self::UnknownGroupComposition {
647                composition,
648                node,
649                target,
650            } => write!(
651                formatter,
652                "group node {node} of composition {composition} names unknown composition {target}"
653            ),
654            Self::CompositionUsesItself { composition } => {
655                write!(formatter, "composition {composition} uses itself")
656            }
657            Self::CompositionNestedTooDeep {
658                composition,
659                maximum,
660            } => write!(
661                formatter,
662                "composition {composition} nests compositions more than {maximum} deep"
663            ),
664            Self::TooManyInlinedNodes { maximum } => write!(
665                formatter,
666                "compositions draw more than {maximum} nodes, counting each group and composition source as the nodes it draws"
667            ),
668            Self::FrameDrawsTooManyNodes {
669                clip,
670                found,
671                maximum,
672            } => write!(
673                formatter,
674                "the clips on the timeline where clip {clip} starts draw {found} composition nodes; a frame draws at most {maximum}"
675            ),
676            Self::InvalidCompositionSource { source } => {
677                write!(
678                    formatter,
679                    "source {source} is not a valid composition source"
680                )
681            }
682            Self::UnknownNodeResource {
683                composition,
684                node,
685                resource,
686            } => write!(
687                formatter,
688                "node {node} of composition {composition} references unknown effect resource {resource}"
689            ),
690            Self::InvalidClipMask { clip } => write!(formatter, "clip {clip} has an invalid mask"),
691            Self::InvalidMatte { clip } => write!(formatter, "clip {clip} has an invalid matte"),
692            Self::InvalidCaptions { track } => {
693                write!(formatter, "track {track} has invalid captions")
694            }
695            Self::InvalidTimeMap { clip } => {
696                write!(formatter, "clip {clip} freezes outside its source range")
697            }
698            Self::InvalidClipTransition { track } => {
699                write!(formatter, "track {track} has an invalid clip transition")
700            }
701            Self::TransitionCycle { track, at } => write!(
702                formatter,
703                "transitions on track {track} active at {} ticks lead from clip to clip in a circle",
704                at.ticks()
705            ),
706            Self::DuplicateTransition {
707                track,
708                first,
709                second,
710            } => write!(
711                formatter,
712                "clips {first} and {second} have more than one transition on track {track}"
713            ),
714            Self::TooManyCompositions { found, maximum } => write!(
715                formatter,
716                "project has {found} compositions; the maximum is {maximum}"
717            ),
718            Self::TooManySources { found, maximum } => {
719                write!(
720                    formatter,
721                    "project has {found} sources; the maximum is {maximum}"
722                )
723            }
724            Self::TooManyTracks { found, maximum } => {
725                write!(
726                    formatter,
727                    "project has {found} tracks; the maximum is {maximum}"
728                )
729            }
730            Self::TooManyClips { found, maximum } => {
731                write!(
732                    formatter,
733                    "project has {found} clips; the maximum is {maximum}"
734                )
735            }
736            Self::TooManyAudioEffects {
737                found,
738                maximum,
739                at_ticks,
740            } => write!(
741                formatter,
742                "project has {found} audio effects holding state at tick {at_ticks}; the maximum at once is {maximum}"
743            ),
744            Self::TooMuchAudioDelay {
745                found_ms,
746                maximum_ms,
747                at_ticks,
748            } => write!(
749                formatter,
750                "project audio effects hold {found_ms} ms of delay at tick {at_ticks}; the maximum at once is {maximum_ms} ms"
751            ),
752            Self::TooManyClipsOnTrack {
753                track,
754                found,
755                maximum,
756            } => write!(
757                formatter,
758                "track {track} has {found} clips; the maximum is {maximum}"
759            ),
760            Self::TooManyCaptionCues {
761                track,
762                found,
763                maximum,
764            } => write!(
765                formatter,
766                "track {track} has {found} caption cues; the maximum is {maximum}"
767            ),
768            Self::TooManyTransitions {
769                track,
770                found,
771                maximum,
772            } => write!(
773                formatter,
774                "track {track} has {found} transitions; the maximum is {maximum}"
775            ),
776            Self::ZeroCanvasDimension => formatter.write_str("canvas dimensions must be positive"),
777            Self::CanvasDimensionTooLarge {
778                width,
779                height,
780                maximum,
781            } => write!(
782                formatter,
783                "canvas {width}x{height} exceeds the maximum edge {maximum}"
784            ),
785            Self::CanvasPixelBudgetExceeded { pixels, maximum } => write!(
786                formatter,
787                "canvas has {pixels} pixels; the maximum is {maximum}"
788            ),
789            Self::InvalidBackground => {
790                formatter.write_str("background color components must be finite and within 0..=1")
791            }
792            Self::InvalidColorPipeline => {
793                formatter.write_str("the HDR peak must be within 100..=10000 nits")
794            }
795            Self::UnsupportedSampleRate {
796                found,
797                minimum,
798                maximum,
799            } => write!(
800                formatter,
801                "sample rate {found} is outside {minimum}..={maximum}"
802            ),
803        }
804    }
805}
806
807impl std::error::Error for ModelError {}
808
809/// Failure while decoding a serialized project document.
810#[derive(Debug)]
811#[non_exhaustive]
812pub enum ProjectLoadError {
813    /// The JSON input exceeded the parser byte budget.
814    TooLarge {
815        /// Supplied UTF-8 byte length.
816        bytes: usize,
817        /// Largest accepted byte length.
818        maximum: usize,
819    },
820    /// JSON syntax or shape was invalid.
821    Json(serde_json::Error),
822    /// The decoded document violated a model invariant.
823    InvalidProject(ModelError),
824}
825
826impl fmt::Display for ProjectLoadError {
827    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
828        match self {
829            Self::TooLarge { bytes, maximum } => {
830                write!(
831                    formatter,
832                    "project JSON is {bytes} bytes; the maximum is {maximum}"
833                )
834            }
835            Self::Json(error) => write!(formatter, "invalid project JSON: {error}"),
836            Self::InvalidProject(error) => write!(formatter, "invalid project: {error}"),
837        }
838    }
839}
840
841impl std::error::Error for ProjectLoadError {
842    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
843        match self {
844            Self::TooLarge { .. } => None,
845            Self::Json(error) => Some(error),
846            Self::InvalidProject(error) => Some(error),
847        }
848    }
849}