#![allow(unsafe_code)]
use std::{
os::raw::{c_int, c_uchar, c_ulong, c_void},
ptr::NonNull,
sync::OnceLock,
};
use anyhow::{Result, bail};
use libloading::{Library, Symbol};
type TjHandle = *mut c_void;
const TJCS_GRAY: c_int = 2;
const TJCS_CMYK: c_int = 3;
const TJCS_YCCK: c_int = 4;
const TJPF_RGB: c_int = 0;
const TJPF_GRAY: c_int = 6;
const TJFLAG_LIMITSCANS: c_int = 32_768;
type TjInitDecompress = unsafe extern "C" fn() -> TjHandle;
type TjDecompressHeader3 = unsafe extern "C" fn(
TjHandle,
*const c_uchar,
c_ulong,
*mut c_int,
*mut c_int,
*mut c_int,
*mut c_int,
) -> c_int;
type TjDecompress2 = unsafe extern "C" fn(
TjHandle,
*const c_uchar,
c_ulong,
*mut c_uchar,
c_int,
c_int,
c_int,
c_int,
c_int,
) -> c_int;
type TjDestroy = unsafe extern "C" fn(TjHandle) -> c_int;
struct TurboJpeg {
_lib: Library,
init: TjInitDecompress,
header: TjDecompressHeader3,
decompress: TjDecompress2,
destroy: TjDestroy,
}
struct TurboJpegHandle<'a> {
api: &'a TurboJpeg,
handle: NonNull<c_void>,
}
impl<'a> TurboJpegHandle<'a> {
fn new(api: &'a TurboJpeg) -> Option<Self> {
let handle = NonNull::new(unsafe { (api.init)() })?;
Some(Self { api, handle })
}
fn as_ptr(&self) -> TjHandle {
self.handle.as_ptr()
}
}
impl Drop for TurboJpegHandle<'_> {
fn drop(&mut self) {
unsafe {
(self.api.destroy)(self.handle.as_ptr());
}
}
}
#[derive(Debug)]
pub(crate) struct DecodedJpeg {
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) channels: usize,
pub(crate) data: Vec<u8>,
}
fn load_turbojpeg() -> Option<TurboJpeg> {
const CANDIDATES: &[&str] = &[
"libturbojpeg.so.0",
"libturbojpeg.so",
"libturbojpeg.0.dylib",
"libturbojpeg.dylib",
];
let lib = CANDIDATES
.iter()
.find_map(|name| unsafe { Library::new(name) }.ok())?;
let (init, header, decompress, destroy) = unsafe {
let init: Symbol<TjInitDecompress> = lib.get(b"tjInitDecompress\0").ok()?;
let header: Symbol<TjDecompressHeader3> = lib.get(b"tjDecompressHeader3\0").ok()?;
let decompress: Symbol<TjDecompress2> = lib.get(b"tjDecompress2\0").ok()?;
let destroy: Symbol<TjDestroy> = lib.get(b"tjDestroy\0").ok()?;
(*init, *header, *decompress, *destroy)
};
Some(TurboJpeg {
_lib: lib,
init,
header,
decompress,
destroy,
})
}
fn turbojpeg() -> Option<&'static TurboJpeg> {
static TJ: OnceLock<Option<TurboJpeg>> = OnceLock::new();
TJ.get_or_init(load_turbojpeg).as_ref()
}
pub(crate) fn available() -> bool {
turbojpeg().is_some()
}
pub(crate) fn is_jpeg(bytes: &[u8]) -> bool {
bytes.len() >= 3 && bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF
}
pub(crate) fn decode_jpeg(
bytes: &[u8],
max_width: Option<u32>,
max_height: Option<u32>,
max_alloc: Option<u64>,
) -> Result<Option<DecodedJpeg>> {
if !is_jpeg(bytes) {
return Ok(None);
}
let Some(tj) = turbojpeg() else {
return Ok(None);
};
unsafe {
let Some(handle) = TurboJpegHandle::new(tj) else {
return Ok(None);
};
let (mut w, mut h, mut subsamp, mut colorspace) = (0_i32, 0_i32, 0_i32, 0_i32);
let hdr = (tj.header)(
handle.as_ptr(),
bytes.as_ptr(),
bytes.len() as c_ulong,
&mut w,
&mut h,
&mut subsamp,
&mut colorspace,
);
if hdr != 0 || w <= 0 || h <= 0 {
return Ok(None);
}
if matches!(colorspace, TJCS_CMYK | TJCS_YCCK) {
return Ok(None);
}
let (width, height) = (w as u32, h as u32);
if max_width.is_some_and(|limit| width > limit)
|| max_height.is_some_and(|limit| height > limit)
{
bail!("Image dimensions exceed configured limits: {width}x{height}");
}
let (pixel_format, channels) = if colorspace == TJCS_GRAY {
(TJPF_GRAY, 1_usize)
} else {
(TJPF_RGB, 3_usize)
};
let nbytes = match u64::from(width)
.checked_mul(u64::from(height))
.and_then(|p| p.checked_mul(channels as u64))
{
Some(n) => n,
None => {
bail!("Image allocation size overflow for dimensions: {width}x{height}");
}
};
if let Some(limit) = max_alloc
&& nbytes > limit
{
bail!("Image allocation {nbytes} bytes exceeds configured limit {limit} bytes");
}
let nbytes = match usize::try_from(nbytes) {
Ok(n) => n,
Err(_) => {
bail!("Image allocation size does not fit in usize: {nbytes} bytes");
}
};
let mut buf = Vec::new();
if let Err(err) = buf.try_reserve_exact(nbytes) {
bail!("Image allocation {nbytes} bytes could not be reserved: {err:?}");
}
let rc = (tj.decompress)(
handle.as_ptr(),
bytes.as_ptr(),
bytes.len() as c_ulong,
buf.as_mut_ptr(),
w,
0,
h,
pixel_format,
TJFLAG_LIMITSCANS,
);
if rc != 0 {
return Ok(None);
}
buf.set_len(nbytes);
Ok(Some(DecodedJpeg {
width,
height,
channels,
data: buf,
}))
}
}