diff --git a/Cargo.lock b/Cargo.lock
index f2a6d5a..79f48aa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -415,6 +415,12 @@ dependencies = [
"miniz_oxide",
]
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -871,6 +877,7 @@ dependencies = [
"anyhow",
"clap",
"env_logger",
+ "fs_extra",
"image",
"log",
"mktemp",
diff --git a/Cargo.toml b/Cargo.toml
index 7a82700..f7a5edf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ edition = "2024"
anyhow = "^1.0"
clap = { version = "^4.5", features = ["derive"] }
env_logger = "^0.11.8"
+fs_extra = "^1.3.0"
image = "^0.25.6"
log = "^0.4.27"
serde = { version = "^1.0", features = ["derive"] }
diff --git a/resources/skel/_templates/photo.html b/resources/skel/_templates/photo.html
index bb7e4af..3ede869 100644
--- a/resources/skel/_templates/photo.html
+++ b/resources/skel/_templates/photo.html
@@ -17,7 +17,7 @@
{% block content %}
-

+
@@ -43,12 +43,12 @@
- {% if image_path.description %}
- {{ image_path.description | safe }}
+ {% if image.description %}
+ {{ image.description | safe }}
{% endif %}
{% endblock %}
diff --git a/src/generate.rs b/src/generate.rs
index 5cc8e82..33e810d 100644
--- a/src/generate.rs
+++ b/src/generate.rs
@@ -102,13 +102,14 @@ impl TryFrom<&AlbumDir> for AlbumContext {
}
/// A Tera context for slide (individual image) pages
-#[derive(Serialize)]
+#[derive(Serialize, Debug)]
struct SlideContext {
// TODO: Path or String?
+ // Path required to get back to the root album
root_path: PathBuf,
image: Image,
- prev_image: Image,
- next_image: Image,
+ prev_image: Option,
+ next_image: Option,
}
pub fn generate(root_path: &PathBuf) -> anyhow::Result {
@@ -120,6 +121,7 @@ pub fn generate(root_path: &PathBuf) -> anyhow::Result {
env::set_current_dir(root_path)?;
let album = AlbumDir::try_from(root_path)?;
+ fs::create_dir(&config.output_dir)?;
copy_static(&config)?;
generate_images(&config, &album)?;
generate_html(&config, &album)?;
@@ -129,6 +131,13 @@ pub fn generate(root_path: &PathBuf) -> anyhow::Result {
}
fn copy_static(config: &Config) -> anyhow::Result<()> {
+ let dst = &config.output_dir.join("static");
+ log::info!("Copying static files from _static to {}", dst.display());
+ fs_extra::dir::copy(
+ "_static",
+ dst,
+ &fs_extra::dir::CopyOptions::new().content_only(true),
+ )?;
Ok(())
}
@@ -139,6 +148,15 @@ fn generate_images(config: &Config, album: &AlbumDir) -> anyhow::Result<()> {
for img in album.iter() {
let orig_image = image::open(&img.path)?;
+ let orig_path = output_path.join(&img.path);
+ log::info!(
+ "Copying original {} -> {}",
+ img.path.display(),
+ orig_path.display()
+ );
+ fs::create_dir_all(orig_path.parent().unwrap_or(Path::new("")))?;
+ orig_image.save(&orig_path)?;
+
let thumb_path = output_path.join(&img.thumb_path);
log::info!(
"Resizing {} -> {}",
@@ -180,7 +198,6 @@ fn generate_html(config: &Config, album: &AlbumDir) -> anyhow::Result<()> {
.ok_or(anyhow!("Missing _templates dir in album dir"))?,
)?;
- // Queue of album dir and depth (distance from root AlbumDir)
let mut dir_queue: VecDeque<&AlbumDir> = VecDeque::from([album]);
while let Some(album) = dir_queue.pop_front() {
let html_path = output_path.join(&album.path).join("index.html");
@@ -197,5 +214,37 @@ fn generate_html(config: &Config, album: &AlbumDir) -> anyhow::Result<()> {
}
}
+ let all_images: Vec<&Image> = album.iter().collect();
+ for (pos, img) in all_images.iter().enumerate() {
+ let img: &Image = *img;
+ let prev_image: Option<&Image> = match pos {
+ 0 => None,
+ n => Some(&all_images[n - 1]),
+ };
+ let next_image: Option<&Image> = all_images.get(pos + 1).map(|i| *i);
+
+ // Find the path to the root by counting the parts of the path
+ let mut path_to_root = PathBuf::new();
+ if let Some(parent) = img.path.parent() {
+ let mut parent = parent.to_path_buf();
+ while parent.pop() {
+ path_to_root = path_to_root.join("..");
+ }
+ }
+
+ log::info!("Rendering image {}", img.html_path.display());
+ let ctx = SlideContext {
+ root_path: path_to_root,
+ image: img.clone(),
+ prev_image: prev_image.cloned(),
+ next_image: next_image.cloned(),
+ };
+ log::debug!("Image context: {ctx:?}");
+ fs::write(
+ output_path.join(&img.html_path),
+ tera.render("photo.html", &tera::Context::from_serialize(&ctx)?)?,
+ )?;
+ }
+
Ok(())
}
diff --git a/src/generate/album_dir.rs b/src/generate/album_dir.rs
index 00b6d24..40d7963 100644
--- a/src/generate/album_dir.rs
+++ b/src/generate/album_dir.rs
@@ -1,7 +1,7 @@
use anyhow::anyhow;
use image::ImageReader;
use serde::Serialize;
-use std::ffi::OsString;
+use std::ffi::{OsStr, OsString};
use std::fs;
use std::path::{Path, PathBuf};
use std::slice::Iter;
@@ -21,6 +21,7 @@ pub struct AlbumDir {
impl AlbumDir {
/// Returns an iterator over all images in the album and subalbums
+ // TODO: Rename to iter_images() and make separate one for dirs?
pub fn iter(&self) -> AlbumIter {
AlbumIter::new(self)
}
@@ -145,10 +146,11 @@ impl<'a> Iterator for AlbumIter<'a> {
}
}
-#[derive(Clone, Hash, PartialEq, Eq, Serialize)]
+#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize)]
pub struct Image {
/// Path to the image, relative to the root album
pub path: PathBuf,
+ pub filename: OsString,
/// Text description of the image which is displayed below it on the HTML page
pub description: String,
@@ -163,17 +165,25 @@ pub struct Image {
impl Image {
pub fn new(path: PathBuf, description: String) -> anyhow::Result {
+ let filename = path
+ .file_name()
+ .ok_or(anyhow!(
+ "Image path {} is missing a filename",
+ path.display()
+ ))?
+ .into();
let thumb_filename = Self::slide_filename(&path, "thumb", true)?;
- let thumb_path = Self::slide_path(&path, "thumb")?;
+ let thumb_path = Self::slide_path(&path, &thumb_filename);
let screen_filename = Self::slide_filename(&path, "screen", true)?;
- let screen_path = Self::slide_path(&path, "screen")?;
+ let screen_path = Self::slide_path(&path, &screen_filename);
// TODO: add "slides" in html path?
let html_filename = Self::slide_filename(&path, "html", false)?;
- let html_path = path.with_extension("html");
+ let html_path = Self::slide_path(&path, &html_filename);
Ok(Image {
path,
description,
+ filename,
thumb_filename,
thumb_path,
screen_filename,
@@ -209,30 +219,13 @@ impl Image {
Ok(new_name.into())
}
- /// Returns the path to the file in the slides dir with the given extention insert, e.g.
- /// "thumb" or "display"
- fn slide_path(path: &PathBuf, ext: &str) -> anyhow::Result {
- let new_ext = match path.extension() {
- Some(e) => {
- ext.to_string()
- + "."
- + e.to_str().ok_or(anyhow!(
- "Image {} extension is not valid UTF-8",
- path.display()
- ))?
- }
- None => ext.to_string(),
- };
-
- let new_path = path.with_extension(new_ext);
- let new_name = new_path
- .file_name()
- .ok_or(anyhow!("Image {} missing a file name", path.display()))?;
- let parent = path
- .parent()
- .ok_or(anyhow!("Image {} has no parent dir", path.display()))?;
-
- Ok(parent.join("slides").join(new_name))
+ /// Returns the path to the file in the slides dir given the path to the original image
+ fn slide_path(path: &PathBuf, file_name: &OsStr) -> PathBuf {
+ let mut new_path = path.clone();
+ new_path.pop();
+ new_path.push("slides");
+ new_path.push(&file_name);
+ new_path
}
}
diff --git a/tests/test_generate.rs b/tests/test_generate.rs
index 03ebdf0..622a139 100644
--- a/tests/test_generate.rs
+++ b/tests/test_generate.rs
@@ -4,7 +4,7 @@ use mktemp::Temp;
use photojawn::generate::generate;
use photojawn::skel::make_skeleton;
use std::collections::{HashSet, VecDeque};
-use std::fs;
+use std::ffi::OsStr;
use std::path::{Path, PathBuf};
#[test]
@@ -26,102 +26,95 @@ fn make_test_album() -> Temp {
let tmpdir = Temp::new_dir().unwrap();
let source_path = Path::new("resources/test_album");
+ log::info!("Creating test album in {}", tmpdir.display());
make_skeleton(&tmpdir.to_path_buf()).unwrap();
-
- let mut dirs: VecDeque = VecDeque::from([source_path.to_path_buf()]);
- while let Some(dir) = dirs.pop_front() {
- for entry in dir.read_dir().unwrap() {
- let entry_path = entry.unwrap().path();
- let path_in_album = entry_path.strip_prefix(&source_path).unwrap();
- if entry_path.is_dir() {
- dirs.push_back(entry_path);
- } else {
- let dest_path = tmpdir.join(&path_in_album);
- fs::create_dir_all(dest_path.parent().unwrap()).unwrap();
- fs::copy(&entry_path, &dest_path).unwrap();
- log::debug!("Copied {} -> {}", entry_path.display(), dest_path.display());
- }
- }
- }
+ fs_extra::dir::copy(
+ &source_path,
+ &tmpdir,
+ &fs_extra::dir::CopyOptions::new().content_only(true),
+ )
+ .unwrap();
tmpdir
}
/// Does basic sanity checks on an output album
-fn check_album(album_dir: PathBuf) -> anyhow::Result<()> {
- log::debug!("Checking album dir {}", album_dir.display());
+fn check_album(root_path: PathBuf) -> anyhow::Result<()> {
+ log::debug!("Checking album dir {}", root_path.display());
// The _static dir should have gotten copied into