1use crate::info::{ColorSpace, Rect, SofKind};
6use j2k_core::CodecError;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum MarkerKind {
13 Soi,
15 Sof,
17 Dqt,
19 Dht,
21 Dri,
23 Sos,
25 Eoi,
27 App14,
29 Other(u8),
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum UnsupportedReason {
36 ArithmeticCoding,
38 Hierarchical,
40 ArithmeticAndHierarchical,
42 DifferentialBaseline,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum HuffmanFailure {
49 CodeOverflow,
51 InvalidSymbol,
53 TableExhausted,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum BuilderConflictReason {
60 NoInput,
62 InputAndScanFragments,
64 ScanFragmentsEmpty,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum TableKind {
71 Quant,
73 HuffmanAc,
75 HuffmanDc,
77}
78
79#[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 Truncated {
86 offset: usize,
88 expected: usize,
90 },
91
92 #[error("invalid marker FF{marker:02X} at offset {offset}")]
93 InvalidMarker {
95 offset: usize,
97 marker: u8,
99 },
100
101 #[error("expected {expected:?}, found FF{found:02X} at offset {offset}")]
102 UnexpectedMarker {
104 offset: usize,
106 expected: MarkerKind,
108 found: u8,
110 },
111
112 #[error("missing required marker {marker:?}")]
113 MissingMarker {
115 marker: MarkerKind,
117 },
118
119 #[error("duplicate {marker:?} at offset {offset}")]
120 DuplicateMarker {
122 offset: usize,
124 marker: MarkerKind,
126 },
127
128 #[error("invalid length {length} for marker FF{marker:02X} at offset {offset}")]
129 InvalidSegmentLength {
131 offset: usize,
133 marker: u8,
135 length: u16,
137 },
138
139 #[error("conflicting duplicate JPEG table {table:?} id={id} at offset {offset}")]
140 ConflictingDuplicateTable {
142 offset: usize,
144 table: TableKind,
146 id: u8,
148 },
149
150 #[error("expected dimensions are required to repair zero SOF dimensions at offset {offset}")]
151 ExpectedDimensionsRequired {
153 offset: usize,
155 },
156
157 #[error(
158 "expected dimensions {expected:?} conflict with SOF dimensions {actual:?} at offset {offset}"
159 )]
160 ConflictingExpectedDimensions {
162 offset: usize,
164 expected: (u16, u16),
166 actual: (u16, u16),
168 },
169
170 #[error("invalid TIFF JPEG assembly at offset {offset}: {reason}")]
171 InvalidJpegAssembly {
173 offset: usize,
175 reason: &'static str,
177 },
178
179 #[error("conflicting DRI values at offset {offset}: existing {existing}, new {new}")]
180 ConflictingDri {
182 offset: usize,
184 existing: u16,
186 new: u16,
188 },
189
190 #[error("unsupported SOF marker FF{marker:02X} ({reason:?})")]
194 UnsupportedSof {
196 marker: u8,
198 reason: UnsupportedReason,
200 },
201
202 #[error("unsupported component count: {count}")]
203 UnsupportedComponentCount {
205 count: u8,
207 },
208
209 #[error("unsupported color space for decode: {color_space:?}")]
210 UnsupportedColorSpace {
212 color_space: ColorSpace,
214 },
215
216 #[error("unsupported bit depth: {depth}")]
217 UnsupportedBitDepth {
219 depth: u8,
221 },
222
223 #[error("unsupported lossless predictor: {predictor}")]
224 UnsupportedPredictor {
226 predictor: u8,
228 },
229
230 #[error("zero dimension in SOF: {width}×{height}")]
231 ZeroDimension {
233 width: u16,
235 height: u16,
237 },
238
239 #[error("dimension overflow: {width}×{height} exceeds 65500")]
240 DimensionOverflow {
242 width: u32,
244 height: u32,
246 },
247
248 #[error("invalid sampling ({h}×{v}) for component {component}")]
249 InvalidSampling {
251 component: u8,
253 h: u8,
255 v: u8,
257 },
258
259 #[error("missing quantization table {table_id} for component {component}")]
260 MissingQuantTable {
262 component: u8,
264 table_id: u8,
266 },
267
268 #[error("missing Huffman table class={class} id={id} for component {component}")]
269 MissingHuffmanTable {
271 component: u8,
273 class: u8,
275 id: u8,
277 },
278
279 #[error(
280 "invalid sequential scan parameters at offset {offset}: Ss={ss} Se={se} Ah={ah} Al={al}"
281 )]
282 InvalidScanParameters {
284 offset: usize,
286 ss: u8,
288 se: u8,
290 ah: u8,
292 al: u8,
294 },
295
296 #[error("unknown scan component id {component} at offset {offset}")]
297 UnknownScanComponent {
299 offset: usize,
301 component: u8,
303 },
304
305 #[error("duplicate scan component id {component} at offset {offset}")]
306 DuplicateScanComponent {
308 offset: usize,
310 component: u8,
312 },
313
314 #[error(
315 "invalid sequential scan component set at offset {offset}: expected {expected} components, found {found}"
316 )]
317 InvalidSequentialComponentSet {
319 offset: usize,
321 expected: u8,
323 found: u8,
325 },
326
327 #[error("invalid sequential scan count for {sof:?}: expected 1, found {count}")]
328 InvalidSequentialScanCount {
330 sof: SofKind,
332 count: u16,
334 },
335
336 #[error("Huffman decode failed near MCU {mcu}: {reason:?}")]
337 HuffmanDecode {
341 mcu: u32,
343 reason: HuffmanFailure,
345 },
346
347 #[error("restart mismatch at offset {offset}: expected RST{expected}, found FF{found:02X}")]
348 RestartMismatch {
350 offset: usize,
352 expected: u8,
354 found: u8,
356 },
357
358 #[error("unexpected EOI at MCU {mcu_at}/{mcu_total}")]
359 UnexpectedEoi {
361 mcu_at: u32,
363 mcu_total: u32,
365 },
366
367 #[error("coefficient overflow at MCU {mcu}, component {component}")]
368 CoefficientOverflow {
370 mcu: u32,
372 component: u8,
374 },
375
376 #[error("decode size {requested} bytes exceeds cap {cap} bytes")]
377 MemoryCapExceeded {
379 requested: usize,
381 cap: usize,
383 },
384
385 #[error("output buffer too small: need {required} bytes, got {provided}")]
386 OutputBufferTooSmall {
388 required: usize,
390 provided: usize,
392 },
393
394 #[error("stride {stride} smaller than row width {row}")]
395 InvalidStride {
397 stride: usize,
399 row: usize,
401 },
402
403 #[error("rect {rect:?} out of image bounds ({width}×{height})")]
404 RectOutOfBounds {
406 rect: Rect,
408 width: u32,
410 height: u32,
412 },
413
414 #[error("downscale not supported for {sof:?} streams")]
415 DownscaleUnsupported {
417 sof: SofKind,
419 },
420
421 #[error("scan fragments overlap at MCU {mcu}")]
422 ScanFragmentsOverlap {
424 mcu: u32,
426 },
427
428 #[error("builder input configuration conflict: {reason:?}")]
429 BuilderConflict {
431 reason: BuilderConflictReason,
433 },
434
435 #[error("decode not yet implemented for {sof:?} — see CHANGELOG for milestone")]
441 NotImplemented {
442 sof: SofKind,
444 },
445
446 #[error("row sink aborted decode")]
447 RowSinkAborted,
449}
450
451impl JpegError {
452 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 pub fn is_truncated(&self) -> bool {
467 matches!(self, Self::Truncated { .. } | Self::UnexpectedEoi { .. })
468 }
469
470 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 pub fn is_not_implemented(&self) -> bool {
488 matches!(self, Self::NotImplemented { .. })
489 }
490
491 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#[derive(Debug, Clone, PartialEq, Eq)]
534#[non_exhaustive]
535pub enum Warning {
536 MissingEoi,
538 SofDimensionsPatched {
540 from: (u16, u16),
542 to: (u16, u16),
544 },
545 NonstandardTables,
547 AdobeApp14Ambiguous {
549 raw_transform: u8,
551 },
552 IccProfileIgnored {
554 size: usize,
556 },
557 UnknownAppMarker {
559 marker: u8,
561 size: usize,
563 },
564 RestartRecovered {
566 offset: usize,
568 },
569 PrecisionClamped {
571 from_bits: u8,
573 to_bits: u8,
575 },
576 UnknownColorProfile,
578 TableCacheMismatch {
580 which: TableKind,
582 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}