use std::io::Cursor;
use std::num::NonZeroU64;
use std::path::Path;
use crabshell_ui::Fit;
use resvg::usvg;
use tiny_skia::{FilterQuality, Pixmap, PixmapPaint, PixmapRef, Transform};
use zune_jpeg::JpegDecoder;
use zune_jpeg::zune_core::bytestream::ZCursor;
use zune_jpeg::zune_core::colorspace::ColorSpace;
use zune_jpeg::zune_core::options::DecoderOptions;
use crate::icons::{decode_png, halve, premultiplied, read_capped, svg_tree};
const MAX_BYTES: u64 = 32 * 1024 * 1024;
const MAX_SVG_BYTES: u64 = 4 * 1024 * 1024;
const MAX_SIDE: u32 = 8192;
const MAX_PIXELS: u64 = 24_000_000;
pub(crate) fn rasterize(path: &Path, width: u32, height: u32, fit: Fit) -> Result<Pixmap, String> {
if width == 0 || height == 0 {
return Err(String::from("an image with no pixels"));
}
match decode(path)? {
Picture::Svg(tree) => svg(&tree, width, height, fit),
Picture::Pixels(image) => scale(image.as_ref(), width, height, fit),
}
}
pub(crate) enum Picture {
Pixels(Pixmap),
Svg(Box<usvg::Tree>),
}
pub(crate) fn decode(path: &Path) -> Result<Picture, String> {
let extension = path
.extension()
.and_then(|extension| extension.to_str())
.map(str::to_ascii_lowercase);
let image = match extension.as_deref() {
Some("svg") => {
let tree = svg_tree(&read_capped(path, MAX_SVG_BYTES)?)?;
return Ok(Picture::Svg(Box::new(tree)));
}
Some("png") => decode_png(&read_capped(path, MAX_BYTES)?, MAX_SIDE, MAX_PIXELS)?,
Some("jpg" | "jpeg") => jpeg(&read_capped(path, MAX_BYTES)?)?,
Some("gif") => gif(&read_capped(path, MAX_BYTES)?)?,
Some("webp") => webp(&read_capped(path, MAX_BYTES)?)?,
_ => {
return Err(String::from(
"images must be .png, .jpg, .gif, .webp or .svg files",
));
}
};
Ok(Picture::Pixels(image))
}
pub(crate) fn check(width: u32, height: u32) -> Result<(), String> {
if width == 0 || height == 0 {
return Err(String::from("an image with no pixels"));
}
if width > MAX_SIDE || height > MAX_SIDE || u64::from(width) * u64::from(height) > MAX_PIXELS {
return Err(format!(
"a {width}x{height} image is larger than {MAX_SIDE} pixels a side or {MAX_PIXELS} pixels"
));
}
Ok(())
}
fn jpeg(data: &[u8]) -> Result<Pixmap, String> {
let options = DecoderOptions::default()
.set_max_width(MAX_SIDE as usize)
.set_max_height(MAX_SIDE as usize)
.jpeg_set_out_colorspace(ColorSpace::RGBA);
let mut decoder = JpegDecoder::new_with_options(ZCursor::new(data), options);
decoder
.decode_headers()
.map_err(|error| error.to_string())?;
let info = decoder.info().ok_or("a JPEG without a size")?;
let (width, height) = (u32::from(info.width), u32::from(info.height));
check(width, height)?;
let rgba = decoder.decode().map_err(|error| error.to_string())?;
premultiplied(rgba, width, height)
}
pub(crate) fn gif(data: &[u8]) -> Result<Pixmap, String> {
let mut options = gif::DecodeOptions::new();
options.set_color_output(gif::ColorOutput::RGBA);
if let Some(limit) = NonZeroU64::new(MAX_PIXELS * 4) {
options.set_memory_limit(gif::MemoryLimit::Bytes(limit));
}
let mut decoder = options
.read_info(Cursor::new(data))
.map_err(|error| error.to_string())?;
let (width, height) = (u32::from(decoder.width()), u32::from(decoder.height()));
check(width, height)?;
let frame = decoder
.read_next_frame()
.map_err(|error| error.to_string())?
.ok_or("a GIF with no frames")?;
if frame.width == 0 || frame.height == 0 {
return Err(String::from("a GIF frame with no pixels"));
}
let mut rgba = vec![0; width as usize * height as usize * 4];
let (left, top) = (u32::from(frame.left), u32::from(frame.top));
let frame_width = usize::from(frame.width);
for (row, line) in frame.buffer.chunks_exact(frame_width * 4).enumerate() {
let y = top + row as u32;
if y >= height || left >= width {
continue;
}
let columns = frame_width.min((width - left) as usize);
let start = (y as usize * width as usize + left as usize) * 4;
rgba[start..start + columns * 4].copy_from_slice(&line[..columns * 4]);
}
premultiplied(rgba, width, height)
}
fn webp(data: &[u8]) -> Result<Pixmap, String> {
let mut decoder =
image_webp::WebPDecoder::new(Cursor::new(data)).map_err(|error| error.to_string())?;
let (width, height) = decoder.dimensions();
check(width, height)?;
decoder.set_memory_limit((MAX_PIXELS * 4) as usize);
let size = decoder
.output_buffer_size()
.ok_or("the WebP is too large to decode")?;
let mut buffer = vec![0; size];
decoder
.read_image(&mut buffer)
.map_err(|error| error.to_string())?;
let rgba = if decoder.has_alpha() {
buffer
} else {
buffer
.as_chunks::<3>()
.0
.iter()
.flat_map(|&[r, g, b]| [r, g, b, 255])
.collect()
};
premultiplied(rgba, width, height)
}
pub(crate) fn svg(tree: &usvg::Tree, width: u32, height: u32, fit: Fit) -> Result<Pixmap, String> {
let size = tree.size();
let mut pixmap = Pixmap::new(width, height).ok_or("an image with no pixels")?;
let transform = placement((size.width(), size.height()), (width, height), fit);
resvg::render(tree, transform, &mut pixmap.as_mut());
Ok(pixmap)
}
pub(crate) fn scale(
image: PixmapRef<'_>,
width: u32,
height: u32,
fit: Fit,
) -> Result<Pixmap, String> {
let mut halved;
let mut image = image;
loop {
let (x, y) = factors(
(image.width() as f32, image.height() as f32),
(width, height),
fit,
);
if x >= 0.5 || y >= 0.5 {
break;
}
halved = halve(image)?;
image = halved.as_ref();
}
let mut pixmap = Pixmap::new(width, height).ok_or("an image with no pixels")?;
let transform = placement(
(image.width() as f32, image.height() as f32),
(width, height),
fit,
);
let paint = PixmapPaint {
quality: FilterQuality::Bicubic,
..PixmapPaint::default()
};
pixmap.draw_pixmap(0, 0, image, &paint, transform, None);
Ok(pixmap)
}
pub(crate) fn factors(source: (f32, f32), target: (u32, u32), fit: Fit) -> (f32, f32) {
let across = target.0 as f32 / source.0;
let down = target.1 as f32 / source.1;
match fit {
Fit::Fill => (across, down),
Fit::Contain => (across.min(down), across.min(down)),
Fit::Cover => (across.max(down), across.max(down)),
}
}
fn placement(source: (f32, f32), target: (u32, u32), fit: Fit) -> Transform {
let (x, y) = factors(source, target, fit);
Transform::from_scale(x, y).post_translate(
(target.0 as f32 - source.0 * x) / 2.0,
(target.1 as f32 - source.1 * y) / 2.0,
)
}