Skip to main content

j2k_jpeg/
error.rs

1// SPDX-License-Identifier: 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/// Fatal JPEG decode or API error.
80#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
81#[non_exhaustive]
82pub enum JpegError {
83    #[error("JPEG truncated at offset {offset}: expected {expected} more bytes")]
84    /// Input ended before the requested bytes could be read.
85    Truncated {
86        /// Byte offset where decoding stopped.
87        offset: usize,
88        /// Additional byte count required.
89        expected: usize,
90    },
91
92    #[error("invalid marker FF{marker:02X} at offset {offset}")]
93    /// Marker byte is not legal in the current JPEG position.
94    InvalidMarker {
95        /// Byte offset of the marker.
96        offset: usize,
97        /// Raw marker byte following `0xff`.
98        marker: u8,
99    },
100
101    #[error("expected {expected:?}, found FF{found:02X} at offset {offset}")]
102    /// A different marker kind was required at this position.
103    UnexpectedMarker {
104        /// Byte offset of the unexpected marker.
105        offset: usize,
106        /// Marker kind expected by the parser.
107        expected: MarkerKind,
108        /// Raw marker byte that was found.
109        found: u8,
110    },
111
112    #[error("missing required marker {marker:?}")]
113    /// Required marker was absent from the stream.
114    MissingMarker {
115        /// Missing marker kind.
116        marker: MarkerKind,
117    },
118
119    #[error("duplicate {marker:?} at offset {offset}")]
120    /// A marker appeared more than once where only one is legal.
121    DuplicateMarker {
122        /// Byte offset of the duplicate marker.
123        offset: usize,
124        /// Duplicated marker kind.
125        marker: MarkerKind,
126    },
127
128    #[error("invalid length {length} for marker FF{marker:02X} at offset {offset}")]
129    /// Marker segment length is invalid for the marker kind.
130    InvalidSegmentLength {
131        /// Byte offset of the segment.
132        offset: usize,
133        /// Raw marker byte following `0xff`.
134        marker: u8,
135        /// Declared segment length.
136        length: u16,
137    },
138
139    #[error("conflicting duplicate JPEG table {table:?} id={id} at offset {offset}")]
140    /// Duplicate DQT/DHT table has different bytes from an earlier definition.
141    ConflictingDuplicateTable {
142        /// Byte offset of the conflicting table segment.
143        offset: usize,
144        /// Table class.
145        table: TableKind,
146        /// Table id.
147        id: u8,
148    },
149
150    #[error("expected dimensions are required to repair zero SOF dimensions at offset {offset}")]
151    /// TIFF/NDPI metadata did not provide dimensions needed to repair a zero SOF.
152    ExpectedDimensionsRequired {
153        /// Byte offset of the SOF marker.
154        offset: usize,
155    },
156
157    #[error(
158        "expected dimensions {expected:?} conflict with SOF dimensions {actual:?} at offset {offset}"
159    )]
160    /// Container-provided dimensions conflict with non-zero SOF dimensions.
161    ConflictingExpectedDimensions {
162        /// Byte offset of the SOF marker.
163        offset: usize,
164        /// Expected dimensions supplied by the caller.
165        expected: (u16, u16),
166        /// Dimensions declared by the SOF marker.
167        actual: (u16, u16),
168    },
169
170    #[error("invalid TIFF JPEG assembly at offset {offset}: {reason}")]
171    /// TIFF/JPEGTables assembly cannot produce a valid JPEG interchange stream.
172    InvalidJpegAssembly {
173        /// Byte offset of the assembly problem.
174        offset: usize,
175        /// Static diagnostic reason.
176        reason: &'static str,
177    },
178
179    #[error("conflicting DRI values at offset {offset}: existing {existing}, new {new}")]
180    /// Duplicate DRI marker conflicts with an earlier DRI value.
181    ConflictingDri {
182        /// Byte offset of the conflicting DRI segment.
183        offset: usize,
184        /// Existing non-zero restart interval.
185        existing: u16,
186        /// New non-zero restart interval.
187        new: u16,
188    },
189
190    /// Unsupported SOF variant. Carries the raw marker byte (e.g. `0xC9` for
191    /// arithmetic extended-sequential) so callers routing to a fallback
192    /// decoder can distinguish FFC5 from FFC9 without relying on `reason`.
193    #[error("unsupported SOF marker FF{marker:02X} ({reason:?})")]
194    /// SOF marker class is outside the decoder's supported JPEG subset.
195    UnsupportedSof {
196        /// Raw SOF marker byte.
197        marker: u8,
198        /// Unsupported feature category.
199        reason: UnsupportedReason,
200    },
201
202    #[error("unsupported component count: {count}")]
203    /// Component count is outside the supported range.
204    UnsupportedComponentCount {
205        /// Declared component count.
206        count: u8,
207    },
208
209    #[error("unsupported color space for decode: {color_space:?}")]
210    /// Color space cannot be produced by the requested decode path.
211    UnsupportedColorSpace {
212        /// Header-derived color space.
213        color_space: ColorSpace,
214    },
215
216    #[error("unsupported bit depth: {depth}")]
217    /// Sample precision is not supported by this decoder path.
218    UnsupportedBitDepth {
219        /// Declared sample precision in bits.
220        depth: u8,
221    },
222
223    #[error("unsupported lossless predictor: {predictor}")]
224    /// Lossless predictor selection is unsupported.
225    UnsupportedPredictor {
226        /// Predictor value from the scan header.
227        predictor: u8,
228    },
229
230    #[error("zero dimension in SOF: {width}×{height}")]
231    /// SOF declares a zero width or height.
232    ZeroDimension {
233        /// Declared width.
234        width: u16,
235        /// Declared height.
236        height: u16,
237    },
238
239    #[error("dimension overflow: {width}×{height} exceeds 65500")]
240    /// Dimensions exceed this crate's safe decode bounds.
241    DimensionOverflow {
242        /// Declared width.
243        width: u32,
244        /// Declared height.
245        height: u32,
246    },
247
248    #[error("invalid sampling ({h}×{v}) for component {component}")]
249    /// Component sampling factors are outside the JPEG legal range.
250    InvalidSampling {
251        /// Component id.
252        component: u8,
253        /// Horizontal sampling factor.
254        h: u8,
255        /// Vertical sampling factor.
256        v: u8,
257    },
258
259    #[error("missing quantization table {table_id} for component {component}")]
260    /// Component references a quantization table that was not defined.
261    MissingQuantTable {
262        /// Component id.
263        component: u8,
264        /// Referenced quantization table id.
265        table_id: u8,
266    },
267
268    #[error("missing Huffman table class={class} id={id} for component {component}")]
269    /// Scan references a Huffman table that was not defined.
270    MissingHuffmanTable {
271        /// Component id.
272        component: u8,
273        /// Huffman table class.
274        class: u8,
275        /// Huffman table id.
276        id: u8,
277    },
278
279    #[error(
280        "invalid sequential scan parameters at offset {offset}: Ss={ss} Se={se} Ah={ah} Al={al}"
281    )]
282    /// Scan spectral selection or approximation parameters are invalid.
283    InvalidScanParameters {
284        /// Byte offset of the scan header.
285        offset: usize,
286        /// Start of spectral selection.
287        ss: u8,
288        /// End of spectral selection.
289        se: u8,
290        /// Successive approximation high bit.
291        ah: u8,
292        /// Successive approximation low bit.
293        al: u8,
294    },
295
296    #[error("unknown scan component id {component} at offset {offset}")]
297    /// Scan references a component id not declared in the SOF.
298    UnknownScanComponent {
299        /// Byte offset of the scan header.
300        offset: usize,
301        /// Unknown component id.
302        component: u8,
303    },
304
305    #[error("duplicate scan component id {component} at offset {offset}")]
306    /// Scan lists the same component more than once.
307    DuplicateScanComponent {
308        /// Byte offset of the scan header.
309        offset: usize,
310        /// Duplicated component id.
311        component: u8,
312    },
313
314    #[error(
315        "invalid sequential scan component set at offset {offset}: expected {expected} components, found {found}"
316    )]
317    /// Sequential scan does not contain the expected component set.
318    InvalidSequentialComponentSet {
319        /// Byte offset of the scan header.
320        offset: usize,
321        /// Expected component count.
322        expected: u8,
323        /// Found component count.
324        found: u8,
325    },
326
327    #[error("invalid sequential scan count for {sof:?}: expected 1, found {count}")]
328    /// Sequential SOF contained an invalid number of scans.
329    InvalidSequentialScanCount {
330        /// SOF kind being decoded.
331        sof: SofKind,
332        /// Observed scan count.
333        count: u16,
334    },
335
336    #[error("Huffman decode failed near MCU {mcu}: {reason:?}")]
337    /// Huffman entropy decoding failed. `mcu` is the current MCU when the
338    /// caller has MCU progress, or `0` for table/bitstream contexts that do
339    /// not track image position.
340    HuffmanDecode {
341        /// Current MCU index, or `0` when the decoder context has no MCU index.
342        mcu: u32,
343        /// Failure category.
344        reason: HuffmanFailure,
345    },
346
347    #[error("restart mismatch at offset {offset}: expected RST{expected}, found FF{found:02X}")]
348    /// Restart marker sequence did not match the expected RST index.
349    RestartMismatch {
350        /// Byte offset of the marker.
351        offset: usize,
352        /// Expected RST index.
353        expected: u8,
354        /// Found raw marker byte.
355        found: u8,
356    },
357
358    #[error("unexpected EOI at MCU {mcu_at}/{mcu_total}")]
359    /// EOI was reached before all MCUs were decoded.
360    UnexpectedEoi {
361        /// MCU index where EOI was found.
362        mcu_at: u32,
363        /// Total MCU count expected for the image.
364        mcu_total: u32,
365    },
366
367    #[error("coefficient overflow at MCU {mcu}, component {component}")]
368    /// Decoded coefficient exceeded the representable range.
369    CoefficientOverflow {
370        /// MCU index.
371        mcu: u32,
372        /// Component index.
373        component: u8,
374    },
375
376    #[error("decode size {requested} bytes exceeds cap {cap} bytes")]
377    /// Requested decode allocation exceeds the configured memory cap.
378    MemoryCapExceeded {
379        /// Requested byte count.
380        requested: usize,
381        /// Configured byte cap.
382        cap: usize,
383    },
384
385    #[error("output buffer too small: need {required} bytes, got {provided}")]
386    /// Caller-provided output buffer is too small.
387    OutputBufferTooSmall {
388        /// Required byte count.
389        required: usize,
390        /// Provided byte count.
391        provided: usize,
392    },
393
394    #[error("stride {stride} smaller than row width {row}")]
395    /// Output stride is smaller than the decoded row size.
396    InvalidStride {
397        /// Caller-provided stride.
398        stride: usize,
399        /// Minimum row byte count.
400        row: usize,
401    },
402
403    #[error("rect {rect:?} out of image bounds ({width}×{height})")]
404    /// Requested decode rectangle is outside image bounds.
405    RectOutOfBounds {
406        /// Requested rectangle.
407        rect: Rect,
408        /// Image width in pixels.
409        width: u32,
410        /// Image height in pixels.
411        height: u32,
412    },
413
414    #[error("downscale not supported for {sof:?} streams")]
415    /// Requested downscale is not supported for the SOF kind.
416    DownscaleUnsupported {
417        /// SOF kind being decoded.
418        sof: SofKind,
419    },
420
421    #[error("scan fragments overlap at MCU {mcu}")]
422    /// Builder-provided scan fragments overlap in MCU space.
423    ScanFragmentsOverlap {
424        /// First overlapping MCU index.
425        mcu: u32,
426    },
427
428    #[error("builder input configuration conflict: {reason:?}")]
429    /// Decoder builder inputs conflict.
430    BuilderConflict {
431        /// Conflict category.
432        reason: BuilderConflictReason,
433    },
434
435    /// Transient pre-1.0 gap: the SOF is parseable and may eventually be
436    /// supported by the decoder, but the current release does not implement
437    /// the requested shape yet. Distinct from `UnsupportedSof` because callers
438    /// routing to a fallback decoder on `is_unsupported()` should NOT reroute
439    /// streams that a newer version of j2k will decode natively.
440    #[error("decode not yet implemented for {sof:?} — see CHANGELOG for milestone")]
441    NotImplemented {
442        /// SOF kind awaiting implementation.
443        sof: SofKind,
444    },
445
446    #[error("row sink aborted decode")]
447    /// Row sink returned an error and aborted row-based decoding.
448    RowSinkAborted,
449}
450
451impl JpegError {
452    /// True if the error is recoverable by routing to a different decoder —
453    /// any `Unsupported*` variant.
454    pub fn is_unsupported(&self) -> bool {
455        matches!(
456            self,
457            Self::UnsupportedSof { .. }
458                | Self::UnsupportedComponentCount { .. }
459                | Self::UnsupportedColorSpace { .. }
460                | Self::UnsupportedBitDepth { .. }
461                | Self::UnsupportedPredictor { .. }
462        )
463    }
464
465    /// True if the input was truncated — caller may retry with more bytes.
466    pub fn is_truncated(&self) -> bool {
467        matches!(self, Self::Truncated { .. } | Self::UnexpectedEoi { .. })
468    }
469
470    /// True if the error indicates caller misuse, not a decode failure.
471    pub fn is_api_misuse(&self) -> bool {
472        matches!(
473            self,
474            Self::OutputBufferTooSmall { .. }
475                | Self::InvalidStride { .. }
476                | Self::RectOutOfBounds { .. }
477                | Self::DownscaleUnsupported { .. }
478                | Self::ScanFragmentsOverlap { .. }
479                | Self::BuilderConflict { .. }
480        )
481    }
482
483    /// True if the error is a transient "not yet implemented" gap — the stream
484    /// is valid and will decode on a future j2k release, so callers
485    /// should *not* reroute to a different decoder permanently. See
486    /// [`Self::is_unsupported`] for errors that are permanent routing decisions.
487    pub fn is_not_implemented(&self) -> bool {
488        matches!(self, Self::NotImplemented { .. })
489    }
490
491    /// Byte offset where the error was detected in the input stream, if any.
492    pub fn offset(&self) -> Option<usize> {
493        match self {
494            Self::Truncated { offset, .. }
495            | Self::InvalidMarker { offset, .. }
496            | Self::UnexpectedMarker { offset, .. }
497            | Self::DuplicateMarker { offset, .. }
498            | Self::InvalidSegmentLength { offset, .. }
499            | Self::InvalidScanParameters { offset, .. }
500            | Self::UnknownScanComponent { offset, .. }
501            | Self::DuplicateScanComponent { offset, .. }
502            | Self::InvalidSequentialComponentSet { offset, .. }
503            | Self::RestartMismatch { offset, .. } => Some(*offset),
504            _ => None,
505        }
506    }
507}
508
509impl CodecError for JpegError {
510    fn is_truncated(&self) -> bool {
511        Self::is_truncated(self)
512    }
513
514    fn is_not_implemented(&self) -> bool {
515        Self::is_not_implemented(self)
516    }
517
518    fn is_unsupported(&self) -> bool {
519        Self::is_unsupported(self)
520    }
521
522    fn is_buffer_error(&self) -> bool {
523        matches!(
524            self,
525            Self::OutputBufferTooSmall { .. }
526                | Self::InvalidStride { .. }
527                | Self::RectOutOfBounds { .. }
528        )
529    }
530}
531
532/// Non-fatal notices emitted during decode. See spec Section 6.
533#[derive(Debug, Clone, PartialEq, Eq)]
534#[non_exhaustive]
535pub enum Warning {
536    /// Stream ended without an EOI marker after otherwise decodable entropy.
537    MissingEoi,
538    /// SOF dimensions were repaired from external context.
539    SofDimensionsPatched {
540        /// Original SOF dimensions.
541        from: (u16, u16),
542        /// Replacement dimensions.
543        to: (u16, u16),
544    },
545    /// Stream uses nonstandard but decodable table layout.
546    NonstandardTables,
547    /// Adobe APP14 transform value could not unambiguously define color.
548    AdobeApp14Ambiguous {
549        /// Raw APP14 transform byte.
550        raw_transform: u8,
551    },
552    /// ICC profile was present but ignored by this decoder.
553    IccProfileIgnored {
554        /// ICC payload size in bytes.
555        size: usize,
556    },
557    /// Unknown APP marker was skipped.
558    UnknownAppMarker {
559        /// Raw APP marker byte.
560        marker: u8,
561        /// Segment payload size in bytes.
562        size: usize,
563    },
564    /// Decoder recovered at a restart marker.
565    RestartRecovered {
566        /// Byte offset near the recovered restart marker.
567        offset: usize,
568    },
569    /// Higher-precision samples were clamped to a lower output precision.
570    PrecisionClamped {
571        /// Source precision in bits.
572        from_bits: u8,
573        /// Output precision in bits.
574        to_bits: u8,
575    },
576    /// Color profile metadata was present but unrecognized.
577    UnknownColorProfile,
578    /// Cached table metadata disagreed with the active stream tables.
579    TableCacheMismatch {
580        /// Table class.
581        which: TableKind,
582        /// Table id.
583        id: u8,
584    },
585}
586
587impl core::fmt::Display for Warning {
588    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
589        match self {
590            Self::MissingEoi => f.write_str("missing EOI"),
591            Self::SofDimensionsPatched { from, to } => {
592                write!(f, "patched SOF dimensions from {from:?} to {to:?}")
593            }
594            Self::NonstandardTables => f.write_str("nonstandard tables"),
595            Self::AdobeApp14Ambiguous { raw_transform } => {
596                write!(f, "ambiguous Adobe APP14 transform {raw_transform}")
597            }
598            Self::IccProfileIgnored { size } => write!(f, "ignored ICC profile of {size} bytes"),
599            Self::UnknownAppMarker { marker, size } => {
600                write!(f, "unknown APP marker FF{marker:02X} ({size} bytes)")
601            }
602            Self::RestartRecovered { offset } => {
603                write!(f, "recovered at restart marker near offset {offset}")
604            }
605            Self::PrecisionClamped { from_bits, to_bits } => {
606                write!(
607                    f,
608                    "precision clamped from {from_bits} bits to {to_bits} bits"
609                )
610            }
611            Self::UnknownColorProfile => f.write_str("unknown color profile"),
612            Self::TableCacheMismatch { which, id } => {
613                write!(f, "table cache mismatch for {which:?} {id}")
614            }
615        }
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622    use crate::info::ColorSpace;
623
624    #[test]
625    fn unsupported_predicate_matches_only_unsupported_variants() {
626        assert!(JpegError::UnsupportedSof {
627            marker: 0xC9,
628            reason: UnsupportedReason::ArithmeticCoding,
629        }
630        .is_unsupported());
631        assert!(JpegError::UnsupportedColorSpace {
632            color_space: ColorSpace::Cmyk,
633        }
634        .is_unsupported());
635        assert!(JpegError::UnsupportedBitDepth { depth: 16 }.is_unsupported());
636        assert!(!JpegError::Truncated {
637            offset: 0,
638            expected: 1
639        }
640        .is_unsupported());
641    }
642
643    #[test]
644    fn truncated_predicate_covers_truncation_and_unexpected_eoi() {
645        assert!(JpegError::Truncated {
646            offset: 10,
647            expected: 5
648        }
649        .is_truncated());
650        assert!(JpegError::UnexpectedEoi {
651            mcu_at: 3,
652            mcu_total: 10
653        }
654        .is_truncated());
655        assert!(!JpegError::InvalidMarker {
656            offset: 4,
657            marker: 0xFF
658        }
659        .is_truncated());
660    }
661
662    #[test]
663    fn api_misuse_predicate_covers_caller_bugs() {
664        assert!(JpegError::OutputBufferTooSmall {
665            required: 100,
666            provided: 64
667        }
668        .is_api_misuse());
669        assert!(JpegError::InvalidStride { stride: 2, row: 8 }.is_api_misuse());
670        assert!(JpegError::BuilderConflict {
671            reason: BuilderConflictReason::NoInput
672        }
673        .is_api_misuse());
674        assert!(!JpegError::Truncated {
675            offset: 0,
676            expected: 1
677        }
678        .is_api_misuse());
679    }
680
681    #[test]
682    fn offset_returns_some_for_byte_positioned_errors() {
683        assert_eq!(
684            JpegError::InvalidMarker {
685                offset: 42,
686                marker: 0xBA
687            }
688            .offset(),
689            Some(42),
690        );
691        assert_eq!(JpegError::UnsupportedBitDepth { depth: 16 }.offset(), None,);
692    }
693
694    #[test]
695    fn not_implemented_predicate_distinguishes_from_unsupported() {
696        let not_impl = JpegError::NotImplemented {
697            sof: SofKind::Progressive8,
698        };
699        assert!(not_impl.is_not_implemented());
700        assert!(
701            !not_impl.is_unsupported(),
702            "NotImplemented is a transient M1b/M2 gap — callers routing on is_unsupported() must NOT \
703             reroute these streams, because M3 adds real support"
704        );
705        assert!(!not_impl.is_truncated());
706        assert!(!not_impl.is_api_misuse());
707
708        let unsupported = JpegError::UnsupportedSof {
709            marker: 0xC9,
710            reason: UnsupportedReason::ArithmeticCoding,
711        };
712        assert!(!unsupported.is_not_implemented());
713        assert!(unsupported.is_unsupported());
714    }
715}