- Commit
- 9983aa825f36680ea6f3a3dde1dbe25e604a7b15
- Parent
- 3bb8e16c40b3805213d1bc10e570c53c0528c0d7
- Author
- Pablo <pablo-pie@riseup.net>
- Date
Refactored the error handling
Custum build of stapix for tikz.pablopie.xyz
Refactored the error handling
3 files changed, 497 insertions, 548 deletions
| Status | Name | Changes | Insertions | Deletions |
| Modified | Cargo.lock | 2 files changed | 2 | 2 |
| Modified | Cargo.toml | 2 files changed | 1 | 1 |
| Modified | src/main.rs | 2 files changed | 494 | 545 |
diff --git a/Cargo.lock b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler" @@ -615,7 +615,7 @@ dependencies = [ [[package]] name = "tikz_gallery_generator" -version = "0.1.0" +version = "0.2.0" dependencies = [ "crossterm", "image",
diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tikz_gallery_generator" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "GPLv3"
diff --git a/src/main.rs b/src/main.rs @@ -1,15 +1,15 @@ use crossterm::style::Stylize; use image::{DynamicImage, io::Reader as ImageReader}; use std::{ - cmp::min, - env, - fmt::{self, Display}, - fs::{self, File}, - io::{self, Write}, - path::PathBuf, - process::{ExitCode, Command}, - sync::mpsc, - os::unix, + cmp::min, + env, + fmt::{self, Display}, + fs::{self, File}, + io::{self, Write}, + path::{Path, PathBuf}, + process::{ExitCode, Command}, + sync::mpsc, + os::unix, }; use gallery_entry::{GalleryEntry, FileFormat, LicenseType}; use threadpool::ThreadPool; @@ -27,13 +27,6 @@ pub struct Escaped<'a>(pub &'a str); /// A wrapper to display lists of command line arguments struct ArgList<'a>(pub &'a [String]); -#[derive(Clone, Copy, PartialEq, Eq)] -enum RenderResult { - Skipped, - Success, - Failure, -} - const FULL_BUILD_OPT: &str = "--full-build"; const TARGET_PATH: &str = "./site"; @@ -58,598 +51,554 @@ const WEBP_IMAGE_QUALITY: f32 = 90.0; const THUMB_HEIGHT: u32 = 500; fn main() -> ExitCode { - infoln!("Running {package} version {version}", - package = env!("CARGO_PKG_NAME"), - version = env!("CARGO_PKG_VERSION")); + infoln!("Running {package} version {version}", + package = env!("CARGO_PKG_NAME"), + version = env!("CARGO_PKG_VERSION")); - let args: Vec<String> = env::args().collect(); + let args: Vec<String> = env::args().collect(); - let (program, config, full_build) = match &args[..] { - [program, config] => (program, config, false), - [program, config, opt] if opt == FULL_BUILD_OPT => { - (program, config, true) - } - [program, _config, ..] => { - errorln!("Unknown arguments: {}", ArgList(&args[2..])); - usage!(program); - return ExitCode::FAILURE; - } - [program] => { - errorln!("Expected 1 command line argument, found none"); - usage!(program); - return ExitCode::FAILURE; - } - [] => unreachable!("args always contains at least the input program"), - }; - - let f = File::open(config); - match f.map(serde_yaml::from_reader::<_, Vec<GalleryEntry>>) { - // Error opening the config file - Err(err) => { - errorln!("Couldn't open {config:?}: {err}"); - usage!(program); - ExitCode::FAILURE - } - // Error parsing the config file - Ok(Err(err)) => { - errorln!("Couldn't parse {config:?}: {err}"); - usage_config!(); - ExitCode::FAILURE - } - Ok(Ok(pics)) => render_gallery(pics, full_build), + let (program, config, full_build) = match &args[..] { + [program, config] => (program, config, false), + [program, config, opt] if opt == FULL_BUILD_OPT => { + (program, config, true) } + [program, _config, ..] => { + errorln!("Unknown arguments: {}", ArgList(&args[2..])); + usage!(program); + return ExitCode::FAILURE; + } + [program] => { + errorln!("Expected 1 command line argument, found none"); + usage!(program); + return ExitCode::FAILURE; + } + [] => unreachable!("args always contains at least the input program"), + }; + + let f = File::open(config); + match f.map(serde_yaml::from_reader::<_, Vec<GalleryEntry>>) { + Err(err) => { + errorln!("Couldn't open {config:?}: {err}"); + usage!(program); + return ExitCode::FAILURE; + } + Ok(Err(err)) => { + errorln!("Couldn't parse {config:?}: {err}"); + usage_config!(); + return ExitCode::FAILURE; + } + Ok(Ok(pics)) => if render_gallery(pics, full_build).is_err() { + return ExitCode::FAILURE; + }, + } + + ExitCode::SUCCESS } /// Coordinates the rendering of all the pages and file conversions -fn render_gallery(pics: Vec<GalleryEntry>, full_build: bool) -> ExitCode { - info!("Copying image files to the target directory..."); - - for pic in &pics { - let mut target_path = PathBuf::from(TARGET_PATH); - target_path.push(IMAGES_PATH); - target_path.push(&pic.file_name); - - if let Err(err) = fs::copy(&pic.path, &target_path) { - errorln!( - "Couldn't copy file {src:?} to {target:?}: {err}", - src = pic.path, - target = target_path, - ); - return ExitCode::FAILURE; - } +fn render_gallery( + pics: Vec<GalleryEntry>, + full_build: bool +) -> Result<(), ()> { + info!("Copying image files to the target directory..."); + + for pic in &pics { + let mut target_path = PathBuf::from(TARGET_PATH); + target_path.push(IMAGES_PATH); + target_path.push(&pic.file_name); + + if let Err(e) = fs::copy(&pic.path, &target_path) { + errorln!( + "Couldn't copy file {src:?} to {target_path:?}: {e}", + src = pic.path, + ); + return Err(()); } + } - info_done!(); + info_done!(); - // ======================================================================== - for pic in &pics { - if pic.alt.is_empty() { - warnln!( - "Empty text alternative was specified for the file {name:?}", - name = pic.file_name - ); - } + // ======================================================================== + for pic in &pics { + if pic.alt.is_empty() { + warnln!( + "Empty text alternative was specified for the file {name:?}", + name = pic.file_name + ); } + } - // ======================================================================== - let num_threads = min(num_cpus::get() + 1, pics.len()); - let rendering_pool = ThreadPool::with_name( - String::from("thumbnails renderer"), - num_threads - ); - let (sender, reciever) = mpsc::channel(); - - infoln!( "Started generating thumbnails (using {num_threads} threads)"); - - for pic in &pics { - let sender = sender.clone(); - let pic = pic.clone(); - rendering_pool.execute(move || { - sender.send(render_thumbnail(pic, full_build)) - .expect("channel should still be alive awaiting for the completion of this task"); - }); - } + // ======================================================================== + let num_threads = min(num_cpus::get() + 1, pics.len()); + let rendering_pool = ThreadPool::with_name( + String::from("thumbnails renderer"), + num_threads + ); + let (sender, reciever) = mpsc::channel(); - for _ in 0..pics.len() { - match reciever.recv() { - Ok(RenderResult::Failure) => return ExitCode::FAILURE, - Ok(RenderResult::Success | RenderResult::Skipped) => {} - Err(_) => { - // Propagate the panic to the main thread: reciever.recv should - // only fail if some of the rendering threads panicked - panic!("rendering thread panicked!"); - } - } - } + infoln!( "Started generating thumbnails (using {num_threads} threads)"); - infoln!("Done generating thumbnails!"); + for pic in &pics { + let thumb_path = thumb_path(pic); - // ======================================================================== - info!("Rendering index.html..."); - if render_index(&pics).is_err() { - return ExitCode::FAILURE; - } - info_done!(); - - for pic in pics { - info!("Rendering HTML page for {name:?}...", name = pic.file_name); - match render_pic_page(&pic, full_build) { - RenderResult::Success => info_done!(), - RenderResult::Skipped => { - info_done!("Skipped! (use {FULL_BUILD_OPT} to overwrite)"); - } - RenderResult::Failure => return ExitCode::FAILURE, - } - } - - ExitCode::SUCCESS -} + // Here we do not want to call fs::symlink_metada: we want to know when was + // the symlink last updated + let thumb_meta = fs::metadata(&thumb_path); -fn render_index(pics: &Vec<GalleryEntry>) -> io::Result<()> { - let mut path = PathBuf::from(TARGET_PATH); - path.push("index.html"); - - let mut f = File::create(path)?; - - writeln!(f, "<!DOCTYPE html>")?; - write_license(&mut f)?; - writeln!(f, "<html lang=\"en\">")?; - writeln!(f, "<head>")?; - writeln!(f, "<title>{PAGE_TITLE}</title>")?; - write_head(&mut f)?; - - // Preload the first 2 pictures in the gallery - for pic in pics.iter().take(2) { - writeln!( - f, - "<link rel=\"preload\" as=\"image\" href=\"{path}\">", - path = ThumbPath(pic), - )?; + if !full_build { + if let (Ok(thumb_m), Some(pic_m)) = (&thumb_meta, &pic.metadata) { + if thumb_m.modified().unwrap() > pic_m.modified().unwrap() { + warnln!( + "Skipped rendering the thumbnail for {name:?} (use {FULL_BUILD_OPT} to overwrite)", + name = pic.file_name + ); + continue; + } + } } - writeln!(f, "</head>")?; - - writeln!(f, "<body>")?; - - writeln!(f, "<main>")?; - writeln!(f, "{}", INTRO_MSG)?; - - writeln!(f, "<div id=\"gallery\" role=\"feed\">")?; - - for pic in pics { - writeln!(f, "<article class=\"picture-container\">")?; - writeln!( - f, - "<a aria-label=\"{name}\" href=\"/{PAGES_PATH}/{name}.html\">", - name = Escaped(&pic.file_name) - )?; - writeln!( - f, - "<img alt=\"{alt}\" src=\"{path}\">", - alt = Escaped(&pic.alt), - path = ThumbPath(pic), - )?; - writeln!(f, "</a>\n</article>")?; + let sender = sender.clone(); + let pic = pic.clone(); + + rendering_pool.execute(move || { + sender.send(render_thumbnail(pic, &thumb_path)) + .expect("channel should still be alive awaiting for the completion of this task"); + }); + } + + for _ in 0..pics.len() { + let msg = reciever.recv(); + if msg.is_err() { + // propagate the panic to the main thread: reciever.recv should + // only fail if some of the rendering threads panicked + panic!("rendering thread panicked!"); } + msg.unwrap()?; + } - writeln!(f, "</div>")?; + infoln!("Done generating thumbnails!"); - writeln!(f, "{}", OUTRO_MSG)?; - writeln!(f, "</main>")?; + // ======================================================================== + info!("Rendering index.html..."); + render_index(&pics).map_err(|_| ())?; + info_done!(); - writeln!(f, "<footer>")?; - writeln!( - f, - "made with 💚 by <a role=\"author\" href=\"https://pablopie.xyz\">@pablo</a>" - )?; - writeln!(f, "</footer>")?; - - writeln!(f, "</body>")?; - writeln!(f, "</html>") -} - -fn render_pic_page(pic: &GalleryEntry, full_build: bool) -> RenderResult { + for pic in pics { let mut path = PathBuf::from(TARGET_PATH); path.push(PAGES_PATH); path.push(pic.file_name.clone() + ".html"); - // Only try to re-render HTML page in case the page is older than the + // only try to re-render HTML page in case the page is older than the // image file + // TODO: measure how useful this optimization really is if !full_build { - if let (Ok(path_m), Some(pic_m)) = (fs::metadata(&path), &pic.metadata) { - if path_m.modified().unwrap() > pic_m.modified().unwrap() { - return RenderResult::Skipped; - } + if let (Ok(path_m), Some(pic_m)) = (fs::metadata(&path), &pic.metadata) { + if path_m.modified().unwrap() > pic_m.modified().unwrap() { + info!("Rendering HTML page for {name:?}...", name = pic.file_name); + continue; } + } } - let mut f = match File::create(&path) { - Ok(file) => file, - Err(err) => { - errorln!("Could not open file {path:?}: {err}"); - return RenderResult::Failure; - } - }; - - /// Does the deeds - fn write_file(f: &mut File, pic: &GalleryEntry) -> io::Result<()> { - writeln!(f, "<!DOCTYPE html>")?; - write_license(f)?; - writeln!(f, "<html lang=\"en\">")?; - writeln!(f, "<head>")?; - writeln!( - f, - "<title>{PAGE_TITLE} ‐ {name}</title>", - name = Escaped(&pic.file_name) - )?; - write_head(f)?; - writeln!( - f, - "<link rel=\"preload\" as=\"image\" href=\"{path}\">", - path = ThumbPath(pic), - )?; - writeln!(f, "</head>")?; - - writeln!(f, "<body>")?; - writeln!(f, "<main>")?; - writeln!( - f, - "<h1 class=\"picture-title\">{name}</h1>", - name = Escaped(&pic.file_name) - )?; - - if pic.caption.is_some() { - writeln!(f, "<figure>")?; - } else { - writeln!(f, "<figure aria-label=\"File {name}\">", - name = Escaped(&pic.file_name))?; - } - writeln!(f, "<div id=\"picture\">")?; - writeln!(f, "<div>")?; - - writeln!(f, "<div class=\"picture-container\">")?; - writeln!( - f, - "<img alt=\"{alt}\" src=\"{path}\">", - alt = Escaped(&pic.alt), - path = ThumbPath(pic), - )?; - writeln!(f, "</div>")?; - - writeln!(f, "<nav id=\"picture-nav\">")?; - writeln!(f, "<ul>")?; - writeln!( - f, - "<li><a href=\"/{IMAGES_PATH}/{name}\">download</a></li>", - name = Escaped(&pic.file_name), - )?; - if let Some(src) = &pic.source { - writeln!(f, "<li><a href=\"{src}\">original source</a></li>")?; - } - writeln!(f, "</ul>")?; - writeln!(f, "</nav>")?; - - writeln!(f, "</div>")?; - writeln!(f, "</div>")?; - if let Some(caption) = &pic.caption { - writeln!(f, "<figcaption>")?; - writeln!(f, "{}", Escaped(caption))?; - writeln!(f, "</figcaption>")?; - } - writeln!(f, "</figure>")?; - writeln!(f, "</main>")?; - - writeln!(f, "<footer>")?; - write!(f, "original work by ")?; - if let Some(url) = &pic.author_url { - writeln!(f, "<a role=\"author\" href=\"{url}\">{author}</a>", - author = Escaped(&pic.author))?; - } else { - writeln!(f, "{}", Escaped(&pic.author))?; - } - writeln!(f, "<br>")?; - match &pic.license { - LicenseType::Cc(license) => { - writeln!( - f, - "licensed under <a role=\"license\" href=\"{url}\">{license}</a>", - url = license.url() - )?; - } - LicenseType::PublicDomain => writeln!(f, "this is public domain")?, - LicenseType::Proprietary => { - writeln!( - f, - "this is distributed under a proprietary license" - )?; - } - } - writeln!(f, "</footer>")?; - - writeln!(f, "</body>")?; - writeln!(f, "</html>") - } + render_pic_page(&pic, &path).map_err(|_| ())?; + } - if let Err(err) = write_file(&mut f, pic) { - errorln!("Could not write to {path:?}: {err}"); - RenderResult::Failure - } else { - RenderResult::Success - } + Ok(()) } -/// Prints the common head elements to a given file -fn write_head(f: &mut File) -> io::Result<()> { - writeln!( - f, - "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">" - )?; - writeln!(f, "<meta name=\"author\" content=\"{AUTHOR}\">")?; - writeln!(f, "<meta name=\"copyright\" content=\"{LICENSE}\">")?; - writeln!( - f, - "<meta content=\"text/html; charset=utf-8\" http-equiv=\"content-type\">" - )?; - writeln!(f, - "<link rel=\"icon\" type=\"image/svg+xml\" sizes=\"16x16 24x24 32x32 48x48 64x64 128x128 256x256 512x512\" href=\"/{FAVICON_PATH}\">")?; - writeln!(f, "<link rel=\"stylesheet\" href=\"/{STYLES_PATH}\">")?; - writeln!(f, "<link rel=\"preload\" as=\"font\" href=\"/{FONTS_PATH}/alfa-slab.woff2\">") -} +fn render_index(pics: &Vec<GalleryEntry>) -> io::Result<()> { + let mut path = PathBuf::from(TARGET_PATH); + path.push("index.html"); -/// Prints a HTML comment with GPL licensing info -fn write_license(f: &mut File) -> io::Result<()> { - writeln!( - f, - "<!-- This program is free software: you can redistribute it and/or modify" - )?; - writeln!( - f, - " it under the terms of the GNU General Public License as published by" - )?; - writeln!( - f, - " the Free Software Foundation, either version 3 of the License, or" - )?; - writeln!(f, " (at your option) any later version.\n")?; - writeln!( - f, - " This program is distributed in the hope that it will be useful," - )?; + let mut f = create_file(&path)?; + + writeln!(f, "<!DOCTYPE html>")?; + write_license(&mut f)?; + writeln!(f, "<html lang=\"en\">")?; + writeln!(f, "<head>")?; + writeln!(f, "<title>{PAGE_TITLE}</title>")?; + write_head(&mut f)?; + + // Preload the first 2 pictures in the gallery + for pic in pics.iter().take(2) { writeln!( - f, - " but WITHOUT ANY WARRANTY; without even the implied warranty of" + f, + "<link rel=\"preload\" as=\"image\" href=\"{path}\">", + path = ThumbPath(pic), )?; + } + + writeln!(f, "</head>")?; + + writeln!(f, "<body>")?; + + writeln!(f, "<main>")?; + writeln!(f, "{}", INTRO_MSG)?; + + writeln!(f, "<div id=\"gallery\" role=\"feed\">")?; + + for pic in pics { + writeln!(f, "<article class=\"picture-container\">")?; writeln!( - f, - " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the" + f, + "<a aria-label=\"{name}\" href=\"/{PAGES_PATH}/{name}.html\">", + name = Escaped(&pic.file_name) )?; - writeln!(f, " GNU General Public License for more details.\n")?; writeln!( - f, - " You should have received a copy of the GNU General Public License" + f, + "<img alt=\"{alt}\" src=\"{path}\">", + alt = Escaped(&pic.alt), + path = ThumbPath(pic), )?; - writeln!( - f, - " along with this program. If not, see <https://www.gnu.org/licenses/>. -->" - ) -} + writeln!(f, "</a>\n</article>")?; + } -fn render_thumbnail(pic: GalleryEntry, full_build: bool) -> RenderResult { - let thumb_path = thumb_path(&pic); + writeln!(f, "</div>")?; - // Here we do not want to call fs::symlink_metada: we want to know when was - // the symlink last updated - let thumb_meta = fs::metadata(&thumb_path); + writeln!(f, "{}", OUTRO_MSG)?; + writeln!(f, "</main>")?; - if !full_build { - if let (Ok(thumb_m), Some(pic_m)) = (&thumb_meta, &pic.metadata) { - if thumb_m.modified().unwrap() > pic_m.modified().unwrap() { - warnln!( - "Skipped rendering the thumbnail for {name:?} (use {FULL_BUILD_OPT} to overwrite)", - name = pic.file_name - ); - return RenderResult::Skipped; - } - } + writeln!(f, "<footer>")?; + writeln!( + f, + "made with 💚 by <a role=\"author\" href=\"https://pablopie.xyz\">@pablo</a>" + )?; + writeln!(f, "</footer>")?; + + writeln!(f, "</body>")?; + writeln!(f, "</html>") +} + +fn render_pic_page(pic: &GalleryEntry, path: &Path) -> io::Result<()> { + let mut f = create_file(path)?; + + writeln!(&mut f, "<!DOCTYPE html>")?; + write_license(&mut f)?; + writeln!(&mut f, "<html lang=\"en\">")?; + writeln!(&mut f, "<head>")?; + writeln!( + &mut f, + "<title>{PAGE_TITLE} ‐ {name}</title>", + name = Escaped(&pic.file_name) + )?; + write_head(&mut f)?; + writeln!( + &mut f, + "<link rel=\"preload\" as=\"image\" href=\"{path}\">", + path = ThumbPath(pic), + )?; + writeln!(&mut f, "</head>")?; + + writeln!(&mut f, "<body>")?; + writeln!(&mut f, "<main>")?; + writeln!( + &mut f, + "<h1 class=\"picture-title\">{name}</h1>", + name = Escaped(&pic.file_name) + )?; + + if pic.caption.is_some() { + writeln!(&mut f, "<figure>")?; + } else { + writeln!(&mut f, "<figure aria-label=\"File {name}\">", + name = Escaped(&pic.file_name))?; + } + writeln!(&mut f, "<div id=\"picture\">")?; + writeln!(&mut f, "<div>")?; + + writeln!(&mut f, "<div class=\"picture-container\">")?; + writeln!( + &mut f, + "<img alt=\"{alt}\" src=\"{path}\">", + alt = Escaped(&pic.alt), + path = ThumbPath(pic), + )?; + writeln!(&mut f, "</div>")?; + + writeln!(&mut f, "<nav id=\"picture-nav\">")?; + writeln!(&mut f, "<ul>")?; + writeln!( + &mut f, + "<li><a href=\"/{IMAGES_PATH}/{name}\">download</a></li>", + name = Escaped(&pic.file_name), + )?; + if let Some(src) = &pic.source { + writeln!(&mut f, "<li><a href=\"{src}\">original source</a></li>")?; + } + writeln!(&mut f, "</ul>")?; + writeln!(&mut f, "</nav>")?; + + writeln!(&mut f, "</div>")?; + writeln!(&mut f, "</div>")?; + if let Some(caption) = &pic.caption { + writeln!(&mut f, "<figcaption>")?; + writeln!(&mut f, "{}", Escaped(caption))?; + writeln!(&mut f, "</figcaption>")?; + } + writeln!(&mut f, "</figure>")?; + writeln!(&mut f, "</main>")?; + + writeln!(&mut f, "<footer>")?; + write!(&mut f, "original work by ")?; + if let Some(url) = &pic.author_url { + writeln!(&mut f, "<a role=\"author\" href=\"{url}\">{author}</a>", + author = Escaped(&pic.author))?; + } else { + writeln!(&mut f, "{}", Escaped(&pic.author))?; + } + writeln!(&mut f, "<br>")?; + match &pic.license { + LicenseType::Cc(license) => { + writeln!( + &mut f, + "licensed under <a role=\"license\" href=\"{url}\">{license}</a>", + url = license.url() + )?; } + LicenseType::PublicDomain => writeln!(&mut f, "this is public domain")?, + LicenseType::Proprietary => { + writeln!( + &mut f, + "this is distributed under a proprietary license" + )?; + } + } + writeln!(&mut f, "</footer>")?; - match pic.file_format { - FileFormat::TeX => { - // tikztosvg -o thumb_path - // -p relsize - // -p xfrac - // -l matrix - // -l patterns - // -l shapes.geometric - // -l arrows - // -q - // pic.path - let mut tikztosvg_cmd = Command::new("tikztosvg"); - tikztosvg_cmd.arg("-o") - .arg(thumb_path.clone()) - .args([ - "-p", "relsize", - "-p", "xfrac", - "-l", "matrix", - "-l", "patterns", - "-l", "shapes.geometric", - "-l", "arrows", - "-q", - ]) - .arg(pic.path); - - match tikztosvg_cmd.status() { - Ok(c) if !c.success() => { - errorln!( - "Failed to run tikztosvg: {command:?} returned exit code {code}", - command = tikztosvg_cmd, - code = c - ); - return RenderResult::Failure; - } - Err(err) => { - errorln!("Failed to run tikztosvg: {err}"); - return RenderResult::Failure; - } - _ => {} - } - }, - FileFormat::Svg => { - let mut src_path = PathBuf::from(TARGET_PATH); - src_path.push(IMAGES_PATH); - src_path.push(&pic.file_name); - - // Here we need the absolute path of the image to prevent issues - // with symlinks - let src_path = match fs::canonicalize(&src_path) { - Ok(path) => path, - Err(err) => { - errorln!( - "Failed to create symlink for {thumb:?}: Could not get absolute path of {src:?}: {err}", - thumb = thumb_path, - src = src_path, - err = err, - ); - return RenderResult::Failure; - } - }; - - // Delete the thumbnail file if it exists already: fs::symlink does - // not override files - if let Ok(true) = thumb_meta.map(|m| m.is_file() || m.is_symlink()) { - let _ = fs::remove_file(&thumb_path); - } - - if let Err(err) = unix::fs::symlink(&src_path, &thumb_path) { - errorln!( - "Failed to create symlink {thumb:?} -> {src:?}: {err}", - thumb = thumb_path, - src = src_path, - ); - return RenderResult::Failure; - } - }, - FileFormat::Jpeg | FileFormat::Png => { - let mut thumb_file = match File::create(&thumb_path) { - Ok(f) => f, - Err(err) => { - errorln!( - "Couldn't open thumbnail file {thumb_path:?}: {err}" - ); - return RenderResult::Failure; - } - }; - - let img_reader = match ImageReader::open(&pic.path) { - Ok(r) => r, - Err(err) => { - errorln!( - "Couldn't open file {path:?} to render thumbnail: {err}", - path = pic.file_name, - ); - return RenderResult::Failure; - } - }; - - let img = match img_reader.decode() { - Ok(img) => img, - Err(err) => { - errorln!( - "Faileded to decode image file {name:?}: {err}", - name = pic.file_name, - ); - return RenderResult::Failure; - } - }; - - let h = THUMB_HEIGHT; - let w = (h * img.width()) / img.height(); - - // We should make sure that the image is in the RGBA8 format so that - // the webp crate can encode it - let img = DynamicImage::from(img.thumbnail(w, h).into_rgba8()); - let mem = webp::Encoder::from_image(&img) - .expect("image should be in the RGBA8 format") - .encode(WEBP_IMAGE_QUALITY); - - if let Err(err) = thumb_file.write_all(&mem) { - errorln!( - "Couldn't write thumnail to file {path:?}: {err}", - path = thumb_path - ); - return RenderResult::Failure; - } - } + writeln!(&mut f, "</body>")?; + writeln!(&mut f, "</html>") +} + +/// Prints the common head elements to a given file +fn write_head(f: &mut File) -> io::Result<()> { + writeln!( + f, + "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">" + )?; + writeln!(f, "<meta name=\"author\" content=\"{AUTHOR}\">")?; + writeln!(f, "<meta name=\"copyright\" content=\"{LICENSE}\">")?; + writeln!( + f, + "<meta content=\"text/html; charset=utf-8\" http-equiv=\"content-type\">" + )?; + writeln!(f, + "<link rel=\"icon\" type=\"image/svg+xml\" sizes=\"16x16 24x24 32x32 48x48 64x64 128x128 256x256 512x512\" href=\"/{FAVICON_PATH}\">")?; + writeln!(f, "<link rel=\"stylesheet\" href=\"/{STYLES_PATH}\">")?; + writeln!(f, "<link rel=\"preload\" as=\"font\" href=\"/{FONTS_PATH}/alfa-slab.woff2\">") +} + +/// Prints a HTML comment with GPL licensing info +fn write_license(f: &mut File) -> io::Result<()> { + writeln!( + f, + "<!-- This program is free software: you can redistribute it and/or modify" + )?; + writeln!( + f, + " it under the terms of the GNU General Public License as published by" + )?; + writeln!( + f, + " the Free Software Foundation, either version 3 of the License, or" + )?; + writeln!(f, " (at your option) any later version.\n")?; + writeln!( + f, + " This program is distributed in the hope that it will be useful," + )?; + writeln!( + f, + " but WITHOUT ANY WARRANTY; without even the implied warranty of" + )?; + writeln!( + f, + " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the" + )?; + writeln!(f, " GNU General Public License for more details.\n")?; + writeln!( + f, + " You should have received a copy of the GNU General Public License" + )?; + writeln!( + f, + " along with this program. If not, see <https://www.gnu.org/licenses/>. -->" + ) +} + +fn render_thumbnail(pic: GalleryEntry, thumb_path: &Path) -> Result<(), ()> { + match pic.file_format { + FileFormat::TeX => { + // tikztosvg -o thumb_path + // -p relsize + // -p xfrac + // -l matrix + // -l patterns + // -l shapes.geometric + // -l arrows + // -q + // pic.path + let mut tikztosvg_cmd = Command::new("tikztosvg"); + tikztosvg_cmd.arg("-o") + .arg(thumb_path) + .args([ + "-p", "relsize", + "-p", "xfrac", + "-l", "matrix", + "-l", "patterns", + "-l", "shapes.geometric", + "-l", "arrows", + "-q", + ]) + .arg(pic.path); + + let exit_code = tikztosvg_cmd + .status() + .map_err(|e| errorln!("Failed to run tikztosvg: {e}"))?; + + if !exit_code.success() { + errorln!( + "Failed to run tikztosvg: {tikztosvg_cmd:?} returned exit code {exit_code}" + ); + return Err(()); + } + }, + FileFormat::Svg => { + let mut src_path = PathBuf::from(TARGET_PATH); + src_path.push(IMAGES_PATH); + src_path.push(&pic.file_name); + + // here we need the absolute path of the image to prevent issues + // with symlinks + let src_path = fs::canonicalize(&src_path) + .map_err(|e| { + errorln!( + "Failed to create symlink for {thumb_path:?}: Could not get absolute path of {src_path:?}: {e}" + ); + })?; + + // TODO: copy the file instead of using a symlink! + // delete the thumbnail file if it exists already: fs::symlink does + // not override files + if thumb_path.exists() { let _ = fs::remove_file(thumb_path); } + + if let Err(e) = unix::fs::symlink(&src_path, thumb_path) { + errorln!( + "Failed to create symlink {thumb_path:?} -> {src_path:?}: {e}" + ); + return Err(()); + } + }, + FileFormat::Jpeg | FileFormat::Png => { + let mut thumb_file = create_file(thumb_path).map_err(|_| ())?; + + let img_reader = ImageReader::open(&pic.path) + .map_err(|e| { + errorln!( + "Couldn't open file {path:?} to render thumbnail: {e}", + path = pic.file_name, + ); + })?; + + let img = img_reader + .decode() + .map_err(|e| { + errorln!( + "Faileded to decode image file {name:?}: {e}", + name = pic.file_name, + ); + })?; + + let h = THUMB_HEIGHT; + let w = (h * img.width()) / img.height(); + + // We should make sure that the image is in the RGBA8 format so that + // the webp crate can encode it + let img = DynamicImage::from(img.thumbnail(w, h).into_rgba8()); + let mem = webp::Encoder::from_image(&img) + .expect("image should be in the RGBA8 format") + .encode(WEBP_IMAGE_QUALITY); + + if let Err(e) = thumb_file.write_all(&mem) { + errorln!( "Couldn't write thumnail to file {thumb_path:?}: {e}"); + return Err(()); + } } + } - infoln!("Rendered thumbnail for {name:?}", name = pic.file_name); - RenderResult::Success + infoln!("Rendered thumbnail for {name:?}", name = pic.file_name); + Ok(()) } +// TODO: rename this? /// Helper to get the correct thumbnail path for a given entry fn thumb_path(pic: &GalleryEntry) -> PathBuf { - let mut result = PathBuf::from(TARGET_PATH); - result.push(THUMBS_PATH); + let mut result = PathBuf::from(TARGET_PATH); + result.push(THUMBS_PATH); - match pic.file_format { - FileFormat::TeX => { - result.push(pic.file_name.clone() + ".svg"); - } - FileFormat::Svg => { - result.push(pic.file_name.clone()); - } - FileFormat::Jpeg | FileFormat::Png => { - result.push(pic.file_name.clone() + ".webp"); - } + match pic.file_format { + FileFormat::TeX => { + result.push(pic.file_name.clone() + ".svg"); + } + FileFormat::Svg => { + result.push(pic.file_name.clone()); + } + FileFormat::Jpeg | FileFormat::Png => { + result.push(pic.file_name.clone() + ".webp"); } + } - result + result } -impl<'a> Display for ThumbPath<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "/{THUMBS_PATH}/{name}", name = Escaped(&self.0.file_name))?; +fn create_file(path: &Path) -> io::Result<File> { + File::create(path) + .map_err(|e| { errorln!("Could not open file {path:?}: {e}"); e }) +} - match self.0.file_format { - FileFormat::TeX => write!(f, ".svg")?, - FileFormat::Svg => {} - FileFormat::Jpeg | FileFormat::Png => write!(f, ".webp")?, - } +impl Display for ThumbPath<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "/{THUMBS_PATH}/{name}", name = Escaped(&self.0.file_name))?; - Ok(()) + match self.0.file_format { + FileFormat::TeX => write!(f, ".svg")?, + FileFormat::Svg => {} + FileFormat::Jpeg | FileFormat::Png => write!(f, ".webp")?, } -} -impl<'a> Display for Escaped<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - for c in self.0.chars() { - match c { - '<' => write!(f, "<")?, - '>' => write!(f, ">")?, - '&' => write!(f, "&")?, - '"' => write!(f, """)?, - '\'' => write!(f, "'")?, - c => c.fmt(f)?, - } - } + Ok(()) + } +} - Ok(()) +impl Display for Escaped<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + for c in self.0.chars() { + match c { + '<' => write!(f, "<")?, + '>' => write!(f, ">")?, + '&' => write!(f, "&")?, + '"' => write!(f, """)?, + '\'' => write!(f, "'")?, + c => c.fmt(f)?, + } } -} -impl<'a> Display for ArgList<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - let mut first = true; - - for arg in self.0 { - if first { - first = false; - write!(f, "{:?}", arg)?; - } else { - write!(f, " {:?}", arg)?; - } - } + Ok(()) + } +} - Ok(()) +impl Display for ArgList<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let mut first = true; + + for arg in self.0 { + if first { + first = false; + write!(f, "{:?}", arg)?; + } else { + write!(f, " {:?}", arg)?; + } } + + Ok(()) + } }