Krolyxon
Commit 8b10288

Branches

Tags

Refs →

remagesrcdecodeimage_rs.rs

297 lines · 9.9 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 let mut img = decode_reader(open()?, format, opts)?;
72 if format == ImageFormat::Tiff {
73 add_tiff_pages(path, &mut img, opts);
74 }
75 Ok(img)
76}
77
78/// A still image from any reader: ICC to sRGB, float tone-mapped, oriented upright.
79pub fn decode_reader<R: BufRead + Seek>(
80 reader: R,
81 format: ImageFormat,
82 opts: &Options,
83) -> anyhow::Result<DecodedImage> {
84 let mut reader = ImageReader::with_format(reader, format);
85 reader.limits(limits());
86 let mut decoder = reader.into_decoder()?;
87 let orientation = if opts.auto_orient {
88 decoder.orientation().unwrap_or(Orientation::NoTransforms)
89 } else {
90 Orientation::NoTransforms
91 };
92 let icc = if opts.color {
93 decoder.icc_profile().ok().flatten()
94 } else {
95 None
96 };
97 let img = DynamicImage::from_decoder(decoder)?;
98 let float = color::is_float(&img);
99 let (size, mut pixels) = color::to_rgba8(img);
100 // Float data is linear scene light; an embedded profile doesn't describe the 8-bit result.
101 if !float && let Some(t) = icc.as_deref().and_then(ToSrgb::from_icc) {
102 t.apply(&mut pixels);
103 }
104 let (size, pixels) = color::orient(size, pixels, orientation);
105 Ok(DecodedImage::still(size, pixels, label(format)))
106}
107
108/// Decode from memory, guessing the format from the bytes (external decoders' output).
109pub fn decode_bytes(bytes: &[u8], opts: &Options) -> anyhow::Result<DecodedImage> {
110 let format = image::guess_format(bytes).context("unrecognised output")?;
111 decode_reader(Cursor::new(bytes), format, opts)
112}
113
114/// All frames of an animation, or `None` when it has only one (decode it as a still, which
115/// keeps its orientation).
116fn animation<'a>(
117 decoder: impl AnimationDecoder<'a>,
118 icc: Option<Vec<u8>>,
119 format: ImageFormat,
120) -> anyhow::Result<Option<DecodedImage>> {
121 let transform = icc.as_deref().and_then(ToSrgb::from_icc);
122 let mut frames = Vec::new();
123 let mut size = [0, 0];
124 let mut bytes = 0;
125 for frame in decoder.into_frames() {
126 let frame = frame?;
127 let (num, den) = frame.delay().numer_denom_ms();
128 let ms = num.checked_div(den).unwrap_or(0);
129 let delay = match Duration::from_millis(ms as u64) {
130 d if d < MIN_DELAY => DEFAULT_DELAY,
131 d => d,
132 };
133 let buf = frame.into_buffer();
134 size = [buf.width(), buf.height()];
135 let mut pixels = buf.into_raw();
136 if let Some(t) = &transform {
137 t.apply(&mut pixels);
138 }
139 bytes += pixels.len();
140 frames.push(Frame { pixels, delay });
141 if bytes > MAX_FRAMES_BYTES {
142 log::warn!("animation truncated at {} frames", frames.len());
143 break;
144 }
145 }
146 match frames.len() {
147 0 => Err(anyhow!("no frames")),
148 1 => Ok(None),
149 n => {
150 log::debug!("{n} frames");
151 Ok(Some(DecodedImage {
152 size,
153 frames,
154 animated: true,
155 format: label(format),
156 source_size: size,
157 refine: None,
158 }))
159 }
160 }
161}
162
163/// Pages after the first, as extra (untimed) frames. Pages of another size are skipped.
164fn add_tiff_pages(path: &Path, img: &mut DecodedImage, opts: &Options) {
165 let Ok(file) = File::open(path) else { return };
166 let Ok(mut d) = tiff::decoder::Decoder::new(BufReader::new(file)) else {
167 return;
168 };
169 let mut bytes = img.bytes();
170 while d.more_images() {
171 if d.next_image().is_err() {
172 break;
173 }
174 match tiff_page(&mut d) {
175 Ok((size, pixels)) => {
176 // `image` rotated the first page; rotate the rest the same way.
177 let o = orientation_of(&mut d, opts);
178 let (size, pixels) = color::orient(size, pixels, o);
179 if size != img.size {
180 log::debug!("TIFF page of another size skipped");
181 continue;
182 }
183 bytes += pixels.len();
184 img.frames.push(Frame {
185 pixels,
186 delay: Duration::ZERO,
187 });
188 }
189 Err(e) => log::debug!("TIFF page skipped: {e:#}"),
190 }
191 if bytes > MAX_FRAMES_BYTES {
192 log::warn!("multi-page TIFF truncated at {} pages", img.frames.len());
193 break;
194 }
195 }
196}
197
198fn orientation_of<R: std::io::Read + Seek>(
199 d: &mut tiff::decoder::Decoder<R>,
200 opts: &Options,
201) -> Orientation {
202 if !opts.auto_orient {
203 return Orientation::NoTransforms;
204 }
205 d.find_tag_unsigned::<u8>(tiff::tags::Tag::Orientation)
206 .ok()
207 .flatten()
208 .and_then(Orientation::from_exif)
209 .unwrap_or(Orientation::NoTransforms)
210}
211
212fn tiff_page<R: std::io::Read + Seek>(
213 d: &mut tiff::decoder::Decoder<R>,
214) -> anyhow::Result<([u32; 2], Vec<u8>)> {
215 use tiff::ColorType as C;
216 use tiff::decoder::DecodingResult as D;
217 let (w, h) = d.dimensions()?;
218 let channels = match d.colortype()? {
219 C::Gray(8 | 16) => 1,
220 C::GrayA(8 | 16) => 2,
221 C::RGB(8 | 16) => 3,
222 C::RGBA(8 | 16) => 4,
223 other => return Err(anyhow!("unsupported colour type {other:?}")),
224 };
225 let samples: Vec<u8> = match d.read_image()? {
226 D::U8(v) => v,
227 D::U16(v) => v.into_iter().map(|s| (s >> 8) as u8).collect(),
228 _ => return Err(anyhow!("unsupported sample format")),
229 };
230 if samples.len() < (w * h) as usize * channels {
231 return Err(anyhow!("short page"));
232 }
233 Ok(([w, h], color::expand(&samples, channels)))
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use image::codecs::gif::GifEncoder;
240 use image::{Delay, Rgba, RgbaImage};
241
242 fn dir() -> std::path::PathBuf {
243 let d = std::env::temp_dir().join(format!("remage-imgrs-{}", std::process::id()));
244 std::fs::create_dir_all(&d).unwrap();
245 d
246 }
247
248 #[test]
249 fn gif_animation_keeps_frames_and_delays() {
250 let path = dir().join("a.gif");
251 {
252 let mut enc = GifEncoder::new(File::create(&path).unwrap());
253 for (c, ms) in [(255, 50), (0, 0)] {
254 let buf = RgbaImage::from_pixel(4, 3, Rgba([c, 0, 0, 255]));
255 let f = image::Frame::from_parts(buf, 0, 0, Delay::from_numer_denom_ms(ms, 1));
256 enc.encode_frame(f).unwrap();
257 }
258 }
259 let img = decode_file(&path, ImageFormat::Gif, &Options::default()).unwrap();
260 assert!(img.animated);
261 assert_eq!(img.size, [4, 3]);
262 assert_eq!(
263 img.delays(),
264 [Duration::from_millis(50), DEFAULT_DELAY],
265 "a zero delay plays at the browser pace"
266 );
267 assert_eq!(img.frame(1)[0], 0);
268 }
269
270 #[test]
271 fn multi_page_tiff() {
272 use tiff::encoder::{TiffEncoder, colortype};
273 let path = dir().join("pages.tif");
274 {
275 let mut enc = TiffEncoder::new(File::create(&path).unwrap()).unwrap();
276 enc.write_image::<colortype::RGB8>(2, 2, &[10; 12]).unwrap();
277 enc.write_image::<colortype::Gray8>(2, 2, &[200; 4])
278 .unwrap();
279 enc.write_image::<colortype::Gray8>(3, 1, &[0; 3]).unwrap();
280 }
281 let img = decode_file(&path, ImageFormat::Tiff, &Options::default()).unwrap();
282 assert!(!img.animated);
283 assert_eq!(img.frames.len(), 2, "the odd-sized page is skipped");
284 assert_eq!(&img.frame(1)[..4], [200, 200, 200, 255]);
285 }
286
287 #[test]
288 fn float_images_are_tonemapped() {
289 let path = dir().join("hdr.exr");
290 let buf = image::Rgb32FImage::from_pixel(2, 1, image::Rgb([4.0, 0.5, 0.0]));
291 DynamicImage::ImageRgb32F(buf).save(&path).unwrap();
292 let img = decode_file(&path, ImageFormat::OpenExr, &Options::default()).unwrap();
293 assert_eq!(img.format, "EXR");
294 let px = img.pixels();
295 assert!(px[0] > px[1] && px[0] < 255 && px[2] == 0, "{px:?}");
296 }
297}