Skip to content

Commit 0effe21

Browse files
committed
fix: add image crate fallback when turbojpeg fails, use shared box_downsample helper
1 parent c706ddd commit 0effe21

1 file changed

Lines changed: 73 additions & 51 deletions

File tree

open-pdf-render/src/interpreter.rs

Lines changed: 73 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)