1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
use alloc::borrow::Cow;
use bytemuck::{Pod, Zeroable};
use image::{
codecs::{farbfeld, hdr, ico, jpeg, pnm, tga, tiff, webp},
guess_format, load_from_memory_with_format, ImageDecoder, ImageEncoder, ImageFormat,
};
use std::io::Cursor;
use crate::util::byte_parsing::{big_endian::U32, strip_pod};
fn shrink_inner(data: &[u8], max_dim: u32) -> Option<Cow<'_, [u8]>> {
let format = guess_format(data).ok()?;
let (width, height) = match format {
ImageFormat::Png => {
// We encounter a lot of PNG images in splits files and decoding
// them with image's PNG decoder seems to decode way more than
// necessary. We really just need to find the width and height. The
// specification is here:
// https://www.w3.org/TR/2003/REC-PNG-20031110/
//
// And it says the following:
//
// "A valid PNG datastream shall begin with a PNG signature,
// immediately followed by an IHDR chunk".
//
// Each chunk is encoded as a length and type and then its chunk
// encoding. An IHDR chunk immediately starts with the width and
// height. This means we can model the beginning of a PNG file as
// follows:
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
struct BeginningOfPng {
// 5.2 PNG signature
png_signature: [u8; 8],
// 5.3 Chunk layout
chunk_len: [u8; 4],
// 11.2.2 IHDR Image header
chunk_type: [u8; 4],
width: U32,
height: U32,
}
// This improves parsing speed of entire splits files by up to 30%.
let beginning_of_png: &BeginningOfPng = strip_pod(&mut &*data)?;
(beginning_of_png.width.get(), beginning_of_png.height.get())
}
ImageFormat::Jpeg => jpeg::JpegDecoder::new(data).ok()?.dimensions(),
ImageFormat::WebP => webp::WebPDecoder::new(data).ok()?.dimensions(),
ImageFormat::Pnm => pnm::PnmDecoder::new(data).ok()?.dimensions(),
ImageFormat::Tiff => tiff::TiffDecoder::new(Cursor::new(data)).ok()?.dimensions(),
ImageFormat::Tga => tga::TgaDecoder::new(Cursor::new(data)).ok()?.dimensions(),
// We always want to re-encode BMP images so we might as well skip the
// dimension checking.
ImageFormat::Bmp => (0, 0),
ImageFormat::Ico => ico::IcoDecoder::new(Cursor::new(data)).ok()?.dimensions(),
ImageFormat::Hdr => hdr::HdrAdapter::new(data).ok()?.dimensions(),
ImageFormat::Farbfeld => farbfeld::FarbfeldDecoder::new(data).ok()?.dimensions(),
// FIXME: For GIF we would need to properly shrink the whole animation.
// The image crate can't properly handle this at this point in time.
// Some properties are not translated over properly it seems. We could
// shrink GIFs that are a single frame, but we also can't tell how many
// frames there are.
// DDS isn't a format we really care for.
// AVIF uses C bindings, so it's not portable.
// The OpenEXR code in the image crate doesn't compile on Big Endian targets.
// And the image format is non-exhaustive.
_ => return Some(data.into()),
};
let is_too_large = width > max_dim || height > max_dim;
Some(if is_too_large || format == ImageFormat::Bmp {
let mut image = load_from_memory_with_format(data, format).ok()?;
if is_too_large {
image = image.thumbnail(max_dim, max_dim);
}
let mut data = Vec::new();
image::codecs::png::PngEncoder::new(&mut data)
.write_image(
image.as_bytes(),
image.width(),
image.height(),
image.color(),
)
.ok()?;
data.into()
} else {
data.into()
})
}
pub fn shrink(data: &[u8], max_dim: u32) -> Cow<'_, [u8]> {
shrink_inner(data, max_dim).unwrap_or_else(|| data.into())
}