basic image HTML rendering. Current tests pass!
This commit is contained in:
parent
bc33331dae
commit
9434eb835d
6 changed files with 156 additions and 113 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
{% block content %}
|
||||
<div id="photo">
|
||||
<img src="{{image_path.screen_path}}" />
|
||||
<img src="{{image.screen_path}}" />
|
||||
</div>
|
||||
|
||||
<div id="nav">
|
||||
|
@ -43,12 +43,12 @@
|
|||
</div>
|
||||
|
||||
<div id="photo-description" class="caption">
|
||||
{% if image_path.description %}
|
||||
{{ image_path.description | safe }}
|
||||
{% if image.description %}
|
||||
{{ image.description | safe }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="download">
|
||||
<a href="../{{image_path.path.name}}">view full size</a>
|
||||
<a href="../{{image.filename}}">view full size</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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<Image>,
|
||||
next_image: Option<Image>,
|
||||
}
|
||||
|
||||
pub fn generate(root_path: &PathBuf) -> anyhow::Result<PathBuf> {
|
||||
|
@ -120,6 +121,7 @@ pub fn generate(root_path: &PathBuf) -> anyhow::Result<PathBuf> {
|
|||
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<PathBuf> {
|
|||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
|
|
@ -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<Self> {
|
||||
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<PathBuf> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<PathBuf> = 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 <output>/static
|
||||
assert!(album_dir.join("static/index.css").exists());
|
||||
assert!(root_path.join("static/index.css").exists());
|
||||
|
||||
let mut dirs: VecDeque<PathBuf> = VecDeque::from([album_dir]);
|
||||
let mut dirs: VecDeque<PathBuf> = VecDeque::from([root_path.clone()]);
|
||||
while let Some(dir) = dirs.pop_front() {
|
||||
let mut files: Vec<PathBuf> = Vec::new();
|
||||
for entry in dir.read_dir().unwrap() {
|
||||
let path = entry.unwrap().path();
|
||||
if path.is_dir() && !path.ends_with(Path::new("slides")) {
|
||||
check_album(path.clone())?;
|
||||
if path.is_dir()
|
||||
&& !path.ends_with(Path::new("slides"))
|
||||
&& path.file_name() != Some(OsStr::new("static"))
|
||||
{
|
||||
dirs.push_back(path.clone());
|
||||
} else if path.is_file() {
|
||||
files.push(path);
|
||||
}
|
||||
let files: Vec<PathBuf> = path
|
||||
.read_dir()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|e| e.unwrap().path())
|
||||
.filter(|e| e.is_file())
|
||||
.collect();
|
||||
}
|
||||
|
||||
// There should be an index.html
|
||||
assert!(path.join("index.html").exists());
|
||||
// There should be an index.html
|
||||
let index_path = dir.join("index.html");
|
||||
assert!(
|
||||
index_path.exists(),
|
||||
"Expected {} to exist",
|
||||
index_path.display()
|
||||
);
|
||||
|
||||
// There should be a cover image
|
||||
let cover_path = path.join("cover.jpg");
|
||||
assert!(&cover_path.exists());
|
||||
// There should be a slides dir
|
||||
let slides_path = dir.join("slides");
|
||||
assert!(
|
||||
slides_path.is_dir(),
|
||||
"Expected {} to be a dir",
|
||||
slides_path.display()
|
||||
);
|
||||
|
||||
// The cover should be equal contents to some other image
|
||||
let cover_contents = fs::read(&cover_path).unwrap();
|
||||
let mut found = false;
|
||||
for file in &files {
|
||||
if file != &cover_path {
|
||||
let file_contents = fs::read(file).unwrap();
|
||||
if file_contents == cover_contents {
|
||||
found = true;
|
||||
}
|
||||
// No two images should have the same path
|
||||
let image_set: HashSet<&PathBuf> = files.iter().collect();
|
||||
assert_eq!(image_set.len(), files.len());
|
||||
|
||||
// For each image in the album (including the cover), in slides there should be a:
|
||||
// - <image>.html
|
||||
// - <image>.screen.<ext>
|
||||
// - <image>.thumb.<ext>
|
||||
for file in &files {
|
||||
if let Some(ext) = file.extension() {
|
||||
if ext != "jpg" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
panic!(
|
||||
"cover.jpg in {} does not have a matching file",
|
||||
path.display()
|
||||
log::debug!("Checking associated files for {}", file.display());
|
||||
|
||||
let html_path = slides_path.join(&file.with_extension("html").file_name().unwrap());
|
||||
assert!(
|
||||
html_path.exists(),
|
||||
"Expected {} to exist",
|
||||
html_path.display()
|
||||
);
|
||||
for ext in ["screen.jpg", "thumb.jpg"] {
|
||||
let img_path = slides_path.join(file.with_extension(ext).file_name().unwrap());
|
||||
assert!(
|
||||
img_path.exists(),
|
||||
"Expected {} to exist",
|
||||
img_path.display()
|
||||
);
|
||||
|
||||
// TODO: Make sure the screen/thumb is smaller than the original
|
||||
}
|
||||
}
|
||||
|
||||
// There should be a slides dir
|
||||
let slides_path = path.join("slides");
|
||||
assert!(slides_path.is_dir());
|
||||
|
||||
// No two images should have the same path
|
||||
let image_set: HashSet<&PathBuf> = files.iter().collect();
|
||||
assert_eq!(image_set.len(), files.len());
|
||||
|
||||
// For each image in the album (including the cover), in slides there should be a:
|
||||
// - <image>.html
|
||||
// - <image>.screen.<ext>
|
||||
// - <image>.thumb.<ext>
|
||||
for file in &files {
|
||||
assert!(slides_path.join(file.with_extension("html")).exists());
|
||||
for ext in ["screen.jpg", "thumb.jpg"] {
|
||||
assert!(slides_path.join(file.with_extension(ext)).exists());
|
||||
|
||||
// Make sure the screen/thumb is smaller than the original
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
// There shouldn't be any .txt or .md files hanging around
|
||||
for file in &files {
|
||||
if let Some(ext) = file.extension() {
|
||||
assert_ne!(ext, "md");
|
||||
assert_ne!(ext, "txt");
|
||||
}
|
||||
// There shouldn't be any .txt or .md files hanging around
|
||||
for file in &files {
|
||||
if let Some(ext) = file.extension() {
|
||||
assert_ne!(ext, "md");
|
||||
assert_ne!(ext, "txt");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue