Krolyxon
Commit 74a9b56

Branches

Tags

Refs →

remagesrcdecodemod.rs

559 lines · 18.2 KB · Rust

1//! Format sniffing and dispatch to the decoder backends.
2//!
3//! Every backend returns straight-alpha RGBA8 in sRGB, upright: ICC/CICP colour is converted,
4//! float images are tone-mapped and EXIF orientation is applied here, so nothing downstream
5//! cares where pixels came from.
6
7pub mod color;
8mod external;
9#[cfg(feature = "heif")]
10mod heif;
11mod image_rs;
12#[cfg(feature = "jxl")]
13mod jxl;
14pub mod meta;
15#[cfg(feature = "raw")]
16mod raw;
17#[cfg(feature = "svg")]
18mod svg;
19
20use std::fs::File;
21use std::io::Read;
22use std::path::Path;
23#[cfg(feature = "svg")]
24use std::sync::Arc;
25use std::sync::LazyLock;
26use std::time::Duration;
27
28use anyhow::anyhow;
29use image::ImageFormat;
30
31pub use external::set_decoders;
32
33/// Refuse single images that would need more than this to decode.
34const MAX_ALLOC: u64 = 4 << 30;
35/// Animations and multi-page files stop adding frames past this many bytes.
36const MAX_FRAMES_BYTES: usize = 1 << 30;
37
38/// One frame of an animation or one page of a document, `size` RGBA8 pixels.
39pub struct Frame {
40 pub pixels: Vec<u8>,
41 /// How long an animation frame shows; zero for pages.
42 pub delay: Duration,
43}
44
45/// Straight-alpha RGBA8 in sRGB, oriented upright, ready to upload.
46pub struct DecodedImage {
47 /// Pixel size of every frame.
48 pub size: [u32; 2],
49 /// At least one.
50 pub frames: Vec<Frame>,
51 /// Frames are timed animation rather than pages.
52 pub animated: bool,
53 pub format: &'static str,
54 /// The size the view works in: the image's own pixels. Scalable or previewed sources
55 /// (SVG, RAW) may hold fewer or more pixels than this.
56 pub source_size: [u32; 2],
57 /// A sharper version can be made on demand (SVG re-raster, full RAW develop).
58 pub refine: Option<Refine>,
59}
60
61/// How to make a sharper version of an image.
62#[derive(Clone)]
63pub enum Refine {
64 #[cfg(feature = "svg")]
65 Svg(Arc<svg::Svg>),
66 /// The embedded preview is showing; develop the raw data in full.
67 #[cfg(feature = "raw")]
68 Raw,
69}
70
71impl Refine {
72 /// Make a version whose longer side is about `long` pixels.
73 pub fn run(&self, path: &Path, long: u32, opts: &Options) -> anyhow::Result<DecodedImage> {
74 let _ = (path, long, opts);
75 match self {
76 #[cfg(feature = "svg")]
77 Refine::Svg(s) => Ok(s.rasterize(long)),
78 #[cfg(feature = "raw")]
79 Refine::Raw => raw::develop(path, opts),
80 #[allow(unreachable_patterns)]
81 _ => unreachable!(),
82 }
83 }
84
85 /// Whether a texture of `have` pixels on its longer side should be remade for `want`.
86 pub fn wants(&self, have: u32, want: u32) -> Option<u32> {
87 let _ = (have, want);
88 match self {
89 #[cfg(feature = "svg")]
90 Refine::Svg(_) => {
91 let want = want.clamp(16, svg::MAX_RASTER);
92 // Sharper when zoomed in; smaller again when zoomed far out.
93 (want > have + have / 8 || want < have / 3).then_some(want)
94 }
95 #[cfg(feature = "raw")]
96 Refine::Raw => (want > have).then_some(u32::MAX),
97 #[allow(unreachable_patterns)]
98 _ => None,
99 }
100 }
101}
102
103impl DecodedImage {
104 pub fn still(size: [u32; 2], pixels: Vec<u8>, format: &'static str) -> Self {
105 Self {
106 size,
107 frames: vec![Frame {
108 pixels,
109 delay: Duration::ZERO,
110 }],
111 animated: false,
112 format,
113 source_size: size,
114 refine: None,
115 }
116 }
117
118 pub fn bytes(&self) -> usize {
119 self.frames.iter().map(|f| f.pixels.len()).sum()
120 }
121
122 /// The first frame's pixels.
123 #[cfg(test)]
124 pub fn pixels(&self) -> &[u8] {
125 &self.frames[0].pixels
126 }
127
128 pub fn frame(&self, i: usize) -> &[u8] {
129 &self.frames[i.min(self.frames.len() - 1)].pixels
130 }
131
132 /// Frame delays when there is more than one frame.
133 pub fn delays(&self) -> Vec<Duration> {
134 self.frames.iter().map(|f| f.delay).collect()
135 }
136}
137
138impl std::fmt::Debug for DecodedImage {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_struct("DecodedImage")
141 .field("size", &self.size)
142 .field("frames", &self.frames.len())
143 .field("format", &self.format)
144 .finish_non_exhaustive()
145 }
146}
147
148/// How to decode.
149#[derive(Clone, Copy, Debug)]
150pub struct Options {
151 /// About how many pixels the longer side needs, for sources that can be rendered at any
152 /// size (SVG) or offer a preview (RAW).
153 #[cfg_attr(not(any(feature = "svg", feature = "raw")), allow(dead_code))]
154 pub target: u32,
155 pub auto_orient: bool,
156 pub color: bool,
157}
158
159impl Default for Options {
160 fn default() -> Self {
161 Self {
162 target: 2048,
163 auto_orient: true,
164 color: true,
165 }
166 }
167}
168
169#[cfg(feature = "raw")]
170const RAW_EXTENSIONS: &[&str] = &[
171 "3fr", "ari", "arw", "cr2", "cr3", "crw", "dcr", "dcs", "dng", "erf", "iiq", "kdc", "mef",
172 "mos", "mrw", "nef", "nrw", "orf", "ori", "pef", "raf", "raw", "rw2", "rwl", "sr2", "srf",
173 "srw", "x3f",
174];
175#[cfg(not(feature = "raw"))]
176const RAW_EXTENSIONS: &[&str] = &[];
177
178static EXTENSIONS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
179 let mut v: Vec<&'static str> = ImageFormat::all()
180 .filter(ImageFormat::reading_enabled)
181 .flat_map(|f| f.extensions_str().iter().copied())
182 .chain(["apng", "jfif"])
183 .collect();
184 if cfg!(feature = "avif") {
185 v.push("avif");
186 }
187 if cfg!(feature = "jxl") {
188 v.push("jxl");
189 }
190 if cfg!(feature = "heif") {
191 v.extend(["heic", "heif", "hif"]);
192 }
193 if cfg!(feature = "svg") {
194 v.extend(["svg", "svgz"]);
195 }
196 v.extend(RAW_EXTENSIONS);
197 v
198});
199
200fn extension(path: &Path) -> Option<String> {
201 path.extension()
202 .and_then(|e| e.to_str())
203 .map(str::to_ascii_lowercase)
204}
205
206/// Whether the extension alone (or an external decoder) says we can decode this file.
207pub fn is_known_extension(path: &Path) -> bool {
208 extension(path).is_some_and(|e| EXTENSIONS.contains(&e.as_str()))
209 || external::find(path).is_some()
210}
211
212/// Which backend decodes a file.
213#[derive(Clone, Debug, PartialEq)]
214enum Kind {
215 Image(ImageFormat),
216 Jxl,
217 Heif,
218 Svg,
219 Raw,
220 External(crate::config::Decoder),
221}
222
223/// The ISO-BMFF `ftyp` brands, major first.
224fn ftyp_brands(head: &[u8]) -> Vec<&[u8]> {
225 if head.len() < 16 || &head[4..8] != b"ftyp" {
226 return Vec::new();
227 }
228 let len = u32::from_be_bytes([head[0], head[1], head[2], head[3]]) as usize;
229 let end = len.clamp(16, head.len());
230 let mut brands = vec![&head[8..12]];
231 brands.extend(
232 head[16..end]
233 .as_chunks::<4>()
234 .0
235 .iter()
236 .map(|b| b.as_slice()),
237 );
238 brands
239}
240
241fn looks_like_svg(head: &[u8]) -> bool {
242 let text = String::from_utf8_lossy(head);
243 let t = text.trim_start_matches('\u{feff}').trim_start();
244 (t.starts_with("<?xml") || t.starts_with("<svg") || t.starts_with("<!--"))
245 && text.contains("<svg")
246}
247
248/// Magic bytes first, the extension second. External decoders configured for a file win.
249fn detect(path: &Path, head: &[u8]) -> Option<Kind> {
250 if let Some(d) = external::find(path) {
251 return Some(Kind::External(d));
252 }
253 let ext = extension(path).unwrap_or_default();
254 let jxl = head.starts_with(&[0xFF, 0x0A]) || head.starts_with(b"\0\0\0\x0cJXL \r\n\x87\n");
255 if jxl && cfg!(feature = "jxl") {
256 return Some(Kind::Jxl);
257 }
258 let brands = ftyp_brands(head);
259 if !brands.is_empty() {
260 let has = |b: &[u8]| brands.contains(&b);
261 if has(b"crx ") && cfg!(feature = "raw") {
262 return Some(Kind::Raw);
263 }
264 if has(b"avif") || has(b"avis") {
265 return Some(Kind::Image(ImageFormat::Avif));
266 }
267 let heif = [
268 b"heic", b"heix", b"heim", b"heis", b"hevc", b"hevx", b"mif1", b"msf1",
269 ];
270 if heif.iter().any(|b| has(*b)) && cfg!(feature = "heif") {
271 return Some(Kind::Heif);
272 }
273 }
274 // Most raw formats are TIFF inside, so the extension decides before the magic does.
275 if RAW_EXTENSIONS.contains(&ext.as_str()) {
276 return Some(Kind::Raw);
277 }
278 if cfg!(feature = "svg")
279 && (looks_like_svg(head) || (ext == "svgz" && head.starts_with(&[0x1F, 0x8B])))
280 {
281 return Some(Kind::Svg);
282 }
283 if let Ok(f) = image::guess_format(head)
284 && f.reading_enabled()
285 {
286 return Some(Kind::Image(f));
287 }
288 match ext.as_str() {
289 "jxl" if cfg!(feature = "jxl") => Some(Kind::Jxl),
290 "heic" | "heif" | "hif" if cfg!(feature = "heif") => Some(Kind::Heif),
291 "svg" | "svgz" if cfg!(feature = "svg") => Some(Kind::Svg),
292 _ => ImageFormat::from_extension(&ext)
293 .filter(ImageFormat::reading_enabled)
294 .map(Kind::Image),
295 }
296}
297
298fn head(path: &Path) -> std::io::Result<Vec<u8>> {
299 let mut buf = Vec::with_capacity(1024);
300 File::open(path)?.take(1024).read_to_end(&mut buf)?;
301 Ok(buf)
302}
303
304/// Whether a file with an unknown extension is an image we can decode.
305pub fn sniff(path: &Path) -> bool {
306 let Ok(head) = head(path) else { return false };
307 // An extension guess alone is not proof; require the content to say so.
308 match detect(path, &head) {
309 Some(Kind::Image(_)) => image::guess_format(&head).is_ok(),
310 Some(Kind::Raw) => false,
311 Some(_) => true,
312 None => false,
313 }
314}
315
316fn label(format: ImageFormat) -> &'static str {
317 match format {
318 ImageFormat::Png => "PNG",
319 ImageFormat::Jpeg => "JPEG",
320 ImageFormat::Gif => "GIF",
321 ImageFormat::WebP => "WEBP",
322 ImageFormat::Pnm => "PNM",
323 ImageFormat::Tiff => "TIFF",
324 ImageFormat::Tga => "TGA",
325 ImageFormat::Dds => "DDS",
326 ImageFormat::Bmp => "BMP",
327 ImageFormat::Ico => "ICO",
328 ImageFormat::Hdr => "HDR",
329 ImageFormat::OpenExr => "EXR",
330 ImageFormat::Farbfeld => "FARBFELD",
331 ImageFormat::Avif => "AVIF",
332 ImageFormat::Qoi => "QOI",
333 _ => "IMAGE",
334 }
335}
336
337/// Decode `path` in full: every frame, colour-converted and upright.
338pub fn decode(path: &Path, opts: &Options) -> anyhow::Result<DecodedImage> {
339 let head = head(path)?;
340 let kind = detect(path, &head).ok_or_else(|| anyhow!("unknown format"))?;
341 match kind {
342 Kind::Image(f) => image_rs::decode_file(path, f, opts),
343 #[cfg(feature = "jxl")]
344 Kind::Jxl => jxl::decode(path, opts),
345 #[cfg(feature = "heif")]
346 Kind::Heif => heif::decode(path, opts),
347 #[cfg(feature = "svg")]
348 Kind::Svg => svg::decode(path, opts),
349 #[cfg(feature = "raw")]
350 Kind::Raw => raw::decode(path, opts),
351 Kind::External(d) => external::decode(path, &d, opts),
352 #[allow(unreachable_patterns)]
353 _ => Err(anyhow!("support for this format is not compiled in")),
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn known_extensions() {
363 assert!(is_known_extension(Path::new("a/b.JPG")));
364 assert!(is_known_extension(Path::new("b.png")));
365 assert!(!is_known_extension(Path::new("b.txt")));
366 assert!(!is_known_extension(Path::new("png")));
367 #[cfg(feature = "jxl")]
368 assert!(is_known_extension(Path::new("a.jxl")));
369 #[cfg(feature = "raw")]
370 assert!(is_known_extension(Path::new("a.NEF")));
371 }
372
373 #[test]
374 fn detection_prefers_magic() {
375 let p = Path::new("x.png");
376 assert_eq!(
377 detect(p, b"\xFF\xD8\xFF\xE0"),
378 Some(Kind::Image(ImageFormat::Jpeg))
379 );
380 let avif = b"\0\0\0\x1cftypavif\0\0\0\0avifmif1miaf";
381 assert_eq!(detect(p, avif), Some(Kind::Image(ImageFormat::Avif)));
382 #[cfg(feature = "heif")]
383 assert_eq!(
384 detect(p, b"\0\0\0\x18ftypheic\0\0\0\0mif1heic"),
385 Some(Kind::Heif)
386 );
387 #[cfg(feature = "jxl")]
388 assert_eq!(detect(p, b"\xFF\x0A\x00"), Some(Kind::Jxl));
389 #[cfg(feature = "svg")]
390 assert_eq!(
391 detect(p, b"<?xml version=\"1.0\"?>\n<svg xmlns=\"x\">"),
392 Some(Kind::Svg)
393 );
394 #[cfg(feature = "raw")]
395 assert_eq!(detect(Path::new("a.nef"), b"MM\0*"), Some(Kind::Raw));
396 assert_eq!(
397 detect(Path::new("a.tif"), b"MM\0*"),
398 Some(Kind::Image(ImageFormat::Tiff))
399 );
400 assert_eq!(detect(Path::new("a.txt"), b"hello"), None);
401 }
402
403 #[test]
404 fn decodes_png_without_extension() {
405 let dir = std::env::temp_dir().join(format!("remage-test-{}", std::process::id()));
406 std::fs::create_dir_all(&dir).unwrap();
407 let path = dir.join("noext");
408 image::RgbaImage::from_pixel(64, 32, image::Rgba([255, 0, 0, 128]))
409 .save_with_format(&path, ImageFormat::Png)
410 .unwrap();
411 assert!(sniff(&path));
412 let img = decode(&path, &Options::default()).unwrap();
413 assert_eq!(img.format, "PNG");
414 assert_eq!(img.source_size, [64, 32]);
415 assert_eq!(img.size, [64, 32]);
416 assert_eq!(img.pixels().len(), 64 * 32 * 4);
417 std::fs::remove_dir_all(&dir).unwrap();
418 }
419
420 /// Decoder tests against `tests/fixtures` (see `make.sh` there).
421 mod fixtures {
422 use super::*;
423
424 fn fixture(name: &str) -> std::path::PathBuf {
425 Path::new(env!("CARGO_MANIFEST_DIR"))
426 .join("tests/fixtures")
427 .join(name)
428 }
429
430 fn load(name: &str) -> DecodedImage {
431 decode(&fixture(name), &Options::default()).unwrap_or_else(|e| panic!("{name}: {e:#}"))
432 }
433
434 fn close(a: &[u8], b: [u8; 3], name: &str, what: &str) {
435 let ok = a[..3].iter().zip(b).all(|(x, y)| x.abs_diff(y) <= 40);
436 assert!(ok, "{name}: {what} is {:?}, expected {b:?}", &a[..4]);
437 }
438
439 /// Red, lime, blue and white quadrants, upright.
440 fn check_quadrants(img: &DecodedImage, frame: usize, name: &str) {
441 assert_eq!(img.size, [16, 12], "{name}");
442 let px = img.frame(frame);
443 let at = |x: usize, y: usize| &px[(y * 16 + x) * 4..][..4];
444 close(at(3, 3), [255, 0, 0], name, "top left");
445 close(at(12, 3), [0, 255, 0], name, "top right");
446 close(at(3, 9), [0, 0, 255], name, "bottom left");
447 close(at(12, 9), [255, 255, 255], name, "bottom right");
448 }
449
450 #[test]
451 fn every_format_decodes_upright() {
452 let mut names = vec![
453 "quadrants.png",
454 "quadrants.jpg",
455 "quadrants.bmp",
456 "quadrants.tga",
457 "quadrants.qoi",
458 "quadrants.ppm",
459 "quadrants.webp",
460 "quadrants.exr",
461 "quadrants.avif",
462 ];
463 if cfg!(feature = "jxl") {
464 names.push("quadrants.jxl");
465 }
466 if cfg!(feature = "heif") {
467 names.push("quadrants.heic");
468 }
469 if cfg!(feature = "svg") {
470 names.extend(["drawing.svg", "drawing.svgz"]);
471 }
472 for name in names {
473 assert!(is_known_extension(&fixture(name)), "{name}");
474 check_quadrants(&load(name), 0, name);
475 }
476 }
477
478 #[test]
479 fn exif_orientations() {
480 for i in 1..=8 {
481 let name = format!("orient-{i}.jpg");
482 check_quadrants(&load(&name), 0, &name);
483 }
484 // With auto_orient off, a rotated file stays as stored (12×16).
485 let opts = Options {
486 auto_orient: false,
487 ..Options::default()
488 };
489 let raw = decode(&fixture("orient-6.jpg"), &opts).unwrap();
490 assert_eq!(raw.size, [12, 16]);
491 }
492
493 #[test]
494 fn animations() {
495 let mut names = vec!["anim.gif", "anim.webp", "anim.png"];
496 if cfg!(feature = "jxl") {
497 names.push("anim.jxl");
498 }
499 for name in names {
500 let img = load(name);
501 assert!(img.animated, "{name}");
502 assert_eq!(img.frames.len(), 3, "{name}");
503 let ms: Vec<u128> = img.delays().iter().map(Duration::as_millis).collect();
504 // ImageMagick writes APNG at a constant frame rate.
505 let want = if name == "anim.png" {
506 [50, 50, 50]
507 } else {
508 [50, 100, 150]
509 };
510 assert_eq!(ms, want, "{name}");
511 for (f, c) in [[255, 0, 0], [0, 255, 0], [0, 0, 255]]
512 .into_iter()
513 .enumerate()
514 {
515 close(img.frame(f), c, name, &format!("frame {f}"));
516 }
517 }
518 }
519
520 #[test]
521 fn pages() {
522 let img = load("pages.tif");
523 assert!(!img.animated);
524 assert_eq!(img.frames.len(), 3);
525 close(img.frame(2), [0, 0, 255], "pages.tif", "page 3");
526 }
527
528 #[test]
529 fn icc_profiles_convert_to_srgb() {
530 use image::ImageEncoder;
531 let icc = moxcms::ColorProfile::new_display_p3().encode().unwrap();
532 let dir = std::env::temp_dir().join(format!("remage-icc-{}", std::process::id()));
533 std::fs::create_dir_all(&dir).unwrap();
534 let path = dir.join("p3.png");
535 let px = [40u8, 160, 90, 255].repeat(4);
536 {
537 let mut enc =
538 image::codecs::png::PngEncoder::new(std::fs::File::create(&path).unwrap());
539 enc.set_icc_profile(icc).unwrap();
540 enc.write_image(&px, 2, 2, image::ExtendedColorType::Rgba8)
541 .unwrap();
542 }
543 let raw = decode(
544 &path,
545 &Options {
546 color: false,
547 ..Options::default()
548 },
549 )
550 .unwrap();
551 assert_eq!(&raw.frame(0)[..4], [40, 160, 90, 255]);
552 let srgb = decode(&path, &Options::default()).unwrap();
553 let p = &srgb.frame(0)[..4];
554 // P3 green is more saturated than sRGB can say: red drops, green rises.
555 assert!(p[0] < 40 && p[1] > 160 && p[3] == 255, "{p:?}");
556 std::fs::remove_dir_all(&dir).unwrap();
557 }
558 }
559}