Video thumbnails using totem-video-thumbnailer and improvements for SVGs

This commit is contained in:
Jeremy Soller 2024-09-25 11:30:17 -06:00
parent 181bed29a4
commit f352890283
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
2 changed files with 151 additions and 71 deletions

View file

@ -34,6 +34,7 @@ rayon = "1"
regex = "1" regex = "1"
serde = { version = "1", features = ["serde_derive"] } serde = { version = "1", features = ["serde_derive"] }
shlex = { version = "1.3" } shlex = { version = "1.3" }
tempfile = "3"
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "cosmic" } trash = { git = "https://github.com/jackpot51/trash-rs.git", branch = "cosmic" }
url = "2.5" url = "2.5"
@ -91,7 +92,6 @@ fork = "0.1"
# cap-std = "3" # cap-std = "3"
# cap-tempfile = "3" # cap-tempfile = "3"
fastrand = "2" fastrand = "2"
tempfile = "3"
test-log = "0.2" test-log = "0.2"
tokio = { version = "1", features = ["rt", "macros"] } tokio = { version = "1", features = ["rt", "macros"] }

View file

@ -42,6 +42,7 @@ use once_cell::sync::Lazy;
use recently_used_xbel::{Error, RecentlyUsed}; use recently_used_xbel::{Error, RecentlyUsed};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
borrow::Cow,
cell::{Cell, RefCell}, cell::{Cell, RefCell},
cmp::Ordering, cmp::Ordering,
collections::HashMap, collections::HashMap,
@ -50,6 +51,7 @@ use std::{
num::NonZeroU16, num::NonZeroU16,
os::unix::fs::MetadataExt, os::unix::fs::MetadataExt,
path::{Path, PathBuf}, path::{Path, PathBuf},
process,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::{Duration, Instant, SystemTime}, time::{Duration, Instant, SystemTime},
}; };
@ -393,11 +395,9 @@ pub fn item_from_entry(
let open_with = mime_apps(&mime); let open_with = mime_apps(&mime);
let thumbnail_opt = if mime.type_() == mime::IMAGE { let thumbnail_opt = if mime.type_() == mime::IMAGE {
if mime.subtype() == mime::SVG { None
Some(ItemThumbnail::Svg) } else if mime.type_() == mime::VIDEO {
} else { None
None
}
} else { } else {
Some(ItemThumbnail::NotImage) Some(ItemThumbnail::NotImage)
}; };
@ -938,8 +938,8 @@ impl ItemMetadata {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ItemThumbnail { pub enum ItemThumbnail {
NotImage, NotImage,
Rgba(image::RgbaImage, (u32, u32)), Rgba(image::RgbaImage, Option<(u32, u32)>),
Svg, Svg(Cow<'static, [u8]>),
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -984,19 +984,21 @@ impl Item {
.unwrap_or(&ItemThumbnail::NotImage) .unwrap_or(&ItemThumbnail::NotImage)
{ {
ItemThumbnail::NotImage => icon, ItemThumbnail::NotImage => icon,
ItemThumbnail::Rgba(_, _) => { ItemThumbnail::Rgba(rgba, _) => {
if let Some(Location::Path(path)) = &self.location_opt { if let Some(Location::Path(path)) = &self.location_opt {
widget::image(widget::image::Handle::from_path(path)).into() if self.mime.type_() == mime::IMAGE {
} else { return widget::image(widget::image::Handle::from_path(path)).into();
icon }
} }
widget::image(widget::image::Handle::from_pixels(
rgba.width(),
rgba.height(),
rgba.as_raw().clone(),
))
.into()
} }
ItemThumbnail::Svg => { ItemThumbnail::Svg(data) => {
if let Some(Location::Path(path)) = &self.location_opt { widget::svg(widget::svg::Handle::from_memory(data.clone())).into()
widget::Svg::from_path(path).into()
} else {
icon
}
} }
} }
} }
@ -1064,19 +1066,13 @@ impl Item {
widget::button::icon(widget::icon::from_name("go-next-symbolic")) widget::button::icon(widget::icon::from_name("go-next-symbolic"))
.on_press(app::Message::TabMessage(None, Message::ItemRight)), .on_press(app::Message::TabMessage(None, Message::ItemRight)),
); );
match self
.thumbnail_opt if self.mime.type_() == mime::IMAGE {
.as_ref() if let Some(path) = self.path_opt() {
.unwrap_or(&ItemThumbnail::NotImage) row = row.push(
{ widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic"))
ItemThumbnail::NotImage => {} .on_press(app::Message::TabMessage(None, Message::Gallery(true))),
ItemThumbnail::Rgba(_, _) | ItemThumbnail::Svg => { );
if let Some(path) = self.path_opt() {
row = row.push(
widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic"))
.on_press(app::Message::TabMessage(None, Message::Gallery(true))),
);
}
} }
} }
column = column.push(row); column = column.push(row);
@ -1158,7 +1154,7 @@ impl Item {
.as_ref() .as_ref()
.unwrap_or(&ItemThumbnail::NotImage) .unwrap_or(&ItemThumbnail::NotImage)
{ {
ItemThumbnail::Rgba(_, (width, height)) => { ItemThumbnail::Rgba(_, Some((width, height))) => {
details = details.push(widget::text(format!("{}x{}", width, height))); details = details.push(widget::text(format!("{}x{}", width, height)));
} }
_ => {} _ => {}
@ -2248,16 +2244,25 @@ impl Tab {
let location = Location::Path(path); let location = Location::Path(path);
for item in items.iter_mut() { for item in items.iter_mut() {
if item.location_opt.as_ref() == Some(&location) { if item.location_opt.as_ref() == Some(&location) {
if let ItemThumbnail::Rgba(rgba, _) = &thumbnail { //TODO: pass handles already generated to avoid blocking main thread?
//TODO: pass handles already generated to avoid blocking main thread match &thumbnail {
let handle = widget::icon::from_raster_pixels( ItemThumbnail::NotImage => {}
rgba.width(), ItemThumbnail::Rgba(rgba, _) => {
rgba.height(), let handle = widget::icon::from_raster_pixels(
rgba.as_raw().clone(), rgba.width(),
); rgba.height(),
item.icon_handle_grid = handle.clone(); rgba.as_raw().clone(),
item.icon_handle_list = handle.clone(); );
item.icon_handle_list_condensed = handle; item.icon_handle_grid = handle.clone();
item.icon_handle_list = handle.clone();
item.icon_handle_list_condensed = handle;
}
ItemThumbnail::Svg(data) => {
let handle = widget::icon::from_svg_bytes(data.clone());
item.icon_handle_grid = handle.clone();
item.icon_handle_list = handle.clone();
item.icon_handle_list_condensed = handle.clone();
}
} }
item.thumbnail_opt = Some(thumbnail); item.thumbnail_opt = Some(thumbnail);
break; break;
@ -2610,7 +2615,7 @@ impl Tab {
.unwrap_or(&ItemThumbnail::NotImage) .unwrap_or(&ItemThumbnail::NotImage)
{ {
ItemThumbnail::NotImage => {} ItemThumbnail::NotImage => {}
ItemThumbnail::Rgba(_, _) => { ItemThumbnail::Rgba(rgba, _) => {
if let Some(path) = item.path_opt() { if let Some(path) = item.path_opt() {
image_opt = Some( image_opt = Some(
//TODO: use widget::image::viewer, when its zoom can be reset //TODO: use widget::image::viewer, when its zoom can be reset
@ -2619,18 +2624,28 @@ impl Tab {
.height(Length::Fill) .height(Length::Fill)
.into(), .into(),
); );
} } else {
}
ItemThumbnail::Svg => {
if let Some(path) = item.path_opt() {
image_opt = Some( image_opt = Some(
widget::Svg::from_path(path) //TODO: use widget::image::viewer, when its zoom can be reset
.width(Length::Fill) widget::image(widget::image::Handle::from_pixels(
.height(Length::Fill) rgba.width(),
.into(), rgba.height(),
rgba.as_raw().clone(),
))
.width(Length::Fill)
.height(Length::Fill)
.into(),
); );
} }
} }
ItemThumbnail::Svg(data) => {
image_opt = Some(
widget::svg(widget::svg::Handle::from_memory(data.clone()))
.width(Length::Fill)
.height(Length::Fill)
.into(),
);
}
} }
} }
} }
@ -3901,36 +3916,101 @@ impl Tab {
} }
if let Some(Location::Path(path)) = item.location_opt.clone() { if let Some(Location::Path(path)) = item.location_opt.clone() {
let mime = item.mime.clone();
subscriptions.push(subscription::channel( subscriptions.push(subscription::channel(
path.clone(), path.clone(),
1, 1,
|mut output| async move { |mut output| async move {
let (path, thumbnail) = tokio::task::spawn_blocking(move || { let (path, thumbnail) = tokio::task::spawn_blocking(move || {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let thumbnail = match image::io::Reader::open(&path) //TODO: configurable thumbnail size?
.and_then(|img| img.with_guessed_format()) let thumbnail_size = (ICON_SIZE_GRID * ICON_SCALE_MAX) as u32;
{ let thumbnail = if mime.type_() == mime::IMAGE && mime.subtype() == mime::SVG {
Ok(reader) => match reader.decode() { //TODO: have a reasonable limit on SVG size?
Ok(image) => { match fs::read(&path) {
//TODO: configurable thumbnail size? Ok(data) => {
let thumbnail_size = //TODO: validate SVG data
(ICON_SIZE_GRID * ICON_SCALE_MAX) as u32; ItemThumbnail::Svg(data.into())
let thumbnail = },
image.thumbnail(thumbnail_size, thumbnail_size);
ItemThumbnail::Rgba(
thumbnail.to_rgba8(),
(image.width(), image.height()),
)
}
Err(err) => { Err(err) => {
log::warn!("failed to decode {:?}: {}", path, err); log::warn!("failed to read {:?}: {}", path, err);
ItemThumbnail::NotImage ItemThumbnail::NotImage
} }
},
Err(err) => {
log::warn!("failed to read {:?}: {}", path, err);
ItemThumbnail::NotImage
} }
} else if mime.type_() == mime::IMAGE {
match image::io::Reader::open(&path)
.and_then(|img| img.with_guessed_format())
{
Ok(reader) => match reader.decode() {
Ok(image) => {
let thumbnail =
image.thumbnail(thumbnail_size, thumbnail_size);
ItemThumbnail::Rgba(
thumbnail.to_rgba8(),
Some((image.width(), image.height())),
)
}
Err(err) => {
log::warn!("failed to decode {:?}: {}", path, err);
ItemThumbnail::NotImage
}
},
Err(err) => {
log::warn!("failed to read {:?}: {}", path, err);
ItemThumbnail::NotImage
}
}
} else if mime.type_() == mime::VIDEO {
//TODO: also support other optional video thumbnailers, using /usr/share/thumbnailers?
match tempfile::NamedTempFile::with_prefix("cosmic-files") {
Ok(file) => {
match process::Command::new("totem-video-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 totem-video-thumbnailer for {:?}: {}", path, status);
ItemThumbnail::NotImage
},
Err(err) => {
log::warn!("failed to run totem-video-thumbnailer for {:?}: {}", path, err);
ItemThumbnail::NotImage
}
}
},
Err(err) => {
log::warn!("failed to create temporary file for thumbnail of {:?}: {}", path, err);
ItemThumbnail::NotImage
}
}
} else {
log::warn!("cannot thumbnail {:?}: mime type {:?} not supported", path, mime.type_());
ItemThumbnail::NotImage
}; };
log::info!("thumbnailed {:?} in {:?}", path, start.elapsed()); log::info!("thumbnailed {:?} in {:?}", path, start.elapsed());
(path, thumbnail) (path, thumbnail)