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