use anyhow::{Context, Result};
use fast_image_resize::images::Image;
use fast_image_resize::{FilterType, PixelType, ResizeAlg, ResizeOptions, Resizer};
use mozjpeg::{ColorSpace, Compress, Decompress};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicUsize, Ordering};
fn fwd_lut() -> &'static [u16; 256] {
static LUT: OnceLock<[u16; 256]> = OnceLock::new();
LUT.get_or_init(|| {
let mut t = [0u16; 256];
for (i, v) in t.iter_mut().enumerate() {
let s = i as f64 / 255.0;
let lin = if s <= 0.04045 {
s / 12.92
} else {
((s + 0.055) / 1.055).powf(2.4)
};
*v = (lin * 65535.0 + 0.5) as u16;
}
t
})
}
fn fwd_lut_f32() -> &'static [f32; 256] {
static LUT: OnceLock<[f32; 256]> = OnceLock::new();
LUT.get_or_init(|| {
let mut t = [0f32; 256];
let fwd = fwd_lut();
for (d, &v) in t.iter_mut().zip(fwd.iter()) {
*d = v as f32;
}
t
})
}
fn back_lut() -> &'static [u8; 65536] {
static LUT: OnceLock<Box<[u8; 65536]>> = OnceLock::new();
LUT.get_or_init(|| {
let mut t = vec![0u8; 65536].into_boxed_slice();
for (i, v) in t.iter_mut().enumerate() {
let lin = i as f64 / 65535.0;
let s = if lin <= 0.003_130_8 {
12.92 * lin
} else {
1.055 * lin.powf(1.0 / 2.4) - 0.055
};
*v = (s * 255.0 + 0.5) as u8;
}
t.try_into().unwrap()
})
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Encoder {
#[default]
Jpegli,
MozFast,
MozSmall,
}
impl Encoder {
pub fn from_preset(preset: &str) -> Self {
match preset {
"fast" => Encoder::MozFast,
"small" => Encoder::MozSmall,
_ => Encoder::Jpegli,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PngEffort {
Fastest,
Fast,
Balanced,
High,
}
#[derive(Clone, Debug)]
pub struct Params {
pub max_width: u32,
pub max_height: u32,
pub quality: f32,
pub encoder: Encoder,
pub parallel: usize,
pub output: Option<ImageFormat>,
pub webp_quality: Option<f32>,
pub png_effort: Option<PngEffort>,
pub png_quantize: Option<bool>,
pub png_quantize_colors: Option<u16>,
pub auto_rotate: Option<bool>,
pub icc: Option<bool>,
pub flatten_bg: Option<[u8; 3]>,
pub linear_light: Option<bool>,
#[cfg(feature = "avif")]
pub avif_quality: Option<u8>,
}
impl Default for Params {
fn default() -> Self {
Params {
max_width: u32::MAX,
max_height: u32::MAX,
quality: 80.0,
encoder: Encoder::Jpegli,
parallel: 1,
output: None,
webp_quality: None,
png_effort: None,
png_quantize: None,
png_quantize_colors: None,
auto_rotate: None,
icc: None,
flatten_bg: None,
linear_light: None,
#[cfg(feature = "avif")]
avif_quality: None,
}
}
}
fn format_max_dimension(format: ImageFormat) -> Option<u32> {
match format {
ImageFormat::Webp => Some(16383),
_ => None,
}
}
fn clamp_to_format(p: &Params, target: ImageFormat) -> Params {
let Some(cap) = format_max_dimension(target) else {
return p.clone();
};
if p.max_width <= cap && p.max_height <= cap {
return p.clone();
}
Params {
max_width: p.max_width.min(cap),
max_height: p.max_height.min(cap),
..p.clone()
}
}
fn fit_dims(src_w: usize, src_h: usize, max_w: u32, max_h: u32) -> (usize, usize) {
let scale = f64::min(
max_w as f64 / src_w as f64,
f64::min(max_h as f64 / src_h as f64, 1.0),
);
(
((src_w as f64 * scale).round() as usize).max(1),
((src_h as f64 * scale).round() as usize).max(1),
)
}
fn dct_scale_num(src_w: usize, src_h: usize, dst_w: usize, dst_h: usize, margin: f64) -> u8 {
let (need_w, need_h) = (
(dst_w as f64 * margin).ceil() as usize,
(dst_h as f64 * margin).ceil() as usize,
);
for num in 1..=8u8 {
let sw = (src_w * num as usize).div_ceil(8);
let sh = (src_h * num as usize).div_ceil(8);
if (sw >= need_w && sh >= need_h) || (sw >= src_w && sh >= src_h) {
return num;
}
}
8
}
fn dct_margin() -> f64 {
crate::config::config().dct_margin
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum ImageFormat {
Jpeg,
Png,
Webp,
Avif,
}
impl ImageFormat {
pub fn content_type(self) -> &'static str {
match self {
ImageFormat::Jpeg => "image/jpeg",
ImageFormat::Png => "image/png",
ImageFormat::Webp => "image/webp",
ImageFormat::Avif => "image/avif",
}
}
pub fn from_token(token: &str) -> Option<ImageFormat> {
match token {
"jpg" | "jpeg" => Some(ImageFormat::Jpeg),
"png" => Some(ImageFormat::Png),
"webp" => Some(ImageFormat::Webp),
"avif" => Some(ImageFormat::Avif),
_ => None,
}
}
fn sniff(header: &[u8; 12]) -> Option<ImageFormat> {
if header.starts_with(&[0xFF, 0xD8]) {
Some(ImageFormat::Jpeg)
} else if header.starts_with(b"\x89PNG\r\n\x1a\n") {
Some(ImageFormat::Png)
} else if &header[0..4] == b"RIFF" && &header[8..12] == b"WEBP" {
Some(ImageFormat::Webp)
} else if &header[4..8] == b"ftyp"
&& (&header[8..12] == b"avif" || &header[8..12] == b"avis")
{
Some(ImageFormat::Avif)
} else {
None
}
}
}
pub fn probe(bytes: &[u8]) -> Result<(ImageFormat, usize, usize), Error> {
probe_inner(bytes).map_err(|e| Error::classify(e, false))
}
fn probe_inner(bytes: &[u8]) -> Result<(ImageFormat, usize, usize)> {
let mut header = [0u8; 12];
anyhow::ensure!(bytes.len() >= 12, "source too short");
header.copy_from_slice(&bytes[..12]);
let format = ImageFormat::sniff(&header).context("unsupported image format")?;
match format {
ImageFormat::Jpeg => {
let (w, h) = crate::panic_guard::catch_unwind_as_error("JPEG header parse", || {
Decompress::new_mem(bytes).map(|dec| dec.size())
})?
.context("parse JPEG")?;
Ok((format, w, h))
}
ImageFormat::Png => {
let mut r = png::Decoder::new(std::io::Cursor::new(bytes))
.read_info()
.context("parse PNG")?;
let info = r.info();
let dims = (info.width as usize, info.height as usize);
let _ = r.next_row();
Ok((format, dims.0, dims.1))
}
ImageFormat::Webp => unsafe {
use libwebp_sys as w;
let mut features: w::WebPBitstreamFeatures = std::mem::zeroed();
let status = w::WebPGetFeatures(bytes.as_ptr(), bytes.len(), &mut features);
anyhow::ensure!(
status == w::VP8StatusCode::VP8_STATUS_OK,
"parse WebP header"
);
Ok((format, features.width as usize, features.height as usize))
},
#[cfg(feature = "avif")]
ImageFormat::Avif => {
let (w, h) = crate::avif::probe_avif(bytes)?;
Ok((format, w, h))
}
#[cfg(not(feature = "avif"))]
ImageFormat::Avif => anyhow::bail!("AVIF support is not enabled in this build"),
}
}
pub fn process(bytes: &[u8], p: &Params) -> Result<(Vec<u8>, ImageFormat), Error> {
process_reader(std::io::Cursor::new(bytes), p, bytes.len())
.map_err(|e| Error::classify(e, false))
}
fn process_reader<R: std::io::Read>(
mut reader: R,
p: &Params,
held_source_bytes: usize,
) -> Result<(Vec<u8>, ImageFormat)> {
let mut header = [0u8; 12];
std::io::Read::read_exact(&mut reader, &mut header).context("source too short")?;
let format = ImageFormat::sniff(&header).context("unsupported image format")?;
let target = p.output.unwrap_or(format);
let p = &clamp_to_format(p, target);
#[cfg(not(feature = "avif"))]
anyhow::ensure!(
target != ImageFormat::Avif,
"AVIF support is not enabled in this build"
);
let reader = std::io::BufReader::new(std::io::Read::chain(&header[..], reader));
let _active = ActiveGuard::enter();
SCRATCH.with(|s| {
let s = &mut *s.borrow_mut();
s.held_source_bytes = held_source_bytes;
let out = match format {
ImageFormat::Jpeg => crate::panic_guard::catch_unwind_as_error("JPEG decode", || {
jpeg::process_jpeg(s, reader, target, p)
})??,
ImageFormat::Png => process_png(s, reader, target, p)?,
ImageFormat::Webp => process_webp(s, reader, target, p)?,
#[cfg(feature = "avif")]
ImageFormat::Avif => process_avif(s, reader, target, p)?,
#[cfg(not(feature = "avif"))]
ImageFormat::Avif => anyhow::bail!("AVIF support is not enabled in this build"),
};
Ok((out, target))
})
}
pub fn process_path(path: &std::path::Path, p: &Params) -> Result<(Vec<u8>, ImageFormat), Error> {
let inner = || -> Result<(Vec<u8>, ImageFormat)> {
let file = std::fs::File::open(path).context("open source")?;
process_reader(file, p, 0)
};
inner().map_err(|e| Error::classify(e, false))
}
#[cfg(feature = "server")]
fn max_source_bytes() -> u64 {
crate::config::config().max_source_bytes
}
#[cfg(feature = "server")]
static UPSTREAM_RETRIES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
#[cfg(feature = "server")]
pub fn upstream_retry_count() -> u64 {
UPSTREAM_RETRIES.load(std::sync::atomic::Ordering::Relaxed)
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ServerFault;
impl std::fmt::Display for ServerFault {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("internal image-processing error")
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SourceRejected;
impl std::fmt::Display for SourceRejected {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("source key rejected")
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct UpstreamFault;
impl std::fmt::Display for UpstreamFault {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("upstream image fetch failed")
}
}
#[cfg(feature = "server")]
fn fetch_client() -> &'static reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT.get_or_init(|| {
let cfg = crate::config::config();
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(cfg.upstream_timeout))
.connect_timeout(std::time::Duration::from_secs(cfg.upstream_connect_timeout))
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("construct the HTTP client")
})
}
#[cfg(feature = "server")]
fn map_send_err(e: reqwest::Error) -> anyhow::Error {
if e.is_timeout() {
anyhow::Error::new(std::io::Error::new(std::io::ErrorKind::TimedOut, e))
} else {
anyhow::Error::new(e)
.context("fetch source")
.context(UpstreamFault)
}
}
#[cfg(feature = "server")]
fn refuse_status(resp: reqwest::Response) -> Result<reqwest::Response> {
let status = resp.status();
if status.as_u16() == 404 {
return Err(anyhow::Error::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"source returned 404",
)));
}
if matches!(status.as_u16(), 400 | 414) {
return Err(
anyhow::anyhow!("origin rejected the request ({status})").context(SourceRejected)
);
}
if status.is_redirection() {
return Err(
anyhow::anyhow!("origin answered {status} (redirects are not followed)")
.context(UpstreamFault),
);
}
if !status.is_success() {
return Err(anyhow::anyhow!("origin answered {status}")
.context("fetch source")
.context(UpstreamFault));
}
Ok(resp)
}
#[cfg(feature = "server")]
async fn fetch_head_async(url: &str) -> Result<reqwest::Response> {
let resp = match fetch_client().get(url).send().await {
Ok(r) => r,
Err(e) if !e.is_timeout() && (e.is_connect() || e.is_request()) => {
UPSTREAM_RETRIES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
fetch_client().get(url).send().await.map_err(map_send_err)?
}
Err(e) => return Err(map_send_err(e)),
};
refuse_status(resp)
}
#[cfg(feature = "server")]
async fn buffer_body_async(mut resp: reqwest::Response) -> Result<Vec<u8>> {
let cap = max_source_bytes();
if let Some(len) = resp.content_length()
&& len > cap
{
return Err(anyhow::Error::new(std::io::Error::new(
std::io::ErrorKind::FileTooLarge,
format!("source is {len} bytes, over the {cap}-byte limit"),
)));
}
let map_body_err = |e: reqwest::Error| {
if e.is_timeout() {
anyhow::Error::new(std::io::Error::new(std::io::ErrorKind::TimedOut, e))
} else {
anyhow::Error::new(e)
.context("read source body")
.context(UpstreamFault)
}
};
let mut buf =
Vec::with_capacity(usize::try_from(resp.content_length().unwrap_or(0)).unwrap_or(0));
while let Some(chunk) = resp.chunk().await.map_err(map_body_err)? {
if (buf.len() as u64).saturating_add(chunk.len() as u64) > cap {
return Err(anyhow::Error::new(std::io::Error::new(
std::io::ErrorKind::FileTooLarge,
"source exceeds OXIMG_MAX_SOURCE_BYTES",
)));
}
buf.extend_from_slice(&chunk);
}
Ok(buf)
}
#[cfg(feature = "server")]
pub async fn fetch_url_async(url: &str) -> Result<Vec<u8>, Error> {
let inner = async { buffer_body_async(fetch_head_async(url).await?).await };
inner.await.map_err(|e| Error::classify(e, true))
}
#[cfg(feature = "server")]
fn block_on_fetch<F>(fut: F) -> F::Output
where
F: std::future::Future + Send + 'static,
F::Output: Send + 'static,
{
static HANDLE: OnceLock<tokio::runtime::Handle> = OnceLock::new();
let handle = HANDLE.get_or_init(|| {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::Builder::new()
.name("oximg-fetch".into())
.spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("build the fetch runtime");
let _ = tx.send(rt.handle().clone());
rt.block_on(std::future::pending::<()>());
})
.expect("spawn the fetch runtime thread");
rx.recv().expect("fetch runtime failed to start")
});
let (tx, rx) = std::sync::mpsc::channel();
handle.spawn(async move {
let _ = tx.send(fut.await);
});
rx.recv().expect("fetch task dropped without a result")
}
#[cfg(feature = "server")]
pub fn process_url(url: &str, p: &Params) -> Result<(Vec<u8>, ImageFormat), Error> {
let bytes = fetch_url(url)?;
process(&bytes, p)
}
#[cfg(feature = "server")]
pub fn process_gcs(bucket: &str, key: &str, p: &Params) -> Result<(Vec<u8>, ImageFormat), Error> {
let bytes = fetch_gcs(bucket, key)?;
process(&bytes, p)
}
#[cfg(feature = "server")]
pub async fn fetch_gcs_async(bucket: &str, key: &str) -> Result<Vec<u8>, Error> {
let inner = async { buffer_body_async(gcs::fetch(bucket, key).await?).await };
inner.await.map_err(|e| Error::classify(e, true))
}
#[cfg(feature = "server")]
pub fn gcs_startup() -> Result<(), String> {
gcs::startup()
}
#[cfg(feature = "server")]
pub fn fetch_url(url: &str) -> Result<Vec<u8>, Error> {
clear_fetch_time();
let t0 = std::time::Instant::now();
let owned = url.to_string();
let result = block_on_fetch(async move { fetch_url_async(&owned).await });
record_fetch_time(t0.elapsed().as_secs_f64());
result
}
#[cfg(feature = "server")]
pub fn fetch_gcs(bucket: &str, key: &str) -> Result<Vec<u8>, Error> {
clear_fetch_time();
let t0 = std::time::Instant::now();
let (bucket, key) = (bucket.to_string(), key.to_string());
let result = block_on_fetch(async move { fetch_gcs_async(&bucket, &key).await });
record_fetch_time(t0.elapsed().as_secs_f64());
result
}
thread_local! {
static SCRATCH: std::cell::RefCell<Scratch> = std::cell::RefCell::new(Scratch::default());
}
#[derive(Default)]
struct Scratch {
jpeg_progressive: bool,
held_source_bytes: usize,
chunk8: Vec<u8>,
src16: Vec<u16>,
dst16: Vec<u16>,
srcbuf: Vec<u8>,
out8: Vec<u8>,
resizer: Option<Resizer>,
#[cfg(feature = "avif")]
y16: Vec<u16>,
#[cfg(feature = "avif")]
cb16: Vec<u16>,
#[cfg(feature = "avif")]
cr16: Vec<u16>,
}
fn scratch_u16(buf: &mut Vec<u16>, len: usize) -> &mut [u16] {
if buf.len() < len {
buf.resize(len, 0);
}
&mut buf[..len]
}
fn scratch_u8(buf: &mut Vec<u8>, len: usize) -> &mut [u8] {
if buf.len() < len {
buf.resize(len, 0);
}
&mut buf[..len]
}
fn u16_as_bytes(buf: &[u16]) -> &[u8] {
unsafe { std::slice::from_raw_parts(buf.as_ptr().cast(), buf.len() * 2) }
}
fn u16_as_bytes_mut(buf: &mut [u16]) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(buf.as_mut_ptr().cast(), buf.len() * 2) }
}
#[allow(clippy::too_many_arguments)]
fn resize_bands(
src_bytes: &[u8],
dec_w: usize,
dec_h: usize,
dst_bytes: &mut [u8],
dst_w: usize,
dst_h: usize,
px: PixelType,
threads: usize,
fallback: &mut Option<Resizer>,
) -> Result<()> {
let opts = ResizeOptions::new()
.resize_alg(ResizeAlg::Convolution(FilterType::Lanczos3))
.use_alpha(false);
let src_view =
fast_image_resize::images::ImageRef::new(dec_w as u32, dec_h as u32, src_bytes, px)?;
if threads <= 1 || dst_h < 2 * threads {
#[cfg(target_arch = "x86_64")]
if !crate::config::config().fir_backend {
if px == PixelType::U16x3 {
return resize_u16x3_picscale(src_bytes, dec_w, dec_h, dst_bytes, dst_w, dst_h);
}
if px == PixelType::U16x4 && crate::resize_avx2::Avx2::available() {
return crate::resize_avx2::resize_u16_avx2(
src_bytes, dec_w, dec_h, dst_bytes, dst_w, dst_h, 4,
);
}
}
#[cfg(target_arch = "aarch64")]
if matches!(px, PixelType::U16x3 | PixelType::U16x4)
&& !crate::config::config().fir_backend
&& std::arch::is_aarch64_feature_detected!("neon")
{
return crate::resize_neon::resize_u16_neon(
src_bytes,
dec_w,
dec_h,
dst_bytes,
dst_w,
dst_h,
px.size() / 2,
);
}
let mut dst_view = Image::from_slice_u8(dst_w as u32, dst_h as u32, dst_bytes, px)?;
let resizer = fallback.get_or_insert_with(Resizer::new);
resizer.resize(&src_view, &mut dst_view, &opts)?;
return Ok(());
}
let row_bytes = dst_w * px.size();
let rows_per = dst_h.div_ceil(threads);
let sy = dec_h as f64 / dst_h as f64;
std::thread::scope(|sc| -> Result<()> {
let mut handles = Vec::new();
for (i, band) in dst_bytes.chunks_mut(rows_per * row_bytes).enumerate() {
let band_h = band.len() / row_bytes;
let crop_top = (i * rows_per) as f64 * sy;
let crop_h = band_h as f64 * sy;
let src_view = &src_view;
handles.push(sc.spawn(move || -> Result<()> {
let mut dst_view = Image::from_slice_u8(dst_w as u32, band_h as u32, band, px)?;
Resizer::new().resize(
src_view,
&mut dst_view,
&opts.crop(0.0, crop_top, dec_w as f64, crop_h),
)?;
Ok(())
}));
}
for h in handles {
h.join().expect("resize band panicked")?;
}
Ok(())
})
}
#[cfg(target_arch = "x86_64")]
fn resize_u16x3_picscale(
src_bytes: &[u8],
src_w: usize,
src_h: usize,
dst_bytes: &mut [u8],
dst_w: usize,
dst_h: usize,
) -> Result<()> {
use pic_scale::{ImageStore, ImageStoreMut, ResamplingFunction, Scaler, ThreadingPolicy};
let (pre, src16, post) = unsafe { src_bytes.align_to::<u16>() };
anyhow::ensure!(pre.is_empty() && post.is_empty(), "unaligned u16 src");
let (pre, dst16, post) = unsafe { dst_bytes.align_to_mut::<u16>() };
anyhow::ensure!(pre.is_empty() && post.is_empty(), "unaligned u16 dst");
let src_store = ImageStore::<u16, 3>::from_slice(src16, src_w, src_h)
.map_err(|e| anyhow::anyhow!("pic-scale src: {e:?}"))?;
let mut dst_store = ImageStoreMut::<u16, 3>::from_slice(dst16, dst_w, dst_h)
.map_err(|e| anyhow::anyhow!("pic-scale dst: {e:?}"))?;
dst_store.bit_depth = 16;
let scaler =
Scaler::new(ResamplingFunction::Lanczos3).set_threading_policy(ThreadingPolicy::Single);
let plan = scaler
.plan_rgb_resampling16(src_store.size(), dst_store.size(), 16)
.map_err(|e| anyhow::anyhow!("pic-scale plan: {e:?}"))?;
plan.resample(&src_store, &mut dst_store)
.map_err(|e| anyhow::anyhow!("pic-scale resample: {e:?}"))?;
Ok(())
}
static ACTIVE_PIPELINES: AtomicUsize = AtomicUsize::new(0);
struct ActiveGuard;
impl ActiveGuard {
fn enter() -> ActiveGuard {
ACTIVE_PIPELINES.fetch_add(1, Ordering::Relaxed);
ActiveGuard
}
}
impl Drop for ActiveGuard {
fn drop(&mut self) {
ACTIVE_PIPELINES.fetch_sub(1, Ordering::Relaxed);
}
}
fn logical_cpus() -> usize {
static N: OnceLock<usize> = OnceLock::new();
*N.get_or_init(|| {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
})
}
fn overlap_mode() -> u8 {
static M: OnceLock<u8> = OnceLock::new();
*M.get_or_init(|| match std::env::var("OXIMG_OVERLAP").as_deref() {
Ok("0") => 0,
Ok("1") => 1,
_ => 2,
})
}
fn overlap_gate() -> bool {
match overlap_mode() {
0 => false,
1 => true,
_ => ACTIVE_PIPELINES.load(Ordering::Relaxed) * 2 <= logical_cpus(),
}
}
fn linear_light(p: &Params) -> bool {
p.linear_light
.unwrap_or_else(|| crate::config::config().linear_light)
}
fn auto_rotate(p: &Params) -> bool {
p.auto_rotate
.unwrap_or_else(|| crate::config::config().auto_rotate)
}
fn icc_passthrough(p: &Params) -> bool {
p.icc
.unwrap_or_else(|| crate::config::config().icc_passthrough)
}
fn target_supports_icc(target: ImageFormat) -> bool {
match target {
ImageFormat::Jpeg | ImageFormat::Png | ImageFormat::Webp => true,
#[cfg(feature = "avif")]
ImageFormat::Avif => true,
#[cfg(not(feature = "avif"))]
ImageFormat::Avif => false,
}
}
pub(crate) const ICC_CAP: usize = 4 * 1024 * 1024;
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct DecodeCost {
pub staged_bytes: u64,
pub resize_input_bytes: u64,
pub output_bytes: u64,
pub whole_source_bytes: u64,
pub compressed_bytes: u64,
}
impl DecodeCost {
pub fn bytes(&self) -> u64 {
self.staged_bytes
.saturating_add(self.resize_input_bytes)
.saturating_add(self.output_bytes)
.saturating_add(self.whole_source_bytes)
.saturating_add(self.compressed_bytes)
}
pub fn full_frame(w: usize, h: usize, channels: u64, p: &Params) -> Self {
let px = (w as u64).saturating_mul(h as u64);
let staged = px.saturating_mul(channels);
DecodeCost {
staged_bytes: staged,
resize_input_bytes: if linear_light(p) { staged * 2 } else { 0 },
..Default::default()
}
}
pub fn streaming() -> Self {
DecodeCost::default()
}
pub fn with_output(mut self, out_w: usize, out_h: usize, channels: u64) -> Self {
let px = (out_w as u64).saturating_mul(out_h as u64);
self.output_bytes = px.saturating_mul(channels).saturating_mul(3);
self
}
pub fn with_progressive_coefficients(mut self, src_w: usize, src_h: usize, comps: u64) -> Self {
self.whole_source_bytes = (src_w as u64)
.saturating_mul(src_h as u64)
.saturating_mul(comps)
.saturating_mul(2);
self
}
pub fn with_compressed(mut self, bytes: usize) -> Self {
self.compressed_bytes = bytes as u64;
self
}
}
impl DecodeCost {
fn report(&self, what: &str) -> String {
format!(
"{what} decode needs about {} bytes (staged {}, resize input {}, \
output {}, whole-source {}, compressed {})",
self.bytes(),
self.staged_bytes,
self.resize_input_bytes,
self.output_bytes,
self.whole_source_bytes,
self.compressed_bytes,
)
}
}
thread_local! {
static LAST_COST: std::cell::Cell<Option<(DecodeCost, &'static str)>> =
const { std::cell::Cell::new(None) };
}
pub fn decode_report_above_threshold() -> Option<String> {
let threshold = crate::config::config().log_decoded_bytes_above?;
let (cost, what) = LAST_COST.get()?;
(cost.bytes() > threshold).then(|| cost.report(what))
}
pub(crate) fn check_decoded_bytes(cost: DecodeCost, what: &'static str) -> Result<()> {
let bytes = cost.bytes();
record_decoded_bytes(bytes);
LAST_COST.set(Some((cost, what)));
let Some(cap) = crate::config::config().max_decoded_bytes else {
return Ok(());
};
if bytes > cap {
anyhow::bail!(std::io::Error::new(
std::io::ErrorKind::FileTooLarge,
format!(
"{}, over the OXIMG_MAX_DECODED_BYTES limit ({cap})",
cost.report(what)
),
));
}
Ok(())
}
#[cfg(feature = "server")]
thread_local! {
static FETCH_SECS: std::cell::Cell<Option<f64>> = const { std::cell::Cell::new(None) };
}
#[cfg(feature = "server")]
pub(crate) fn clear_fetch_time() {
FETCH_SECS.set(None);
}
#[cfg(feature = "server")]
pub(crate) fn record_fetch_time(seconds: f64) {
FETCH_SECS.set(Some(FETCH_SECS.get().unwrap_or(0.0) + seconds));
}
#[cfg(feature = "server")]
pub fn last_fetch_seconds() -> Option<f64> {
FETCH_SECS.get()
}
pub(crate) fn decoded_bytes_cap_set() -> bool {
crate::config::config().max_decoded_bytes.is_some()
}
pub const DECODED_BYTES_BOUNDS: [u64; 13] = [
1 << 20,
1 << 21,
1 << 22,
1 << 23,
1 << 24,
1 << 25,
1 << 26,
1 << 27,
1 << 28,
1 << 29,
1 << 30,
1 << 31,
1 << 32,
];
static DECODED_BYTES_BUCKETS: [std::sync::atomic::AtomicU64; DECODED_BYTES_BOUNDS.len() + 1] =
[const { std::sync::atomic::AtomicU64::new(0) }; DECODED_BYTES_BOUNDS.len() + 1];
static DECODED_BYTES_SUM: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
fn record_decoded_bytes(bytes: u64) {
let slot = DECODED_BYTES_BOUNDS
.iter()
.position(|b| bytes <= *b)
.unwrap_or(DECODED_BYTES_BOUNDS.len());
DECODED_BYTES_BUCKETS[slot].fetch_add(1, Ordering::Relaxed);
DECODED_BYTES_SUM.fetch_add(bytes, Ordering::Relaxed);
}
pub fn decoded_bytes_histogram() -> ([u64; DECODED_BYTES_BOUNDS.len() + 1], u64) {
let mut counts = [0u64; DECODED_BYTES_BOUNDS.len() + 1];
for (dst, src) in counts.iter_mut().zip(DECODED_BYTES_BUCKETS.iter()) {
*dst = src.load(Ordering::Relaxed);
}
(counts, DECODED_BYTES_SUM.load(Ordering::Relaxed))
}
pub(crate) fn check_src_pixels(w: usize, h: usize) -> Result<()> {
let cap = crate::config::config().max_src_pixels;
let px = (w as u64).saturating_mul(h as u64);
if px > cap {
anyhow::bail!(std::io::Error::new(
std::io::ErrorKind::FileTooLarge,
format!("source is {w}x{h} ({px} pixels), over the OXIMG_MAX_SRC_PIXELS limit ({cap})"),
));
}
Ok(())
}
mod cmyk;
mod encode;
mod error;
mod formats;
mod fuse;
#[cfg(feature = "server")]
mod gcs;
mod jpeg;
#[cfg(test)]
mod tests;
use cmyk::*;
pub use encode::encode;
use encode::*;
pub use error::{Error, ErrorKind};
use formats::*;
use fuse::*;
pub use jpeg::decode_and_resize;
#[cfg_attr(not(test), allow(unused_imports))] use jpeg::*;