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}