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