tikz-gallery-generator

Custum build of stapix for tikz.pablopie.xyz

Commit
9983aa825f36680ea6f3a3dde1dbe25e604a7b15
Parent
3bb8e16c40b3805213d1bc10e570c53c0528c0d7
Author
Pablo <pablo-pie@riseup.net>
Date

Refactored the error handling

Diffstats

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} &dash; {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} &dash; {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, "&lt;")?,
-                '>'  => write!(f, "&gt;")?,
-                '&'  => write!(f, "&amp;")?,
-                '"'  => write!(f, "&quot;")?,
-                '\'' => write!(f, "&apos;")?,
-                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, "&lt;")?,
+        '>'  => write!(f, "&gt;")?,
+        '&'  => write!(f, "&amp;")?,
+        '"'  => write!(f, "&quot;")?,
+        '\'' => write!(f, "&apos;")?,
+        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(())
+  }
 }