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> {