@@ -356,67 +356,67 @@ impl Interpreter {
356356 } )
357357 }
358358
359- /// Decode JPEG using turbojpeg with native scaled DCT decoding.
360- /// When `max_pixels > 0`, picks the smallest scaling factor (1/1, 1/2,
361- /// 1/4, 1/8) that keeps the decoded pixel count at or below the budget.
362- /// The scaling happens DURING the DCT decode — no full-resolution
363- /// decode is ever performed. This is what makes it fast.
359+ /// Decode JPEG with optional pixel-budget downscaling.
360+ /// Strategy:
361+ /// 1. Try turbojpeg with native scaled DCT decoding (fastest)
362+ /// 2. Fall back to `image` crate + box downsample if turbojpeg fails
364363 fn decode_jpeg_scaled ( jpeg_data : & [ u8 ] , max_pixels : u32 ) -> Option < ( u32 , u32 , Vec < u8 > ) > {
365- let mut decompressor = turbojpeg:: Decompressor :: new ( ) . ok ( ) ? ;
366- let header = decompressor . read_header ( jpeg_data) . ok ( ) ? ;
367- let full_w = header . width ;
368- let full_h = header . height ;
364+ // ─── Try turbojpeg native scaled decode ──────────────────────────
365+ if let Some ( result ) = Self :: try_turbojpeg ( jpeg_data, max_pixels ) {
366+ return Some ( result ) ;
367+ }
369368
370- // Pick best scaling factor if we have a pixel budget.
371- // turbojpeg supports native 1/2, 1/4, 1/8 etc. decode — the DCT
372- // coefficients are only partially reconstructed, making it much
373- // faster than full decode + downsample.
374- if max_pixels > 0 && ( full_w * full_h) as u32 > max_pixels {
375- let factors = turbojpeg:: Decompressor :: supported_scaling_factors ( ) ;
376- let mut best: Option < turbojpeg:: ScalingFactor > = None ;
377- for & factor in & factors {
378- let sw = factor. scale ( full_w as usize ) ;
379- let sh = factor. scale ( full_h as usize ) ;
380- if ( sw * sh) as u32 <= max_pixels {
381- best = Some ( factor) ;
382- break ;
383- }
384- }
385- if best. is_none ( ) {
386- best = factors. last ( ) . copied ( ) ;
387- }
388- if let Some ( factor) = best {
389- let _ = decompressor. set_scaling_factor ( factor) ;
390- }
369+ // ─── Fallback: image crate + post-decode downsample ──────────────
370+ let img = image:: load_from_memory_with_format ( jpeg_data, image:: ImageFormat :: Jpeg ) . ok ( ) ?;
371+ let img = img. to_rgba8 ( ) ;
372+ let ( w, h) = ( img. width ( ) , img. height ( ) ) ;
373+ let mut rgba = img. into_raw ( ) ;
374+
375+ // Downsample if over budget
376+ if max_pixels > 0 && w * h > max_pixels {
377+ let ( new_w, new_h, small) = Self :: box_downsample ( & rgba, w, h, max_pixels) ;
378+ return Some ( ( new_w, new_h, small) ) ;
391379 }
380+ Some ( ( w, h, rgba) )
381+ }
382+
383+ /// Try turbojpeg native scaled DCT decode. Returns None on any failure.
384+ fn try_turbojpeg ( jpeg_data : & [ u8 ] , max_pixels : u32 ) -> Option < ( u32 , u32 , Vec < u8 > ) > {
385+ let mut dc = turbojpeg:: Decompressor :: new ( ) . ok ( ) ?;
386+ let header = dc. read_header ( jpeg_data) . ok ( ) ?;
387+ let full_w = header. width ;
388+ let full_h = header. height ;
392389
393- // Read header — then apply the scaling factor to get output dimensions
394- let header2 = decompressor. read_header ( jpeg_data) . ok ( ) ?;
395- // If we set a scaling factor, compute the scaled output size
396- let ( out_w, out_h) = if max_pixels > 0 && ( full_w * full_h) as u32 > max_pixels {
390+ // Pick scaling factor that fits the pixel budget
391+ let chosen_factor = if max_pixels > 0 && ( full_w * full_h) as u32 > max_pixels {
397392 let factors = turbojpeg:: Decompressor :: supported_scaling_factors ( ) ;
398- let mut sw = header2. width ;
399- let mut sh = header2. height ;
400- for & factor in & factors {
401- let fw = factor. scale ( full_w as usize ) ;
402- let fh = factor. scale ( full_h as usize ) ;
403- if ( fw * fh) as u32 <= max_pixels {
404- sw = fw;
405- sh = fh;
393+ // factors are sorted largest-first (1/1, 7/8, 3/4, ..., 1/8)
394+ let mut picked = None ;
395+ for & f in & factors {
396+ let sw = f. scale ( full_w) ;
397+ let sh = f. scale ( full_h) ;
398+ if sw > 0 && sh > 0 && ( sw * sh) as u32 <= max_pixels {
399+ picked = Some ( f) ;
406400 break ;
407401 }
408402 }
409- if sw == header2. width && sh == header2. height {
410- if let Some ( & last) = factors. last ( ) {
411- sw = last. scale ( full_w as usize ) ;
412- sh = last. scale ( full_h as usize ) ;
413- }
403+ // If nothing fits, use the smallest
404+ if picked. is_none ( ) {
405+ picked = factors. last ( ) . copied ( ) ;
414406 }
415- ( sw , sh )
407+ picked
416408 } else {
417- ( header2 . width , header2 . height )
409+ None
418410 } ;
419411
412+ if let Some ( factor) = chosen_factor {
413+ dc. set_scaling_factor ( factor) . ok ( ) ?;
414+ }
415+
416+ // Compute scaled output dimensions using the chosen factor
417+ let out_w = chosen_factor. map_or ( full_w, |f| f. scale ( full_w) ) . max ( 1 ) ;
418+ let out_h = chosen_factor. map_or ( full_h, |f| f. scale ( full_h) ) . max ( 1 ) ;
419+
420420 let mut image = turbojpeg:: Image {
421421 pixels : vec ! [ 0u8 ; out_w * out_h * 4 ] ,
422422 width : out_w,
@@ -425,11 +425,33 @@ impl Interpreter {
425425 format : turbojpeg:: PixelFormat :: RGBA ,
426426 } ;
427427
428- decompressor. decompress ( jpeg_data, image. as_deref_mut ( ) ) . ok ( ) ?;
429-
428+ dc. decompress ( jpeg_data, image. as_deref_mut ( ) ) . ok ( ) ?;
430429 Some ( ( out_w as u32 , out_h as u32 , image. pixels ) )
431430 }
432431
432+ /// Fast box-filter downsample of RGBA data to fit a pixel budget.
433+ fn box_downsample ( rgba : & [ u8 ] , w : u32 , h : u32 , max_pixels : u32 ) -> ( u32 , u32 , Vec < u8 > ) {
434+ let ratio = ( max_pixels as f64 / ( w as f64 * h as f64 ) ) . sqrt ( ) ;
435+ let nw = ( ( w as f64 * ratio) . ceil ( ) as u32 ) . max ( 1 ) ;
436+ let nh = ( ( h as f64 * ratio) . ceil ( ) as u32 ) . max ( 1 ) ;
437+ let sx = w as f64 / nw as f64 ;
438+ let sy = h as f64 / nh as f64 ;
439+ let mut small = Vec :: with_capacity ( ( nw * nh * 4 ) as usize ) ;
440+ for dy in 0 ..nh {
441+ for dx in 0 ..nw {
442+ let src_x = ( dx as f64 * sx) as usize ;
443+ let src_y = ( dy as f64 * sy) as usize ;
444+ let idx = ( src_y * w as usize + src_x) * 4 ;
445+ if idx + 3 < rgba. len ( ) {
446+ small. extend_from_slice ( & rgba[ idx..idx + 4 ] ) ;
447+ } else {
448+ small. extend_from_slice ( & [ 0 , 0 , 0 , 255 ] ) ;
449+ }
450+ }
451+ }
452+ ( nw, nh, small)
453+ }
454+
433455 /// Decode a non-JPEG image (raw/deflated pixel data) with optional
434456 /// box-filter downsampling when exceeding the pixel budget.
435457 fn decode_raw_image (
0 commit comments