Krolyxon
Commit e515e5a

Branches

Tags

Refs →

remagesrcdecodeimage_rs.rs

382 lines · 13.2 KB · Rust

1//! The `image` crate: stills (with ICC and EXIF orientation), GIF/APNG/WebP animation, and
2//! multi-page TIFF.
3
4use std::fs::File;
5use std::io::{BufRead, BufReader, Cursor, Seek};
6use std::path::Path;
7use std::time::Duration;
8
9use anyhow::{Context, anyhow};
10use image::codecs::gif::GifDecoder;
11use image::codecs::png::PngDecoder;
12use image::codecs::webp::WebPDecoder;
13use image::metadata::Orientation;
14use image::{AnimationDecoder, DynamicImage, ImageDecoder, ImageFormat, ImageReader};
15
16use super::color::{self, ToSrgb};
17use super::{DecodedImage, Frame, MAX_ALLOC, MAX_FRAMES_BYTES, Options, label};
18
19/// Browsers show GIF frames with tiny delays at this pace, and so do we.
20const MIN_DELAY: Duration = Duration::from_millis(20);
21const DEFAULT_DELAY: Duration = Duration::from_millis(100);
22
23fn limits() -> image::Limits {
24 let mut l = image::Limits::default();
25 l.max_alloc = Some(MAX_ALLOC);
26 l
27}
28
29pub fn decode_file(
30 path: &Path,
31 format: ImageFormat,
32 opts: &Options,
33) -> anyhow::Result<DecodedImage> {
34 let open = || -> anyhow::Result<BufReader<File>> { Ok(BufReader::new(File::open(path)?)) };
35 let icc = |d: &mut dyn ImageDecoder| {
36 if opts.color {
37 d.icc_profile().ok().flatten()
38 } else {
39 None
40 }
41 };
42 let animated = match format {
43 ImageFormat::Gif => {
44 let mut d = GifDecoder::new(open()?)?;
45 let icc = icc(&mut d);
46 animation(d, icc, format)?
47 }
48 ImageFormat::Png => {
49 let mut d = PngDecoder::new(open()?)?;
50 if d.is_apng()? {
51 let icc = icc(&mut d);
52 animation(d.apng()?, icc, format)?
53 } else {
54 None
55 }
56 }
57 ImageFormat::WebP => {
58 let mut d = WebPDecoder::new(open()?)?;
59 if d.has_animation() {
60 let icc = icc(&mut d);
61 animation(d, icc, format)?
62 } else {
63 None
64 }
65 }
66 _ => None,
67 };
68 if let Some(img) = animated {
69 return Ok(img);
70 }
71 if format == ImageFormat::Jpeg {
72 match decode_jpeg(path, opts) {
73 Ok(Some(img)) => return Ok(img),
74 Ok(None) => {}
75 Err(e) => log::debug!("fast JPEG path failed, retrying: {e:#}"),
76 }
77 }
78 let mut img = decode_reader(open()?, format, opts)?;
79 if format == ImageFormat::Tiff {
80 add_tiff_pages(path, &mut img, opts);
81 }
82 Ok(img)
83}
84
85/// JPEG straight to RGBA through zune-jpeg. `image` would hand over RGB, and widening 24 MP
86/// costs a quarter of the decode again. `None` for the colour spaces (CMYK, YCCK, plain RGB)
87/// that zune-jpeg can't write as RGBA; those go the long way.
88fn decode_jpeg(path: &Path, opts: &Options) -> anyhow::Result<Option<DecodedImage>> {
89 use zune_core::colorspace::ColorSpace;
90 let data = std::fs::read(path)?;
91 let options = zune_core::options::DecoderOptions::default()
92 .set_strict_mode(false)
93 .set_max_width(usize::MAX)
94 .set_max_height(usize::MAX)
95 .jpeg_set_out_colorspace(ColorSpace::RGBA);
96 let mut d = zune_jpeg::JpegDecoder::new_with_options(
97 zune_core::bytestream::ZCursor::new(data.as_slice()),
98 options,
99 );
100 d.decode_headers()?;
101 if !matches!(
102 d.input_colorspace(),
103 Some(ColorSpace::YCbCr | ColorSpace::Luma)
104 ) {
105 return Ok(None);
106 }
107 let (w, h) = d.dimensions().context("no JPEG header")?;
108 if (w as u64) * (h as u64) * 4 > MAX_ALLOC {
109 return Err(anyhow!("image too large"));
110 }
111 let orientation = d
112 .exif()
113 .filter(|_| opts.auto_orient)
114 .and_then(|e| Orientation::from_exif_chunk(e))
115 .unwrap_or(Orientation::NoTransforms);
116 let icc = if opts.color { d.icc_profile() } else { None };
117 let mut pixels = d.decode()?;
118 if pixels.len() != w * h * 4 {
119 return Err(anyhow!("JPEG decoded to an unexpected size"));
120 }
121 if let Some(t) = icc.as_deref().and_then(ToSrgb::from_icc) {
122 t.apply(&mut pixels);
123 }
124 let size = [w as u32, h as u32];
125 Ok(Some(
126 DecodedImage::still(size, pixels, label(ImageFormat::Jpeg)).turned(orientation),
127 ))
128}
129
130/// A still image from any reader: ICC to sRGB, float tone-mapped. Orientation is left to the
131/// renderer.
132pub fn decode_reader<R: BufRead + Seek>(
133 reader: R,
134 format: ImageFormat,
135 opts: &Options,
136) -> anyhow::Result<DecodedImage> {
137 let mut reader = ImageReader::with_format(reader, format);
138 reader.limits(limits());
139 let mut decoder = reader.into_decoder()?;
140 let orientation = if opts.auto_orient {
141 decoder.orientation().unwrap_or(Orientation::NoTransforms)
142 } else {
143 Orientation::NoTransforms
144 };
145 let icc = if opts.color {
146 decoder.icc_profile().ok().flatten()
147 } else {
148 None
149 };
150 let img = DynamicImage::from_decoder(decoder)?;
151 let float = color::is_float(&img);
152 let (size, mut pixels) = color::to_rgba8(img);
153 // Float data is linear scene light; an embedded profile doesn't describe the 8-bit result.
154 if !float && let Some(t) = icc.as_deref().and_then(ToSrgb::from_icc) {
155 t.apply(&mut pixels);
156 }
157 Ok(DecodedImage::still(size, pixels, label(format)).turned(orientation))
158}
159
160/// Decode from memory, guessing the format from the bytes (external decoders' output).
161pub fn decode_bytes(bytes: &[u8], opts: &Options) -> anyhow::Result<DecodedImage> {
162 let format = image::guess_format(bytes).context("unrecognised output")?;
163 decode_reader(Cursor::new(bytes), format, opts)
164}
165
166/// All frames of an animation, or `None` when it has only one (decode it as a still, which
167/// knows its orientation).
168fn animation<'a>(
169 decoder: impl AnimationDecoder<'a>,
170 icc: Option<Vec<u8>>,
171 format: ImageFormat,
172) -> anyhow::Result<Option<DecodedImage>> {
173 let transform = icc.as_deref().and_then(ToSrgb::from_icc);
174 let mut frames = Vec::new();
175 let mut size = [0, 0];
176 let mut bytes = 0;
177 for frame in decoder.into_frames() {
178 let frame = frame?;
179 let (num, den) = frame.delay().numer_denom_ms();
180 let ms = num.checked_div(den).unwrap_or(0);
181 let delay = match Duration::from_millis(ms as u64) {
182 d if d < MIN_DELAY => DEFAULT_DELAY,
183 d => d,
184 };
185 let buf = frame.into_buffer();
186 size = [buf.width(), buf.height()];
187 let mut pixels = buf.into_raw();
188 if let Some(t) = &transform {
189 t.apply(&mut pixels);
190 }
191 bytes += pixels.len();
192 frames.push(Frame { pixels, delay });
193 if bytes > MAX_FRAMES_BYTES {
194 log::warn!("animation truncated at {} frames", frames.len());
195 break;
196 }
197 }
198 match frames.len() {
199 0 => Err(anyhow!("no frames")),
200 1 => Ok(None),
201 n => {
202 log::debug!("{n} frames");
203 Ok(Some(DecodedImage {
204 size,
205 frames,
206 animated: true,
207 format: label(format),
208 source_size: size,
209 refine: None,
210 orientation: Orientation::NoTransforms,
211 }))
212 }
213 }
214}
215
216/// Pages after the first, as extra (untimed) frames. Pages of another size or orientation
217/// are skipped.
218fn add_tiff_pages(path: &Path, img: &mut DecodedImage, opts: &Options) {
219 let Ok(file) = File::open(path) else { return };
220 let Ok(mut d) = tiff::decoder::Decoder::new(BufReader::new(file)) else {
221 return;
222 };
223 let mut bytes = img.bytes();
224 while d.more_images() {
225 if d.next_image().is_err() {
226 break;
227 }
228 match tiff_page(&mut d) {
229 Ok((size, pixels)) => {
230 // Every frame shares the first page's texture and placement.
231 if size != img.size || orientation_of(&mut d, opts) != img.orientation {
232 log::debug!("TIFF page of another size skipped");
233 continue;
234 }
235 bytes += pixels.len();
236 img.frames.push(Frame {
237 pixels,
238 delay: Duration::ZERO,
239 });
240 }
241 Err(e) => log::debug!("TIFF page skipped: {e:#}"),
242 }
243 if bytes > MAX_FRAMES_BYTES {
244 log::warn!("multi-page TIFF truncated at {} pages", img.frames.len());
245 break;
246 }
247 }
248}
249
250fn orientation_of<R: std::io::Read + Seek>(
251 d: &mut tiff::decoder::Decoder<R>,
252 opts: &Options,
253) -> Orientation {
254 if !opts.auto_orient {
255 return Orientation::NoTransforms;
256 }
257 d.find_tag_unsigned::<u8>(tiff::tags::Tag::Orientation)
258 .ok()
259 .flatten()
260 .and_then(Orientation::from_exif)
261 .unwrap_or(Orientation::NoTransforms)
262}
263
264fn tiff_page<R: std::io::Read + Seek>(
265 d: &mut tiff::decoder::Decoder<R>,
266) -> anyhow::Result<([u32; 2], Vec<u8>)> {
267 use tiff::ColorType as C;
268 use tiff::decoder::DecodingResult as D;
269 let (w, h) = d.dimensions()?;
270 let channels = match d.colortype()? {
271 C::Gray(8 | 16) => 1,
272 C::GrayA(8 | 16) => 2,
273 C::RGB(8 | 16) => 3,
274 C::RGBA(8 | 16) => 4,
275 other => return Err(anyhow!("unsupported colour type {other:?}")),
276 };
277 let samples: Vec<u8> = match d.read_image()? {
278 D::U8(v) => v,
279 D::U16(v) => v.into_iter().map(|s| (s >> 8) as u8).collect(),
280 _ => return Err(anyhow!("unsupported sample format")),
281 };
282 if samples.len() < (w * h) as usize * channels {
283 return Err(anyhow!("short page"));
284 }
285 Ok(([w, h], color::expand(&samples, channels)))
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use image::codecs::gif::GifEncoder;
292 use image::{Delay, Rgba, RgbaImage};
293
294 fn dir() -> std::path::PathBuf {
295 let d = std::env::temp_dir().join(format!("remage-imgrs-{}", std::process::id()));
296 std::fs::create_dir_all(&d).unwrap();
297 d
298 }
299
300 #[test]
301 fn gif_animation_keeps_frames_and_delays() {
302 let path = dir().join("a.gif");
303 {
304 let mut enc = GifEncoder::new(File::create(&path).unwrap());
305 for (c, ms) in [(255, 50), (0, 0)] {
306 let buf = RgbaImage::from_pixel(4, 3, Rgba([c, 0, 0, 255]));
307 let f = image::Frame::from_parts(buf, 0, 0, Delay::from_numer_denom_ms(ms, 1));
308 enc.encode_frame(f).unwrap();
309 }
310 }
311 let img = decode_file(&path, ImageFormat::Gif, &Options::default()).unwrap();
312 assert!(img.animated);
313 assert_eq!(img.size, [4, 3]);
314 assert_eq!(
315 img.delays(),
316 [Duration::from_millis(50), DEFAULT_DELAY],
317 "a zero delay plays at the browser pace"
318 );
319 assert_eq!(img.frame(1)[0], 0);
320 }
321
322 #[test]
323 fn jpeg_fast_path() {
324 use image::codecs::jpeg::JpegEncoder;
325 let dir = dir();
326 // Grey and colour both come out as opaque RGBA.
327 let grey = dir.join("grey.jpg");
328 let buf = image::GrayImage::from_pixel(16, 8, image::Luma([200]));
329 JpegEncoder::new_with_quality(File::create(&grey).unwrap(), 95)
330 .encode_image(&buf)
331 .unwrap();
332 let img = decode_jpeg(&grey, &Options::default()).unwrap().unwrap();
333 assert_eq!((img.size, img.pixels().len()), ([16, 8], 16 * 8 * 4));
334 let p = &img.pixels()[..4];
335 assert!(p[0].abs_diff(200) <= 2 && p[0] == p[1] && p[1] == p[2] && p[3] == 255);
336
337 let rgb = dir.join("rgb.jpg");
338 let buf = image::RgbImage::from_pixel(16, 8, image::Rgb([250, 10, 10]));
339 JpegEncoder::new_with_quality(File::create(&rgb).unwrap(), 95)
340 .encode_image(&buf)
341 .unwrap();
342 let img = decode_file(&rgb, ImageFormat::Jpeg, &Options::default()).unwrap();
343 let p = &img.pixels()[..4];
344 assert!(p[0] > 235 && p[1] < 30 && p[2] < 30 && p[3] == 255, "{p:?}");
345 // The same pixels as the long way round.
346 let slow = decode_reader(
347 BufReader::new(File::open(&rgb).unwrap()),
348 ImageFormat::Jpeg,
349 &Options::default(),
350 )
351 .unwrap();
352 assert_eq!(img.pixels(), slow.pixels());
353 }
354
355 #[test]
356 fn multi_page_tiff() {
357 use tiff::encoder::{TiffEncoder, colortype};
358 let path = dir().join("pages.tif");
359 {
360 let mut enc = TiffEncoder::new(File::create(&path).unwrap()).unwrap();
361 enc.write_image::<colortype::RGB8>(2, 2, &[10; 12]).unwrap();
362 enc.write_image::<colortype::Gray8>(2, 2, &[200; 4])
363 .unwrap();
364 enc.write_image::<colortype::Gray8>(3, 1, &[0; 3]).unwrap();
365 }
366 let img = decode_file(&path, ImageFormat::Tiff, &Options::default()).unwrap();
367 assert!(!img.animated);
368 assert_eq!(img.frames.len(), 2, "the odd-sized page is skipped");
369 assert_eq!(&img.frame(1)[..4], [200, 200, 200, 255]);
370 }
371
372 #[test]
373 fn float_images_are_tonemapped() {
374 let path = dir().join("hdr.exr");
375 let buf = image::Rgb32FImage::from_pixel(2, 1, image::Rgb([4.0, 0.5, 0.0]));
376 DynamicImage::ImageRgb32F(buf).save(&path).unwrap();
377 let img = decode_file(&path, ImageFormat::OpenExr, &Options::default()).unwrap();
378 assert_eq!(img.format, "EXR");
379 let px = img.pixels();
380 assert!(px[0] > px[1] && px[0] < 255 && px[2] == 0, "{px:?}");
381 }
382}