hyprforge_image/format.rs
1//! Which formats *this build* can decode.
2//!
3//! Asked of `image` rather than kept as a list here, because the answer
4//! is decided by cargo features in the workspace root and a second list
5//! would be a second opinion — right until someone switched a feature on
6//! and forgot this file.
7//!
8//! What it is for: a viewer needs to know whether a file is worth opening
9//! before it opens it (for the filmstrip, which meets a folder of mixed
10//! things), and the `.desktop` file must claim exactly the MIME types the
11//! binary can actually open. A `.desktop` that claims `image/webp` while
12//! the decoder is absent is a file association that opens a window and
13//! shows an error.
14//!
15//! # The trap in `reading_enabled`
16//!
17//! `image`'s own `ImageFormat::reading_enabled()` answers `cfg!(feature =
18//! "avif")` for AVIF — but that feature is the *encoder* (ravif).
19//! Decoding AVIF needs `avif-native` and the system libdav1d. So if
20//! anyone ever enables `avif` to write one, this module would start
21//! claiming AVIF is readable and the viewer would advertise a MIME type
22//! it cannot open. [`decodable_formats`] filters that case out by hand,
23//! and `tests::avif_is_never_reported_as_decodable` fails if the
24//! situation changes.
25
26use std::path::Path;
27
28/// Every format this build can actually decode.
29pub fn decodable_formats() -> Vec<image::ImageFormat> {
30 image::ImageFormat::all()
31 .filter(|f| f.reading_enabled())
32 // See the module doc: `reading_enabled` keys AVIF off the
33 // encoder feature. Until `avif-native` and libdav1d are a
34 // deliberate decision, AVIF is not something this can open.
35 .filter(|f| *f != image::ImageFormat::Avif)
36 .collect()
37}
38
39/// Every MIME type this build can decode, sorted, for the `.desktop`
40/// file's `MimeType=` line and for matching against a shared-MIME lookup.
41pub fn decodable_mime_types() -> Vec<&'static str> {
42 let mut types: Vec<&'static str> =
43 decodable_formats().into_iter().map(|f| f.to_mime_type()).collect();
44 types.sort_unstable();
45 types.dedup();
46 types
47}
48
49/// Whether this path *looks* like a picture this build can open.
50///
51/// By extension, and deliberately: this is asked once per entry while
52/// building a filmstrip for a folder, and opening every file in a
53/// directory to read a magic number turns listing it into as many opens
54/// as there are files — the same reasoning
55/// `hyprforge_listing::types::EntryKind::classify` gives for classifying
56/// by name.
57///
58/// So it can be wrong, in one direction that matters: a file this says
59/// yes to may still fail to decode, and the caller has to be ready for
60/// that. It is never the *only* check.
61pub fn looks_decodable(path: &Path) -> bool {
62 image::ImageFormat::from_path(path).map(|f| decodable_formats().contains(&f)).unwrap_or(false)
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 /// The two the suite has always had. If these ever come back false,
70 /// something removed a feature from the workspace root and every app
71 /// that draws a wallpaper broke with it.
72 #[test]
73 fn png_and_jpeg_are_always_decodable() {
74 assert!(looks_decodable(Path::new("/a/photo.png")));
75 assert!(looks_decodable(Path::new("/a/photo.jpg")));
76 assert!(looks_decodable(Path::new("/a/photo.JPEG")));
77 }
78
79 /// The guard from the module doc. This test is the thing that turns
80 /// a silent lie into a red build if someone enables `avif` for
81 /// encoding without `avif-native`.
82 #[test]
83 fn avif_is_never_reported_as_decodable() {
84 assert!(!decodable_formats().contains(&image::ImageFormat::Avif));
85 assert!(!looks_decodable(Path::new("/a/photo.avif")));
86 assert!(!decodable_mime_types().contains(&"image/avif"));
87 }
88
89 /// HEIC is not a format `image` supports under any feature, so a
90 /// viewer must never claim it — the file manager's own
91 /// `EntryKind::classify` calls `.heic` an image, which is what makes
92 /// this worth pinning rather than assuming.
93 #[test]
94 fn heic_is_not_claimed_either() {
95 assert!(!looks_decodable(Path::new("/a/photo.heic")));
96 }
97
98 #[test]
99 fn a_file_that_is_not_a_picture_is_not_claimed() {
100 assert!(!looks_decodable(Path::new("/a/notes.txt")));
101 assert!(!looks_decodable(Path::new("/a/clip.mp4")));
102 assert!(!looks_decodable(Path::new("/a/no-extension")));
103 }
104
105 /// What the `.desktop` file's `MimeType=` line is built from — it has
106 /// to be non-empty and every entry has to look like a MIME type.
107 #[test]
108 fn the_mime_list_is_usable_in_a_desktop_entry() {
109 let types = decodable_mime_types();
110 assert!(types.contains(&"image/png"));
111 assert!(types.contains(&"image/jpeg"));
112 for t in &types {
113 assert!(t.contains('/'), "{t} is not a mime type");
114 }
115 }
116}