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",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs_extra"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
|
@ -871,6 +877,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"fs_extra",
|
||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
"mktemp",
|
"mktemp",
|
||||||
|
|
|
@ -11,6 +11,7 @@ edition = "2024"
|
||||||
anyhow = "^1.0"
|
anyhow = "^1.0"
|
||||||
clap = { version = "^4.5", features = ["derive"] }
|
clap = { version = "^4.5", features = ["derive"] }
|
||||||
env_logger = "^0.11.8"
|
env_logger = "^0.11.8"
|
||||||
|
fs_extra = "^1.3.0"
|
||||||
image = "^0.25.6"
|
image = "^0.25.6"
|
||||||
log = "^0.4.27"
|
log = "^0.4.27"
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="photo">
|
<div id="photo">
|
||||||
<img src="{{image_path.screen_path}}" />
|
<img src="{{image.screen_path}}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="nav">
|
<div id="nav">
|
||||||
|
@ -43,12 +43,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="photo-description" class="caption">
|
<div id="photo-description" class="caption">
|
||||||
{% if image_path.description %}
|
{% if image.description %}
|
||||||
{{ image_path.description | safe }}
|
{{ image.description | safe }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="download">
|
<div id="download">
|
||||||
<a href="../{{image_path.path.name}}">view full size</a>
|
<a href="../{{image.filename}}">view full size</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -102,13 +102,14 @@ impl TryFrom<&AlbumDir> for AlbumContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Tera context for slide (individual image) pages
|
/// A Tera context for slide (individual image) pages
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, Debug)]
|
||||||
struct SlideContext {
|
struct SlideContext {
|
||||||
// TODO: Path or String?
|
// TODO: Path or String?
|
||||||
|
// Path required to get back to the root album
|
||||||
root_path: PathBuf,
|
root_path: PathBuf,
|
||||||
image: Image,
|
image: Image,
|
||||||
prev_image: Image,
|
prev_image: Option<Image>,
|
||||||
next_image: Image,
|
next_image: Option<Image>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate(root_path: &PathBuf) -> anyhow::Result<PathBuf> {
|
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)?;
|
env::set_current_dir(root_path)?;
|
||||||
let album = AlbumDir::try_from(root_path)?;
|
let album = AlbumDir::try_from(root_path)?;
|
||||||
|
|
||||||
|
fs::create_dir(&config.output_dir)?;
|
||||||
copy_static(&config)?;
|
copy_static(&config)?;
|
||||||
generate_images(&config, &album)?;
|
generate_images(&config, &album)?;
|
||||||
generate_html(&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<()> {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,6 +148,15 @@ fn generate_images(config: &Config, album: &AlbumDir) -> anyhow::Result<()> {
|
||||||
for img in album.iter() {
|
for img in album.iter() {
|
||||||
let orig_image = image::open(&img.path)?;
|
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);
|
let thumb_path = output_path.join(&img.thumb_path);
|
||||||
log::info!(
|
log::info!(
|
||||||
"Resizing {} -> {}",
|
"Resizing {} -> {}",
|
||||||
|
@ -180,7 +198,6 @@ fn generate_html(config: &Config, album: &AlbumDir) -> anyhow::Result<()> {
|
||||||
.ok_or(anyhow!("Missing _templates dir in album dir"))?,
|
.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]);
|
let mut dir_queue: VecDeque<&AlbumDir> = VecDeque::from([album]);
|
||||||
while let Some(album) = dir_queue.pop_front() {
|
while let Some(album) = dir_queue.pop_front() {
|
||||||
let html_path = output_path.join(&album.path).join("index.html");
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use image::ImageReader;
|
use image::ImageReader;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::ffi::OsString;
|
use std::ffi::{OsStr, OsString};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::slice::Iter;
|
use std::slice::Iter;
|
||||||
|
@ -21,6 +21,7 @@ pub struct AlbumDir {
|
||||||
|
|
||||||
impl AlbumDir {
|
impl AlbumDir {
|
||||||
/// Returns an iterator over all images in the album and subalbums
|
/// 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 {
|
pub fn iter(&self) -> AlbumIter {
|
||||||
AlbumIter::new(self)
|
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 {
|
pub struct Image {
|
||||||
/// Path to the image, relative to the root album
|
/// Path to the image, relative to the root album
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
pub filename: OsString,
|
||||||
|
|
||||||
/// Text description of the image which is displayed below it on the HTML page
|
/// Text description of the image which is displayed below it on the HTML page
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
@ -163,17 +165,25 @@ pub struct Image {
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
pub fn new(path: PathBuf, description: String) -> anyhow::Result<Self> {
|
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_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_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?
|
// TODO: add "slides" in html path?
|
||||||
let html_filename = Self::slide_filename(&path, "html", false)?;
|
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 {
|
Ok(Image {
|
||||||
path,
|
path,
|
||||||
description,
|
description,
|
||||||
|
filename,
|
||||||
thumb_filename,
|
thumb_filename,
|
||||||
thumb_path,
|
thumb_path,
|
||||||
screen_filename,
|
screen_filename,
|
||||||
|
@ -209,30 +219,13 @@ impl Image {
|
||||||
Ok(new_name.into())
|
Ok(new_name.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to the file in the slides dir with the given extention insert, e.g.
|
/// Returns the path to the file in the slides dir given the path to the original image
|
||||||
/// "thumb" or "display"
|
fn slide_path(path: &PathBuf, file_name: &OsStr) -> PathBuf {
|
||||||
fn slide_path(path: &PathBuf, ext: &str) -> anyhow::Result<PathBuf> {
|
let mut new_path = path.clone();
|
||||||
let new_ext = match path.extension() {
|
new_path.pop();
|
||||||
Some(e) => {
|
new_path.push("slides");
|
||||||
ext.to_string()
|
new_path.push(&file_name);
|
||||||
+ "."
|
new_path
|
||||||
+ 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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ use mktemp::Temp;
|
||||||
use photojawn::generate::generate;
|
use photojawn::generate::generate;
|
||||||
use photojawn::skel::make_skeleton;
|
use photojawn::skel::make_skeleton;
|
||||||
use std::collections::{HashSet, VecDeque};
|
use std::collections::{HashSet, VecDeque};
|
||||||
use std::fs;
|
use std::ffi::OsStr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -26,77 +26,55 @@ fn make_test_album() -> Temp {
|
||||||
let tmpdir = Temp::new_dir().unwrap();
|
let tmpdir = Temp::new_dir().unwrap();
|
||||||
let source_path = Path::new("resources/test_album");
|
let source_path = Path::new("resources/test_album");
|
||||||
|
|
||||||
|
log::info!("Creating test album in {}", tmpdir.display());
|
||||||
make_skeleton(&tmpdir.to_path_buf()).unwrap();
|
make_skeleton(&tmpdir.to_path_buf()).unwrap();
|
||||||
|
fs_extra::dir::copy(
|
||||||
let mut dirs: VecDeque<PathBuf> = VecDeque::from([source_path.to_path_buf()]);
|
&source_path,
|
||||||
while let Some(dir) = dirs.pop_front() {
|
&tmpdir,
|
||||||
for entry in dir.read_dir().unwrap() {
|
&fs_extra::dir::CopyOptions::new().content_only(true),
|
||||||
let entry_path = entry.unwrap().path();
|
)
|
||||||
let path_in_album = entry_path.strip_prefix(&source_path).unwrap();
|
.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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpdir
|
tmpdir
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Does basic sanity checks on an output album
|
/// Does basic sanity checks on an output album
|
||||||
fn check_album(album_dir: PathBuf) -> anyhow::Result<()> {
|
fn check_album(root_path: PathBuf) -> anyhow::Result<()> {
|
||||||
log::debug!("Checking album dir {}", album_dir.display());
|
log::debug!("Checking album dir {}", root_path.display());
|
||||||
|
|
||||||
// The _static dir should have gotten copied into <output>/static
|
// 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() {
|
while let Some(dir) = dirs.pop_front() {
|
||||||
|
let mut files: Vec<PathBuf> = Vec::new();
|
||||||
for entry in dir.read_dir().unwrap() {
|
for entry in dir.read_dir().unwrap() {
|
||||||
let path = entry.unwrap().path();
|
let path = entry.unwrap().path();
|
||||||
if path.is_dir() && !path.ends_with(Path::new("slides")) {
|
if path.is_dir()
|
||||||
check_album(path.clone())?;
|
&& !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
|
// There should be an index.html
|
||||||
assert!(path.join("index.html").exists());
|
let index_path = dir.join("index.html");
|
||||||
|
assert!(
|
||||||
// There should be a cover image
|
index_path.exists(),
|
||||||
let cover_path = path.join("cover.jpg");
|
"Expected {} to exist",
|
||||||
assert!(&cover_path.exists());
|
index_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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
panic!(
|
|
||||||
"cover.jpg in {} does not have a matching file",
|
|
||||||
path.display()
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// There should be a slides dir
|
// There should be a slides dir
|
||||||
let slides_path = path.join("slides");
|
let slides_path = dir.join("slides");
|
||||||
assert!(slides_path.is_dir());
|
assert!(
|
||||||
|
slides_path.is_dir(),
|
||||||
|
"Expected {} to be a dir",
|
||||||
|
slides_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
// No two images should have the same path
|
// No two images should have the same path
|
||||||
let image_set: HashSet<&PathBuf> = files.iter().collect();
|
let image_set: HashSet<&PathBuf> = files.iter().collect();
|
||||||
|
@ -107,12 +85,28 @@ fn check_album(album_dir: PathBuf) -> anyhow::Result<()> {
|
||||||
// - <image>.screen.<ext>
|
// - <image>.screen.<ext>
|
||||||
// - <image>.thumb.<ext>
|
// - <image>.thumb.<ext>
|
||||||
for file in &files {
|
for file in &files {
|
||||||
assert!(slides_path.join(file.with_extension("html")).exists());
|
if let Some(ext) = file.extension() {
|
||||||
for ext in ["screen.jpg", "thumb.jpg"] {
|
if ext != "jpg" {
|
||||||
assert!(slides_path.join(file.with_extension(ext)).exists());
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!("Checking associated files for {}", file.display());
|
||||||
|
|
||||||
// Make sure the screen/thumb is smaller than the original
|
let html_path = slides_path.join(&file.with_extension("html").file_name().unwrap());
|
||||||
todo!();
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +118,5 @@ fn check_album(album_dir: PathBuf) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue