SimplePanoramaViewer/src/main.rs

200 lines
6.4 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, Result};
use base64::Engine;
use clap::{Arg, Command};
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, path::PathBuf, thread::JoinHandle, num::NonZeroU32};
use tempfile::tempdir;
use warp::Filter;
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
webview::WebViewBuilder,
};
use fast_image_resize as fr;
#[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"))?
};
info!("Open `{}`", img_path.display());
let (width, height) = image::image_dimensions(&img_path)?;
const MAX_IMG_SIZE: u32 = 20_000_000;
let img_data_path = if height * width > MAX_IMG_SIZE {
// @todo Option pas de resize / taille en ligne de commande
// @todo Option pas de resize / taille dans une config (confy)
// @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
};
let img_data = std::fs::read(&img_data_path)?;
tmp_dir.close()?;
let img_base_64 = base64::engine::general_purpose::STANDARD.encode(img_data);
info!("Generate HTML");
let html = std::str::from_utf8(Embed::get("index.html").unwrap().data.as_ref())?
.replace("__BASE_64_IMG__", &img_base_64);
run_server(html);
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")?
.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(html: String) -> JoinHandle<()> {
std::thread::spawn(|| {
let async_runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let hello = warp::any().map(move || warp::reply::html(html.clone()));
async_runtime.block_on(warp::serve(hello).run(([127, 0, 0, 1], 62371)));
})
}
/// 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())
}