tikz-gallery-generator

Custum build of stapix for tikz.pablopie.xyz

Commit
ff93a62057c6e455dc51be62794868785f93bcfd
Parent
e0695d753661a1ed5b0201246f0c621827b8d5ce
Author
Pablo <pablo-escobar@riseup.net>
Date

Updated the core of the application to proccess SVG and TikZ files too

Diffstat

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&apos;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> {