Skip to main content

j2k_jpeg/
error.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Typed error and warning taxonomy. See spec Section 6.
4
5use crate::info::{ColorSpace, Rect, SofKind};
6use j2k_core::CodecError;
7
8/// A category of JPEG marker. Carried in [`JpegError::UnexpectedMarker`] and
9/// related variants so callers can branch on marker class without parsing the
10/// raw byte.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum MarkerKind {
13    /// Start of image (`FFD8`).
14    Soi,
15    /// Start of frame (any of `FFC0..=FFC3`).
16    Sof,
17    /// Define quantization table (`FFDB`).
18    Dqt,
19    /// Define Huffman table (`FFC4`).
20    Dht,
21    /// Define restart interval (`FFDD`).
22    Dri,
23    /// Start of scan (`FFDA`).
24    Sos,
25    /// End of image (`FFD9`).
26    Eoi,
27    /// Adobe APP14 (`FFEE`).
28    App14,
29    /// Any other marker, raw byte preserved.
30    Other(u8),
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34/// Reason an otherwise recognized SOF marker cannot be decoded.
35pub enum UnsupportedReason {
36    /// Stream uses arithmetic entropy coding.
37    ArithmeticCoding,
38    /// Stream uses hierarchical JPEG coding.
39    Hierarchical,
40    /// Stream combines arithmetic entropy coding and hierarchical coding.
41    ArithmeticAndHierarchical,
42    /// Stream uses a differential baseline SOF.
43    DifferentialBaseline,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47/// Huffman entropy decoder failure category.
48pub enum HuffmanFailure {
49    /// A Huffman code exceeded the representable code space.
50    CodeOverflow,
51    /// A decoded symbol is invalid for its context.
52    InvalidSymbol,
53    /// The entropy stream ended before a symbol could be decoded.
54    TableExhausted,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58/// Invalid decoder-builder input configuration.
59pub enum BuilderConflictReason {
60    /// No input source was provided.
61    NoInput,
62    /// Raw input bytes and scan fragments were both provided.
63    InputAndScanFragments,
64    /// Scan-fragment mode was selected without any fragments.
65    ScanFragmentsEmpty,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69/// JPEG table class used in diagnostics.
70pub enum TableKind {
71    /// Quantization table.
72    Quant,
73    /// AC Huffman table.
74    HuffmanAc,
75    /// DC Huffman table.
76    HuffmanDc,
77}
78
79/// Cross-scan progressive coefficient-state violation.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum ProgressiveScanStateError {
83    /// An initial scan selected a coefficient that was already initialized.
84    DuplicateInitial {
85        /// Approximation-low value retained from the earlier scan.
86        previous_al: u8,
87        /// Approximation-low value declared by the duplicate scan.
88        al: u8,
89    },
90    /// A refinement scan selected a coefficient with no initial scan.
91    RefinementBeforeInitial {
92        /// Approximation-high value declared by the refinement scan.
93        ah: u8,
94        /// Approximation-low value declared by the refinement scan.
95        al: u8,
96    },
97    /// A refinement scan skipped or repeated an approximation level.
98    RefinementMismatch {
99        /// Approximation-low value retained from the previous scan.
100        previous_al: u8,
101        /// Approximation-high value declared by the refinement scan.
102        ah: u8,
103        /// Approximation-low value declared by the refinement scan.
104        al: u8,
105    },
106    /// EOI or physical EOF arrived before an initial DC scan for the component.
107    MissingInitialDc,
108}
109
110impl core::fmt::Display for ProgressiveScanStateError {
111    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
112        match self {
113            Self::DuplicateInitial { previous_al, al } => write!(
114                f,
115                "duplicate initial scan (previous Al={previous_al}, new Al={al})"
116            ),
117            Self::RefinementBeforeInitial { ah, al } => {
118                write!(f, "refinement before initial scan (Ah={ah}, Al={al})")
119            }
120            Self::RefinementMismatch {
121                previous_al,
122                ah,
123                al,
124            } => write!(
125                f,
126                "non-contiguous refinement (previous Al={previous_al}, Ah={ah}, Al={al})"
127            ),
128            Self::MissingInitialDc => f.write_str("missing initial DC scan at stream termination"),
129        }
130    }
131}
132
133/// Entropy condition that made a progressive scan terminator invalid.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135#[non_exhaustive]
136pub enum ProgressiveScanTerminationError {
137    /// An EOB run extends beyond the final coded block in the scan.
138    ResidualEobRun {
139        /// Number of blocks still covered after the scan's final block.
140        remaining: u32,
141    },
142    /// Complete entropy bytes or too many buffered bits remain after decoding.
143    ExcessEntropy {
144        /// Complete bytes between the decoder cursor and the parsed boundary.
145        unread_bytes: usize,
146        /// Real (non-synthetic) bits still buffered by the entropy reader.
147        unread_bits: u8,
148    },
149    /// The final partial entropy byte is not padded exclusively with one bits.
150    InvalidPadding {
151        /// Number of real padding bits that remained buffered.
152        unread_bits: u8,
153    },
154    /// The entropy reader observed a different boundary than the parser.
155    TerminalMismatch {
156        /// Parser-recorded absolute terminal offset.
157        expected_offset: usize,
158        /// Parser-recorded marker, or `None` for physical EOF.
159        expected_marker: Option<u8>,
160        /// Entropy-reader absolute marker offset, or `None` at physical EOF.
161        found_offset: Option<usize>,
162        /// Entropy-reader marker, or `None` at physical EOF.
163        found_marker: Option<u8>,
164    },
165}
166
167impl core::fmt::Display for ProgressiveScanTerminationError {
168    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
169        match self {
170            Self::ResidualEobRun { remaining } => {
171                write!(f, "EOB run extends {remaining} blocks past scan end")
172            }
173            Self::ExcessEntropy {
174                unread_bytes,
175                unread_bits,
176            } => write!(
177                f,
178                "excess entropy remains ({unread_bytes} bytes, {unread_bits} buffered bits)"
179            ),
180            Self::InvalidPadding { unread_bits } => {
181                write!(f, "invalid one-bit padding across {unread_bits} bits")
182            }
183            Self::TerminalMismatch {
184                expected_offset,
185                expected_marker,
186                found_offset,
187                found_marker,
188            } => write!(
189                f,
190                "terminal mismatch (expected {expected_marker:?} at {expected_offset}, found {found_marker:?} at {found_offset:?})"
191            ),
192        }
193    }
194}
195
196/// Fatal JPEG decode or API error.
197#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
198#[non_exhaustive]
199pub enum JpegError {
200    #[error("JPEG truncated at offset {offset}: expected {expected} more bytes")]
201    /// Input ended before the requested bytes could be read.
202    Truncated {
203        /// Byte offset where decoding stopped.
204        offset: usize,
205        /// Additional byte count required.
206        expected: usize,
207    },
208
209    #[error("invalid marker FF{marker:02X} at offset {offset}")]
210    /// Marker byte is not legal in the current JPEG position.
211    InvalidMarker {
212        /// Byte offset of the marker.
213        offset: usize,
214        /// Raw marker byte following `0xff`.
215        marker: u8,
216    },
217
218    #[error("expected {expected:?}, found FF{found:02X} at offset {offset}")]
219    /// A different marker kind was required at this position.
220    UnexpectedMarker {
221        /// Byte offset of the unexpected marker.
222        offset: usize,
223        /// Marker kind expected by the parser.
224        expected: MarkerKind,
225        /// Raw marker byte that was found.
226        found: u8,
227    },
228
229    #[error("missing required marker {marker:?}")]
230    /// Required marker was absent from the stream.
231    MissingMarker {
232        /// Missing marker kind.
233        marker: MarkerKind,
234    },
235
236    #[error("duplicate {marker:?} at offset {offset}")]
237    /// A marker appeared more than once where only one is legal.
238    DuplicateMarker {
239        /// Byte offset of the duplicate marker.
240        offset: usize,
241        /// Duplicated marker kind.
242        marker: MarkerKind,
243    },
244
245    #[error("invalid length {length} for marker FF{marker:02X} at offset {offset}")]
246    /// Marker segment length is invalid for the marker kind.
247    InvalidSegmentLength {
248        /// Byte offset of the segment.
249        offset: usize,
250        /// Raw marker byte following `0xff`.
251        marker: u8,
252        /// Declared segment length.
253        length: u16,
254    },
255
256    #[error("conflicting duplicate JPEG table {table:?} id={id} at offset {offset}")]
257    /// Duplicate DQT/DHT table has different bytes from an earlier definition.
258    ConflictingDuplicateTable {
259        /// Byte offset of the conflicting table segment.
260        offset: usize,
261        /// Table class.
262        table: TableKind,
263        /// Table id.
264        id: u8,
265    },
266
267    #[error(
268        "zero quantization value in table {table} at coefficient {coefficient} (offset {offset})"
269    )]
270    /// DQT entries must be non-zero; a zero divisor destroys coefficient data.
271    InvalidQuantizationValue {
272        /// Byte offset of the zero value in the DQT payload.
273        offset: usize,
274        /// Quantization-table identifier (`Tq`).
275        table: u8,
276        /// Quantization entry index in transmitted order.
277        coefficient: u8,
278    },
279
280    #[error("expected dimensions are required to repair zero SOF dimensions at offset {offset}")]
281    /// TIFF/NDPI metadata did not provide dimensions needed to repair a zero SOF.
282    ExpectedDimensionsRequired {
283        /// Byte offset of the SOF marker.
284        offset: usize,
285    },
286
287    #[error(
288        "expected dimensions {expected:?} conflict with SOF dimensions {actual:?} at offset {offset}"
289    )]
290    /// Container-provided dimensions conflict with non-zero SOF dimensions.
291    ConflictingExpectedDimensions {
292        /// Byte offset of the SOF marker.
293        offset: usize,
294        /// Expected dimensions supplied by the caller.
295        expected: (u16, u16),
296        /// Dimensions declared by the SOF marker.
297        actual: (u16, u16),
298    },
299
300    #[error("invalid TIFF JPEG assembly at offset {offset}: {reason}")]
301    /// TIFF/JPEGTables assembly cannot produce a valid JPEG interchange stream.
302    InvalidJpegAssembly {
303        /// Byte offset of the assembly problem.
304        offset: usize,
305        /// Static diagnostic reason.
306        reason: &'static str,
307    },
308
309    #[error("conflicting DRI values at offset {offset}: existing {existing}, new {new}")]
310    /// Duplicate DRI marker conflicts with an earlier DRI value.
311    ConflictingDri {
312        /// Byte offset of the conflicting DRI segment.
313        offset: usize,
314        /// Existing non-zero restart interval.
315        existing: u16,
316        /// New non-zero restart interval.
317        new: u16,
318    },
319
320    /// Unsupported SOF variant. Carries the raw marker byte (e.g. `0xC9` for
321    /// arithmetic extended-sequential) so callers routing to a fallback
322    /// decoder can distinguish FFC5 from FFC9 without relying on `reason`.
323    #[error("unsupported SOF marker FF{marker:02X} ({reason:?})")]
324    /// SOF marker class is outside the decoder's supported JPEG subset.
325    UnsupportedSof {
326        /// Raw SOF marker byte.
327        marker: u8,
328        /// Unsupported feature category.
329        reason: UnsupportedReason,
330    },
331
332    #[error("unsupported component count: {count}")]
333    /// Component count is outside the supported range.
334    UnsupportedComponentCount {
335        /// Declared component count.
336        count: u8,
337    },
338
339    #[error("unsupported color space for decode: {color_space:?}")]
340    /// Color space cannot be produced by the requested decode path.
341    UnsupportedColorSpace {
342        /// Header-derived color space.
343        color_space: ColorSpace,
344    },
345
346    #[error("unsupported bit depth: {depth}")]
347    /// Sample precision is not supported by this decoder path.
348    UnsupportedBitDepth {
349        /// Declared sample precision in bits.
350        depth: u8,
351    },
352
353    #[error("unsupported lossless predictor: {predictor}")]
354    /// Lossless predictor selection is unsupported.
355    UnsupportedPredictor {
356        /// Predictor value from the scan header.
357        predictor: u8,
358    },
359
360    #[error("zero dimension in SOF: {width}×{height}")]
361    /// SOF declares a zero width or height.
362    ZeroDimension {
363        /// Declared width.
364        width: u16,
365        /// Declared height.
366        height: u16,
367    },
368
369    #[error("dimension overflow: {width}×{height} exceeds 65500")]
370    /// Dimensions exceed this crate's safe decode bounds.
371    DimensionOverflow {
372        /// Declared width.
373        width: u32,
374        /// Declared height.
375        height: u32,
376    },
377
378    #[error("invalid sampling ({h}×{v}) for component {component}")]
379    /// Component sampling factors are outside the JPEG legal range.
380    InvalidSampling {
381        /// Component id.
382        component: u8,
383        /// Horizontal sampling factor.
384        h: u8,
385        /// Vertical sampling factor.
386        v: u8,
387    },
388
389    #[error("missing quantization table {table_id} for component {component}")]
390    /// Component references a quantization table that was not defined.
391    MissingQuantTable {
392        /// Component id.
393        component: u8,
394        /// Referenced quantization table id.
395        table_id: u8,
396    },
397
398    #[error(
399        "progressive quantization table {table_id} changed for component {component} at scan offset {offset}"
400    )]
401    /// A component's quantization table changed after its first progressive
402    /// scan. Coefficients from that component's scans cannot then share one
403    /// dequantization table.
404    ProgressiveQuantTableChanged {
405        /// Entropy-data offset of the later scan that resolved a new table.
406        offset: usize,
407        /// Frame-component identifier.
408        component: u8,
409        /// Quantization-table identifier (`Tq`) declared by the frame component.
410        table_id: u8,
411    },
412
413    #[error("missing Huffman table class={class} id={id} for component {component}")]
414    /// Scan references a Huffman table that was not defined.
415    MissingHuffmanTable {
416        /// Component id.
417        component: u8,
418        /// Huffman table class.
419        class: u8,
420        /// Huffman table id.
421        id: u8,
422    },
423
424    #[error(
425        "invalid sequential scan parameters at offset {offset}: Ss={ss} Se={se} Ah={ah} Al={al}"
426    )]
427    /// Scan spectral selection or approximation parameters are invalid.
428    InvalidScanParameters {
429        /// Byte offset of the scan header.
430        offset: usize,
431        /// Start of spectral selection.
432        ss: u8,
433        /// End of spectral selection.
434        se: u8,
435        /// Successive approximation high bit.
436        ah: u8,
437        /// Successive approximation low bit.
438        al: u8,
439    },
440
441    #[error(
442        "invalid progressive state at FF{marker:02X} offset {offset} for component {component} coefficient {coefficient}: {state}"
443    )]
444    /// Progressive scans do not form a legal coefficient/refinement script.
445    InvalidProgressiveScanState {
446        /// Byte offset of SOS, EOI, or physical EOF that exposed the violation.
447        offset: usize,
448        /// Raw context (`0xDA` for SOS; `0xD9` for EOI or expected EOI at EOF).
449        marker: u8,
450        /// Frame-component identifier.
451        component: u8,
452        /// Spectral coefficient index (`0..=63`).
453        coefficient: u8,
454        /// Exact cross-scan state violation.
455        state: ProgressiveScanStateError,
456    },
457
458    #[error(
459        "invalid progressive scan termination at offset {offset} (entropy starts at {scan_offset}): {state}"
460    )]
461    /// Progressive entropy did not end exactly at its parser-recorded boundary.
462    InvalidProgressiveScanTermination {
463        /// Parser-recorded absolute terminal offset.
464        offset: usize,
465        /// Absolute offset of the scan's first entropy byte.
466        scan_offset: usize,
467        /// Exact terminal-state violation.
468        state: ProgressiveScanTerminationError,
469    },
470
471    #[error("unknown scan component id {component} at offset {offset}")]
472    /// Scan references a component id not declared in the SOF.
473    UnknownScanComponent {
474        /// Byte offset of the scan header.
475        offset: usize,
476        /// Unknown component id.
477        component: u8,
478    },
479
480    #[error("duplicate scan component id {component} at offset {offset}")]
481    /// Scan lists the same component more than once.
482    DuplicateScanComponent {
483        /// Byte offset of the scan header.
484        offset: usize,
485        /// Duplicated component id.
486        component: u8,
487    },
488
489    #[error(
490        "invalid sequential scan component set at offset {offset}: expected {expected} components, found {found}"
491    )]
492    /// Sequential scan does not contain the expected component set.
493    InvalidSequentialComponentSet {
494        /// Byte offset of the scan header.
495        offset: usize,
496        /// Expected component count.
497        expected: u8,
498        /// Found component count.
499        found: u8,
500    },
501
502    #[error("invalid sequential scan count for {sof:?}: expected 1, found {count}")]
503    /// Sequential SOF contained an invalid number of scans.
504    InvalidSequentialScanCount {
505        /// SOF kind being decoded.
506        sof: SofKind,
507        /// Observed scan count.
508        count: u16,
509    },
510
511    #[error("Huffman decode failed near MCU {mcu}: {reason:?}")]
512    /// Huffman entropy decoding failed. `mcu` is the current MCU when the
513    /// caller has MCU progress, or `0` for table/bitstream contexts that do
514    /// not track image position.
515    HuffmanDecode {
516        /// Current MCU index, or `0` when the decoder context has no MCU index.
517        mcu: u32,
518        /// Failure category.
519        reason: HuffmanFailure,
520    },
521
522    #[error("restart mismatch at offset {offset}: expected RST{expected}, found FF{found:02X}")]
523    /// Restart marker sequence did not match the expected RST index.
524    RestartMismatch {
525        /// Byte offset of the marker.
526        offset: usize,
527        /// Expected RST index.
528        expected: u8,
529        /// Found raw marker byte.
530        found: u8,
531    },
532
533    #[error("unexpected EOI at MCU {mcu_at}/{mcu_total}")]
534    /// EOI was reached before all MCUs were decoded.
535    UnexpectedEoi {
536        /// MCU index where EOI was found.
537        mcu_at: u32,
538        /// Total MCU count expected for the image.
539        mcu_total: u32,
540    },
541
542    #[error("coefficient overflow at MCU {mcu}, component {component}")]
543    /// Decoded coefficient exceeded the representable range.
544    CoefficientOverflow {
545        /// MCU index.
546        mcu: u32,
547        /// Component index.
548        component: u8,
549    },
550
551    #[error("decode size {requested} bytes exceeds cap {cap} bytes")]
552    /// Requested decode allocation exceeds the configured memory cap.
553    MemoryCapExceeded {
554        /// Requested byte count.
555        requested: usize,
556        /// Configured byte cap.
557        cap: usize,
558    },
559
560    #[error("host allocation failed for {bytes} bytes")]
561    /// The host allocator could not reserve decoder or extraction storage.
562    HostAllocationFailed {
563        /// Requested host byte count.
564        bytes: usize,
565    },
566
567    #[error("output buffer too small: need {required} bytes, got {provided}")]
568    /// Caller-provided output buffer is too small.
569    OutputBufferTooSmall {
570        /// Required byte count.
571        required: usize,
572        /// Provided byte count.
573        provided: usize,
574    },
575
576    #[error("stride {stride} smaller than row width {row}")]
577    /// Output stride is smaller than the decoded row size.
578    InvalidStride {
579        /// Caller-provided stride.
580        stride: usize,
581        /// Minimum row byte count.
582        row: usize,
583    },
584
585    #[error("rect {rect:?} out of image bounds ({width}×{height})")]
586    /// Requested decode rectangle is outside image bounds.
587    RectOutOfBounds {
588        /// Requested rectangle.
589        rect: Rect,
590        /// Image width in pixels.
591        width: u32,
592        /// Image height in pixels.
593        height: u32,
594    },
595
596    #[error("downscale not supported for {sof:?} streams")]
597    /// Requested downscale is not supported for the SOF kind.
598    DownscaleUnsupported {
599        /// SOF kind being decoded.
600        sof: SofKind,
601    },
602
603    #[error("scan fragments overlap at MCU {mcu}")]
604    /// Builder-provided scan fragments overlap in MCU space.
605    ScanFragmentsOverlap {
606        /// First overlapping MCU index.
607        mcu: u32,
608    },
609
610    #[error("builder input configuration conflict: {reason:?}")]
611    /// Decoder builder inputs conflict.
612    BuilderConflict {
613        /// Conflict category.
614        reason: BuilderConflictReason,
615    },
616
617    /// Transient pre-1.0 gap: the SOF is parseable and may eventually be
618    /// supported by the decoder, but the current release does not implement
619    /// the requested shape yet. Distinct from `UnsupportedSof` because callers
620    /// routing to a fallback decoder on `is_unsupported()` should NOT reroute
621    /// streams that a newer version of j2k will decode natively.
622    #[error("decode not yet implemented for {sof:?} — see CHANGELOG for milestone")]
623    NotImplemented {
624        /// SOF kind awaiting implementation.
625        sof: SofKind,
626    },
627
628    #[error("internal JPEG invariant failed: {reason}")]
629    /// Decoder state violated an invariant that should have been enforced
630    /// before reaching the current path.
631    InternalInvariant {
632        /// Static diagnostic reason.
633        reason: &'static str,
634    },
635
636    #[error("row sink aborted decode")]
637    /// Row sink returned an error and aborted row-based decoding.
638    RowSinkAborted,
639}
640
641impl JpegError {
642    /// True if the error is recoverable by routing to a different decoder —
643    /// any `Unsupported*` variant.
644    #[must_use]
645    pub fn is_unsupported(&self) -> bool {
646        matches!(
647            self,
648            Self::UnsupportedSof { .. }
649                | Self::UnsupportedComponentCount { .. }
650                | Self::UnsupportedColorSpace { .. }
651                | Self::UnsupportedBitDepth { .. }
652                | Self::UnsupportedPredictor { .. }
653        )
654    }
655
656    /// True if the input was truncated — caller may retry with more bytes.
657    #[must_use]
658    pub fn is_truncated(&self) -> bool {
659        matches!(self, Self::Truncated { .. } | Self::UnexpectedEoi { .. })
660    }
661
662    /// True if the error indicates caller misuse, not a decode failure.
663    #[must_use]
664    pub fn is_api_misuse(&self) -> bool {
665        matches!(
666            self,
667            Self::OutputBufferTooSmall { .. }
668                | Self::InvalidStride { .. }
669                | Self::RectOutOfBounds { .. }
670                | Self::DownscaleUnsupported { .. }
671                | Self::ScanFragmentsOverlap { .. }
672                | Self::BuilderConflict { .. }
673        )
674    }
675
676    /// True if the error is a transient "not yet implemented" gap — the stream
677    /// is valid and will decode on a future j2k release, so callers
678    /// should *not* reroute to a different decoder permanently. See
679    /// [`Self::is_unsupported`] for errors that are permanent routing decisions.
680    #[must_use]
681    pub fn is_not_implemented(&self) -> bool {
682        matches!(self, Self::NotImplemented { .. })
683    }
684
685    /// Byte offset where the error was detected in the input stream, if any.
686    #[must_use]
687    pub fn offset(&self) -> Option<usize> {
688        match self {
689            Self::Truncated { offset, .. }
690            | Self::InvalidMarker { offset, .. }
691            | Self::UnexpectedMarker { offset, .. }
692            | Self::DuplicateMarker { offset, .. }
693            | Self::InvalidSegmentLength { offset, .. }
694            | Self::InvalidQuantizationValue { offset, .. }
695            | Self::InvalidScanParameters { offset, .. }
696            | Self::InvalidProgressiveScanState { offset, .. }
697            | Self::InvalidProgressiveScanTermination { offset, .. }
698            | Self::ProgressiveQuantTableChanged { offset, .. }
699            | Self::UnknownScanComponent { offset, .. }
700            | Self::DuplicateScanComponent { offset, .. }
701            | Self::InvalidSequentialComponentSet { offset, .. }
702            | Self::RestartMismatch { offset, .. } => Some(*offset),
703            _ => None,
704        }
705    }
706}
707
708#[doc(hidden)]
709impl CodecError for JpegError {
710    fn is_truncated(&self) -> bool {
711        Self::is_truncated(self)
712    }
713
714    fn is_not_implemented(&self) -> bool {
715        Self::is_not_implemented(self)
716    }
717
718    fn is_unsupported(&self) -> bool {
719        Self::is_unsupported(self)
720    }
721
722    fn is_buffer_error(&self) -> bool {
723        matches!(
724            self,
725            Self::OutputBufferTooSmall { .. }
726                | Self::InvalidStride { .. }
727                | Self::RectOutOfBounds { .. }
728        )
729    }
730}
731
732/// Non-fatal notices emitted during decode. See spec Section 6.
733#[derive(Debug, Clone, PartialEq, Eq)]
734#[non_exhaustive]
735pub enum Warning {
736    /// Stream ended without an EOI marker after otherwise decodable entropy.
737    MissingEoi,
738    /// SOF dimensions were repaired from external context.
739    SofDimensionsPatched {
740        /// Original SOF dimensions.
741        from: (u16, u16),
742        /// Replacement dimensions.
743        to: (u16, u16),
744    },
745    /// Stream uses nonstandard but decodable table layout.
746    NonstandardTables,
747    /// Adobe APP14 transform value could not unambiguously define color.
748    AdobeApp14Ambiguous {
749        /// Raw APP14 transform byte.
750        raw_transform: u8,
751    },
752    /// ICC profile was present but ignored by this decoder.
753    IccProfileIgnored {
754        /// ICC payload size in bytes.
755        size: usize,
756    },
757    /// Unknown APP marker was skipped.
758    UnknownAppMarker {
759        /// Raw APP marker byte.
760        marker: u8,
761        /// Segment payload size in bytes.
762        size: usize,
763    },
764    /// Decoder recovered at a restart marker.
765    RestartRecovered {
766        /// Byte offset near the recovered restart marker.
767        offset: usize,
768    },
769    /// Higher-precision samples were clamped to a lower output precision.
770    PrecisionClamped {
771        /// Source precision in bits.
772        from_bits: u8,
773        /// Output precision in bits.
774        to_bits: u8,
775    },
776    /// Color profile metadata was present but unrecognized.
777    UnknownColorProfile,
778    /// Cached table metadata disagreed with the active stream tables.
779    TableCacheMismatch {
780        /// Table class.
781        which: TableKind,
782        /// Table id.
783        id: u8,
784    },
785}
786
787impl core::fmt::Display for Warning {
788    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
789        match self {
790            Self::MissingEoi => f.write_str("missing EOI"),
791            Self::SofDimensionsPatched { from, to } => {
792                write!(f, "patched SOF dimensions from {from:?} to {to:?}")
793            }
794            Self::NonstandardTables => f.write_str("nonstandard tables"),
795            Self::AdobeApp14Ambiguous { raw_transform } => {
796                write!(f, "ambiguous Adobe APP14 transform {raw_transform}")
797            }
798            Self::IccProfileIgnored { size } => write!(f, "ignored ICC profile of {size} bytes"),
799            Self::UnknownAppMarker { marker, size } => {
800                write!(f, "unknown APP marker FF{marker:02X} ({size} bytes)")
801            }
802            Self::RestartRecovered { offset } => {
803                write!(f, "recovered at restart marker near offset {offset}")
804            }
805            Self::PrecisionClamped { from_bits, to_bits } => {
806                write!(
807                    f,
808                    "precision clamped from {from_bits} bits to {to_bits} bits"
809                )
810            }
811            Self::UnknownColorProfile => f.write_str("unknown color profile"),
812            Self::TableCacheMismatch { which, id } => {
813                write!(f, "table cache mismatch for {which:?} {id}")
814            }
815        }
816    }
817}
818
819#[cfg(test)]
820mod tests {
821    use super::*;
822    use crate::info::ColorSpace;
823
824    #[test]
825    fn unsupported_predicate_matches_only_unsupported_variants() {
826        assert!(JpegError::UnsupportedSof {
827            marker: 0xC9,
828            reason: UnsupportedReason::ArithmeticCoding,
829        }
830        .is_unsupported());
831        assert!(JpegError::UnsupportedColorSpace {
832            color_space: ColorSpace::Cmyk,
833        }
834        .is_unsupported());
835        assert!(JpegError::UnsupportedBitDepth { depth: 16 }.is_unsupported());
836        assert!(!JpegError::Truncated {
837            offset: 0,
838            expected: 1
839        }
840        .is_unsupported());
841    }
842
843    #[test]
844    fn truncated_predicate_covers_truncation_and_unexpected_eoi() {
845        assert!(JpegError::Truncated {
846            offset: 10,
847            expected: 5
848        }
849        .is_truncated());
850        assert!(JpegError::UnexpectedEoi {
851            mcu_at: 3,
852            mcu_total: 10
853        }
854        .is_truncated());
855        assert!(!JpegError::InvalidMarker {
856            offset: 4,
857            marker: 0xFF
858        }
859        .is_truncated());
860    }
861
862    #[test]
863    fn api_misuse_predicate_covers_caller_bugs() {
864        assert!(JpegError::OutputBufferTooSmall {
865            required: 100,
866            provided: 64
867        }
868        .is_api_misuse());
869        assert!(JpegError::InvalidStride { stride: 2, row: 8 }.is_api_misuse());
870        assert!(JpegError::BuilderConflict {
871            reason: BuilderConflictReason::NoInput
872        }
873        .is_api_misuse());
874        assert!(!JpegError::Truncated {
875            offset: 0,
876            expected: 1
877        }
878        .is_api_misuse());
879    }
880
881    #[test]
882    fn offset_returns_some_for_byte_positioned_errors() {
883        assert_eq!(
884            JpegError::InvalidMarker {
885                offset: 42,
886                marker: 0xBA
887            }
888            .offset(),
889            Some(42),
890        );
891        assert_eq!(JpegError::UnsupportedBitDepth { depth: 16 }.offset(), None,);
892        assert_eq!(
893            JpegError::InvalidQuantizationValue {
894                offset: 73,
895                table: 2,
896                coefficient: 63,
897            }
898            .offset(),
899            Some(73),
900        );
901    }
902
903    #[test]
904    fn not_implemented_predicate_distinguishes_from_unsupported() {
905        let not_impl = JpegError::NotImplemented {
906            sof: SofKind::Progressive8,
907        };
908        assert!(not_impl.is_not_implemented());
909        assert!(
910            !not_impl.is_unsupported(),
911            "NotImplemented is a transient M1b/M2 gap — callers routing on is_unsupported() must NOT \
912             reroute these streams, because M3 adds real support"
913        );
914        assert!(!not_impl.is_truncated());
915        assert!(!not_impl.is_api_misuse());
916
917        let unsupported = JpegError::UnsupportedSof {
918            marker: 0xC9,
919            reason: UnsupportedReason::ArithmeticCoding,
920        };
921        assert!(!unsupported.is_not_implemented());
922        assert!(unsupported.is_unsupported());
923    }
924}