photojawn/src/generate/album_dir.rs

302 lines
10 KiB
Rust

use anyhow::anyhow;
use image::ImageReader;
use serde::Serialize;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::path::{Path, PathBuf};
use std::slice::Iter;
/// An album directory, which has images and possibly child albums
#[derive(Clone, Serialize)]
pub struct AlbumDir {
pub path: PathBuf,
pub images: Vec<Image>,
pub cover: Option<Image>,
// TOOD: Remove the parent reference? Causes a lot of issues
// parent: Option<Box<&'a AlbumDir>>,
pub children: Vec<AlbumDir>,
pub description: String,
}
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)
}
/// Create an AlbumDir recursively from a path. The root path is so that we can make every path
/// relative to the root.
fn from_path(p: &Path, root: &Path) -> anyhow::Result<Self> {
let mut images = vec![];
let mut cover: Option<Image> = None;
let mut children = vec![];
let mut description = String::new();
for entry in p.read_dir()? {
let entry_path = entry?.path().to_path_buf();
if entry_path.is_file() {
if let Some(filename) = entry_path.file_name() {
if filename == "description.txt" {
description = fs::read_to_string(entry_path)?;
} else if filename == "description.md" {
let _conents = fs::read_to_string(entry_path)?;
// TODO: render markdown
todo!();
} else if filename.to_string_lossy().starts_with("cover") {
cover = Some(Image::new(
entry_path.strip_prefix(&root)?.to_path_buf(),
String::new(),
)?);
} else {
let reader = ImageReader::open(&entry_path)?.with_guessed_format()?;
if reader.format().is_some() {
// Found an image
let mut description = String::new();
// Read in any associated description file
if entry_path.with_extension(".txt").exists() {
description = fs::read_to_string(&entry_path)?;
} else if entry_path.with_extension(".md").exists() {
let _contents = fs::read(entry_path)?;
// TODO: render markdown
todo!();
}
images.push(Image::new(
entry_path.strip_prefix(&root)?.to_path_buf(),
description,
)?);
}
}
}
} else if entry_path.is_dir() {
if let Some(dirname) = entry_path.file_name().and_then(|n| n.to_str()) {
if dirname.starts_with("_") {
// Likely a templates or static dir
continue;
} else if dirname == "site" {
// Is a generated site dir, don't descend into it
continue;
}
children.push(AlbumDir::from_path(&entry_path, &root)?);
}
}
}
if cover.is_none() && images.len() > 0 {
cover = Some(images[0].clone());
}
Ok(AlbumDir {
path: p.strip_prefix(root)?.to_path_buf(),
images,
cover,
children,
description,
})
}
}
impl TryFrom<&PathBuf> for AlbumDir {
type Error = anyhow::Error;
fn try_from(p: &PathBuf) -> anyhow::Result<AlbumDir> {
AlbumDir::from_path(p, p)
}
}
/// An iterator which walks through all of the images in an album, and its sub-albums
pub struct AlbumIter<'a> {
image_iter: Box<dyn Iterator<Item = &'a Image> + 'a>,
children_iter: Iter<'a, AlbumDir>,
}
impl<'a> AlbumIter<'a> {
fn new(ad: &'a AlbumDir) -> Self {
Self {
image_iter: Box::new(ad.images.iter()),
children_iter: ad.children.iter(),
}
}
}
impl<'a> Iterator for AlbumIter<'a> {
type Item = &'a Image;
fn next(&mut self) -> Option<Self::Item> {
if let Some(img) = self.image_iter.next() {
return Some(img);
}
for album in self.children_iter.by_ref() {
// Set the child album as the current image iterator
self.image_iter = Box::new(album.iter());
// If we found a child album with an image, return the image. Otherwise we'll keep
// iterating over children.
if let Some(i) = self.image_iter.next() {
return Some(i);
}
}
None
}
}
#[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,
pub thumb_filename: OsString,
pub thumb_path: PathBuf,
pub screen_filename: OsString,
pub screen_path: PathBuf,
pub html_filename: OsString,
pub html_path: PathBuf,
}
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_filename);
let screen_filename = Self::slide_filename(&path, "screen", true)?;
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 = Self::slide_path(&path, &html_filename);
Ok(Image {
path,
description,
filename,
thumb_filename,
thumb_path,
screen_filename,
screen_path,
html_filename,
html_path,
})
}
/// Returns the filename for a given slide type. For example if ext = "thumb" and the current
/// filename is "blah.jpg" this will return "blah.thumb.jpg". If keep_ext if false, it would
/// return "blah.thumb"
fn slide_filename(path: &PathBuf, ext: &str, keep_ext: bool) -> anyhow::Result<OsString> {
let mut new_ext: OsString = ext.into();
if keep_ext {
if let Some(e) = path.extension() {
new_ext = OsString::from(
ext.to_string()
+ "."
+ e.to_str().ok_or(anyhow!(
"Image {} extension is not valid UTF-8",
path.display()
))?,
)
}
}
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()))?;
Ok(new_name.into())
}
/// 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
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn basic_album_iter() {
let mut ad = AlbumDir {
path: "".into(),
description: "".to_string(),
cover: Some(Image::new("foo".into(), "".to_string()).unwrap()),
images: vec![
Image::new("foo".into(), "".to_string()).unwrap(),
Image::new("bar".into(), "".to_string()).unwrap(),
],
children: vec![],
};
// A child album with some images
ad.children.push(AlbumDir {
path: "subdir".into(),
description: "".to_string(),
cover: Some(Image::new("subdir/foo".into(), "".to_string()).unwrap()),
images: vec![
Image::new("subdir/foo".into(), "".to_string()).unwrap(),
Image::new("subdir/bar".into(), "".to_string()).unwrap(),
],
children: vec![AlbumDir {
path: "subdir/deeper_subdir".into(),
description: "".to_string(),
cover: Some(
Image::new("subdir/deeper_subdir/image.jpg".into(), "".to_string()).unwrap(),
),
images: vec![
Image::new("subdir/deeper_subdir/image.jpg".into(), String::new()).unwrap(),
],
children: vec![],
}],
});
// A child album with no images
ad.children.push(AlbumDir {
description: "".to_string(),
cover: None,
path: "another_subdir".into(),
images: vec![],
children: vec![],
});
let imgs: HashSet<&str> = ad.iter().map(|i| i.path.to_str().unwrap()).collect();
let expected: HashSet<&str> = HashSet::from([
"foo",
"bar",
"subdir/foo",
"subdir/bar",
"subdir/deeper_subdir/image.jpg",
]);
assert_eq!(imgs, expected);
}
#[test]
fn image_paths() {
let img = Image::new(PathBuf::from("foo/bar/image.jpg"), String::new()).unwrap();
assert_eq!(
img.thumb_path,
PathBuf::from("foo/bar/slides/image.thumb.jpg")
);
assert_eq!(
img.screen_path,
PathBuf::from("foo/bar/slides/image.screen.jpg")
);
}
}