@@ -293,9 +293,11 @@ impl Interpreter {
293293 state. restore ( ) ;
294294 }
295295
296- /// Decode and draw an image XObject. When `max_decode_pixels` is set,
297- /// images larger than that limit are downsampled after decode to cap
298- /// memory usage and speed up rendering (used for thumbnails).
296+ /// Decode and draw an image XObject. When `max_decode_pixels > 0`,
297+ /// JPEGs are decoded at reduced resolution via turbojpeg's native
298+ /// scaled DCT decoding (1/2, 1/4, or 1/8) — this is done during the
299+ /// decode itself, avoiding full-resolution decode entirely. Non-JPEG
300+ /// images are decoded at full resolution then box-filtered down.
299301 fn handle_image_execute (
300302 stream : & lopdf:: Stream ,
301303 renderer : & mut SkiaRenderer ,
@@ -304,23 +306,8 @@ impl Interpreter {
304306 max_decode_pixels : u32 ,
305307 ) {
306308 let dict = & stream. dict ;
307- let width = dict. get ( b"Width" ) . ok ( )
308- . and_then ( |o| match o {
309- Object :: Integer ( i) => Some ( * i as u32 ) ,
310- Object :: Reference ( id) => doc. get_object ( * id) . ok ( ) . and_then ( |o| {
311- if let Object :: Integer ( i) = o { Some ( * i as u32 ) } else { None }
312- } ) ,
313- _ => None ,
314- } ) . unwrap_or ( 0 ) ;
315- let height = dict. get ( b"Height" ) . ok ( )
316- . and_then ( |o| match o {
317- Object :: Integer ( i) => Some ( * i as u32 ) ,
318- Object :: Reference ( id) => doc. get_object ( * id) . ok ( ) . and_then ( |o| {
319- if let Object :: Integer ( i) = o { Some ( * i as u32 ) } else { None }
320- } ) ,
321- _ => None ,
322- } ) . unwrap_or ( 0 ) ;
323-
309+ let width = Self :: read_int ( dict, b"Width" , doc) . unwrap_or ( 0 ) ;
310+ let height = Self :: read_int ( dict, b"Height" , doc) . unwrap_or ( 0 ) ;
324311 if width == 0 || height == 0 { return ; }
325312
326313 let filter = dict. get ( b"Filter" ) . ok ( ) . and_then ( |o| match o {
@@ -335,111 +322,191 @@ impl Interpreter {
335322 _ => None ,
336323 } ) ;
337324 let filter_name = filter. as_deref ( ) . unwrap_or ( b"" ) ;
325+ let is_jpeg = filter_name == b"DCTDecode" ;
338326
339- // Decode image to RGBA
340- let ( mut img_w, mut img_h, mut rgba) = if filter_name == b"DCTDecode" {
327+ // ─── JPEG: use turbojpeg with native scaled DCT decoding ─────────
328+ let ( img_w, img_h, rgba) = if is_jpeg {
341329 let raw = & stream. content ;
342- match image:: load_from_memory_with_format ( raw, image:: ImageFormat :: Jpeg ) {
343- Ok ( img) => {
344- let img = img. to_rgba8 ( ) ;
345- let w = img. width ( ) ;
346- let h = img. height ( ) ;
347- ( w, h, img. into_raw ( ) )
348- }
349- Err ( _) => return ,
330+ match Self :: decode_jpeg_scaled ( raw, max_decode_pixels) {
331+ Some ( result) => result,
332+ None => return ,
333+ }
334+ } else {
335+ // ─── Non-JPEG: raw pixel decode + optional box downsample ────
336+ match Self :: decode_raw_image ( dict, stream, doc, width, height, max_decode_pixels) {
337+ Some ( result) => result,
338+ None => return ,
339+ }
340+ } ;
341+
342+ state. save ( ) ;
343+ state. concat_matrix ( 1.0 , 0.0 , 0.0 , -1.0 , 0.0 , 1.0 ) ;
344+ renderer. draw_image ( img_w, img_h, & rgba, & state. current ) ;
345+ state. restore ( ) ;
346+ }
347+
348+ /// Read an integer from a PDF dict, resolving indirect references.
349+ fn read_int ( dict : & Dictionary , key : & [ u8 ] , doc : & Document ) -> Option < u32 > {
350+ dict. get ( key) . ok ( ) . and_then ( |o| match o {
351+ Object :: Integer ( i) => Some ( * i as u32 ) ,
352+ Object :: Reference ( id) => doc. get_object ( * id) . ok ( ) . and_then ( |o| {
353+ if let Object :: Integer ( i) = o { Some ( * i as u32 ) } else { None }
354+ } ) ,
355+ _ => None ,
356+ } )
357+ }
358+
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.
364+ 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 ;
369+
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 ( ) ;
350387 }
388+ if let Some ( factor) = best {
389+ let _ = decompressor. set_scaling_factor ( factor) ;
390+ }
391+ }
392+
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 {
397+ 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;
406+ break ;
407+ }
408+ }
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+ }
414+ }
415+ ( sw, sh)
351416 } else {
352- let bits = dict. get ( b"BitsPerComponent" ) . ok ( )
353- . and_then ( |o| if let Object :: Integer ( i) = o { Some ( * i as u8 ) } else { None } )
354- . unwrap_or ( 8 ) ;
355- if bits != 8 { return ; }
417+ ( header2. width , header2. height )
418+ } ;
419+
420+ let mut image = turbojpeg:: Image {
421+ pixels : vec ! [ 0u8 ; out_w * out_h * 4 ] ,
422+ width : out_w,
423+ pitch : out_w * 4 ,
424+ height : out_h,
425+ format : turbojpeg:: PixelFormat :: RGBA ,
426+ } ;
427+
428+ decompressor. decompress ( jpeg_data, image. as_deref_mut ( ) ) . ok ( ) ?;
356429
357- let cs_name = dict. get ( b"ColorSpace" ) . ok ( ) . and_then ( |o| match o {
430+ Some ( ( out_w as u32 , out_h as u32 , image. pixels ) )
431+ }
432+
433+ /// Decode a non-JPEG image (raw/deflated pixel data) with optional
434+ /// box-filter downsampling when exceeding the pixel budget.
435+ fn decode_raw_image (
436+ dict : & Dictionary ,
437+ stream : & lopdf:: Stream ,
438+ doc : & Document ,
439+ width : u32 ,
440+ height : u32 ,
441+ max_pixels : u32 ,
442+ ) -> Option < ( u32 , u32 , Vec < u8 > ) > {
443+ let bits = dict. get ( b"BitsPerComponent" ) . ok ( )
444+ . and_then ( |o| if let Object :: Integer ( i) = o { Some ( * i as u8 ) } else { None } )
445+ . unwrap_or ( 8 ) ;
446+ if bits != 8 { return None ; }
447+
448+ let cs_name = dict. get ( b"ColorSpace" ) . ok ( ) . and_then ( |o| match o {
449+ Object :: Name ( n) => Some ( n. clone ( ) ) ,
450+ Object :: Reference ( id) => doc. get_object ( * id) . ok ( ) . and_then ( |o| {
451+ if let Object :: Name ( n) = o { Some ( n. clone ( ) ) } else { None }
452+ } ) ,
453+ Object :: Array ( arr) => arr. first ( ) . and_then ( |o| match o {
358454 Object :: Name ( n) => Some ( n. clone ( ) ) ,
359- Object :: Reference ( id) => doc. get_object ( * id) . ok ( ) . and_then ( |o| {
360- if let Object :: Name ( n) = o { Some ( n. clone ( ) ) } else { None }
361- } ) ,
362- Object :: Array ( arr) => arr. first ( ) . and_then ( |o| match o {
363- Object :: Name ( n) => Some ( n. clone ( ) ) ,
364- _ => None ,
365- } ) ,
366455 _ => None ,
367- } ) ;
368- let components: usize = match cs_name. as_deref ( ) {
369- Some ( b"DeviceCMYK" ) => 4 ,
370- Some ( b"DeviceGray" ) | Some ( b"CalGray" ) => 1 ,
371- _ => 3 ,
372- } ;
373-
374- let raw_pixels = match stream. decompressed_content ( ) {
375- Ok ( p) => p,
376- Err ( _) => return ,
377- } ;
378- let expected = width as usize * height as usize * components;
379- if raw_pixels. len ( ) < expected { return ; }
380-
381- let mut out = Vec :: with_capacity ( width as usize * height as usize * 4 ) ;
382- let mut idx = 0 ;
383- for _ in 0 ..( width as usize * height as usize ) {
456+ } ) ,
457+ _ => None ,
458+ } ) ;
459+ let components: usize = match cs_name. as_deref ( ) {
460+ Some ( b"DeviceCMYK" ) => 4 ,
461+ Some ( b"DeviceGray" ) | Some ( b"CalGray" ) => 1 ,
462+ _ => 3 ,
463+ } ;
464+
465+ let raw_pixels = stream. decompressed_content ( ) . ok ( ) ?;
466+ let expected = width as usize * height as usize * components;
467+ if raw_pixels. len ( ) < expected { return None ; }
468+
469+ // Determine output size — downsample if over budget
470+ let ( out_w, out_h, step_x, step_y) = if max_pixels > 0 && width * height > max_pixels {
471+ let ratio = ( max_pixels as f64 / ( width as f64 * height as f64 ) ) . sqrt ( ) ;
472+ let nw = ( ( width as f64 * ratio) . ceil ( ) as u32 ) . max ( 1 ) ;
473+ let nh = ( ( height as f64 * ratio) . ceil ( ) as u32 ) . max ( 1 ) ;
474+ ( nw, nh, width as f64 / nw as f64 , height as f64 / nh as f64 )
475+ } else {
476+ ( width, height, 1.0 , 1.0 )
477+ } ;
478+
479+ let mut rgba = Vec :: with_capacity ( ( out_w * out_h * 4 ) as usize ) ;
480+ for dy in 0 ..out_h {
481+ for dx in 0 ..out_w {
482+ let src_x = ( dx as f64 * step_x) as usize ;
483+ let src_y = ( dy as f64 * step_y) as usize ;
484+ let idx = ( src_y * width as usize + src_x) * components;
384485 match components {
385486 1 => {
386487 let g = raw_pixels[ idx] ;
387- out. extend_from_slice ( & [ g, g, g, 255 ] ) ;
388- idx += 1 ;
488+ rgba. extend_from_slice ( & [ g, g, g, 255 ] ) ;
389489 }
390490 3 => {
391- out. extend_from_slice ( & [ raw_pixels[ idx] , raw_pixels[ idx+1 ] , raw_pixels[ idx+2 ] , 255 ] ) ;
392- idx += 3 ;
491+ rgba. extend_from_slice ( & [ raw_pixels[ idx] , raw_pixels[ idx+1 ] , raw_pixels[ idx+2 ] , 255 ] ) ;
393492 }
394493 4 => {
395494 let c = raw_pixels[ idx] as f32 / 255.0 ;
396495 let m = raw_pixels[ idx+1 ] as f32 / 255.0 ;
397496 let y = raw_pixels[ idx+2 ] as f32 / 255.0 ;
398497 let k = raw_pixels[ idx+3 ] as f32 / 255.0 ;
399- out . extend_from_slice ( & [
498+ rgba . extend_from_slice ( & [
400499 ( 255.0 * ( 1.0 - c) * ( 1.0 - k) ) as u8 ,
401500 ( 255.0 * ( 1.0 - m) * ( 1.0 - k) ) as u8 ,
402501 ( 255.0 * ( 1.0 - y) * ( 1.0 - k) ) as u8 ,
403502 255 ,
404503 ] ) ;
405- idx += 4 ;
406- }
407- _ => { out. extend_from_slice ( & [ 0 , 0 , 0 , 255 ] ) ; idx += components; }
408- }
409- }
410- ( width, height, out)
411- } ;
412-
413- // Downsample if image exceeds the pixel budget (fast box filter).
414- // For thumbnails this turns a 5000×5000 decode into a 200×200 draw.
415- if max_decode_pixels > 0 && img_w * img_h > max_decode_pixels {
416- let ratio = ( max_decode_pixels as f64 / ( img_w as f64 * img_h as f64 ) ) . sqrt ( ) ;
417- let new_w = ( ( img_w as f64 * ratio) . ceil ( ) as u32 ) . max ( 1 ) ;
418- let new_h = ( ( img_h as f64 * ratio) . ceil ( ) as u32 ) . max ( 1 ) ;
419- let sx = img_w as f64 / new_w as f64 ;
420- let sy = img_h as f64 / new_h as f64 ;
421- let mut small = Vec :: with_capacity ( ( new_w * new_h * 4 ) as usize ) ;
422- for dy in 0 ..new_h {
423- for dx in 0 ..new_w {
424- let src_x = ( dx as f64 * sx) as usize ;
425- let src_y = ( dy as f64 * sy) as usize ;
426- let src_idx = ( src_y * img_w as usize + src_x) * 4 ;
427- if src_idx + 3 < rgba. len ( ) {
428- small. extend_from_slice ( & rgba[ src_idx..src_idx + 4 ] ) ;
429- } else {
430- small. extend_from_slice ( & [ 0 , 0 , 0 , 255 ] ) ;
431504 }
505+ _ => { rgba. extend_from_slice ( & [ 0 , 0 , 0 , 255 ] ) ; }
432506 }
433507 }
434- img_w = new_w;
435- img_h = new_h;
436- rgba = small;
437508 }
438-
439- state. save ( ) ;
440- state. concat_matrix ( 1.0 , 0.0 , 0.0 , -1.0 , 0.0 , 1.0 ) ;
441- renderer. draw_image ( img_w, img_h, & rgba, & state. current ) ;
442- state. restore ( ) ;
509+ Some ( ( out_w, out_h, rgba) )
443510 }
444511
445512 pub fn extract_commands (
0 commit comments