pub mod bitstream;
pub mod color;
pub mod cpu;
pub mod huffman;
pub mod idct;
pub mod markers;
pub mod mcu;
pub mod types;
#[cfg(all(target_os = "linux", feature = "v4l2"))]
mod v4l2;
#[cfg(all(target_os = "linux", feature = "nvjpeg"))]
mod nvjpeg;
#[cfg(all(target_os = "linux", feature = "nvjpeg"))]
pub(crate) use nvjpeg::is_nvjpeg_available as nvjpeg_available;
#[cfg(all(target_os = "linux", feature = "v4l2"))]
pub(crate) fn v4l2_available() -> bool {
v4l2::device_available()
}
#[cfg(all(target_os = "linux", any(feature = "v4l2", feature = "nvjpeg")))]
pub(crate) fn env_flag(name: &str) -> bool {
std::env::var(name)
.map(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
.unwrap_or(false)
}
use crate::error::{CodecError, UnsupportedFeature};
use crate::exif::read_exif_orientation;
use crate::options::ImageInfo;
use crate::pixel::ImagePixel;
use edgefirst_tensor::{PixelFormat, Tensor};
pub struct JpegDecoderState {
mcu_scratch: Option<mcu::McuScratch>,
huffman_cache: huffman::HuffmanCache,
pub(crate) preferred_format: Option<PixelFormat>,
pub(crate) chroma_upsample: crate::decoder::ChromaUpsample,
pub(crate) fast_dct: bool,
#[cfg(all(target_os = "linux", feature = "v4l2"))]
v4l2: v4l2::V4l2Probe,
#[cfg(all(target_os = "linux", feature = "nvjpeg"))]
nvjpeg: nvjpeg::NvJpegProbe,
}
impl JpegDecoderState {
pub fn new() -> Self {
Self {
mcu_scratch: None,
huffman_cache: huffman::HuffmanCache::default(),
preferred_format: None,
chroma_upsample: crate::decoder::ChromaUpsample::default(),
fast_dct: std::env::var("EDGEFIRST_CODEC_DCT")
.map(|v| v.trim().eq_ignore_ascii_case("fast"))
.unwrap_or(false),
#[cfg(all(target_os = "linux", feature = "v4l2"))]
v4l2: v4l2::V4l2Probe::default(),
#[cfg(all(target_os = "linux", feature = "nvjpeg"))]
nvjpeg: nvjpeg::NvJpegProbe::default(),
}
}
}
impl Default for JpegDecoderState {
fn default() -> Self {
Self::new()
}
}
fn native_format(headers: &markers::JpegHeaders) -> crate::Result<PixelFormat> {
let comps = &headers.header.components;
match comps.len() {
1 => Ok(PixelFormat::Grey),
3 => {
let max_h = headers.header.max_h_samp as usize;
let max_v = headers.header.max_v_samp as usize;
let cb = comps[1].sampling;
let cr = comps[2].sampling;
if cb.h == cr.h && cb.v == cr.v && cb.h as usize > 0 && cb.v as usize > 0 {
let h_ratio = max_h / cb.h as usize;
let v_ratio = max_v / cb.v as usize;
return Ok(match (h_ratio, v_ratio) {
(1, 1) => PixelFormat::Nv24, (2, 1) => PixelFormat::Nv16, (2, 2) => PixelFormat::Nv12, _ => PixelFormat::Nv12, });
}
Ok(PixelFormat::Nv12)
}
n => Err(CodecError::Unsupported(
UnsupportedFeature::JpegComponentCount {
components: n as u8,
},
)),
}
}
fn resolve_output_format(
headers: &markers::JpegHeaders,
native: PixelFormat,
preferred: Option<PixelFormat>,
) -> crate::Result<PixelFormat> {
Ok(match preferred {
Some(PixelFormat::Rgb) if native == PixelFormat::Nv24 => PixelFormat::Rgb,
Some(PixelFormat::Rgb)
if native == PixelFormat::Nv12 && mcu::is_native_420(&headers.header) =>
{
PixelFormat::Rgb
}
Some(PixelFormat::Rgb) => return Err(CodecError::UnsupportedFormat(PixelFormat::Rgb)),
Some(PixelFormat::Nv12)
if matches!(
native,
PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv24
) =>
{
PixelFormat::Nv12
}
_ => native,
})
}
fn native_row_stride(width: usize) -> usize {
width.next_multiple_of(2).next_multiple_of(64)
}
pub fn peek_jpeg_info(data: &[u8]) -> crate::Result<ImageInfo> {
let headers = markers::parse_markers(data)?;
let img_w = headers.header.width as usize;
let img_h = headers.header.height as usize;
let format = native_format(&headers)?;
let (rotation_degrees, flip_horizontal) = headers
.exif_data
.as_deref()
.map(read_exif_orientation)
.unwrap_or((0, false));
Ok(ImageInfo {
width: img_w,
height: img_h,
format,
row_stride: native_row_stride(img_w),
rotation_degrees,
flip_horizontal,
})
}
pub fn decode_jpeg_into<T: ImagePixel>(
data: &[u8],
dst: &mut Tensor<T>,
state: &mut JpegDecoderState,
) -> crate::Result<ImageInfo> {
let _span = tracing::trace_span!(
"codec.decode_jpeg",
dtype = std::any::type_name::<T>(),
n_bytes = data.len(),
)
.entered();
let mut headers = {
let _s = tracing::trace_span!("codec.decode_jpeg.parse_markers").entered();
markers::parse_markers_with_cache(data, Some(&mut state.huffman_cache))?
};
let result = decode_jpeg_into_parsed(data, dst, state, &headers);
state
.huffman_cache
.restore(&mut headers.dc_tables, &mut headers.ac_tables);
result
}
fn decode_jpeg_into_parsed<T: ImagePixel>(
data: &[u8],
dst: &mut Tensor<T>,
state: &mut JpegDecoderState,
headers: &markers::JpegHeaders,
) -> crate::Result<ImageInfo> {
let img_w = headers.header.width as usize;
let img_h = headers.header.height as usize;
let native_fmt = native_format(headers)?;
let output_fmt = resolve_output_format(headers, native_fmt, state.preferred_format)?;
#[cfg(all(target_os = "linux", any(feature = "v4l2", feature = "nvjpeg")))]
let allow_hw = output_fmt == native_fmt;
if T::dtype() != edgefirst_tensor::DType::U8 {
return Err(CodecError::UnsupportedDtype(T::dtype()));
}
let (rotation_degrees, flip_horizontal) = headers
.exif_data
.as_deref()
.map(read_exif_orientation)
.unwrap_or((0, false));
let cap_w = dst.width().unwrap_or(0);
let cap_h = dst.height().unwrap_or(0);
dst.configure_image(img_w, img_h, output_fmt)
.map_err(|e| match e {
edgefirst_tensor::Error::InsufficientCapacity { .. } => {
CodecError::InsufficientCapacity {
image: (img_w, img_h),
tensor: (cap_w, cap_h),
}
}
other => CodecError::Tensor(other),
})?;
let dst_stride = dst
.effective_row_stride()
.unwrap_or(native_row_stride(img_w));
#[cfg(all(target_os = "linux", feature = "nvjpeg"))]
if allow_hw {
match state
.nvjpeg
.try_decode::<T>(data, headers, dst, output_fmt, img_w, img_h, dst_stride)
{
Ok(Some(mut info)) => {
info.rotation_degrees = rotation_degrees;
info.flip_horizontal = flip_horizontal;
dst.set_colorimetry(None);
return Ok(info);
}
Ok(None) => {}
Err(nvjpeg::NvJpegDecode::Fallback(why)) => {
log::debug!("nvjpeg jpeg decode fell back: {why}");
}
Err(nvjpeg::NvJpegDecode::Fatal(e)) => return Err(e),
}
}
#[cfg(all(target_os = "linux", feature = "v4l2"))]
if allow_hw {
match state
.v4l2
.try_decode::<T>(data, headers, dst, output_fmt, img_w, img_h, dst_stride)
{
Ok(Some(mut info)) => {
info.rotation_degrees = rotation_degrees;
info.flip_horizontal = flip_horizontal;
dst.set_colorimetry(Some(edgefirst_tensor::Colorimetry::jfif()));
return Ok(info);
}
Ok(None) => {}
Err(v4l2::V4l2Decode::Fallback(why)) => {
log::debug!("v4l2 jpeg decode fell back to cpu: {why}");
}
Err(v4l2::V4l2Decode::Fatal(e)) => return Err(e),
}
}
dst.cpu_access()
.require(edgefirst_tensor::CpuAccess::Write)?;
match &mut state.mcu_scratch {
Some(scratch) => scratch.ensure_capacity(headers),
None => state.mcu_scratch = Some(mcu::McuScratch::new(headers)),
}
let mcu_scratch = state.mcu_scratch.as_mut().unwrap();
{
let mut map = crate::pixel::map_decode_dst(dst)?;
let dst_bytes: &mut [T] = &mut map;
let dst_u8: &mut [u8] = unsafe {
std::slice::from_raw_parts_mut(dst_bytes.as_mut_ptr() as *mut u8, dst_bytes.len())
};
let _s = tracing::trace_span!("codec.decode_jpeg.mcu_loop").entered();
mcu::decode_image(
data,
headers,
mcu_scratch,
dst_u8,
dst_stride,
output_fmt,
state.fast_dct,
)?;
}
if output_fmt == PixelFormat::Rgb {
dst.set_colorimetry(None);
} else {
dst.set_colorimetry(Some(edgefirst_tensor::Colorimetry::jfif()));
}
Ok(ImageInfo {
width: img_w,
height: img_h,
format: output_fmt,
row_stride: dst_stride,
rotation_degrees,
flip_horizontal,
})
}