Generically support external thumbnailers
This commit is contained in:
parent
e236023561
commit
898823f69c
3 changed files with 214 additions and 57 deletions
|
|
@ -24,6 +24,7 @@ mod operation;
|
||||||
mod spawn_detached;
|
mod spawn_detached;
|
||||||
use tab::Location;
|
use tab::Location;
|
||||||
pub mod tab;
|
pub mod tab;
|
||||||
|
mod thumbnailer;
|
||||||
|
|
||||||
pub(crate) fn err_str<T: ToString>(err: T) -> String {
|
pub(crate) fn err_str<T: ToString>(err: T) -> String {
|
||||||
err.to_string()
|
err.to_string()
|
||||||
|
|
|
||||||
105
src/tab.rs
105
src/tab.rs
|
|
@ -68,6 +68,7 @@ use crate::{
|
||||||
mime_icon::{mime_for_path, mime_icon},
|
mime_icon::{mime_for_path, mime_icon},
|
||||||
mounter::Mounters,
|
mounter::Mounters,
|
||||||
mouse_area,
|
mouse_area,
|
||||||
|
thumbnailer::thumbnailer,
|
||||||
};
|
};
|
||||||
use unix_permissions_ext::UNIXPermissionsExt;
|
use unix_permissions_ext::UNIXPermissionsExt;
|
||||||
use uzers::{get_group_by_gid, get_user_by_uid};
|
use uzers::{get_group_by_gid, get_user_by_uid};
|
||||||
|
|
@ -937,100 +938,90 @@ pub enum ItemThumbnail {
|
||||||
impl ItemThumbnail {
|
impl ItemThumbnail {
|
||||||
pub fn new(path: &Path, mime: mime::Mime, thumbnail_size: u32) -> Self {
|
pub fn new(path: &Path, mime: mime::Mime, thumbnail_size: u32) -> Self {
|
||||||
if mime.type_() == mime::IMAGE && mime.subtype() == mime::SVG {
|
if mime.type_() == mime::IMAGE && mime.subtype() == mime::SVG {
|
||||||
|
// Try built-in svg thumbnailer
|
||||||
//TODO: have a reasonable limit on SVG size?
|
//TODO: have a reasonable limit on SVG size?
|
||||||
match fs::read(&path) {
|
match fs::read(&path) {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
//TODO: validate SVG data
|
//TODO: validate SVG data
|
||||||
ItemThumbnail::Svg(data.into())
|
return ItemThumbnail::Svg(data.into());
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to read {:?}: {}", path, err);
|
log::warn!("failed to read {:?}: {}", path, err);
|
||||||
ItemThumbnail::NotImage
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if mime.type_() == mime::IMAGE {
|
} else if mime.type_() == mime::IMAGE {
|
||||||
|
// Try built-in image thumbnailer
|
||||||
match image::io::Reader::open(&path).and_then(|img| img.with_guessed_format()) {
|
match image::io::Reader::open(&path).and_then(|img| img.with_guessed_format()) {
|
||||||
Ok(reader) => match reader.decode() {
|
Ok(reader) => match reader.decode() {
|
||||||
Ok(image) => {
|
Ok(image) => {
|
||||||
let thumbnail = image.thumbnail(thumbnail_size, thumbnail_size);
|
let thumbnail = image.thumbnail(thumbnail_size, thumbnail_size);
|
||||||
ItemThumbnail::Rgba(
|
return ItemThumbnail::Rgba(
|
||||||
thumbnail.to_rgba8(),
|
thumbnail.to_rgba8(),
|
||||||
Some((image.width(), image.height())),
|
Some((image.width(), image.height())),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to decode {:?}: {}", path, err);
|
log::warn!("failed to decode {:?}: {}", path, err);
|
||||||
ItemThumbnail::NotImage
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to read {:?}: {}", path, err);
|
log::warn!("failed to read {:?}: {}", path, err);
|
||||||
ItemThumbnail::NotImage
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
//TODO: also support other external thumbnailers, using /usr/share/thumbnailers?
|
|
||||||
let (thumbnailer, prefix) = if mime.type_() == mime::VIDEO {
|
// Try external thumbnailers
|
||||||
("totem-video-thumbnailer", "cosmic-files-")
|
for thumbnailer in thumbnailer(&mime) {
|
||||||
} else if mime == mime::APPLICATION_PDF {
|
let prefix = if thumbnailer.exec.starts_with("evince-thumbnailer ") {
|
||||||
//TODO: apparmor config for evince-thumbnailer does not allow /tmp/cosmic-files*
|
//TODO: apparmor config for evince-thumbnailer does not allow /tmp/cosmic-files*
|
||||||
("evince-thumbnailer", "gnome-desktop-")
|
"gnome-desktop-"
|
||||||
} else {
|
} else {
|
||||||
return ItemThumbnail::NotImage;
|
"cosmic-files-"
|
||||||
};
|
};
|
||||||
match tempfile::NamedTempFile::with_prefix(prefix) {
|
let file = match tempfile::NamedTempFile::with_prefix(prefix) {
|
||||||
Ok(file) => {
|
Ok(ok) => ok,
|
||||||
match process::Command::new(thumbnailer)
|
|
||||||
.arg("-l")
|
|
||||||
.arg("-s")
|
|
||||||
.arg(format!("{}", thumbnail_size))
|
|
||||||
.arg(&path)
|
|
||||||
.arg(file.path())
|
|
||||||
.status()
|
|
||||||
{
|
|
||||||
Ok(status) => {
|
|
||||||
if status.success() {
|
|
||||||
match image::io::Reader::open(file.path())
|
|
||||||
.and_then(|img| img.with_guessed_format())
|
|
||||||
{
|
|
||||||
Ok(reader) => match reader.decode() {
|
|
||||||
Ok(image) => ItemThumbnail::Rgba(image.to_rgba8(), None),
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to decode {:?}: {}", path, err);
|
|
||||||
ItemThumbnail::NotImage
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to read {:?}: {}", path, err);
|
|
||||||
ItemThumbnail::NotImage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::warn!(
|
|
||||||
"failed to run {} for {:?}: {}",
|
|
||||||
thumbnailer,
|
|
||||||
path,
|
|
||||||
status
|
|
||||||
);
|
|
||||||
ItemThumbnail::NotImage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to run {} for {:?}: {}", thumbnailer, path, err);
|
|
||||||
ItemThumbnail::NotImage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"failed to create temporary file for thumbnail of {:?}: {}",
|
"failed to create temporary file for thumbnail of {:?}: {}",
|
||||||
path,
|
path,
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
ItemThumbnail::NotImage
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(mut command) = thumbnailer.command(&path, file.path(), thumbnail_size) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match command.status() {
|
||||||
|
Ok(status) => {
|
||||||
|
if status.success() {
|
||||||
|
match image::io::Reader::open(file.path())
|
||||||
|
.and_then(|img| img.with_guessed_format())
|
||||||
|
{
|
||||||
|
Ok(reader) => match reader.decode() {
|
||||||
|
Ok(image) => {
|
||||||
|
return ItemThumbnail::Rgba(image.to_rgba8(), None);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to decode {:?}: {}", path, err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to read {:?}: {}", path, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("failed to run {:?} for {:?}: {}", thumbnailer, path, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to run {:?} for {:?}: {}", thumbnailer, path, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ItemThumbnail::NotImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
165
src/thumbnailer.rs
Normal file
165
src/thumbnailer.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
use mime_guess::Mime;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
collections::HashMap,
|
||||||
|
env, fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process,
|
||||||
|
sync::Mutex,
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Thumbnailer {
|
||||||
|
pub exec: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Thumbnailer {
|
||||||
|
pub fn command(
|
||||||
|
&self,
|
||||||
|
input: &Path,
|
||||||
|
output: &Path,
|
||||||
|
thumbnail_size: u32,
|
||||||
|
) -> Option<process::Command> {
|
||||||
|
let args_vec: Vec<String> = shlex::split(&self.exec)?;
|
||||||
|
let mut args = args_vec.iter();
|
||||||
|
let mut command = process::Command::new(args.next()?);
|
||||||
|
for arg in args {
|
||||||
|
if arg.starts_with('%') {
|
||||||
|
match arg.as_str() {
|
||||||
|
"%f" | "%F" | "%u" | "%U" => {
|
||||||
|
command.arg(input);
|
||||||
|
}
|
||||||
|
"%o" => {
|
||||||
|
command.arg(output);
|
||||||
|
}
|
||||||
|
"%s" => {
|
||||||
|
command.arg(format!("{}", thumbnail_size));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::warn!(
|
||||||
|
"unsupported thumbnailer Exec code {:?} in {:?}",
|
||||||
|
arg,
|
||||||
|
self.exec
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
command.arg(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ThumbnailerCache {
|
||||||
|
cache: HashMap<Mime, Vec<Thumbnailer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThumbnailerCache {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut thumbnailer_cache = Self {
|
||||||
|
cache: HashMap::new(),
|
||||||
|
};
|
||||||
|
thumbnailer_cache.reload();
|
||||||
|
thumbnailer_cache
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reload(&mut self) {
|
||||||
|
use crate::localize::LANGUAGE_SORTER;
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
self.cache.clear();
|
||||||
|
|
||||||
|
let mut search_dirs = Vec::new();
|
||||||
|
match xdg::BaseDirectories::new() {
|
||||||
|
Ok(xdg_dirs) => {
|
||||||
|
search_dirs.push(xdg_dirs.get_data_home().join("thumbnailers"));
|
||||||
|
for data_dir in xdg_dirs.get_data_dirs() {
|
||||||
|
search_dirs.push(data_dir.join("thumbnailers"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to get xdg base directories: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut thumbnailer_paths = Vec::new();
|
||||||
|
for dir in search_dirs {
|
||||||
|
log::trace!("looking for thumbnailers in {:?}", dir);
|
||||||
|
match fs::read_dir(&dir) {
|
||||||
|
Ok(entries) => {
|
||||||
|
for entry_res in entries {
|
||||||
|
match entry_res {
|
||||||
|
Ok(entry) => thumbnailer_paths.push(entry.path()),
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to read entry in directory {:?}: {}", dir, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to read directory {:?}: {}", dir, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: handle directory specific behavior
|
||||||
|
for path in thumbnailer_paths {
|
||||||
|
let entry = match freedesktop_entry_parser::parse_entry(&path) {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("failed to parse {:?}: {}", path, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//TODO: use TryExec?
|
||||||
|
let section = entry.section("Thumbnailer Entry");
|
||||||
|
let Some(exec) = section.attr("Exec") else {
|
||||||
|
log::warn!("missing Exec attribute for thumbnailer {:?}", path);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(mime_types) = section.attr("MimeType") else {
|
||||||
|
log::warn!("missing MimeType attribute for thumbnailer {:?}", path);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
for mime_type in mime_types.split_terminator(';') {
|
||||||
|
if let Ok(mime) = mime_type.parse::<Mime>() {
|
||||||
|
log::trace!("thumbnailer {}={:?}", mime, path);
|
||||||
|
let apps = self
|
||||||
|
.cache
|
||||||
|
.entry(mime.clone())
|
||||||
|
.or_insert_with(|| Vec::with_capacity(1));
|
||||||
|
apps.push(Thumbnailer {
|
||||||
|
exec: exec.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
log::info!("loaded thumbnailer cache in {:?}", elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, key: &Mime) -> Vec<Thumbnailer> {
|
||||||
|
self.cache
|
||||||
|
.get(&key)
|
||||||
|
.map_or_else(|| Vec::new(), |x| x.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static THUMBNAILER_CACHE: Lazy<Mutex<ThumbnailerCache>> =
|
||||||
|
Lazy::new(|| Mutex::new(ThumbnailerCache::new()));
|
||||||
|
|
||||||
|
pub fn thumbnailer(mime: &Mime) -> Vec<Thumbnailer> {
|
||||||
|
let thumbnailer_cache = THUMBNAILER_CACHE.lock().unwrap();
|
||||||
|
thumbnailer_cache.get(mime)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue