use std::cmp;
use std::io;
use std::time::Instant;
use crate::utils::usize_in_mib;
use image::codecs::{
gif::GifDecoder, jpeg::JpegDecoder, png::PngDecoder, tiff::TiffDecoder, webp::WebPDecoder,
};
use image::{ColorType, GenericImageView, ImageDecoder, ImageFormat, ImageResult};
use lz4_flex::frame::{BlockSize, FrameDecoder, FrameEncoder, FrameInfo};
pub fn lz4_compress<R: io::Read>(reader: &mut R) -> anyhow::Result<Vec<u8>> {
let mut frame_info = FrameInfo::new();
frame_info.block_size = BlockSize::Max256KB;
let mut lz4_enc = FrameEncoder::with_frame_info(frame_info, Vec::with_capacity(8 * 1_024));
io::copy(reader, &mut lz4_enc)?;
let mut lz4_blob = lz4_enc.finish()?;
lz4_blob.shrink_to_fit();
Ok(lz4_blob)
}
pub fn lz4_decompress(blob: &[u8], size: usize) -> anyhow::Result<Vec<u8>> {
let mut lz4_dec = FrameDecoder::new(io::Cursor::new(blob));
let mut decompressed = Vec::with_capacity(size);
io::copy(&mut lz4_dec, &mut decompressed)?;
decompressed.truncate(size);
Ok(decompressed)
}
pub type ImageParts = (Vec<u8>, (u32, u32));
pub fn decode_and_compress(contents: &[u8]) -> anyhow::Result<ImageParts> {
let maybe_streamed = match image::guess_format(contents)? {
ImageFormat::Png => stream_decode_and_compress(contents, PngDecoder::new)?,
ImageFormat::Jpeg => stream_decode_and_compress(contents, JpegDecoder::new)?,
ImageFormat::Gif => stream_decode_and_compress(contents, GifDecoder::new)?,
ImageFormat::Tiff => stream_decode_and_compress(contents, TiffDecoder::new)?,
ImageFormat::WebP => stream_decode_and_compress(contents, WebPDecoder::new)?,
_ => None,
};
match maybe_streamed {
Some(streamed) => Ok(streamed),
None => fallback_decode_and_compress(contents),
}
}
fn stream_decode_and_compress<'img, Dec>(
contents: &'img [u8],
decoder_constructor: fn(io::Cursor<&'img [u8]>) -> ImageResult<Dec>,
) -> anyhow::Result<Option<ImageParts>>
where
Dec: ImageDecoder<'img>,
{
let dec = decoder_constructor(io::Cursor::new(contents))?;
let total_size = dec.total_bytes();
let dimensions = dec.dimensions();
let start = Instant::now();
let Some(mut adapter) = Rgba8Adapter::new(dec) else {
return Ok(None);
};
let maybe_image_parts = lz4_compress(&mut adapter).ok().map(|lz4_blob| {
tracing::debug!(
"Streaming image decode & compression:\n\
- Full {:.2} MiB\n\
- Compressed {:.2} MiB\n\
- Time {:.2?}",
usize_in_mib(total_size as usize),
usize_in_mib(lz4_blob.len()),
start.elapsed(),
);
(lz4_blob, dimensions)
});
Ok(maybe_image_parts)
}
enum Rgba8Adapter<'img> {
Rgba8(Box<dyn io::Read + 'img>),
Rgb8 {
source: Box<dyn io::Read + 'img>,
scratch: Vec<u8>,
},
}
impl<'img> Rgba8Adapter<'img> {
fn new<Dec: ImageDecoder<'img>>(dec: Dec) -> Option<Self> {
let adapter = match dec.color_type() {
ColorType::Rgba8 => Self::Rgba8(Box::new(dec.into_reader().ok()?)),
ColorType::Rgb8 => Self::Rgb8 {
source: Box::new(dec.into_reader().ok()?),
scratch: Vec::new(),
},
_ => return None,
};
Some(adapter)
}
}
impl<'img> io::Read for Rgba8Adapter<'img> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Rgba8(inner) => inner.read(buf),
Self::Rgb8 { source, scratch } => {
if scratch.len() > buf.len() {
buf.copy_from_slice(&scratch[..buf.len()]);
scratch.copy_within(buf.len().., 0);
scratch.truncate(scratch.len() - buf.len());
return Ok(buf.len());
}
let (left, right) = buf.split_at_mut(scratch.len());
left.copy_from_slice(scratch);
let num_pixels = right.len() / 3 + 1;
scratch.resize(num_pixels * 4, 0);
let n = source.read(&mut scratch[..num_pixels * 3])?;
if n == 0 {
scratch.clear();
return Ok(left.len());
}
let bytes_transformed = n * 4 / 3;
let mut rgb_end = n - 1;
let mut rgba_end = bytes_transformed - 1;
loop {
scratch[rgba_end] = u8::MAX;
scratch[rgba_end - 1] = scratch[rgb_end];
scratch[rgba_end - 2] = scratch[rgb_end - 1];
scratch[rgba_end - 3] = scratch[rgb_end - 2];
rgba_end = match rgba_end.checked_sub(4) {
Some(n) => n,
None => break,
};
rgb_end -= 3;
}
right.copy_from_slice(&scratch[..right.len()]);
scratch.copy_within(right.len().., 0);
scratch.truncate(scratch.len() - right.len());
Ok(left.len() + cmp::min(right.len(), bytes_transformed))
}
}
}
}
fn fallback_decode_and_compress(contents: &[u8]) -> anyhow::Result<(Vec<u8>, (u32, u32))> {
let image = image::load_from_memory(contents)?;
let dimensions = image.dimensions();
let image_data = image.into_rgba8().into_raw();
tracing::debug!(
"Decoded full image in memory {:.3} MiB",
usize_in_mib(image_data.len()),
);
lz4_compress(&mut io::Cursor::new(image_data)).map(|lz4_blob| (lz4_blob, dimensions))
}