Provide thumbnailing

This commit is contained in:
Jeremy Soller 2025-07-11 11:27:48 -06:00
parent 9cc1660537
commit 55654e1231
No known key found for this signature in database
GPG key ID: 670FDFB5428E05CA
7 changed files with 128 additions and 11 deletions

View file

@ -1,7 +1,7 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::{fs, io};
use std::{fs, io, path::PathBuf};
use clap_lex::RawArgs;
use log::warn;
@ -25,9 +25,42 @@ pub fn parse() -> Arguments {
Err(os_str) => warn!("unexpected flag: -{}", os_str.to_string_lossy()),
}
}
} else if let Some((long, _opt_value)) = arg.to_long() {
} else if let Some((long, opt_value)) = arg.to_long() {
match long {
Ok("help") => print_help(),
Ok("size") => {
if let Some(value) = opt_value
.or_else(|| raw_args.next_os(&mut cursor))
.map(|x| x.to_string_lossy())
{
let mut parts = value.split('x');
let width_str = parts.next().unwrap_or("");
let width = match width_str.parse::<u32>() {
Ok(ok) => ok,
Err(err) => {
warn!("failed to parse size '{}': {}", value, err);
continue;
}
};
let height = match parts.next().unwrap_or(width_str).parse::<u32>() {
Ok(ok) => ok,
Err(err) => {
warn!("failed to parse size '{}': {}", value, err);
continue;
}
};
arguments.size_opt = Some((width, height));
} else {
warn!("size requires value");
}
}
Ok("thumbnail") => {
if let Some(value) = opt_value.or_else(|| raw_args.next_os(&mut cursor)) {
arguments.thumbnail_opt = Some(PathBuf::from(value));
} else {
warn!("thumbnail requires value");
}
}
Ok("version") => print_version(),
_ => warn!("unexpected flag: {}", arg.display()),
}
@ -61,6 +94,8 @@ pub struct Arguments {
pub urls: Option<Vec<Url>>,
/// Single URL only
pub url_opt: Option<Url>,
pub thumbnail_opt: Option<PathBuf>,
pub size_opt: Option<(u32, u32)>,
}
// #[derive(Debug)]
@ -113,8 +148,10 @@ libcosmic-based multimedia player for music and videos.
Project home page: https://github.com/pop-os/cosmic-player
Options:
-h, --help Show this message
-V, --version Show the version of cosmic-player"#
-h, --help Show this message
-V, --version Show the version of cosmic-player
--thumbnail <output> Generate thumbnail and save in output
--size <width>x<height> Thumbnail size in pixels"#
);
std::process::exit(0);

View file

@ -45,6 +45,7 @@ mod menu;
#[cfg(feature = "mpris-server")]
mod mpris;
mod project;
mod thumbnail;
static CONTROLS_TIMEOUT: Duration = Duration::new(2, 0);
@ -71,6 +72,25 @@ fn language_name(code: &str) -> Option<String> {
/// Runs application with these settings
#[rustfmt::skip]
fn main() -> Result<(), Box<dyn Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
let args = argparse::parse();
if let Some(output) = args.thumbnail_opt {
let Some(input) = args.url_opt else {
log::error!("thumbnailer can only handle exactly one URL");
process::exit(1);
};
match thumbnail::main(&input, &output, args.size_opt) {
Ok(()) => process::exit(0),
Err(err) => {
log::error!("failed to thumbnail '{}': {}", input, err);
process::exit(1);
}
}
}
#[cfg(all(unix, not(target_os = "redox")))]
match fork::daemon(true, true) {
Ok(fork::Fork::Child) => (),
@ -81,12 +101,8 @@ fn main() -> Result<(), Box<dyn Error>> {
}
}
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
localize::localize();
let args = argparse::parse();
let (config_handler, config) = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {
Ok(config_handler) => {
let config = match Config::get_entry(&config_handler) {

46
src/thumbnail.rs Normal file
View file

@ -0,0 +1,46 @@
use cosmic::iced_core::image::Data;
use iced_video_player::{Position, Video};
use image::{DynamicImage, ImageFormat, RgbaImage};
use std::{error::Error, num::NonZero, path::Path, time::Duration};
use url::Url;
pub fn main(
input: &Url,
output: &Path,
size_opt: Option<(u32, u32)>,
) -> Result<(), Box<dyn Error>> {
let mut image = {
let thumbnails = {
let mut video = Video::new(input)?;
let duration = video.duration();
//TODO: how best to decide time?
let position = if duration.as_secs_f64() < 20.0 {
// If less than 20 seconds, divide duration by 2
Position::Time(duration / 2)
} else {
// If more than 20 seconds, thumbnail at 10 seconds
Position::Time(Duration::new(10, 0))
};
video.thumbnails([position], NonZero::new(1).unwrap())?
};
//TODO: do not require clone of pixels data
match thumbnails[0].data() {
Data::Rgba {
width,
height,
pixels,
} => RgbaImage::from_raw(*width, *height, pixels.to_vec())
.map(DynamicImage::ImageRgba8)
.ok_or_else(|| format!("failed to convert thumbnail")),
_ => Err(format!("unsupported thumbnail handle {:?}", thumbnails[0])),
}
}?;
if let Some((width, height)) = size_opt {
image = image.thumbnail(width, height);
}
image.save_with_format(output, ImageFormat::Png)?;
Ok(())
}