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}