blob: 765642ecaf8613cd045245bffea1124708f04454 [file]
// Copyright 2026 The PDFium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use zune_jpeg::zune_core::bytestream::ZCursor;
use zune_jpeg::zune_core::colorspace::ColorSpace;
use zune_jpeg::JpegDecoder;
#[cxx::bridge(namespace = "fxcodec::rust_jpeg")]
mod ffi {
pub struct JpegHeaderInfo {
pub width: u32,
pub height: u32,
pub num_components: i32,
pub bits_per_component: i32,
pub color_transform: bool,
pub x_density: u16,
pub y_density: u16,
pub density_unit: u8,
pub max_h_samp: u8,
pub max_v_samp: u8,
}
extern "Rust" {
fn read_jpeg_info(src: &[u8], info: &mut JpegHeaderInfo) -> bool;
fn decode_jpeg_to_buf(
src: &[u8],
out_buf: &mut [u8],
out_pitch: usize,
scale_denom: u32,
) -> bool;
}
}
fn parse_jfif_density(src: &[u8]) -> (u16, u16, u8) {
let mut pos = 0;
while pos + 1 < src.len() {
if src[pos] == 0xff && src[pos + 1] == 0xd8 {
pos += 2;
break;
}
pos += 1;
}
while pos + 3 < src.len() {
if src[pos] != 0xff {
break;
}
let marker = src[pos + 1];
if marker == 0xff {
pos += 1;
continue;
}
pos += 2;
if marker == 0xd8 || marker == 0xd9 || marker == 0x00 {
continue;
}
if pos + 2 > src.len() {
break;
}
let len = u16::from_be_bytes([src[pos], src[pos + 1]]) as usize;
if marker == 0xe0 && len >= 16 && pos + len <= src.len() {
let payload = &src[pos + 2..pos + len];
if payload.starts_with(b"JFIF\0") && payload.len() >= 12 {
let density_unit = payload[7];
let x_density = u16::from_be_bytes([payload[8], payload[9]]);
let y_density = u16::from_be_bytes([payload[10], payload[11]]);
return (x_density, y_density, density_unit);
}
}
pos += len;
}
(0, 0, 0)
}
fn read_jpeg_info(src: &[u8], info: &mut ffi::JpegHeaderInfo) -> bool {
let cursor = ZCursor::new(src);
let mut decoder = JpegDecoder::new(cursor);
if decoder.decode_headers().is_err() {
return false;
}
let img_info = match decoder.info() {
Some(i) => i,
None => return false,
};
info.width = img_info.width as u32;
info.height = img_info.height as u32;
info.num_components = img_info.components as i32;
info.bits_per_component = 8;
info.color_transform =
matches!(decoder.input_colorspace(), Some(ColorSpace::YCbCr) | Some(ColorSpace::YCCK));
let (max_h_samp, max_v_samp) = match img_info.sample_ratio {
zune_jpeg::SampleRatios::None => (1, 1),
zune_jpeg::SampleRatios::H => (2, 1),
zune_jpeg::SampleRatios::V => (1, 2),
zune_jpeg::SampleRatios::HV => (2, 2),
zune_jpeg::SampleRatios::Generic(h, v) => (h as u8, v as u8),
};
info.max_h_samp = max_h_samp;
info.max_v_samp = max_v_samp;
let (x_density, y_density, density_unit) = parse_jfif_density(src);
info.x_density = x_density;
info.y_density = y_density;
info.density_unit = density_unit;
true
}
fn ycck_to_cmyk_inplace(pixels: &mut [u8]) {
for chunk in pixels.chunks_exact_mut(4) {
let y = chunk[0] as i32;
let cb = chunk[1] as i32 - 128;
let cr = chunk[2] as i32 - 128;
let k = chunk[3];
// 16-bit fixed-point scaling (SCALE = 65536, ONE_HALF = 32768).
let r = y + ((91881 * cr + 32768) >> 16);
let g = y + ((-22554 * cb - 46802 * cr + 32768) >> 16);
let b = y + ((116130 * cb + 32768) >> 16);
chunk[0] = (255 - r).clamp(0, 255) as u8;
chunk[1] = (255 - g).clamp(0, 255) as u8;
chunk[2] = (255 - b).clamp(0, 255) as u8;
chunk[3] = k;
}
}
fn decode_jpeg_to_buf(src: &[u8], out_buf: &mut [u8], out_pitch: usize, scale_denom: u32) -> bool {
let cursor = ZCursor::new(src);
let mut decoder = JpegDecoder::new(cursor);
if decoder.decode_headers().is_err() {
return false;
}
let img_info = match decoder.info() {
Some(i) => i,
None => return false,
};
let is_ycck = decoder.input_colorspace() == Some(ColorSpace::YCCK);
if img_info.components == 1 {
let options = decoder.options().jpeg_set_out_colorspace(ColorSpace::Luma);
decoder.set_options(options);
} else if img_info.components == 4 {
let out_cs = if is_ycck { ColorSpace::YCCK } else { ColorSpace::CMYK };
let options = decoder.options().jpeg_set_out_colorspace(out_cs);
decoder.set_options(options);
}
let output_colorspace = match decoder.output_colorspace() {
Some(cs) => cs,
None => return false,
};
let mut pixels = match decoder.decode() {
Ok(p) => p,
Err(_) => return false,
};
if is_ycck {
ycck_to_cmyk_inplace(&mut pixels);
}
let orig_width = img_info.width as usize;
let orig_height = img_info.height as usize;
let num_comps = output_colorspace.num_components();
let src_row_len = orig_width * num_comps;
if pixels.len() < orig_height * src_row_len {
return false;
}
let scale = (scale_denom as usize).max(1);
let out_width = orig_width.div_ceil(scale);
let out_height = orig_height.div_ceil(scale);
let dst_row_len = out_width * num_comps;
if scale == 1 {
for y in 0..out_height {
let src_row = &pixels[y * src_row_len..(y + 1) * src_row_len];
let dst_start = y * out_pitch;
if dst_start + dst_row_len > out_buf.len() {
return false;
}
out_buf[dst_start..dst_start + dst_row_len].copy_from_slice(src_row);
}
} else {
for out_y in 0..out_height {
let dst_start = out_y * out_pitch;
if dst_start + dst_row_len > out_buf.len() {
return false;
}
let dst_row = &mut out_buf[dst_start..dst_start + dst_row_len];
let y_start = out_y * scale;
let y_end = (y_start + scale).min(orig_height);
for out_x in 0..out_width {
let x_start = out_x * scale;
let x_end = (x_start + scale).min(orig_width);
let count = (y_end - y_start) * (x_end - x_start);
let dst_pixel = &mut dst_row[out_x * num_comps..(out_x + 1) * num_comps];
if count == 0 {
continue;
}
for c in 0..num_comps {
let mut sum: u32 = 0;
for sy in y_start..y_end {
let row_offset = sy * src_row_len;
for sx in x_start..x_end {
sum += pixels[row_offset + sx * num_comps + c] as u32;
}
}
dst_pixel[c] = ((sum + (count as u32 / 2)) / count as u32) as u8;
}
}
}
}
true
}