SimplePanoramaViewer/src/main.rs

240 lines
7.8 KiB
Rust
Raw Normal View History

2022-09-21 20:45:16 +00:00
#![windows_subsystem = "windows"]
2023-11-06 20:41:19 +00:00
use anyhow::{anyhow, bail, Result};
2022-09-21 20:45:16 +00:00
use clap::{Arg, Command};
2023-11-05 19:57:38 +00:00
use fast_image_resize as fr;
use image::{io::Reader as ImageReader, DynamicImage};
2022-09-05 12:37:55 +00:00
use log::info;
2022-09-21 20:45:16 +00:00
use native_dialog::{FileDialog, MessageDialog};
use rexiv2::Metadata;
2022-09-05 12:16:10 +00:00
use rust_embed::RustEmbed;
2022-09-18 19:37:36 +00:00
use simplelog::{ColorChoice, ConfigBuilder, LevelFilter, TermLogger, TerminalMode};
2023-11-05 19:57:38 +00:00
use std::{fs::File, io::BufWriter, num::NonZeroU32, path::PathBuf, thread::JoinHandle};
2022-09-21 20:45:16 +00:00
use tempfile::tempdir;
2022-09-18 19:37:36 +00:00
use warp::Filter;
2022-09-05 12:16:10 +00:00
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,
2022-09-05 12:37:55 +00:00
ConfigBuilder::new()
2022-09-18 19:37:36 +00:00
.set_time_offset_to_local()
.unwrap()
2022-09-05 12:37:55 +00:00
.build(),
2022-09-05 12:16:10 +00:00
TerminalMode::Mixed,
ColorChoice::Auto,
)?;
2022-09-21 20:45:16 +00:00
let tmp_dir = tempdir()?;
let cmd = Command::new("Simple panorama viewer")
2023-11-05 19:57:38 +00:00
.arg(
Arg::new("filename")
.value_parser(clap::value_parser!(PathBuf))
.help("Image path"),
)
2022-09-21 20:45:16 +00:00
.get_matches();
let img_path = if let Some(img_path) = cmd.get_one::<PathBuf>("filename") {
img_path.clone()
2022-09-05 12:16:10 +00:00
} else {
2022-09-05 12:37:55 +00:00
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()
};
2022-09-05 12:16:10 +00:00
FileDialog::new()
2022-09-05 12:37:55 +00:00
.add_filter(
"Images",
&[
"jpg", "JPG", "jpeg", "pjpeg", "pjpg", "PJPG", "webp", "avif",
],
)
2022-09-21 20:45:16 +00:00
.set_location(dir)
.show_open_single_file()?
2022-09-05 12:16:10 +00:00
.ok_or_else(|| anyhow!("No file"))?
};
2023-11-06 20:41:19 +00:00
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());
}
2022-09-05 12:37:55 +00:00
info!("Open `{}`", img_path.display());
2022-09-21 20:45:16 +00:00
let (width, height) = image::image_dimensions(&img_path)?;
const MAX_IMG_SIZE: u32 = 20_000_000;
2023-11-06 20:41:19 +00:00
info!("Image size {} * {}", width, height);
2022-09-21 20:45:16 +00:00
let img_data_path = if height * width > MAX_IMG_SIZE {
2023-10-16 19:35:11 +00:00
// @todo utilisation de rfd au lieu de message_dialog
2022-09-21 20:45:16 +00:00
// @todo Option pas de resize / taille en ligne de commande
// @todo Option pas de resize / taille dans une config (confy)
2023-10-16 19:35:11 +00:00
// @todo Demande ensergistre laction la première fois
// @todo Option always en ligne de commnade
2022-09-21 20:45:16 +00:00
// @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
))
2022-09-21 20:45:16 +00:00
.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)?;
2022-09-21 20:45:16 +00:00
// 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
};
2022-09-05 12:37:55 +00:00
2023-11-06 20:41:19 +00:00
info!("Run server");
run_server(img_data_path);
2022-09-05 12:16:10 +00:00
2022-09-05 12:37:55 +00:00
info!("Create webview");
2022-09-05 12:16:10 +00:00
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Simple panorama viewer")
2022-09-05 12:37:55 +00:00
.with_maximized(true)
2022-09-05 12:16:10 +00:00
.build(&event_loop)?;
2022-09-18 19:37:36 +00:00
let _webview = WebViewBuilder::new(window)?
2023-11-05 19:57:38 +00:00
.with_url("http://127.0.0.1:62371/index.html")?
2022-09-18 19:37:36 +00:00
.build()?;
2022-09-05 12:16:10 +00:00
2022-09-05 12:37:55 +00:00
info!("Event loop");
2022-09-05 12:16:10 +00:00
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
2022-09-05 12:37:55 +00:00
Event::NewEvents(StartCause::Init) => info!("Panorama open"),
2022-09-05 12:16:10 +00:00
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
2022-09-05 12:37:55 +00:00
} => {
info!("Close");
*control_flow = ControlFlow::Exit
}
2022-09-05 12:16:10 +00:00
_ => (),
}
});
}
2022-09-18 19:37:36 +00:00
2023-11-06 20:41:19 +00:00
fn run_server(img_path: PathBuf) -> JoinHandle<()> {
2022-09-18 19:37:36 +00:00
std::thread::spawn(|| {
2023-10-16 19:35:11 +00:00
info!("Create runtime");
2022-09-18 19:37:36 +00:00
let async_runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
2023-10-16 19:35:11 +00:00
info!("Create response");
2023-11-06 20:41:19 +00:00
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)
});
2023-11-05 19:57:38 +00:00
let img = warp::path!("img").map(move || std::fs::read(&img_path).unwrap());
2023-10-16 19:35:11 +00:00
info!("Launch webserver");
2023-11-06 20:41:19 +00:00
async_runtime
.block_on(warp::serve(img.or(index).or(css).or(js)).run(([127, 0, 0, 1], 62371)));
2022-09-18 19:37:36 +00:00
})
}
2023-11-06 20:41:19 +00:00
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())
}