- Commit
- ff93a62057c6e455dc51be62794868785f93bcfd
- Parent
- e0695d753661a1ed5b0201246f0c621827b8d5ce
- Author
- Pablo <pablo-escobar@riseup.net>
- Date
Updated the core of the application to proccess SVG and TikZ files too
Custum build of stapix for tikz.pablopie.xyz
Updated the core of the application to proccess SVG and TikZ files too
3 files changed, 263 insertions, 110 deletions
Status | File Name | N° Changes | Insertions | Deletions |
Modified | Cargo.lock | 4 | 2 | 2 |
Modified | src/gallery_entry.rs | 28 | 28 | 0 |
Modified | src/main.rs | 341 | 233 | 108 |
diff --git a/Cargo.lock b/Cargo.lock @@ -293,9 +293,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libwebp-sys"
diff --git a/src/gallery_entry.rs b/src/gallery_entry.rs @@ -33,11 +33,14 @@ const LICENSES: &[&str] = &[ "CC-BY-NC-ND-4", ]; +const FILE_FORMATS: &[&str] = &["tikz", "eps", "png", "jpeg", "jpg"]; + /// Info on a individual entry on the gallery #[derive(Debug, Clone, PartialEq, Eq)] pub struct GalleryEntry { pub path: PathBuf, pub file_name: String, + pub file_format: FileFormat, pub alt: String, pub caption: Option<String>, pub license: Option<LicenseType>, @@ -46,6 +49,14 @@ pub struct GalleryEntry { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileFormat { + TikZ, + Svg, + Png, + Jpeg, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct LicenseType(CreativeCommons); #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -357,11 +368,28 @@ impl<'de> Deserialize<'de> for GalleryEntry { .map_err(|_| D::Error::unknown_variant(&license, LICENSES))?; let path = PathBuf::from(&path_str); + let file_format = match path.extension().and_then(|s| s.to_str()) { + None => { + return Err(D::Error::invalid_value( + Unexpected::Str(&path_str), + &"valid file path (couldn't guess file format)", + )); + }, + Some("tikz") => FileFormat::TikZ, + Some("svg") => FileFormat::Svg, + Some("png") => FileFormat::Png, + Some("jpeg" | "jpg") => FileFormat::Jpeg, + Some(ext) => { + return Err(D::Error::unknown_variant(ext, FILE_FORMATS)); + } + }; + if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) { Ok(Self { path: path.clone(), alt: alt.trim().to_string(), file_name: String::from(file_name), + file_format, caption, author: author.trim().to_string(), author_url: author_url.map(|s| s.trim().to_string()),
diff --git a/src/main.rs b/src/main.rs @@ -7,36 +7,45 @@ use std::{ fs::{self, File}, io::{self, Write}, path::PathBuf, - process::ExitCode, + process::{ExitCode, Command}, sync::mpsc, + os::unix, }; -use gallery_entry::GalleryEntry; +use gallery_entry::{GalleryEntry, FileFormat}; use threadpool::ThreadPool; mod gallery_entry; +/// A wrapper for displaying the path for the thumbnail of a given path +pub struct ThumbPath<'a>(pub &'a GalleryEntry); + /// A wrapper for HTML-escaped strings pub struct Escaped<'a>(pub &'a str); -const TARGET_PATH: &str = "./site"; -const PAGES_PATH: &str = "pix"; -const PHOTOS_PATH: &str = "assets/photos"; +const TARGET_PATH: &str = "site"; +const PAGES_PATH: &str = "tikz"; +const IMAGES_PATH: &str = "assets/images"; const THUMBS_PATH: &str = "assets/thumbs"; -const FAVICON_PATH: &str = "assets/favicon.ico"; -const ICON_PATH: &str = "assets/icon.svg"; -const STYLES_PATH: &str = "styles.css"; - -const PAGE_TITLE: &str = "Pablo's Photo Gallery"; -const AUTHOR: &str = "Pablo"; -const LICENSE: &str = "GPLv3"; +const FAVICON_PATH: &str = "assets/favicon.svg"; +const FONTS_PATH: &str = "assets/fonts"; +const STYLES_PATH: &str = "assets/css/styles.css"; + +const PAGE_TITLE: &str = "TikZ Gallery"; +const AUTHOR: &str = "Pablo"; +const LICENSE: &str = "GPLv3"; + +const INTRO_MSG: &str = "This is a gallery with most of the mathematical +drawings I've ever used on lectures of mine. Most of the files in here are +<code>.tikz</code> files containing +<a href=\"https://github.com/pgf-tikz/pgf\">TikZ</a> code I wrote, but some +other files are included too. For further information on how to use the +pictures in this gallery and what the licensing terms are please check out +the <a href=\"/using.html\">Using This Gallery</a> page."; /// WebP image quality -const IMAGE_QUALITY: f32 = 50.0; - -/// Target height of the thumbnails, depending on wether the image is vertical -/// or horizontal -const HORIZONTAL_THUMB_HEIGHT: u32 = 300; -const VERTICAL_THUMB_HEIGHT: u32 = 800; +const WEBP_IMAGE_QUALITY: f32 = 90.0; +/// Target height of the thumbnails +const THUMB_HEIGHT: u32 = 500; macro_rules! log { ($fmt:literal) => { @@ -175,8 +184,9 @@ fn render_gallery(pics: Vec<GalleryEntry>) -> ExitCode { log!("Copying image files to the target directory..."); for pic in &pics { - let mut target_path = PathBuf::from(TARGET_PATH); - target_path.push(PHOTOS_PATH); + let mut target_path = PathBuf::from("."); + target_path.push(TARGET_PATH); + target_path.push(IMAGES_PATH); target_path.push(&pic.file_name); if let Err(err) = fs::copy(&pic.path, &target_path) { @@ -206,8 +216,7 @@ fn render_gallery(pics: Vec<GalleryEntry>) -> ExitCode { ); let (sender, reciever) = mpsc::channel(); - logln!("Started rendering WebP thumbnails (using {n} threads)", - n = num_threads); + logln!("Started rendering thumbnails (using {n} threads)", n = num_threads); for pic in &pics { let sender = sender.clone(); @@ -245,7 +254,8 @@ fn render_gallery(pics: Vec<GalleryEntry>) -> ExitCode { } fn render_index(pics: &Vec<GalleryEntry>) -> io::Result<()> { - let mut path = PathBuf::from(TARGET_PATH); + let mut path = PathBuf::from("."); + path.push(TARGET_PATH); path.push("index.html"); let mut f = File::create(path)?; @@ -258,45 +268,47 @@ fn render_index(pics: &Vec<GalleryEntry>) -> io::Result<()> { write_head(&mut f)?; for pic in pics.iter().take(10) { - // TODO: Preload mp4 thumbnails for GIF files writeln!( f, - "<link rel=\"preload\" as=\"image\" href=\"/{THUMBS_PATH}/{name}.webp\">", - name = Escaped(&pic.file_name) + "<link rel=\"preload\" as=\"image\" href=\"{path}\">", + path = ThumbPath(pic), )?; } writeln!(f, "</head>")?; writeln!(f, "<body>")?; - write_nav(&mut f)?; + writeln!(f, "<main>")?; - writeln!(f, "<ul id=\"gallery\">")?; + writeln!(f, "<h1>Tikz Gallery</h1>")?; + writeln!(f, "<p>\n{}\n</p>", INTRO_MSG)?; + + writeln!(f, "<div id=\"gallery\" role=\"feed\">")?; for pic in pics { - writeln!(f, "<li>")?; + writeln!(f, "<article class=\"picture-container\">")?; writeln!( f, "<a aria-label=\"{name}\" href=\"/{PAGES_PATH}/{name}.html\">", name = Escaped(&pic.file_name) )?; - // TODO: Link to mp4 thumbnails for GIF files writeln!( f, - "<img alt=\"{alt}\" src=\"/{THUMBS_PATH}/{name}.webp\">", + "<img alt=\"{alt}\" src=\"{path}\">", alt = Escaped(&pic.alt), - name = Escaped(&pic.file_name) + path = ThumbPath(pic), )?; - writeln!(f, "</a>\n</li>")?; + writeln!(f, "</a>\n</article>")?; } - writeln!(f, "</ul>")?; + writeln!(f, "</div>")?; + writeln!(f, "</main>")?; writeln!(f, "<footer>")?; writeln!( f, - "made with 💛 by <a role=\"author\" href=\"https://pablopie.xyz\">@pablo</a>" + "made with 💚 by <a role=\"author\" href=\"https://pablopie.xyz\">@pablo</a>" )?; writeln!(f, "</footer>")?; @@ -304,8 +316,10 @@ fn render_index(pics: &Vec<GalleryEntry>) -> io::Result<()> { writeln!(f, "</html>") } +// TODO: Render the image source somehow if possible fn render_pic_page(pic: &GalleryEntry) -> io::Result<()> { - let mut path = PathBuf::from(TARGET_PATH); + let mut path = PathBuf::from("."); + path.push(TARGET_PATH); path.push(PAGES_PATH); path.push(pic.file_name.clone() + ".html"); @@ -330,28 +344,38 @@ fn render_pic_page(pic: &GalleryEntry) -> io::Result<()> { write_head(&mut f)?; writeln!( f, - "<link rel=\"preload\" as=\"image\" href=\"/{PHOTOS_PATH}/{n}\">", - n = Escaped(&pic.file_name) + "<link rel=\"preload\" as=\"image\" href=\"{path}\">", + path = ThumbPath(pic), )?; writeln!(f, "</head>")?; writeln!(f, "<body>")?; - write_nav(&mut f)?; - writeln!(f, "<main>")?; + writeln!( + f, + "<h1 class=\"picture-title\">{name}</h1>", + name = Escaped(&pic.file_name) + )?; + if pic.caption.is_some() { - writeln!(f, "<figure>")?; + writeln!(f, "<figure id=\"picture\">")?; } else { - writeln!(f, "<figure aria-label=\"File {name}\">", + writeln!(f, "<figure id=\"picture\" aria-label=\"File {name}\">", name = Escaped(&pic.file_name))?; } - writeln!(f, "<div id=\"picture-container\">")?; + writeln!(f, "<div class=\"picture-container\">")?; + writeln!( + f, + "<a aria-label=\"{name}\" href=\"/{IMAGES_PATH}/{name}\">", + name = Escaped(&pic.file_name) + )?; writeln!( f, - "<img alt=\"{alt}\" src=\"/{PHOTOS_PATH}/{file_name}\">", + "<img alt=\"{alt}\" src=\"{path}\">", alt = Escaped(&pic.alt), - file_name = Escaped(&pic.file_name) + path = ThumbPath(pic), )?; + writeln!(f, "</a>")?; writeln!(f, "</div>")?; if let Some(caption) = &pic.caption { writeln!(f, "<figcaption>")?; @@ -382,15 +406,6 @@ fn render_pic_page(pic: &GalleryEntry) -> io::Result<()> { writeln!(f, "</html>") } -fn write_nav(f: &mut File) -> io::Result<()> { - writeln!(f, "<header>")?; - writeln!(f, "<nav>")?; - writeln!(f, "<img aria-hidden=\"true\" alt=\"Website icon\" width=\"24\" height=\"24\" src=\"/{ICON_PATH}\">")?; - writeln!(f, "<a href=\"/index.html\">photos.pablopie.xyz</a>")?; - writeln!(f, "</nav>")?; - writeln!(f, "</header>") -} - /// Prints the common head elements to a given file fn write_head(f: &mut File) -> io::Result<()> { writeln!( @@ -403,8 +418,10 @@ fn write_head(f: &mut File) -> io::Result<()> { f, "<meta content=\"text/html; charset=utf-8\" http-equiv=\"content-type\">" )?; - writeln!(f, "<link rel=\"icon\" href=\"/{FAVICON_PATH}\" type=\"image/x-icon\" sizes=\"16x16 24x24 32x32\">")?; - writeln!(f, "<link rel=\"stylesheet\" href=\"/{STYLES_PATH}\">") + 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 @@ -447,11 +464,8 @@ fn write_license(f: &mut File) -> io::Result<()> { /// Returns `true` if rendering succeded (or was skipped) and `false` /// otherwise. All warinings and error messages should be logged in here. -// TODO: Render GIF files as mp4 instead fn render_thumbnail(pic: GalleryEntry) -> bool { - let mut thumb_path = PathBuf::from(TARGET_PATH); - thumb_path.push(THUMBS_PATH); - thumb_path.push(pic.file_name.clone() + ".webp"); + let thumb_path = thumb_path(&pic); // Only try to render thumbnail in case the thumbnail file in the machine // is older than the source file @@ -468,61 +482,172 @@ fn render_thumbnail(pic: GalleryEntry) -> bool { } } - let mut thumb_file = match File::create(&thumb_path) { - Ok(f) => f, - Err(err) => { - errorln!("Couldn't open WebP thumbnail file {thumb_path:?}: {err}", - thumb_path = thumb_path, err = err); - return false; + match pic.file_format { + FileFormat::TikZ => { + // TODO: Remove the dependancie on tikztosvg to optimize IO? + // currently tikztosvg creates new directories in tmp for each + // single file. Maybe we could optimize IO by reusing the same dir + // for all operations on a given thread + + // 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 false; + } + Err(err) => { + errorln!("Failed to run tikztosvg: {err}", err = err); + return false; + } + _ => {} + } + }, + FileFormat::Svg => { + let mut thumb_abs_path = match env::current_dir() { + Ok(path) => path, + Err(err) => { + errorln!( + "Failed to create symlink for {thumb:?}: Could not get current working directory: {err}", + thumb = thumb_path, + err = err + ); + return false; + } + }; + thumb_abs_path.push(TARGET_PATH); + thumb_abs_path.push(IMAGES_PATH); + thumb_abs_path.push(&pic.file_name); + + let mut src_path = PathBuf::from("."); + src_path.push(TARGET_PATH); + src_path.push(IMAGES_PATH); + src_path.push(&pic.file_name); + + if let Err(err) = unix::fs::symlink(&src_path, &thumb_abs_path) { + errorln!( + "Failed to create symlink {thumb:?} -> {src:?}: {err}", + thumb = thumb_abs_path, + src = src_path, + err = err + ); + return false; + } + }, + 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}", + thumb_path = thumb_path, err = err); + return false; + } + }; + + 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, + err = err + ); + return false; + } + }; + + let img = match img_reader.decode() { + Ok(img) => img, + Err(err) => { + errorln!( + "Faileded to decode image file {name:?}: {err}", + name = pic.file_name, + err = err + ); + return false; + } + }; + + 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, err = err); + return false; + } } - }; + } - let img_reader = match ImageReader::open(&pic.path) { - Ok(r) => r, - Err(err) => { - errorln!( - "Couldn't open file {path:?} to render WebP thumbnail: {err}", - path = pic.file_name, - err = err - ); - return false; + logln!("Rendered thumbnail for {name:?}", name = pic.file_name); + true +} + +/// Helper to get the correct thumbnail path for a given entry +fn thumb_path(pic: &GalleryEntry) -> PathBuf { + let mut result = PathBuf::from("."); + result.push(TARGET_PATH); + result.push(THUMBS_PATH); + + match pic.file_format { + FileFormat::TikZ => { + 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"); + } + } - let img = match img_reader.decode() { - Ok(img) => img, - Err(err) => { - errorln!( - "Faileded to decode image file {name:?}: {err}", - name = pic.file_name, - err = err - ); - return false; + 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))?; + + match self.0.file_format { + FileFormat::TikZ => write!(f, ".svg")?, + FileFormat::Svg => {} + FileFormat::Jpeg | FileFormat::Png => write!(f, ".webp")?, } - }; - let h = if img.width() > img.height() { - HORIZONTAL_THUMB_HEIGHT - } else { - VERTICAL_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(IMAGE_QUALITY); - - if let Err(err) = thumb_file.write_all(&mem) { - errorln!("Couldn't write WebP thumnail to file {path:?}: {err}", - path = thumb_path, err = err); - return false; + Ok(()) } - - logln!("Rendered WebP thumbnail for {name:?}", name = pic.file_name); - true } impl<'a> Display for Escaped<'a> {