SimplePanoramaViewer/src/main.rs

240 lines
7.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![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::<PathBuf>("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 laction 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<DynamicImage> {
// 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())
}