Video thumbnails using totem-video-thumbnailer and improvements for SVGs
This commit is contained in:
parent
181bed29a4
commit
f352890283
2 changed files with 151 additions and 71 deletions
|
|
@ -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"] }
|
||||||
|
|
||||||
|
|
|
||||||
220
src/tab.rs
220
src/tab.rs
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue