Skip to main content

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}