Krolyxon
Branch master

Branches

Tags

Refs →

remagesrcdecodemod.rs

591 lines · 19.5 KB · Rust

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