use std::io::Cursor;
use image::codecs::bmp::BmpDecoder;
use image::codecs::gif::GifDecoder;
use image::codecs::jpeg::JpegDecoder;
use image::codecs::png::PngDecoder;
use image::codecs::webp::WebPDecoder;
use image::metadata::{LoopCount, Orientation};
use image::{AnimationDecoder, DynamicImage, ImageDecoder, ImageFormat};
use tiny_skia::{
ColorU8, FillRule, FilterQuality, IntSize, Mask, Paint, Pattern, Pixmap, PixmapPaint,
PixmapRef, SpreadMode, Transform,
};
pub(super) const DEFAULT_MAX_PIXELS: u64 = 64_000_000;
pub(super) const DEFAULT_MAX_BYTES: u64 = 512 * 1024 * 1024;
const MIN_FRAME_DELAY: f64 = 0.010;
const DEFAULT_FRAME_DELAY: f64 = 0.100;
#[derive(Clone, Copy, Debug)]
pub(super) struct Limits {
pub max_pixels: u64,
pub max_bytes: u64,
}
impl Default for Limits {
fn default() -> Self {
Limits {
max_pixels: DEFAULT_MAX_PIXELS,
max_bytes: DEFAULT_MAX_BYTES,
}
}
}
pub(super) struct Decoded {
pub frames: Vec<Pixmap>,
pub delays: Vec<f64>,
pub loop_count: u32,
}
impl std::fmt::Debug for Decoded {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Decoded")
.field("frames", &self.frames.len())
.field("delays", &self.delays)
.field("loop_count", &self.loop_count)
.finish()
}
}
pub(super) fn decode(bytes: &[u8], limits: Limits) -> Result<Decoded, String> {
let format = image::guess_format(bytes).map_err(|_| {
"unrecognized image format (expected PNG, JPEG, WebP, GIF or BMP)".to_string()
})?;
let cursor = Cursor::new(bytes);
match format {
ImageFormat::Png => {
let mut decoder = PngDecoder::new(cursor).map_err(describe(limits))?;
if decoder.is_apng().map_err(describe(limits))? {
let (width, height) = decoder.dimensions();
check_size(width, height, limits)?;
decoder
.set_limits(image_limits(limits))
.map_err(describe(limits))?;
decode_animation(
width,
height,
decoder.apng().map_err(describe(limits))?,
limits,
)
} else {
decode_still(decoder, limits)
}
}
ImageFormat::Gif => {
let mut decoder = GifDecoder::new(cursor).map_err(describe(limits))?;
let (width, height) = decoder.dimensions();
check_size(width, height, limits)?;
decoder
.set_limits(image_limits(limits))
.map_err(describe(limits))?;
decode_animation(width, height, decoder, limits)
}
ImageFormat::WebP => {
let mut decoder = WebPDecoder::new(cursor).map_err(describe(limits))?;
if decoder.has_animation() {
let (width, height) = decoder.dimensions();
check_size(width, height, limits)?;
decoder
.set_limits(image_limits(limits))
.map_err(describe(limits))?;
decode_animation(width, height, decoder, limits)
} else {
decode_still(decoder, limits)
}
}
ImageFormat::Jpeg => {
decode_still(JpegDecoder::new(cursor).map_err(describe(limits))?, limits)
}
ImageFormat::Bmp => {
decode_still(BmpDecoder::new(cursor).map_err(describe(limits))?, limits)
}
other => Err(format!(
"unsupported image format {} (expected PNG, JPEG, WebP, GIF or BMP)",
other
.extensions_str()
.first()
.unwrap_or(&"?")
.to_uppercase()
)),
}
}
fn describe(limits: Limits) -> impl Fn(image::ImageError) -> String {
move |err| match err {
image::ImageError::Limits(_) => too_many_bytes(limits),
other => other.to_string(),
}
}
fn image_limits(limits: Limits) -> image::Limits {
let mut result = image::Limits::default();
result.max_alloc = Some(limits.max_bytes);
result
}
pub(super) fn check_size(width: u32, height: u32, limits: Limits) -> Result<u64, String> {
if width == 0 || height == 0 {
return Err("image is empty".to_string());
}
let pixels = width as u64 * height as u64;
if pixels > limits.max_pixels {
return Err(format!(
"image is {}x{} ({} pixels), over the limit of {} pixels (maxPixels)",
width, height, pixels, limits.max_pixels
));
}
let bytes = pixels * 4;
if bytes > limits.max_bytes {
return Err(too_many_bytes(limits));
}
Ok(bytes)
}
fn too_many_bytes(limits: Limits) -> String {
format!(
"decoded image needs more than {} bytes (maxBytes)",
limits.max_bytes
)
}
fn decode_still(mut decoder: impl ImageDecoder, limits: Limits) -> Result<Decoded, String> {
let (width, height) = decoder.dimensions();
check_size(width, height, limits)?;
decoder
.set_limits(image_limits(limits))
.map_err(describe(limits))?;
let orientation = decoder.orientation().unwrap_or(Orientation::NoTransforms);
let mut image = DynamicImage::from_decoder(decoder).map_err(describe(limits))?;
image.apply_orientation(orientation);
let rgba = image.into_rgba8();
let (width, height) = rgba.dimensions();
let frame = rgba_to_pixmap(rgba.into_raw(), width, height)
.ok_or_else(|| "image is too large to decode".to_string())?;
Ok(Decoded {
frames: vec![frame],
delays: vec![0.0],
loop_count: 0,
})
}
fn decode_animation<'a>(
width: u32,
height: u32,
decoder: impl AnimationDecoder<'a>,
limits: Limits,
) -> Result<Decoded, String> {
let frame_bytes = check_size(width, height, limits)?;
let loop_count = match decoder.loop_count() {
LoopCount::Infinite => 0,
LoopCount::Finite(n) => n.get(),
};
let mut frames = Vec::new();
let mut delays = Vec::new();
let mut total = 0u64;
for frame in decoder.into_frames() {
let frame = frame.map_err(describe(limits))?;
total += frame_bytes;
if total > limits.max_bytes {
return Err(too_many_bytes(limits));
}
let (numerator, denominator) = frame.delay().numer_denom_ms();
delays.push(frame_delay(numerator as f64 / denominator.max(1) as f64));
let buffer = frame.into_buffer();
if buffer.dimensions() != (width, height) {
return Err("animation frame does not match the canvas size".to_string());
}
frames.push(
rgba_to_pixmap(buffer.into_raw(), width, height)
.ok_or_else(|| "image is too large to decode".to_string())?,
);
}
if frames.is_empty() {
return Err("image has no frames".to_string());
}
Ok(Decoded {
frames,
delays,
loop_count,
})
}
pub(super) fn frame_delay(ms: f64) -> f64 {
let seconds = ms / 1000.0;
if !seconds.is_finite() || seconds <= MIN_FRAME_DELAY {
DEFAULT_FRAME_DELAY
} else {
seconds
}
}
pub(super) fn rgba_to_pixmap(mut rgba: Vec<u8>, width: u32, height: u32) -> Option<Pixmap> {
if rgba.len() as u64 != width as u64 * height as u64 * 4 {
return None;
}
let (pixels, _) = rgba.as_chunks_mut::<4>();
for pixel in pixels {
if pixel[3] != 255 {
let c = ColorU8::from_rgba(pixel[0], pixel[1], pixel[2], pixel[3]).premultiply();
pixel.copy_from_slice(&[c.red(), c.green(), c.blue(), c.alpha()]);
}
}
Pixmap::from_vec(rgba, IntSize::from_wh(width, height)?)
}
pub(super) fn is_opaque(pixmap: &Pixmap) -> bool {
pixmap.pixels().iter().all(|pixel| pixel.alpha() == 255)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) enum Fit {
Fill,
Contain,
Cover,
}
impl Fit {
pub fn parse(name: &str) -> Result<Fit, String> {
match name {
"fill" => Ok(Fit::Fill),
"contain" => Ok(Fit::Contain),
"cover" => Ok(Fit::Cover),
_ => Err(format!(
"fit must be \"fill\", \"contain\" or \"cover\", got \"{}\"",
name
)),
}
}
}
pub(super) type RectF = (f32, f32, f32, f32);
pub(super) fn place(image_w: f32, image_h: f32, target: RectF, fit: Fit) -> (RectF, RectF) {
let (x, y, box_w, box_h) = target;
let full = (0.0, 0.0, image_w, image_h);
match fit {
Fit::Fill => (full, target),
Fit::Contain => {
let s = (box_w / image_w).min(box_h / image_h);
let (w, h) = (image_w * s, image_h * s);
(full, (x + (box_w - w) / 2.0, y + (box_h - h) / 2.0, w, h))
}
Fit::Cover => {
let s = (box_w / image_w).max(box_h / image_h);
let (w, h) = (box_w / s, box_h / s);
(((image_w - w) / 2.0, (image_h - h) / 2.0, w, h), target)
}
}
}
pub(super) fn snap(dest: RectF) -> (i32, i32, u32, u32) {
let clamp = |v: f32| v.round().clamp(-(1 << 24) as f32, (1 << 24) as f32) as i64;
let (left, top) = (clamp(dest.0), clamp(dest.1));
let (right, bottom) = (clamp(dest.0 + dest.2), clamp(dest.1 + dest.3));
(
left as i32,
top as i32,
(right - left).max(0) as u32,
(bottom - top).max(0) as u32,
)
}
type Taps = (usize, Vec<f32>);
fn filter_taps(src_len: u32, start: f32, len: f32, out_len: u32) -> Vec<Taps> {
let scale = len / out_len as f32;
let support = scale.max(1.0);
let lo = (start.floor().max(0.0) as usize).min(src_len.max(1) as usize - 1);
let hi = ((start + len).ceil() as usize)
.min(src_len as usize)
.max(lo + 1);
(0..out_len)
.map(|i| {
let center = start + (i as f32 + 0.5) * scale;
let mut first = ((center - support).floor().max(lo as f32) as usize).min(hi - 1);
let last = ((center + support).ceil() as usize).clamp(first + 1, hi);
let mut weights: Vec<f32> = (first..last)
.map(|j| (1.0 - ((j as f32 + 0.5 - center) / support).abs()).max(0.0))
.collect();
while weights.len() > 1 && weights[weights.len() - 1] == 0.0 {
weights.pop();
}
let leading = weights
.iter()
.take(weights.len() - 1)
.take_while(|w| **w == 0.0)
.count();
weights.drain(..leading);
first += leading;
let sum: f32 = weights.iter().sum();
if sum > 0.0 {
weights.iter_mut().for_each(|w| *w /= sum);
(first, weights)
} else {
let nearest = (center.floor().max(0.0) as usize).clamp(lo, hi - 1);
(nearest, vec![1.0])
}
})
.collect()
}
pub(super) fn resample(source: &Pixmap, src: RectF, width: u32, height: u32) -> Option<Pixmap> {
if width == 0 || height == 0 || src.2 <= 0.0 || src.3 <= 0.0 {
return None;
}
let columns = filter_taps(source.width(), src.0, src.2, width);
let rows = filter_taps(source.height(), src.1, src.3, height);
let first_row = rows.iter().map(|(first, _)| *first).min()?;
let last_row = rows.iter().map(|(first, w)| first + w.len()).max()?;
let stride = source.width() as usize * 4;
let data = source.data();
let out_stride = width as usize * 4;
let mut middle = vec![0f32; (last_row - first_row) * out_stride];
for (r, row) in (first_row..last_row).enumerate() {
let line = &data[row * stride..(row + 1) * stride];
let target = &mut middle[r * out_stride..(r + 1) * out_stride];
for (x, (first, weights)) in columns.iter().enumerate() {
let mut acc = [0f32; 4];
for (k, w) in weights.iter().enumerate() {
let p = &line[(first + k) * 4..(first + k) * 4 + 4];
for (a, v) in acc.iter_mut().zip(p) {
*a += *v as f32 * w;
}
}
target[x * 4..x * 4 + 4].copy_from_slice(&acc);
}
}
let mut out = Pixmap::new(width, height)?;
let out_data = out.data_mut();
for (y, (first, weights)) in rows.iter().enumerate() {
let target = &mut out_data[y * out_stride..(y + 1) * out_stride];
for x in 0..width as usize {
let mut acc = [0f32; 4];
for (k, w) in weights.iter().enumerate() {
let base = (first + k - first_row) * out_stride + x * 4;
for (a, v) in acc.iter_mut().zip(&middle[base..base + 4]) {
*a += v * w;
}
}
let alpha = acc[3].round().clamp(0.0, 255.0);
let pixel = &mut target[x * 4..x * 4 + 4];
for (out, value) in pixel.iter_mut().zip(&acc[..3]) {
*out = value.round().clamp(0.0, alpha) as u8;
}
pixel[3] = alpha as u8;
}
}
Some(out)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn draw(
target: &mut Pixmap,
source: PixmapRef,
opaque: bool,
left: i32,
top: i32,
opacity: f32,
radius: f32,
mask: Option<&Mask>,
) {
if radius > 0.0 {
let Some(path) = super::rounded_rect(
left as f32,
top as f32,
source.width() as f32,
source.height() as f32,
radius,
) else {
return;
};
let paint = Paint {
shader: Pattern::new(
source,
SpreadMode::Pad,
FilterQuality::Nearest,
opacity,
Transform::from_translate(left as f32, top as f32),
),
anti_alias: true,
..Paint::default()
};
target.fill_path(
&path,
&paint,
FillRule::Winding,
Transform::identity(),
mask,
);
} else if opaque && opacity >= 1.0 && mask.is_none() {
blit(target, source, left, top);
} else {
let paint = PixmapPaint {
opacity,
quality: FilterQuality::Nearest,
..PixmapPaint::default()
};
target.draw_pixmap(left, top, source, &paint, Transform::identity(), mask);
}
}
fn blit(target: &mut Pixmap, source: PixmapRef, left: i32, top: i32) {
let (tw, th) = (target.width() as i64, target.height() as i64);
let (sw, sh) = (source.width() as i64, source.height() as i64);
let (x0, y0) = ((left as i64).max(0), (top as i64).max(0));
let (x1, y1) = ((left as i64 + sw).min(tw), (top as i64 + sh).min(th));
if x0 >= x1 || y0 >= y1 {
return;
}
let count = ((x1 - x0) * 4) as usize;
let src = source.data();
let dst = target.data_mut();
for y in y0..y1 {
let from = (((y - top as i64) * sw + (x0 - left as i64)) * 4) as usize;
let to = ((y * tw + x0) * 4) as usize;
dst[to..to + count].copy_from_slice(&src[from..from + count]);
}
}
#[cfg(test)]
mod tests {
use super::*;
const JPEG: &[u8] = include_bytes!("../../../../tests/fixtures/quadrants.jpg");
const GIF: &[u8] = include_bytes!("../../../../tests/fixtures/spin.gif");
const WEBP: &[u8] = include_bytes!("../../../../tests/fixtures/still.webp");
const ANIMATED_WEBP: &[u8] = include_bytes!("../../../../tests/fixtures/blink.webp");
const BMP: &[u8] = include_bytes!("../../../../tests/fixtures/pixels.bmp");
const PNG: &[u8] = include_bytes!("../../../../tests/fixtures/dot.png");
fn rgba(pixmap: &Pixmap, x: u32, y: u32) -> (u8, u8, u8, u8) {
let p = pixmap.pixel(x, y).unwrap();
(p.red(), p.green(), p.blue(), p.alpha())
}
fn size(image: &Decoded) -> (u32, u32) {
(image.frames[0].width(), image.frames[0].height())
}
fn near(actual: (u8, u8, u8, u8), expected: (u8, u8, u8, u8)) -> bool {
let d = |a: u8, b: u8| (a as i32 - b as i32).abs() <= 8;
d(actual.0, expected.0)
&& d(actual.1, expected.1)
&& d(actual.2, expected.2)
&& d(actual.3, expected.3)
}
#[test]
fn jpeg_decodes_its_quadrants() {
let image = decode(JPEG, Limits::default()).unwrap();
assert_eq!(size(&image), (16, 16));
assert_eq!(image.frames.len(), 1);
let frame = &image.frames[0];
assert!(near(rgba(frame, 3, 3), (255, 0, 0, 255)));
assert!(near(rgba(frame, 12, 3), (0, 255, 0, 255)));
assert!(near(rgba(frame, 3, 12), (0, 0, 255, 255)));
assert!(near(rgba(frame, 12, 12), (255, 255, 255, 255)));
}
#[test]
fn webp_still_keeps_alpha() {
let image = decode(WEBP, Limits::default()).unwrap();
assert_eq!((size(&image), image.frames.len()), ((4, 3), 1));
assert_eq!(rgba(&image.frames[0], 0, 0), (255, 0, 0, 255));
let corner = rgba(&image.frames[0], 3, 2);
assert_eq!((corner.0, corner.1, corner.3), (0, 0, 128));
assert!(corner.2 <= 128 && corner.2 >= 126);
assert!(!is_opaque(&image.frames[0]));
}
#[test]
fn animated_gif_decodes_every_frame_and_delay() {
let image = decode(GIF, Limits::default()).unwrap();
assert_eq!(size(&image), (4, 4));
assert_eq!(image.frames.len(), 3);
assert_eq!(rgba(&image.frames[0], 1, 1), (255, 0, 0, 255));
assert_eq!(rgba(&image.frames[1], 1, 1), (0, 255, 0, 255));
assert_eq!(rgba(&image.frames[2], 1, 1), (0, 0, 255, 255));
assert_eq!(image.delays, vec![0.05, 0.1, 0.2]);
assert_eq!(image.loop_count, 0);
}
#[test]
fn animated_webp_decodes_every_frame_and_loop_count() {
let image = decode(ANIMATED_WEBP, Limits::default()).unwrap();
assert_eq!(size(&image), (4, 4));
assert_eq!(image.frames.len(), 2);
assert_eq!(rgba(&image.frames[0], 2, 2), (255, 0, 0, 255));
assert_eq!(rgba(&image.frames[1], 2, 2), (0, 0, 255, 255));
assert_eq!(image.delays, vec![0.08, 0.1]);
assert_eq!(image.loop_count, 3);
}
#[test]
fn bmp_and_png_decode() {
let bmp = decode(BMP, Limits::default()).unwrap();
assert_eq!(size(&bmp), (3, 2));
assert_eq!(rgba(&bmp.frames[0], 0, 0), (255, 0, 0, 255));
assert_eq!(rgba(&bmp.frames[0], 2, 1), (128, 128, 128, 255));
let png = decode(PNG, Limits::default()).unwrap();
assert_eq!(size(&png), (4, 3));
}
#[test]
fn unknown_bytes_are_rejected() {
let err = decode(b"definitely not an image", Limits::default()).unwrap_err();
assert!(err.contains("unrecognized image format"), "{}", err);
assert!(decode(&GIF[..20], Limits::default()).is_err());
}
#[test]
fn limits_reject_large_images() {
let tight = Limits {
max_pixels: 15,
max_bytes: DEFAULT_MAX_BYTES,
};
let err = decode(GIF, tight).unwrap_err();
assert!(err.contains("maxPixels"), "{}", err);
let frames = Limits {
max_pixels: DEFAULT_MAX_PIXELS,
max_bytes: 150,
};
let err = decode(GIF, frames).unwrap_err();
assert!(err.contains("maxBytes"), "{}", err);
assert!(check_size(0, 10, Limits::default()).is_err());
}
#[test]
fn short_delays_are_clamped_like_browsers() {
assert_eq!(frame_delay(0.0), 0.1);
assert_eq!(frame_delay(10.0), 0.1);
assert_eq!(frame_delay(20.0), 0.02);
assert_eq!(frame_delay(f64::NAN), 0.1);
}
#[test]
fn rgba_images_are_premultiplied() {
let pixmap = rgba_to_pixmap(vec![255, 255, 255, 128, 0, 0, 0, 0], 2, 1).unwrap();
let pixels = pixmap.pixels();
assert_eq!(pixels[0].alpha(), 128);
assert!(pixels[0].red() <= 128);
assert_eq!(pixels[1].alpha(), 0);
assert!(rgba_to_pixmap(vec![0; 7], 2, 1).is_none());
}
#[test]
fn fits_place_images() {
let target = (10.0, 20.0, 100.0, 100.0);
assert_eq!(
place(200.0, 100.0, target, Fit::Fill),
((0.0, 0.0, 200.0, 100.0), target)
);
assert_eq!(
place(200.0, 100.0, target, Fit::Contain),
((0.0, 0.0, 200.0, 100.0), (10.0, 45.0, 100.0, 50.0))
);
assert_eq!(
place(200.0, 100.0, target, Fit::Cover),
((50.0, 0.0, 100.0, 100.0), target)
);
assert!(Fit::parse("stretch").is_err());
assert_eq!(Fit::parse("cover"), Ok(Fit::Cover));
}
#[test]
fn snapping_keeps_edges_whole() {
assert_eq!(snap((0.4, 0.6, 10.2, 10.0)), (0, 1, 11, 10));
assert_eq!(snap((5.0, 5.0, 0.2, 0.2)), (5, 5, 0, 0));
}
#[test]
fn identity_taps_copy_pixels() {
let taps = filter_taps(4, 0.0, 4.0, 4);
for (i, (first, weights)) in taps.iter().enumerate() {
assert_eq!(*first, i);
assert_eq!(weights, &[1.0]);
}
let taps = filter_taps(4, 0.0, 4.0, 2);
assert_eq!(taps[0].0, 0);
let sum: f32 = taps[0].1.iter().sum();
assert!((sum - 1.0).abs() < 1e-6);
}
#[test]
fn resampling_averages_and_preserves_solid_colors() {
let mut source = Pixmap::new(4, 2).unwrap();
for (i, pixel) in source.pixels_mut().iter_mut().enumerate() {
let v = if i % 2 == 0 { 0 } else { 255 };
*pixel = ColorU8::from_rgba(v, v, v, 255).premultiply();
}
let half = resample(&source, (0.0, 0.0, 4.0, 2.0), 2, 1).unwrap();
let gray = rgba(&half, 0, 0);
assert!(gray.0 > 100 && gray.0 < 155 && gray.3 == 255, "{:?}", gray);
let mut solid = Pixmap::new(3, 3).unwrap();
solid.fill(tiny_skia::Color::from_rgba8(10, 200, 30, 255));
let big = resample(&solid, (0.0, 0.0, 3.0, 3.0), 17, 5).unwrap();
assert!(
big.pixels()
.iter()
.all(|p| (p.red(), p.green(), p.blue(), p.alpha()) == (10, 200, 30, 255))
);
assert!(is_opaque(&big));
let same = resample(&source, (0.0, 0.0, 4.0, 2.0), 4, 2).unwrap();
assert_eq!(same.data(), source.data());
let crop = resample(&source, (1.0, 0.0, 1.0, 1.0), 1, 1).unwrap();
assert_eq!(rgba(&crop, 0, 0), (255, 255, 255, 255));
}
#[test]
fn opaque_frames_blit_and_clip() {
let mut target = Pixmap::new(4, 4).unwrap();
target.fill(tiny_skia::Color::WHITE);
let mut source = Pixmap::new(2, 2).unwrap();
source.fill(tiny_skia::Color::from_rgba8(255, 0, 0, 255));
draw(&mut target, source.as_ref(), true, 3, -1, 1.0, 0.0, None);
assert_eq!(rgba(&target, 3, 0), (255, 0, 0, 255));
assert_eq!(rgba(&target, 2, 0), (255, 255, 255, 255));
assert_eq!(rgba(&target, 3, 1), (255, 255, 255, 255));
draw(&mut target, source.as_ref(), true, 10, 10, 1.0, 0.0, None);
draw(&mut target, source.as_ref(), true, -5, 0, 1.0, 0.0, None);
draw(&mut target, source.as_ref(), true, 0, 2, 0.5, 0.0, None);
let blended = rgba(&target, 0, 2);
assert!(blended.1 > 100 && blended.1 < 155, "{:?}", blended);
let mut target = Pixmap::new(20, 20).unwrap();
target.fill(tiny_skia::Color::WHITE);
let mut source = Pixmap::new(20, 20).unwrap();
source.fill(tiny_skia::Color::BLACK);
draw(&mut target, source.as_ref(), true, 0, 0, 1.0, 8.0, None);
assert!(rgba(&target, 0, 0).0 > 200);
assert_eq!(rgba(&target, 10, 10), (0, 0, 0, 255));
}
}