/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin */ use anyhow::{Context, Result, anyhow}; use image::imageops::FilterType; use jellycache::{Cache, HashKey}; use log::{debug, info}; use rgb::FromSlice; use std::io::Cursor; pub fn transcode( cache: &Cache, key: &str, quality: u32, speed: u8, width: usize, ) -> Result> { cache.cache( &format!( "transcode/image/{}-W{width}-Q{quality}-S{speed}", HashKey(key) ), move || { let input = cache .read(key)? .ok_or(anyhow!("transcode cache key missing"))?; info!("encoding image (speed={speed}, quality={quality}, width={width})"); // TODO: use better image library that supports AVIF // TODO: magic experimentally found and probably not working in all cases but fine as long as our avif enc uses that let is_avif = matches!( input[0..input.len().min(12)], [ 0x00, 0x00, 0x00, _, b'f', b't', b'y', b'p', b'a', b'v', b'i', b'f', ] ); let original = if is_avif { libavif_image::read(&input).unwrap().to_rgba8() } else { let reader = image::ImageReader::new(Cursor::new(input)); let reader = reader.with_guessed_format().context("guessing format")?; debug!("guessed format (or fallback): {:?}", reader.format()); reader.decode().context("decoding image")?.to_rgba8() }; let image = image::imageops::resize( &original, width as u32, width as u32 * original.height() / original.width(), FilterType::Lanczos3, ); let pixels = image.to_vec(); let encoded = ravif::Encoder::new() .with_speed(speed.clamp(1, 10)) .with_quality(quality.clamp(1, 100) as f32) .encode_rgba(imgref::Img::new( pixels.as_rgba(), image.width() as usize, image.height() as usize, )) .context("avif encoding")?; debug!("transcode finished"); Ok(encoded.avif_file) }, ) }