#![windows_subsystem = "windows"] use anyhow::{anyhow, bail, Result}; use clap::{Arg, Command}; use fast_image_resize as fr; use image::{io::Reader as ImageReader, DynamicImage}; use log::info; use native_dialog::{FileDialog, MessageDialog}; use rexiv2::Metadata; use rust_embed::RustEmbed; use simplelog::{ColorChoice, ConfigBuilder, LevelFilter, TermLogger, TerminalMode}; use std::{fs::File, io::BufWriter, num::NonZeroU32, path::PathBuf, thread::JoinHandle}; use tempfile::tempdir; use warp::Filter; use wry::{ application::{ event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::WindowBuilder, }, webview::WebViewBuilder, }; #[derive(RustEmbed)] #[folder = "embed/"] struct Embed; fn main() -> Result<()> { // Init log TermLogger::init( LevelFilter::Info, ConfigBuilder::new() .set_time_offset_to_local() .unwrap() .build(), TerminalMode::Mixed, ColorChoice::Auto, )?; let tmp_dir = tempdir()?; let cmd = Command::new("Simple panorama viewer") .arg( Arg::new("filename") .value_parser(clap::value_parser!(PathBuf)) .help("Image path"), ) .get_matches(); let img_path = if let Some(img_path) = cmd.get_one::("filename") { img_path.clone() } else { let user_dirs = directories::UserDirs::new().unwrap(); let dir = if let Some(img_dir) = user_dirs.picture_dir() { img_dir } else { user_dirs.home_dir() }; FileDialog::new() .add_filter( "Images", &[ "jpg", "JPG", "jpeg", "pjpeg", "pjpg", "PJPG", "webp", "avif", ], ) .set_location(dir) .show_open_single_file()? .ok_or_else(|| anyhow!("No file"))? }; if !img_path.exists() { MessageDialog::new() .set_title("Image error") .set_text(&format!("Image `{}` does not exist", img_path.display())) .set_type(native_dialog::MessageType::Error) .show_alert()?; bail!("File `{}` does not exist !", img_path.display()); } info!("Open `{}`", img_path.display()); let (width, height) = image::image_dimensions(&img_path)?; const MAX_IMG_SIZE: u32 = 20_000_000; info!("Image size {} * {}", width, height); let img_data_path = if height * width > MAX_IMG_SIZE { // @todo utilisation de rfd au lieu de message_dialog // @todo Option pas de resize / taille en ligne de commande // @todo Option pas de resize / taille dans une config (confy) // @todo Demande ensergistre l’action la première fois // @todo Option always en ligne de commnade // @todo Icon let ratio = (height * width) as f64 / MAX_IMG_SIZE as f64; let new_height = (height as f64 / ratio.sqrt()) as u32; let new_width = (width as f64 / ratio.sqrt()) as u32; if MessageDialog::new() .set_title("Resize") .set_text(&format!( "Resize the file to {} × {}", new_width, new_height )) .show_confirm()? { // Resize image let img = ImageReader::open(&img_path)?.decode()?; info!("Resize image to {} × {}", new_width, new_height); let image_resized = fast_image_resize(&img, new_width, new_height)?; // Save file and add metadata let tmp_img_path = tmp_dir.path().join("img.jpg"); image_resized.write_to( &mut BufWriter::new(File::create(&tmp_img_path)?), image::ImageOutputFormat::Jpeg(95), )?; let metadata = Metadata::new_from_path(&img_path)?; metadata.save_to_file(&tmp_img_path)?; tmp_img_path } else { img_path } } else { img_path }; info!("Run server"); run_server(img_data_path); info!("Create webview"); let event_loop = EventLoop::new(); let window = WindowBuilder::new() .with_title("Simple panorama viewer") .with_maximized(true) .build(&event_loop)?; let _webview = WebViewBuilder::new(window)? .with_url("http://127.0.0.1:62371/index.html")? .build()?; info!("Event loop"); event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; match event { Event::NewEvents(StartCause::Init) => info!("Panorama open"), Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { info!("Close"); *control_flow = ControlFlow::Exit } _ => (), } }); } fn run_server(img_path: PathBuf) -> JoinHandle<()> { std::thread::spawn(|| { info!("Create runtime"); let async_runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); info!("Create response"); let index = warp::path("index.html").map(|| { info!("Request `index.html`"); let datas = get_file_data("index.html"); warp::reply::html(datas) }); let css = warp::path!("css" / String).map(move |val: String| { info!("Request css `{}`", &val); let datas = get_file_data(&format!("css/{}", val)); warp::http::Response::builder().body(datas) }); let js = warp::path!("js" / String).map(move |val: String| { info!("Request js `{}`", &val); let datas = get_file_data(&format!("js/{}", val)); warp::http::Response::builder().body(datas) }); let img = warp::path!("img").map(move || std::fs::read(&img_path).unwrap()); info!("Launch webserver"); async_runtime .block_on(warp::serve(img.or(index).or(css).or(js)).run(([127, 0, 0, 1], 62371))); }) } fn get_file_data(filename: &str) -> String { if let Some(file) = Embed::get(filename) { std::str::from_utf8(file.data.as_ref()) .unwrap_or("") .to_string() } else { "".to_string() } } /// Fast resize pub fn fast_image_resize( img: &DynamicImage, max_width: u32, max_height: u32, ) -> Result { // Create source image let width = NonZeroU32::new(img.width()).unwrap(); let height = NonZeroU32::new(img.height()).unwrap(); let src_image = fr::Image::from_vec_u8(width, height, img.to_rgb8().into_raw(), fr::PixelType::U8x3)?; let mut src_view = src_image.view(); // Create container for data of destination image let (dst_width, dst_height) = if width > height { ( NonZeroU32::new(max_width).unwrap(), NonZeroU32::new((max_width * img.height()) / img.width()).unwrap(), ) } else { ( NonZeroU32::new((max_height * img.width()) / img.height()).unwrap(), NonZeroU32::new(max_height).unwrap(), ) }; src_view.set_crop_box_to_fit_dst_size(dst_width, dst_height, None); let mut dst_image = fr::Image::new(dst_width, dst_height, src_view.pixel_type()); // Get mutable view of destination image data let mut dst_view = dst_image.view_mut(); // Create Resizer instance and resize source image into buffer of destination image let mut resizer = fr::Resizer::new(fr::ResizeAlg::Convolution(fr::FilterType::Lanczos3)); resizer.resize(&src_view, &mut dst_view)?; // Create Dynamic image let img_buffer = image::RgbImage::from_raw( dst_image.width().get(), dst_image.height().get(), dst_image.into_vec(), ) .unwrap(); Ok(img_buffer.into()) }