add image generation
This commit is contained in:
parent
574a60ae8d
commit
5d5c988ba4
5 changed files with 123 additions and 23 deletions
|
@ -6,8 +6,11 @@ use std::path::PathBuf;
|
||||||
#[derive(Deserialize, Debug, PartialEq)]
|
#[derive(Deserialize, Debug, PartialEq)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
/// Tuple of how big thumbnails should be - (width, height)
|
||||||
pub thumbnail_size: (u32, u32),
|
pub thumbnail_size: (u32, u32),
|
||||||
|
/// Tuple of how big thumbnails should be - (width, height)
|
||||||
pub view_size: (u32, u32),
|
pub view_size: (u32, u32),
|
||||||
|
/// Directory inside the album that the site should be output to
|
||||||
pub output_dir: PathBuf,
|
pub output_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,21 @@ mod album_dir;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
pub use album_dir::AlbumDir;
|
pub use album_dir::AlbumDir;
|
||||||
|
use image::imageops::FilterType;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
const IMG_RESIZE_FILTER: FilterType = FilterType::Lanczos3;
|
||||||
|
|
||||||
pub fn generate(root_path: &PathBuf) -> anyhow::Result<PathBuf> {
|
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 config = Config::from_album(root_path.to_path_buf())?;
|
||||||
let orig_path = env::current_dir()?;
|
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)?;
|
let album = AlbumDir::try_from(root_path)?;
|
||||||
env::set_current_dir(&root_path)?;
|
|
||||||
|
|
||||||
generate_images(&config, &album)?;
|
generate_images(&config, &album)?;
|
||||||
generate_html(&config, &album)?;
|
generate_html(&config, &album)?;
|
||||||
|
@ -19,6 +26,35 @@ pub fn generate(root_path: &PathBuf) -> anyhow::Result<PathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_images(config: &Config, album: &AlbumDir) -> anyhow::Result<()> {
|
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)?;
|
||||||
|
|
||||||
|
let thumb_path = output_path.join(img.thumb_path()?);
|
||||||
|
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)?;
|
||||||
|
log::info!("Resized {} -> {}", img.path.display(), thumb_path.display());
|
||||||
|
|
||||||
|
// TODO: resize to screen size
|
||||||
|
let screen_path = output_path.join(img.screen_path()?);
|
||||||
|
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)?;
|
||||||
|
log::info!(
|
||||||
|
"Resized {} -> {}",
|
||||||
|
img.path.display(),
|
||||||
|
screen_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
use image::ImageReader;
|
use image::ImageReader;
|
||||||
use std::ffi::OsString;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::slice::Iter;
|
use std::slice::Iter;
|
||||||
|
|
||||||
/// An album directory, which has images and possibly child albums
|
/// An album directory, which has images and possibly child albums
|
||||||
|
@ -18,21 +18,20 @@ 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
|
||||||
fn iter(&self) -> AlbumIter {
|
pub fn iter(&self) -> AlbumIter {
|
||||||
AlbumIter::new(self)
|
AlbumIter::new(self)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&PathBuf> for AlbumDir {
|
/// Create an AlbumDir recursively from a path. The root path is so that we can make every path
|
||||||
type Error = anyhow::Error;
|
/// relative to the root.
|
||||||
|
fn from_path(p: &Path, root: &Path) -> anyhow::Result<Self> {
|
||||||
fn try_from(p: &PathBuf) -> anyhow::Result<AlbumDir> {
|
|
||||||
let mut images = vec![];
|
let mut images = vec![];
|
||||||
let mut children = vec![];
|
let mut children = vec![];
|
||||||
let mut description = "".to_string();
|
let mut description = String::new();
|
||||||
|
|
||||||
for entry in p.read_dir()? {
|
for entry in p.read_dir()? {
|
||||||
let entry_path = entry?.path();
|
// use strip_prefix() to make the path relative to the root directory
|
||||||
|
let entry_path = entry?.path().strip_prefix(root)?.to_path_buf();
|
||||||
|
|
||||||
if entry_path.is_file() {
|
if entry_path.is_file() {
|
||||||
if let Some(filename) = entry_path.file_name() {
|
if let Some(filename) = entry_path.file_name() {
|
||||||
|
@ -56,6 +55,7 @@ impl TryFrom<&PathBuf> for AlbumDir {
|
||||||
// TODO: render markdown
|
// TODO: render markdown
|
||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
|
|
||||||
images.push(Image {
|
images.push(Image {
|
||||||
path: entry_path,
|
path: entry_path,
|
||||||
description,
|
description,
|
||||||
|
@ -81,7 +81,7 @@ impl TryFrom<&PathBuf> for AlbumDir {
|
||||||
// Find all directories in directory and make AlbumDirs out of them,
|
// Find all directories in directory and make AlbumDirs out of them,
|
||||||
// but skip dirs known to have interesting stuff
|
// but skip dirs known to have interesting stuff
|
||||||
Ok(AlbumDir {
|
Ok(AlbumDir {
|
||||||
path: p.clone(),
|
path: p.strip_prefix(root)?.to_path_buf(),
|
||||||
images,
|
images,
|
||||||
children,
|
children,
|
||||||
description,
|
description,
|
||||||
|
@ -89,11 +89,16 @@ impl TryFrom<&PathBuf> for AlbumDir {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: from-path, find all images and children
|
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
|
/// An iterator which walks through all of the images in an album, and its sub-albums
|
||||||
struct AlbumIter<'a> {
|
pub struct AlbumIter<'a> {
|
||||||
root_album: &'a AlbumDir,
|
|
||||||
image_iter: Box<dyn Iterator<Item = &'a Image> + 'a>,
|
image_iter: Box<dyn Iterator<Item = &'a Image> + 'a>,
|
||||||
children_iter: Iter<'a, AlbumDir>,
|
children_iter: Iter<'a, AlbumDir>,
|
||||||
}
|
}
|
||||||
|
@ -101,7 +106,6 @@ struct AlbumIter<'a> {
|
||||||
impl<'a> AlbumIter<'a> {
|
impl<'a> AlbumIter<'a> {
|
||||||
fn new(ad: &'a AlbumDir) -> Self {
|
fn new(ad: &'a AlbumDir) -> Self {
|
||||||
Self {
|
Self {
|
||||||
root_album: ad,
|
|
||||||
image_iter: Box::new(ad.images.iter()),
|
image_iter: Box::new(ad.images.iter()),
|
||||||
children_iter: ad.children.iter(),
|
children_iter: ad.children.iter(),
|
||||||
}
|
}
|
||||||
|
@ -131,9 +135,50 @@ impl<'a> Iterator for AlbumIter<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||||
struct Image {
|
pub struct Image {
|
||||||
path: PathBuf,
|
/// Path to the image, relative to the root album
|
||||||
description: String,
|
pub path: PathBuf,
|
||||||
|
|
||||||
|
/// Text description of the image which is displayed below it on the HTML page
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Image {
|
||||||
|
pub fn thumb_path(&self) -> anyhow::Result<PathBuf> {
|
||||||
|
self.slide_path("thumb")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn screen_path(&self) -> anyhow::Result<PathBuf> {
|
||||||
|
self.slide_path("screen")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the file in the slides dir with the given extention insert, e.g.
|
||||||
|
/// "thumb" or "display"
|
||||||
|
fn slide_path(&self, ext: &str) -> anyhow::Result<PathBuf> {
|
||||||
|
// TODO: Return path relative to the output dir?
|
||||||
|
let new_ext = match self.path.extension() {
|
||||||
|
Some(e) => {
|
||||||
|
ext.to_string()
|
||||||
|
+ "."
|
||||||
|
+ e.to_str().ok_or(anyhow!(
|
||||||
|
"Image {} extension is not valid UTF-8",
|
||||||
|
self.path.display()
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
None => ext.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_path = self.path.with_extension(new_ext);
|
||||||
|
let new_name = new_path
|
||||||
|
.file_name()
|
||||||
|
.ok_or(anyhow!("Image {} missing a file name", self.path.display()))?;
|
||||||
|
let parent = self
|
||||||
|
.path
|
||||||
|
.parent()
|
||||||
|
.ok_or(anyhow!("Image {} has no parent dir", self.path.display()))?;
|
||||||
|
|
||||||
|
Ok(parent.join("slides").join(new_name))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -200,4 +245,20 @@ mod tests {
|
||||||
]);
|
]);
|
||||||
assert_eq!(imgs, expected);
|
assert_eq!(imgs, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn image_paths() {
|
||||||
|
let img = Image {
|
||||||
|
path: PathBuf::from("foo/bar/image.jpg"),
|
||||||
|
description: String::new(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
img.thumb_path().unwrap(),
|
||||||
|
PathBuf::from("foo/bar/slides/image.thumb.jpg")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
img.screen_path().unwrap(),
|
||||||
|
PathBuf::from("foo/bar/slides/image.screen.jpg")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ fn main() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
Commands::Generate { quick } => {
|
Commands::Generate { quick } => {
|
||||||
println!("Generate, quick: {quick}");
|
println!("Generate, quick: {quick}");
|
||||||
generate(&album_path.to_path_buf());
|
generate(&album_path.to_path_buf())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ fn make_test_album() -> Temp {
|
||||||
let dest_path = tmpdir.join(&path_in_album);
|
let dest_path = tmpdir.join(&path_in_album);
|
||||||
fs::create_dir_all(dest_path.parent().unwrap()).unwrap();
|
fs::create_dir_all(dest_path.parent().unwrap()).unwrap();
|
||||||
fs::copy(&entry_path, &dest_path).unwrap();
|
fs::copy(&entry_path, &dest_path).unwrap();
|
||||||
log::debug!("{} -> {}", entry_path.display(), dest_path.display());
|
log::debug!("Copied {} -> {}", entry_path.display(), dest_path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ fn check_album(album_dir: PathBuf) -> anyhow::Result<()> {
|
||||||
while let Some(dir) = dirs.pop_front() {
|
while let Some(dir) = dirs.pop_front() {
|
||||||
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() {
|
if path.is_dir() && !path.ends_with(Path::new("slides")) {
|
||||||
check_album(path.clone())?;
|
check_album(path.clone())?;
|
||||||
}
|
}
|
||||||
let files: Vec<PathBuf> = path
|
let files: Vec<PathBuf> = path
|
Loading…
Add table
Reference in a new issue