1use crate::adapter::summarize_device_batch;
6use crate::decoder::{Decoder, JpegView};
7use crate::error::JpegError;
8use crate::info::{ColorSpace, Info, Rect, SofKind};
9use crate::DeviceBatchSummary;
10use j2k_core::{BackendRequest, Downscale, PixelFormat};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum JpegDecodeOp {
15 Full,
17 Region(Rect),
19 Scaled(Downscale),
21 RegionScaled {
23 roi: Rect,
25 scale: Downscale,
27 },
28}
29
30impl JpegDecodeOp {
31 fn scale(self) -> Downscale {
32 match self {
33 Self::Full | Self::Region(_) => Downscale::None,
34 Self::Scaled(scale) | Self::RegionScaled { scale, .. } => scale,
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct JpegCapabilityRequest {
42 pub op: JpegDecodeOp,
44 pub fmt: PixelFormat,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct JpegDecodeRequest {
51 pub backend: BackendRequest,
53 pub fmt: PixelFormat,
55 pub op: JpegDecodeOp,
57}
58
59impl JpegDecodeRequest {
60 #[must_use]
62 pub const fn capability(self) -> JpegCapabilityRequest {
63 JpegCapabilityRequest {
64 op: self.op,
65 fmt: self.fmt,
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum JpegResolvedDecodePath {
73 CpuHost,
75 OwnedCudaRgb8,
77 MetalFast,
79 Rejected {
81 backend: BackendRequest,
83 reason: &'static str,
85 },
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct JpegResolvedDecode {
91 pub request: JpegDecodeRequest,
93 pub capabilities: JpegCapabilityReport,
95 pub output_rect: Rect,
97 pub path: JpegResolvedDecodePath,
99}
100
101impl JpegResolvedDecode {
102 pub fn inspect(input: &[u8], request: JpegDecodeRequest) -> Result<Self, JpegError> {
109 let capabilities = JpegCapabilityReport::inspect(input, request.capability())?;
110 Ok(Self::from_capabilities(capabilities, request))
111 }
112
113 #[must_use]
115 pub fn from_capabilities(
116 capabilities: JpegCapabilityReport,
117 request: JpegDecodeRequest,
118 ) -> Self {
119 let output_rect = output_rect_for_request(&capabilities.info, request.op);
120 let path = capabilities.resolve_path(request.backend);
121 Self {
122 request,
123 capabilities,
124 output_rect,
125 path,
126 }
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct JpegBackendEligibility {
133 pub eligible: bool,
135 pub reason: Option<&'static str>,
137}
138
139impl JpegBackendEligibility {
140 const fn eligible() -> Self {
141 Self {
142 eligible: true,
143 reason: None,
144 }
145 }
146
147 const fn rejected(reason: &'static str) -> Self {
148 Self {
149 eligible: false,
150 reason: Some(reason),
151 }
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct JpegCapabilityReport {
158 pub request: JpegCapabilityRequest,
160 pub info: Info,
162 pub device: DeviceBatchSummary,
164 pub cpu: JpegBackendEligibility,
166 pub owned_cuda: JpegBackendEligibility,
168 pub metal_fast: JpegBackendEligibility,
170}
171
172impl JpegCapabilityReport {
173 pub fn inspect(input: &[u8], request: JpegCapabilityRequest) -> Result<Self, JpegError> {
181 let view = JpegView::parse(input)?;
182 let info = view.info().clone();
183 let has_lossless_subsampled_color_capability_shape =
184 view.has_lossless_subsampled_color_capability_shape();
185 match Decoder::from_view(view) {
186 Ok(decoder) => Ok(Self::for_decoder(&decoder, request)),
187 Err(err)
188 if can_report_from_parsed_info(
189 &err,
190 has_lossless_subsampled_color_capability_shape,
191 ) =>
192 {
193 Ok(Self::for_planner_rejected_info(info, request, &err))
194 }
195 Err(err) => Err(err),
196 }
197 }
198
199 #[must_use]
201 pub fn for_decoder(decoder: &Decoder<'_>, request: JpegCapabilityRequest) -> Self {
202 let info = decoder.info().clone();
203 let device = summarize_device_batch(decoder, 4);
204 Self {
205 request,
206 info: info.clone(),
207 device,
208 cpu: cpu_eligibility(&info, request),
209 owned_cuda: owned_cuda_eligibility(&info, device, request),
210 metal_fast: metal_fast_eligibility(&info, device, request),
211 }
212 }
213
214 #[expect(
215 clippy::needless_pass_by_value,
216 reason = "the small Copy capability request is stored by value in the resulting report"
217 )]
218 fn for_parsed_info(info: Info, request: JpegCapabilityRequest) -> Self {
219 let device = unavailable_device_summary(&info);
220 Self {
221 request,
222 info: info.clone(),
223 device,
224 cpu: cpu_eligibility(&info, request),
225 owned_cuda: owned_cuda_eligibility(&info, device, request),
226 metal_fast: metal_fast_eligibility(&info, device, request),
227 }
228 }
229
230 fn for_planner_rejected_info(
231 info: Info,
232 request: JpegCapabilityRequest,
233 err: &JpegError,
234 ) -> Self {
235 let mut report = Self::for_parsed_info(info, request);
236 if report.cpu.eligible && matches!(err, JpegError::NotImplemented { .. }) {
237 report.cpu = JpegBackendEligibility::rejected(
238 "JPEG CPU decode planner rejected this stream shape before decode",
239 );
240 }
241 report
242 }
243
244 #[must_use]
250 pub fn metal_resident_rgb8_batch_output(&self) -> JpegBackendEligibility {
251 metal_resident_rgb8_batch_output_eligibility(self.device, self.request)
252 }
253
254 #[must_use]
256 pub fn resolve_path(&self, backend: BackendRequest) -> JpegResolvedDecodePath {
257 match backend {
258 BackendRequest::Cpu => {
259 if self.cpu.eligible {
260 JpegResolvedDecodePath::CpuHost
261 } else {
262 JpegResolvedDecodePath::Rejected {
263 backend,
264 reason: self
265 .cpu
266 .reason
267 .unwrap_or("JPEG CPU decode rejected this request"),
268 }
269 }
270 }
271 BackendRequest::Auto => JpegResolvedDecodePath::CpuHost,
272 BackendRequest::Cuda => {
273 if self.owned_cuda.eligible {
274 JpegResolvedDecodePath::OwnedCudaRgb8
275 } else {
276 JpegResolvedDecodePath::Rejected {
277 backend,
278 reason: self
279 .owned_cuda
280 .reason
281 .unwrap_or("J2K-owned CUDA JPEG decode rejected this request"),
282 }
283 }
284 }
285 BackendRequest::Metal => {
286 if self.metal_fast.eligible {
287 JpegResolvedDecodePath::MetalFast
288 } else {
289 JpegResolvedDecodePath::Rejected {
290 backend,
291 reason: self
292 .metal_fast
293 .reason
294 .unwrap_or("JPEG Metal fast path rejected this request"),
295 }
296 }
297 }
298 }
299 }
300}
301
302fn output_rect_for_request(info: &Info, op: JpegDecodeOp) -> Rect {
303 match op {
304 JpegDecodeOp::Full => Rect::full(info.dimensions),
305 JpegDecodeOp::Region(roi) => roi,
306 JpegDecodeOp::Scaled(scale) => scaled_rect(Rect::full(info.dimensions), scale),
307 JpegDecodeOp::RegionScaled { roi, scale } => scaled_rect(roi, scale),
308 }
309}
310
311fn scaled_rect(rect: Rect, scale: Downscale) -> Rect {
312 let denom = scale.denominator();
313 let x_end = rect.x.saturating_add(rect.w);
314 let y_end = rect.y.saturating_add(rect.h);
315 let x0 = rect.x / denom;
316 let y0 = rect.y / denom;
317 let x1 = x_end.div_ceil(denom);
318 let y1 = y_end.div_ceil(denom);
319 Rect {
320 x: x0,
321 y: y0,
322 w: x1.saturating_sub(x0),
323 h: y1.saturating_sub(y0),
324 }
325}
326
327fn cpu_eligibility(info: &Info, request: JpegCapabilityRequest) -> JpegBackendEligibility {
328 match info.sof_kind {
329 SofKind::Extended12 if is_twelve_bit_output_request(request.fmt) => {
330 return twelve_bit_eligibility(info, request.fmt, TwelveBitSof::Extended);
331 }
332 SofKind::Progressive12 if is_twelve_bit_output_request(request.fmt) => {
333 return twelve_bit_eligibility(info, request.fmt, TwelveBitSof::Progressive);
334 }
335 SofKind::Extended12 | SofKind::Progressive12 => {
336 return JpegBackendEligibility::rejected(
337 "JPEG CPU decode does not yet support this 12-bit JPEG output",
338 )
339 }
340 SofKind::Lossless
341 if matches!(
342 request.fmt,
343 PixelFormat::Gray8
344 | PixelFormat::Gray16
345 | PixelFormat::Rgb8
346 | PixelFormat::Rgba8
347 | PixelFormat::Rgb16
348 | PixelFormat::Rgba16
349 ) =>
350 {
351 return match (info.color_space, info.bit_depth, request.fmt) {
352 (ColorSpace::Grayscale, 8, PixelFormat::Gray8)
353 | (ColorSpace::Grayscale, 16, PixelFormat::Gray16) => {
354 JpegBackendEligibility::eligible()
355 }
356 (
357 ColorSpace::Rgb | ColorSpace::YCbCr,
358 8,
359 PixelFormat::Rgb8 | PixelFormat::Rgba8,
360 )
361 | (
362 ColorSpace::Rgb | ColorSpace::YCbCr,
363 16,
364 PixelFormat::Rgb16 | PixelFormat::Rgba16,
365 )
366 if is_supported_lossless_color_sampling(info) =>
367 {
368 JpegBackendEligibility::eligible()
369 }
370 (ColorSpace::Rgb, 8, PixelFormat::Rgb8 | PixelFormat::Rgba8)
371 | (ColorSpace::Rgb, 16, PixelFormat::Rgb16 | PixelFormat::Rgba16) => JpegBackendEligibility::rejected(
372 "JPEG CPU lossless SOF3 APP14 RGB decode currently supports 4:4:4 sampling, even-width 8/16-bit 4:2:2 sampling, or even-dimension 8/16-bit 4:2:0 sampling",
373 ),
374 (ColorSpace::YCbCr, 8, PixelFormat::Rgb8 | PixelFormat::Rgba8)
375 | (ColorSpace::YCbCr, 16, PixelFormat::Rgb16 | PixelFormat::Rgba16) => JpegBackendEligibility::rejected(
376 "JPEG CPU lossless SOF3 YCbCr decode currently supports 4:4:4 sampling, even-width 8/16-bit 4:2:2 sampling, or even-dimension 8/16-bit 4:2:0 sampling",
377 ),
378 _ => JpegBackendEligibility::rejected(
379 "JPEG CPU lossless SOF3 decode currently supports 8-bit Gray8, 16-bit Gray16, 8-bit YCbCr Rgb8/Rgba8 including even-width 4:2:2 and even-dimension 4:2:0, 16-bit YCbCr Rgb16/Rgba16 including even-width 4:2:2 and even-dimension 4:2:0, 8-bit APP14 RGB Rgb8/Rgba8 including even-width 4:2:2 and even-dimension 4:2:0, or 16-bit APP14 RGB Rgb16/Rgba16 including even-width 4:2:2 and even-dimension 4:2:0 output only",
380 ),
381 };
382 }
383 SofKind::Lossless => {
384 return JpegBackendEligibility::rejected(
385 "JPEG CPU decode does not yet support lossless SOF3 JPEG",
386 )
387 }
388 SofKind::Baseline8 | SofKind::Extended8 | SofKind::Progressive8 => {}
389 }
390
391 match (request.fmt, request.op.scale()) {
392 (PixelFormat::Rgb8 | PixelFormat::Rgba8 | PixelFormat::Gray8, _) => {
393 JpegBackendEligibility::eligible()
394 }
395 (PixelFormat::Rgb16 | PixelFormat::Rgba16 | PixelFormat::Gray16, _) => {
396 JpegBackendEligibility::rejected("JPEG CPU decode does not support 16-bit output")
397 }
398 _ => JpegBackendEligibility::rejected("unsupported JPEG CPU output format"),
399 }
400}
401
402#[derive(Debug, Clone, Copy)]
403enum TwelveBitSof {
404 Extended,
405 Progressive,
406}
407
408impl TwelveBitSof {
409 const fn ycbcr_sampling_reason(self) -> &'static str {
410 match self {
411 Self::Extended => {
412 "JPEG CPU 12-bit extended YCbCr decode currently supports 4:4:4, 4:2:2, or 4:2:0 sampling only"
413 }
414 Self::Progressive => {
415 "JPEG CPU 12-bit progressive YCbCr decode currently supports 4:4:4, 4:2:2, or 4:2:0 sampling only"
416 }
417 }
418 }
419
420 const fn rgb_sampling_reason(self) -> &'static str {
421 match self {
422 Self::Extended => {
423 "JPEG CPU 12-bit extended RGB decode currently supports 4:4:4, 4:2:2, or 4:2:0 sampling only"
424 }
425 Self::Progressive => {
426 "JPEG CPU 12-bit progressive RGB decode currently supports 4:4:4, 4:2:2, or 4:2:0 sampling only"
427 }
428 }
429 }
430
431 const fn four_component_sampling_reason(self) -> &'static str {
432 match self {
433 Self::Extended => {
434 "JPEG CPU 12-bit extended four-component CMYK/YCCK decode currently supports 4:4:4, 4:2:2, or 4:2:0 sampling only"
435 }
436 Self::Progressive => {
437 "JPEG CPU 12-bit progressive four-component CMYK/YCCK decode currently supports 4:4:4, 4:2:2, or 4:2:0 sampling only"
438 }
439 }
440 }
441
442 const fn output_reason(self) -> &'static str {
443 match self {
444 Self::Extended => {
445 "JPEG CPU 12-bit extended decode currently supports grayscale Gray16/Rgb16/Rgba16, APP14 RGB 4:4:4/4:2:2/4:2:0 Rgb16/Rgba16, YCbCr 4:4:4/4:2:2/4:2:0 Rgb16/Rgba16, or CMYK/YCCK 4:4:4/4:2:2/4:2:0 Rgb16/Rgba16 only"
446 }
447 Self::Progressive => {
448 "JPEG CPU 12-bit progressive decode currently supports grayscale Gray16/Rgb16/Rgba16, APP14 RGB 4:4:4/4:2:2/4:2:0 Rgb16/Rgba16, YCbCr 4:4:4/4:2:2/4:2:0 Rgb16/Rgba16, or CMYK/YCCK 4:4:4/4:2:2/4:2:0 Rgb16/Rgba16 only"
449 }
450 }
451 }
452}
453
454fn is_twelve_bit_output_request(fmt: PixelFormat) -> bool {
455 matches!(
456 fmt,
457 PixelFormat::Gray16 | PixelFormat::Rgb16 | PixelFormat::Rgba16
458 )
459}
460
461fn twelve_bit_eligibility(
462 info: &Info,
463 fmt: PixelFormat,
464 sof: TwelveBitSof,
465) -> JpegBackendEligibility {
466 match (info.color_space, fmt) {
467 (ColorSpace::Grayscale, PixelFormat::Gray16 | PixelFormat::Rgb16 | PixelFormat::Rgba16) => {
468 JpegBackendEligibility::eligible()
469 }
470 (ColorSpace::Rgb | ColorSpace::YCbCr, PixelFormat::Rgb16 | PixelFormat::Rgba16)
471 if is_supported_12bit_three_component_sampling(info) =>
472 {
473 JpegBackendEligibility::eligible()
474 }
475 (ColorSpace::Cmyk | ColorSpace::Ycck, PixelFormat::Rgb16 | PixelFormat::Rgba16)
476 if is_supported_extended12_four_component_sampling(info) =>
477 {
478 JpegBackendEligibility::eligible()
479 }
480 (ColorSpace::YCbCr, PixelFormat::Rgb16 | PixelFormat::Rgba16) => {
481 JpegBackendEligibility::rejected(sof.ycbcr_sampling_reason())
482 }
483 (ColorSpace::Rgb, PixelFormat::Rgb16 | PixelFormat::Rgba16) => {
484 JpegBackendEligibility::rejected(sof.rgb_sampling_reason())
485 }
486 (ColorSpace::Cmyk | ColorSpace::Ycck, PixelFormat::Rgb16 | PixelFormat::Rgba16) => {
487 JpegBackendEligibility::rejected(sof.four_component_sampling_reason())
488 }
489 _ => JpegBackendEligibility::rejected(sof.output_reason()),
490 }
491}
492
493fn is_supported_extended12_four_component_sampling(info: &Info) -> bool {
494 info.sampling.len() == 4
495 && matches!(
496 (
497 info.sampling.max_h,
498 info.sampling.max_v,
499 info.sampling.components()
500 ),
501 (1, 1, [(1, 1), (1, 1), (1, 1), (1, 1)])
502 | (2, 1, [(2, 1), (1, 1), (1, 1), (1, 1)])
503 | (2, 2, [(2, 2), (1, 1), (1, 1), (1, 1)])
504 )
505}
506
507fn is_supported_12bit_three_component_sampling(info: &Info) -> bool {
508 info.sampling.len() == 3
509 && matches!(
510 (
511 info.sampling.max_h,
512 info.sampling.max_v,
513 info.sampling.components()
514 ),
515 (1, 1, [(1, 1), (1, 1), (1, 1)])
516 | (2, 1, [(2, 1), (1, 1), (1, 1)])
517 | (2, 2, [(2, 2), (1, 1), (1, 1)])
518 )
519}
520
521fn is_supported_lossless_color_sampling(info: &Info) -> bool {
522 info.sampling.len() == 3
523 && matches!(
524 (
525 info.bit_depth,
526 info.dimensions.0.is_multiple_of(2),
527 info.dimensions.1.is_multiple_of(2),
528 info.sampling.max_h,
529 info.sampling.max_v,
530 info.sampling.components()
531 ),
532 (_, _, _, 1, 1, [(1, 1), (1, 1), (1, 1)])
533 | (8 | 16, true, _, 2, 1, [(2, 1), (1, 1), (1, 1)])
534 | (8 | 16, true, true, 2, 2, [(2, 2), (1, 1), (1, 1)])
535 )
536}
537
538fn owned_cuda_eligibility(
539 info: &Info,
540 device: DeviceBatchSummary,
541 request: JpegCapabilityRequest,
542) -> JpegBackendEligibility {
543 if request.op != JpegDecodeOp::Full || request.fmt != PixelFormat::Rgb8 {
544 return JpegBackendEligibility::rejected(
545 "J2K-owned CUDA JPEG decode currently supports full-tile RGB8 fast 4:2:0, 4:2:2, or 4:4:4 only",
546 );
547 }
548 if !matches!(info.sof_kind, SofKind::Baseline8 | SofKind::Extended8) {
549 return JpegBackendEligibility::rejected(
550 "J2K-owned CUDA JPEG decode supports baseline/extended 8-bit sequential JPEG only",
551 );
552 }
553 if info.color_space != ColorSpace::YCbCr
554 || !(device.matches_fast_420 || device.matches_fast_422 || device.matches_fast_444)
555 {
556 return JpegBackendEligibility::rejected(
557 "J2K-owned CUDA JPEG decode currently requires a YCbCr 4:2:0, 4:2:2, or 4:4:4 fast packet shape",
558 );
559 }
560 if !owned_cuda_rgb8_output_is_addressable(info.dimensions) {
561 return JpegBackendEligibility::rejected(
562 "J2K-owned CUDA JPEG decode requires RGB8 output addressable by u32 byte offsets",
563 );
564 }
565 JpegBackendEligibility::eligible()
566}
567
568fn owned_cuda_rgb8_output_is_addressable((width, height): (u32, u32)) -> bool {
569 u64::from(width)
570 .checked_mul(u64::from(height))
571 .and_then(|pixels| pixels.checked_mul(3))
572 .is_some_and(|bytes| bytes <= u64::from(u32::MAX) + 1)
573}
574
575fn metal_fast_eligibility(
576 info: &Info,
577 device: DeviceBatchSummary,
578 request: JpegCapabilityRequest,
579) -> JpegBackendEligibility {
580 if !matches!(
581 request.fmt,
582 PixelFormat::Gray8 | PixelFormat::Rgb8 | PixelFormat::Rgba8
583 ) {
584 return JpegBackendEligibility::rejected(
585 "JPEG Metal fast path supports Gray8, Rgb8, or Rgba8 output formats",
586 );
587 }
588 if !matches!(info.sof_kind, SofKind::Baseline8 | SofKind::Extended8) {
589 return JpegBackendEligibility::rejected(
590 "JPEG Metal fast path currently supports baseline/extended 8-bit sequential JPEG only",
591 );
592 }
593 if !matches!(
594 info.color_space,
595 ColorSpace::Grayscale | ColorSpace::YCbCr | ColorSpace::Rgb
596 ) {
597 return JpegBackendEligibility::rejected(
598 "JPEG Metal fast path requires grayscale, YCbCr, or RGB input color",
599 );
600 }
601 if device.matches_fast_420 || device.matches_fast_422 || device.matches_fast_444 {
602 JpegBackendEligibility::eligible()
603 } else {
604 JpegBackendEligibility::rejected(
605 "JPEG Metal fast path requires a fast 4:2:0, 4:2:2, or 4:4:4 packet shape",
606 )
607 }
608}
609
610fn metal_resident_rgb8_batch_output_eligibility(
611 device: DeviceBatchSummary,
612 request: JpegCapabilityRequest,
613) -> JpegBackendEligibility {
614 if request.fmt != PixelFormat::Rgb8 {
615 return JpegBackendEligibility::rejected(
616 "JPEG Metal reusable resident batch output currently supports RGB8 output only",
617 );
618 }
619 if !(device.matches_fast_420 || device.matches_fast_422 || device.matches_fast_444) {
620 return JpegBackendEligibility::rejected(
621 "JPEG Metal reusable resident batch output requires a fast 4:2:0, 4:2:2, or 4:4:4 packet shape",
622 );
623 }
624
625 match request.op {
626 JpegDecodeOp::Full => JpegBackendEligibility::eligible(),
627 JpegDecodeOp::Scaled(scale) | JpegDecodeOp::RegionScaled { scale, .. }
628 if supports_metal_resident_batch_scale(scale) =>
629 {
630 JpegBackendEligibility::eligible()
631 }
632 JpegDecodeOp::Scaled(_) | JpegDecodeOp::RegionScaled { .. } => {
633 JpegBackendEligibility::rejected(
634 "JPEG Metal reusable resident batch output currently supports half, quarter, or eighth scaling",
635 )
636 }
637 JpegDecodeOp::Region(_) => JpegBackendEligibility::rejected(
638 "JPEG Metal reusable resident batch output currently supports full, scaled, or region-scaled decode shapes",
639 ),
640 }
641}
642
643fn supports_metal_resident_batch_scale(scale: Downscale) -> bool {
644 matches!(
645 scale,
646 Downscale::Half | Downscale::Quarter | Downscale::Eighth
647 )
648}
649
650fn can_report_from_parsed_info(
651 err: &JpegError,
652 has_lossless_subsampled_color_capability_shape: bool,
653) -> bool {
654 match err {
655 JpegError::UnsupportedColorSpace { .. } => true,
656 JpegError::NotImplemented { sof } if *sof != SofKind::Lossless => true,
657 JpegError::NotImplemented {
658 sof: SofKind::Lossless,
659 } => has_lossless_subsampled_color_capability_shape,
660 _ => false,
661 }
662}
663
664fn unavailable_device_summary(info: &Info) -> DeviceBatchSummary {
665 DeviceBatchSummary {
666 restart_interval: info.restart_interval,
667 checkpoint_count: 0,
668 matches_fast_420: false,
669 matches_fast_422: false,
670 matches_fast_444: false,
671 }
672}