252 lines
7.8 KiB
Rust
252 lines
7.8 KiB
Rust
pub mod album_dir;
|
|
|
|
use crate::config::Config;
|
|
use album_dir::{AlbumDir, Image};
|
|
use anyhow::anyhow;
|
|
use image::imageops::FilterType;
|
|
use serde::Serialize;
|
|
use std::collections::VecDeque;
|
|
use std::env;
|
|
use std::ffi::OsStr;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use tera::Tera;
|
|
|
|
const IMG_RESIZE_FILTER: FilterType = FilterType::Lanczos3;
|
|
|
|
#[derive(Serialize, Debug)]
|
|
struct Breadcrumb {
|
|
path: PathBuf,
|
|
name: String,
|
|
}
|
|
|
|
/// A Tera context for album pages
|
|
#[derive(Serialize, Debug)]
|
|
struct AlbumContext {
|
|
// Path required to get back to the root album
|
|
root_path: PathBuf,
|
|
|
|
name: String,
|
|
description: String,
|
|
|
|
images: Vec<Image>,
|
|
|
|
// Path to the cover image thumbnail within /slides/, relative to the album dir. Used when
|
|
// linking to an album from a parent album
|
|
cover_thumbnail_path: PathBuf,
|
|
|
|
// list of:
|
|
// - relative dir to walk up to root, e.g. "../../.."
|
|
// - dir name
|
|
breadcrumbs: Vec<Breadcrumb>,
|
|
|
|
// Immediate children of this album
|
|
children: Vec<AlbumContext>,
|
|
}
|
|
|
|
impl TryFrom<&AlbumDir> for AlbumContext {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(album: &AlbumDir) -> anyhow::Result<Self> {
|
|
let name: String = match album.path.file_name() {
|
|
Some(n) => n.to_string_lossy().to_string(),
|
|
None => String::new(),
|
|
};
|
|
|
|
// Build breadcrumbs
|
|
let mut breadcrumbs: Vec<Breadcrumb> = Vec::new();
|
|
{
|
|
let mut album_path = album.path.clone();
|
|
let mut relpath: PathBuf = PathBuf::new();
|
|
while album_path.pop() {
|
|
let filename: &OsStr = album_path.file_name().unwrap_or(OsStr::new("Home"));
|
|
relpath.push("..");
|
|
breadcrumbs.push(Breadcrumb {
|
|
path: relpath.clone(),
|
|
name: filename.to_string_lossy().to_string(),
|
|
});
|
|
}
|
|
}
|
|
breadcrumbs.reverse();
|
|
log::debug!("Crumbs for {}: {breadcrumbs:?}", album.path.display());
|
|
|
|
// The first breadcrumb path is the relative path to the root album
|
|
let root_path = if !breadcrumbs.is_empty() {
|
|
breadcrumbs[0].path.clone()
|
|
} else {
|
|
PathBuf::new()
|
|
};
|
|
|
|
// Pick a cover image thumbnail and build a path to it
|
|
let cover_thumbnail_path = match &album.cover {
|
|
Some(i) => Path::new("slides").join(&i.thumb_filename),
|
|
None => PathBuf::from(""),
|
|
};
|
|
|
|
let children: Vec<AlbumContext> = album
|
|
.children
|
|
.iter()
|
|
.map(AlbumContext::try_from)
|
|
.collect::<anyhow::Result<Vec<AlbumContext>>>()?;
|
|
let images: Vec<Image> = album.images.clone();
|
|
|
|
Ok(AlbumContext {
|
|
root_path,
|
|
name,
|
|
description: album.description.clone(),
|
|
breadcrumbs,
|
|
images,
|
|
children,
|
|
cover_thumbnail_path,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A Tera context for slide (individual image) pages
|
|
#[derive(Serialize, Debug)]
|
|
struct SlideContext {
|
|
// Path required to get back to the root album
|
|
root_path: PathBuf,
|
|
image: Image,
|
|
prev_image: Option<Image>,
|
|
next_image: Option<Image>,
|
|
}
|
|
|
|
pub fn generate(root_path: &PathBuf) -> anyhow::Result<PathBuf> {
|
|
log::debug!("Generating album in {}", root_path.display());
|
|
let config = Config::from_album(root_path.to_path_buf())?;
|
|
let orig_path = env::current_dir()?;
|
|
|
|
// Jump into the root path so that all paths are relative to the root of the album
|
|
env::set_current_dir(root_path)?;
|
|
let album = AlbumDir::try_from(root_path)?;
|
|
|
|
fs::create_dir_all(&config.output_dir)?;
|
|
copy_static(&config)?;
|
|
generate_images(&config, &album)?;
|
|
generate_html(&config, &album)?;
|
|
|
|
env::set_current_dir(orig_path)?;
|
|
Ok(root_path.join(config.output_dir))
|
|
}
|
|
|
|
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)
|
|
.overwrite(true),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_images(config: &Config, album: &AlbumDir) -> anyhow::Result<()> {
|
|
let output_path = album.path.join(&config.output_dir);
|
|
// TODO: use par_iter() ?
|
|
// TODO: progress bar ?
|
|
for img in album.iter() {
|
|
let orig_image = image::open(&img.path)?;
|
|
|
|
// TODO: If orig_path is the same as the original image, and quick mode is on, skip to next
|
|
// image
|
|
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 {} -> {}",
|
|
img.path.display(),
|
|
thumb_path.display()
|
|
);
|
|
fs::create_dir_all(thumb_path.parent().unwrap_or(Path::new("")))?;
|
|
orig_image
|
|
.resize(
|
|
config.thumbnail_size.0,
|
|
config.thumbnail_size.1,
|
|
IMG_RESIZE_FILTER,
|
|
)
|
|
.save(&thumb_path)?;
|
|
|
|
let screen_path = output_path.join(&img.screen_path);
|
|
log::info!(
|
|
"Resizing {} -> {}",
|
|
img.path.display(),
|
|
screen_path.display()
|
|
);
|
|
fs::create_dir_all(thumb_path.parent().unwrap_or(Path::new("")))?;
|
|
orig_image
|
|
.resize(config.view_size.0, config.view_size.1, IMG_RESIZE_FILTER)
|
|
.save(&screen_path)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_html(config: &Config, album: &AlbumDir) -> anyhow::Result<()> {
|
|
let output_path = album.path.join(&config.output_dir);
|
|
let tera = Tera::new(
|
|
album
|
|
.path
|
|
.join("_templates/*.html")
|
|
.to_str()
|
|
.ok_or(anyhow!("Missing _templates dir in album dir"))?,
|
|
)?;
|
|
|
|
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");
|
|
log::info!("Rendering album {}", html_path.display());
|
|
let ctx = AlbumContext::try_from(album)?;
|
|
log::debug!("Album context: {ctx:?}");
|
|
fs::write(
|
|
output_path.join(&album.path).join("index.html"),
|
|
tera.render("album.html", &tera::Context::from_serialize(&ctx)?)?,
|
|
)?;
|
|
|
|
for child in album.children.iter() {
|
|
dir_queue.push_back(child);
|
|
}
|
|
|
|
for (pos, img) in album.images.iter().enumerate() {
|
|
let prev_image: Option<&Image> = match pos {
|
|
0 => None,
|
|
n => Some(&album.images[n - 1]),
|
|
};
|
|
let next_image: Option<&Image> = album.images.get(pos + 1);
|
|
|
|
// Find the path to the root by counting the parts of the path
|
|
// Start with 1 .. to get out of the slides dir
|
|
let mut path_to_root = PathBuf::from("..");
|
|
if let Some(parent) = img.path.parent() {
|
|
let mut parent = parent.to_path_buf();
|
|
while parent.pop() {
|
|
path_to_root.push("..");
|
|
}
|
|
}
|
|
|
|
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(())
|
|
}
|