Skip to content

Commit 2d3b7ad

Browse files
authored
Merge pull request #1 from PluginMike/heic-raw-photos
Heic raw photos
2 parents ffa506f + 0ad55b6 commit 2d3b7ad

9 files changed

Lines changed: 40 additions & 18 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,30 +71,30 @@ bind = SUPER, E, exec, whale-cabinet
7171
Needs Rust (via [rustup](https://rustup.rs)), Node ≥ 20, WebKitGTK 4.1 and the usual build tools.
7272
Required at runtime (install.sh checks): zoxide (jump box, Frequent), fd (quick open), git (badges), gvfs with its
7373
smb/dav/nfs backends (network locations) and polkit (administrator actions).
74-
The optional tools light up extra features: poppler (PDF previews), ffmpegthumbnailer + ffmpeg (video thumbnails,
74+
The optional tools light up extra features: poppler (PDF previews), libvips (HEIC and camera RAW/DNG photos), ffmpegthumbnailer + ffmpeg (video thumbnails,
7575
media details), libarchive/zip/7-Zip (compress/extract), wl-clipboard (copy/paste with other apps), udisks2 (devices),
7676
libsecret's secret-tool (remembering network passwords).
7777

7878
**Arch / CachyOS / Manjaro**
7979
```sh
8080
sudo pacman -S --needed base-devel webkit2gtk-4.1 curl wget file openssl librsvg libappindicator-gtk3 nodejs npm rustup
8181
sudo pacman -S --needed zoxide fd git gvfs gvfs-smb gvfs-dnssd gvfs-nfs polkit
82-
sudo pacman -S --needed poppler ffmpegthumbnailer ffmpeg libarchive zip 7zip wl-clipboard udisks2 # optional
82+
sudo pacman -S --needed poppler libvips libheif libraw ffmpegthumbnailer ffmpeg libarchive zip 7zip wl-clipboard udisks2 # optional
8383
```
8484

8585
**Fedora**
8686
```sh
8787
sudo dnf install webkit2gtk4.1-devel openssl-devel curl wget file libappindicator-gtk3-devel librsvg2-devel libxdo-devel nodejs npm
8888
sudo dnf group install c-development
8989
sudo dnf install zoxide fd-find git gvfs gvfs-smb gvfs-fuse polkit
90-
sudo dnf install poppler-utils ffmpegthumbnailer ffmpeg-free bsdtar zip p7zip wl-clipboard udisks2 # optional
90+
sudo dnf install poppler-utils vips-tools vips-heif ffmpegthumbnailer ffmpeg-free bsdtar zip p7zip wl-clipboard udisks2 # optional
9191
```
9292

9393
**Debian / Ubuntu**
9494
```sh
9595
sudo apt install build-essential libwebkit2gtk-4.1-dev libssl-dev libxdo-dev libayatana-appindicator3-dev librsvg2-dev curl wget file nodejs npm
9696
sudo apt install zoxide fd-find git gvfs gvfs-backends gvfs-fuse pkexec
97-
sudo apt install poppler-utils ffmpegthumbnailer ffmpeg libarchive-tools zip 7zip wl-clipboard udisks2 # optional
97+
sudo apt install poppler-utils libvips-tools ffmpegthumbnailer ffmpeg libarchive-tools zip 7zip wl-clipboard udisks2 # optional
9898
```
9999

100100
Then:
@@ -287,7 +287,7 @@ selects the extra copies keeping the newest, the oldest or the one in a folder y
287287

288288
- Image, video and PDF thumbnails load lazily in the list and grid, using the shared freedesktop cache
289289
(`~/.cache/thumbnails`, same as Dolphin/Nautilus). Missing ones are generated (videos need `ffmpegthumbnailer`,
290-
PDFs `pdftoppm` from poppler).
290+
PDFs `pdftoppm` from poppler, HEIC and RAW/DNG photos `vipsthumbnail` from libvips).
291291
- The info panel (F11) previews the selection: images, video/audio players, the first page of PDFs,
292292
syntax-highlighted text/code, and rendered Markdown (tables, task lists, code blocks, relative images and links;
293293
"View source" toggles). Markdown is sanitized before it's shown.

‎package-lock.json‎

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "whale-cabinet",
33
"private": true,
4-
"version": "1.0.0",
4+
"version": "1.1.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

‎src-tauri/Cargo.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src-tauri/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "whale-cabinet"
3-
version = "1.0.0"
3+
version = "1.1.0"
44
edition = "2021"
55
description = "A Linux file manager with a flat filing-cabinet look"
66
license = "GPL-3.0-or-later"

‎src-tauri/src/preview.rs‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ fn kind(path: &Path) -> &'static str {
3333
"png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp" | "tif" | "tiff" | "ico" => "image",
3434
"mp4" | "mkv" | "webm" | "avi" | "mov" | "wmv" | "flv" | "m4v" | "mpg" | "mpeg" => "video",
3535
"pdf" => "pdf",
36+
// image-rs can't decode these; libvips can (libheif / libraw), and applies the EXIF rotation
37+
"heic" | "heif" | "dng" | "raw" | "cr2" | "cr3" | "nef" | "arw" | "raf" | "orf" | "rw2" | "pef" | "srw" => "photo",
3638
_ => match infer::get_from_path(path).ok().flatten().map(|t| t.matcher_type()) {
3739
Some(infer::MatcherType::Image) => "image",
3840
Some(infer::MatcherType::Video) => "video",
@@ -45,18 +47,20 @@ fn kind(path: &Path) -> &'static str {
4547
fn render(src: &Path, size: u32) -> Result<image::DynamicImage, String> {
4648
match kind(src) {
4749
"image" => image::open(src).map(|i| i.thumbnail(size, size)).map_err(|e| e.to_string()),
48-
k @ ("video" | "pdf") => {
50+
k @ ("video" | "pdf" | "photo") => {
4951
let tmp = tempfile_path();
5052
let ok = if k == "video" {
5153
Command::new("ffmpegthumbnailer").arg("-i").arg(src).arg("-o").arg(&tmp).args(["-s", &size.to_string(), "-c", "png"]).output()
54+
} else if k == "photo" {
55+
Command::new("vipsthumbnail").arg(src).arg("-o").arg(&tmp).args(["-s", &format!("{size}>")]).output() // ">": shrink only
5256
} else {
5357
// pdftoppm appends ".png" to the output root with -singlefile
5458
Command::new("pdftoppm").args(["-png", "-singlefile", "-f", "1", "-l", "1", "-scale-to", &size.to_string()]).arg(src).arg(tmp.with_extension("")).output()
5559
}
56-
.map_err(|e| format!("{e} (is {} installed?)", if k == "video" { "ffmpegthumbnailer" } else { "poppler" }))?;
60+
.map_err(|e| format!("{e} (is {} installed?)", match k { "video" => "ffmpegthumbnailer", "photo" => "libvips", _ => "poppler" }))?;
5761
let img = image::open(&tmp).map_err(|e| format!("{}: {e}", String::from_utf8_lossy(&ok.stderr).trim()));
5862
let _ = fs::remove_file(&tmp);
59-
img.map(|i| i.thumbnail(size, size))
63+
if k == "photo" { img } else { img.map(|i| i.thumbnail(size, size)) }
6064
}
6165
_ => Err("no thumbnailer for this type".into()),
6266
}
@@ -264,6 +268,18 @@ trailer<</Root 1 0 R>>\n%%EOF\n").unwrap();
264268
assert!(thumbnail(&root, &img, 800).unwrap().starts_with(root.join("whale-cabinet/previews/800")));
265269
}
266270

271+
#[test]
272+
fn heic_through_libvips() {
273+
let t = tempfile::tempdir().unwrap();
274+
let (png, heic) = (t.path().join("a.png"), t.path().join("IMG_0001.HEIC"));
275+
image::DynamicImage::new_rgb8(600, 300).save(&png).unwrap();
276+
if Command::new("vips").arg("copy").arg(&png).arg(&heic).status().map_or(true, |s| !s.success()) {
277+
return eprintln!("skipped: no libvips with HEIF");
278+
}
279+
let th = thumbnail(&t.path().join("cache"), &heic, 1200).unwrap();
280+
assert_eq!(image::image_dimensions(&th).unwrap(), (600, 300), "never upscaled");
281+
}
282+
267283
#[test]
268284
fn failures_are_remembered() {
269285
let t = tempfile::tempdir().unwrap();

‎src-tauri/tauri.conf.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"productName": "Whale Cabinet",
33
"mainBinaryName": "whale-cabinet",
4-
"version": "1.0.0",
4+
"version": "1.1.0",
55
"identifier": "whale-cabinet",
66
"build": {
77
"beforeDevCommand": "npm run dev",

‎src/preview.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,12 @@ export function copyButton(text: () => string, label = "Copy") {
138138
}
139139
const tools = (...bs: HTMLElement[]) => { const t = document.createElement("div"); t.className = "pv-tools"; t.append(...bs); return t; };
140140

141-
// WebKit can't show these itself (they still get thumbnails where a thumbnailer can)
142-
const NO_WEB = new Set(["heic", "heif", "raw", "cr2", "nef", "tif", "tiff"]);
141+
// WebKit can't show these itself: the viewer shows a big decoded PNG from the thumbnailer (libvips / image-rs)
142+
const NO_WEB = new Set("heic heif raw dng cr2 cr3 nef arw raf orf rw2 pef srw tif tiff".split(" "));
143143
const TEXTY = new Set(["pdf", "md", "markdown", "txt", "log", "csv", "conf", "ini", "desktop"]);
144144
/** Can the viewer show this one properly? (decides "open in the viewer" for Enter / double-click) */
145145
export const previewable = (e: Entry) => !e.dir && !e.special && !e.broken && !e.trashId &&
146-
(!!e.preview || (["img", "vid", "aud", "code"].includes(kindOf(e)) && !NO_WEB.has(ext(e))) || TEXTY.has(ext(e)));
146+
(!!e.preview || ["img", "vid", "aud", "code"].includes(kindOf(e)) || TEXTY.has(ext(e)));
147147

148148
/** Every page of a PDF, each rendered (poppler) as it scrolls near. */
149149
async function pdfPages(e: Entry) {
@@ -178,6 +178,11 @@ async function previewOf(e: Entry, big: boolean): Promise<Big | null> {
178178
const url = assetUrl(e.path);
179179
if (e.preview) { const i = new Image(); i.src = e.preview; i.draggable = false; return { el: i }; }
180180
if (e.dir || e.special || e.broken) return null;
181+
if (k === "img" && NO_WEB.has(x)) {
182+
const p = await invoke<string>("thumbnail", { path: e.path, size: big ? 2400 : 600 }).catch(() => "");
183+
if (!p) return null;
184+
const i = new Image(); i.src = assetUrl(p); i.draggable = false; return { el: i };
185+
}
181186
if (k === "img") { const i = new Image(); i.src = url; i.draggable = false; return { el: i }; }
182187
if (k === "vid") { const v = document.createElement("video"); v.src = url; v.controls = true; v.preload = "metadata"; return { el: v }; }
183188
if (k === "aud") { const a = document.createElement("audio"); a.src = url; a.controls = true; a.preload = "metadata"; return { el: a }; }

‎src/util.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const fmtDate = (ms: number) => (ms ? dateFmt.format(ms) : "");
3333

3434
export const KINDS: Record<string, string> = {};
3535
for (const [k, list] of Object.entries({
36-
img: "png jpg jpeg gif webp svg bmp tiff tif avif heic ico jxl raw cr2 nef",
36+
img: "png jpg jpeg gif webp svg bmp tiff tif avif heic heif ico jxl raw dng cr2 cr3 nef arw raf orf rw2 pef srw",
3737
vid: "mp4 mkv webm avi mov wmv flv m4v mpg mpeg",
3838
aud: "mp3 flac ogg opus wav m4a aac wma",
3939
arc: "zip tar gz xz bz2 zst 7z rar tgz iso deb rpm appimage",

0 commit comments

Comments
 (0)