Revamp the images template functions

This commit is contained in:
Vincent Prouillet 2021-05-12 21:07:18 +02:00
parent b0937fa5b7
commit 7fb99eaa44
14 changed files with 336 additions and 196 deletions

View file

@ -5,6 +5,7 @@
### Breaking ### Breaking
- Newlines are now required after the closing `+++` of front-matter - Newlines are now required after the closing `+++` of front-matter
- `resize_image` now returns a map: `{url, static_path}` instead of just the URL so you can follow up with other functions
- i18n rework: languages now have their sections in `config.toml` to set up all their options - i18n rework: languages now have their sections in `config.toml` to set up all their options
1. taxonomies don't have a `lang` anymore in the config, you need to declare them in their respective language section 1. taxonomies don't have a `lang` anymore in the config, you need to declare them in their respective language section
2. the `config` variable in templates has been changed and is now a stripped down language aware version of the previous `config` 2. the `config` variable in templates has been changed and is now a stripped down language aware version of the previous `config`

3
Cargo.lock generated
View file

@ -1022,6 +1022,7 @@ dependencies = [
name = "imageproc" name = "imageproc"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"config",
"errors", "errors",
"image", "image",
"lazy_static", "lazy_static",
@ -2592,6 +2593,8 @@ dependencies = [
"nom-bibtex", "nom-bibtex",
"rendering", "rendering",
"reqwest", "reqwest",
"serde",
"serde_derive",
"serde_json", "serde_json",
"sha2", "sha2",
"svg_metadata", "svg_metadata",

View file

@ -10,7 +10,8 @@ regex = "1.0"
tera = "1" tera = "1"
image = "0.23" image = "0.23"
rayon = "1" rayon = "1"
webp="0.1.1" webp = "0.1.1"
errors = { path = "../errors" } errors = { path = "../errors" }
utils = { path = "../utils" } utils = { path = "../utils" }
config = { path = "../config" }

View file

@ -11,10 +11,12 @@ use lazy_static::lazy_static;
use rayon::prelude::*; use rayon::prelude::*;
use regex::Regex; use regex::Regex;
use config::Config;
use errors::{Error, Result}; use errors::{Error, Result};
use utils::fs as ufs; use utils::fs as ufs;
static RESIZED_SUBDIR: &str = "processed_images"; static RESIZED_SUBDIR: &str = "processed_images";
const DEFAULT_Q_JPG: u8 = 75;
lazy_static! { lazy_static! {
pub static ref RESIZED_FILENAME: Regex = pub static ref RESIZED_FILENAME: Regex =
@ -51,14 +53,12 @@ impl ResizeOp {
match op { match op {
"fit_width" => { "fit_width" => {
if width.is_none() { if width.is_none() {
return Err("op=\"fit_width\" requires a `width` argument".to_string().into()); return Err("op=\"fit_width\" requires a `width` argument".into());
} }
} }
"fit_height" => { "fit_height" => {
if height.is_none() { if height.is_none() {
return Err("op=\"fit_height\" requires a `height` argument" return Err("op=\"fit_height\" requires a `height` argument".into());
.to_string()
.into());
} }
} }
"scale" | "fit" | "fill" => { "scale" | "fit" | "fill" => {
@ -132,8 +132,6 @@ impl Hash for ResizeOp {
} }
} }
} }
const DEFAULT_Q_JPG: u8 = 75;
/// Thumbnail image format /// Thumbnail image format
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format { pub enum Format {
@ -215,6 +213,7 @@ impl Hash for Format {
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct ImageOp { pub struct ImageOp {
source: String, source: String,
input_path: PathBuf,
op: ResizeOp, op: ResizeOp,
format: Format, format: Format,
/// Hash of the above parameters /// Hash of the above parameters
@ -227,18 +226,9 @@ pub struct ImageOp {
} }
impl ImageOp { impl ImageOp {
pub fn new(source: String, op: ResizeOp, format: Format) -> ImageOp {
let mut hasher = DefaultHasher::new();
hasher.write(source.as_ref());
op.hash(&mut hasher);
format.hash(&mut hasher);
let hash = hasher.finish();
ImageOp { source, op, format, hash, collision_id: 0 }
}
pub fn from_args( pub fn from_args(
source: String, source: String,
input_path: PathBuf,
op: &str, op: &str,
width: Option<u32>, width: Option<u32>,
height: Option<u32>, height: Option<u32>,
@ -247,18 +237,24 @@ impl ImageOp {
) -> Result<ImageOp> { ) -> Result<ImageOp> {
let op = ResizeOp::from_args(op, width, height)?; let op = ResizeOp::from_args(op, width, height)?;
let format = Format::from_args(&source, format, quality)?; let format = Format::from_args(&source, format, quality)?;
Ok(Self::new(source, op, format))
let mut hasher = DefaultHasher::new();
hasher.write(source.as_ref());
op.hash(&mut hasher);
format.hash(&mut hasher);
let hash = hasher.finish();
Ok(ImageOp { source, input_path, op, format, hash, collision_id: 0 })
} }
fn perform(&self, content_path: &Path, target_path: &Path) -> Result<()> { fn perform(&self, target_path: &Path) -> Result<()> {
use ResizeOp::*; use ResizeOp::*;
let src_path = content_path.join(&self.source); if !ufs::file_stale(&self.input_path, target_path) {
if !ufs::file_stale(&src_path, target_path) {
return Ok(()); return Ok(());
} }
let mut img = image::open(&src_path)?; let mut img = image::open(&self.input_path)?;
let (img_w, img_h) = img.dimensions(); let (img_w, img_h) = img.dimensions();
const RESIZE_FILTER: FilterType = FilterType::Lanczos3; const RESIZE_FILTER: FilterType = FilterType::Lanczos3;
@ -266,8 +262,8 @@ impl ImageOp {
let img = match self.op { let img = match self.op {
Scale(w, h) => img.resize_exact(w, h, RESIZE_FILTER), Scale(w, h) => img.resize_exact(w, h, RESIZE_FILTER),
FitWidth(w) => img.resize(w, u32::max_value(), RESIZE_FILTER), FitWidth(w) => img.resize(w, u32::MAX, RESIZE_FILTER),
FitHeight(h) => img.resize(u32::max_value(), h, RESIZE_FILTER), FitHeight(h) => img.resize(u32::MAX, h, RESIZE_FILTER),
Fit(w, h) => { Fit(w, h) => {
if img_w > w || img_h > h { if img_w > w || img_h > h {
img.resize(w, h, RESIZE_FILTER) img.resize(w, h, RESIZE_FILTER)
@ -328,14 +324,15 @@ impl ImageOp {
} }
} }
/// A strcture into which image operations can be enqueued and then performed. /// A struct into which image operations can be enqueued and then performed.
/// All output is written in a subdirectory in `static_path`, /// All output is written in a subdirectory in `static_path`,
/// taking care of file stale status based on timestamps and possible hash collisions. /// taking care of file stale status based on timestamps and possible hash collisions.
#[derive(Debug)] #[derive(Debug)]
pub struct Processor { pub struct Processor {
content_path: PathBuf, /// The base path of the Zola site
resized_path: PathBuf, base_path: PathBuf,
resized_url: String, base_url: String,
output_dir: PathBuf,
/// A map of a ImageOps by their stored hash. /// A map of a ImageOps by their stored hash.
/// Note that this cannot be a HashSet, because hashset handles collisions and we don't want that, /// Note that this cannot be a HashSet, because hashset handles collisions and we don't want that,
/// we need to be aware of and handle collisions ourselves. /// we need to be aware of and handle collisions ourselves.
@ -345,30 +342,18 @@ pub struct Processor {
} }
impl Processor { impl Processor {
pub fn new(content_path: PathBuf, static_path: &Path, base_url: &str) -> Processor { pub fn new(base_path: PathBuf, config: &Config) -> Processor {
Processor { Processor {
content_path, output_dir: base_path.join("static").join(RESIZED_SUBDIR),
resized_path: static_path.join(RESIZED_SUBDIR), base_url: config.make_permalink(RESIZED_SUBDIR),
resized_url: Self::resized_url(base_url), base_path,
img_ops: HashMap::new(), img_ops: HashMap::new(),
img_ops_collisions: Vec::new(), img_ops_collisions: Vec::new(),
} }
} }
fn resized_url(base_url: &str) -> String { pub fn set_base_url(&mut self, config: &Config) {
if base_url.ends_with('/') { self.base_url = config.make_permalink(RESIZED_SUBDIR);
format!("{}{}", base_url, RESIZED_SUBDIR)
} else {
format!("{}/{}", base_url, RESIZED_SUBDIR)
}
}
pub fn set_base_url(&mut self, base_url: &str) {
self.resized_url = Self::resized_url(base_url);
}
pub fn source_exists(&self, source: &str) -> bool {
self.content_path.join(source).exists()
} }
pub fn num_img_ops(&self) -> usize { pub fn num_img_ops(&self) -> usize {
@ -427,25 +412,25 @@ impl Processor {
format!("{:016x}{:02x}.{}", hash, collision_id, format.extension()) format!("{:016x}{:02x}.{}", hash, collision_id, format.extension())
} }
fn op_url(&self, hash: u64, collision_id: u32, format: Format) -> String { /// Adds the given operation to the queue but do not process it immediately.
format!("{}/{}", &self.resized_url, Self::op_filename(hash, collision_id, format)) /// Returns (path in static folder, final URL).
} pub fn insert(&mut self, img_op: ImageOp) -> (PathBuf, String) {
pub fn insert(&mut self, img_op: ImageOp) -> String {
let hash = img_op.hash; let hash = img_op.hash;
let format = img_op.format; let format = img_op.format;
let collision_id = self.insert_with_collisions(img_op); let collision_id = self.insert_with_collisions(img_op);
self.op_url(hash, collision_id, format) let filename = Self::op_filename(hash, collision_id, format);
let url = format!("{}{}", self.base_url, filename);
(Path::new("static").join(RESIZED_SUBDIR).join(filename), url)
} }
pub fn prune(&self) -> Result<()> { pub fn prune(&self) -> Result<()> {
// Do not create folders if they don't exist // Do not create folders if they don't exist
if !self.resized_path.exists() { if !self.output_dir.exists() {
return Ok(()); return Ok(());
} }
ufs::ensure_directory_exists(&self.resized_path)?; ufs::ensure_directory_exists(&self.output_dir)?;
let entries = fs::read_dir(&self.resized_path)?; let entries = fs::read_dir(&self.output_dir)?;
for entry in entries { for entry in entries {
let entry_path = entry?.path(); let entry_path = entry?.path();
if entry_path.is_file() { if entry_path.is_file() {
@ -466,15 +451,15 @@ impl Processor {
pub fn do_process(&mut self) -> Result<()> { pub fn do_process(&mut self) -> Result<()> {
if !self.img_ops.is_empty() { if !self.img_ops.is_empty() {
ufs::ensure_directory_exists(&self.resized_path)?; ufs::ensure_directory_exists(&self.output_dir)?;
} }
self.img_ops self.img_ops
.par_iter() .par_iter()
.map(|(hash, op)| { .map(|(hash, op)| {
let target = let target =
self.resized_path.join(Self::op_filename(*hash, op.collision_id, op.format)); self.output_dir.join(Self::op_filename(*hash, op.collision_id, op.format));
op.perform(&self.content_path, &target) op.perform(&target)
.map_err(|e| Error::chain(format!("Failed to process image: {}", op.source), e)) .map_err(|e| Error::chain(format!("Failed to process image: {}", op.source), e))
}) })
.collect::<Result<()>>() .collect::<Result<()>>()

View file

@ -3,11 +3,15 @@ mod page;
mod section; mod section;
mod ser; mod ser;
use std::fs::read_dir;
use std::path::{Path, PathBuf};
pub use self::file_info::FileInfo; pub use self::file_info::FileInfo;
pub use self::page::Page; pub use self::page::Page;
pub use self::section::Section; pub use self::section::Section;
pub use self::ser::{SerializingPage, SerializingSection}; pub use self::ser::{SerializingPage, SerializingSection};
use config::Config;
use rendering::Heading; use rendering::Heading;
pub fn has_anchor(headings: &[Heading], anchor: &str) -> bool { pub fn has_anchor(headings: &[Heading], anchor: &str) -> bool {
@ -23,9 +27,67 @@ pub fn has_anchor(headings: &[Heading], anchor: &str) -> bool {
false false
} }
/// Looks into the current folder for the path and see if there's anything that is not a .md
/// file. Those will be copied next to the rendered .html file
pub fn find_related_assets(path: &Path, config: &Config) -> Vec<PathBuf> {
let mut assets = vec![];
for entry in read_dir(path).unwrap().filter_map(std::result::Result::ok) {
let entry_path = entry.path();
if entry_path.is_file() {
match entry_path.extension() {
Some(e) => match e.to_str() {
Some("md") => continue,
_ => assets.push(entry_path.to_path_buf()),
},
None => continue,
}
}
}
if let Some(ref globset) = config.ignored_content_globset {
// `find_related_assets` only scans the immediate directory (it is not recursive) so our
// filtering only needs to work against the file_name component, not the full suffix. If
// `find_related_assets` was changed to also return files in subdirectories, we could
// use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter
// against the remaining path. Note that the current behaviour effectively means that
// the `ignored_content` setting in the config file is limited to single-file glob
// patterns (no "**" patterns).
assets = assets
.into_iter()
.filter(|path| match path.file_name() {
None => false,
Some(file) => !globset.is_match(file),
})
.collect();
}
assets
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::fs::File;
use config::Config;
use tempfile::tempdir;
#[test]
fn can_find_related_assets() {
let tmp_dir = tempdir().expect("create temp dir");
File::create(tmp_dir.path().join("index.md")).unwrap();
File::create(tmp_dir.path().join("example.js")).unwrap();
File::create(tmp_dir.path().join("graph.jpg")).unwrap();
File::create(tmp_dir.path().join("fail.png")).unwrap();
let assets = find_related_assets(tmp_dir.path(), &Config::default());
assert_eq!(assets.len(), 3);
assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 3);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "example.js").count(), 1);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "graph.jpg").count(), 1);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "fail.png").count(), 1);
}
#[test] #[test]
fn can_find_anchor_at_root() { fn can_find_anchor_at_root() {

View file

@ -12,14 +12,14 @@ use config::Config;
use errors::{Error, Result}; use errors::{Error, Result};
use front_matter::{split_page_content, InsertAnchor, PageFrontMatter}; use front_matter::{split_page_content, InsertAnchor, PageFrontMatter};
use rendering::{render_content, Heading, RenderContext}; use rendering::{render_content, Heading, RenderContext};
use utils::fs::{find_related_assets, read_file};
use utils::site::get_reading_analytics; use utils::site::get_reading_analytics;
use utils::slugs::slugify_paths;
use utils::templates::render_template; use utils::templates::render_template;
use crate::content::file_info::FileInfo; use crate::content::file_info::FileInfo;
use crate::content::has_anchor;
use crate::content::ser::SerializingPage; use crate::content::ser::SerializingPage;
use utils::slugs::slugify_paths; use crate::content::{find_related_assets, has_anchor};
use utils::fs::read_file;
lazy_static! { lazy_static! {
// Based on https://regex101.com/r/H2n38Z/1/tests // Based on https://regex101.com/r/H2n38Z/1/tests
@ -43,7 +43,7 @@ pub struct Page {
pub raw_content: String, pub raw_content: String,
/// All the non-md files we found next to the .md file /// All the non-md files we found next to the .md file
pub assets: Vec<PathBuf>, pub assets: Vec<PathBuf>,
/// All the non-md files we found next to the .md file as string for use in templates /// All the non-md files we found next to the .md file
pub serialized_assets: Vec<String>, pub serialized_assets: Vec<String>,
/// The HTML rendered of the page /// The HTML rendered of the page
pub content: String, pub content: String,
@ -216,27 +216,7 @@ impl Page {
if page.file.name == "index" { if page.file.name == "index" {
let parent_dir = path.parent().unwrap(); let parent_dir = path.parent().unwrap();
let assets = find_related_assets(parent_dir); page.assets = find_related_assets(parent_dir, config);
if let Some(ref globset) = config.ignored_content_globset {
// `find_related_assets` only scans the immediate directory (it is not recursive) so our
// filtering only needs to work against the file_name component, not the full suffix. If
// `find_related_assets` was changed to also return files in subdirectories, we could
// use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter
// against the remaining path. Note that the current behaviour effectively means that
// the `ignored_content` setting in the config file is limited to single-file glob
// patterns (no "**" patterns).
page.assets = assets
.into_iter()
.filter(|path| match path.file_name() {
None => false,
Some(file) => !globset.is_match(file),
})
.collect();
} else {
page.assets = assets;
}
page.serialized_assets = page.serialize_assets(&base_path); page.serialized_assets = page.serialize_assets(&base_path);
} else { } else {
page.assets = vec![]; page.assets = vec![];

View file

@ -8,13 +8,13 @@ use config::Config;
use errors::{Error, Result}; use errors::{Error, Result};
use front_matter::{split_section_content, SectionFrontMatter}; use front_matter::{split_section_content, SectionFrontMatter};
use rendering::{render_content, Heading, RenderContext}; use rendering::{render_content, Heading, RenderContext};
use utils::fs::{find_related_assets, read_file}; use utils::fs::read_file;
use utils::site::get_reading_analytics; use utils::site::get_reading_analytics;
use utils::templates::render_template; use utils::templates::render_template;
use crate::content::file_info::FileInfo; use crate::content::file_info::FileInfo;
use crate::content::has_anchor;
use crate::content::ser::SerializingSection; use crate::content::ser::SerializingSection;
use crate::content::{find_related_assets, has_anchor};
use crate::library::Library; use crate::library::Library;
// Default is used to create a default index section if there is no _index.md in the root content directory // Default is used to create a default index section if there is no _index.md in the root content directory
@ -36,7 +36,7 @@ pub struct Section {
pub content: String, pub content: String,
/// All the non-md files we found next to the .md file /// All the non-md files we found next to the .md file
pub assets: Vec<PathBuf>, pub assets: Vec<PathBuf>,
/// All the non-md files we found next to the .md file as string for use in templates /// All the non-md files we found next to the .md file as string
pub serialized_assets: Vec<String>, pub serialized_assets: Vec<String>,
/// All direct pages of that section /// All direct pages of that section
pub pages: Vec<DefaultKey>, pub pages: Vec<DefaultKey>,
@ -122,27 +122,7 @@ impl Section {
let mut section = Section::parse(path, &content, config, base_path)?; let mut section = Section::parse(path, &content, config, base_path)?;
let parent_dir = path.parent().unwrap(); let parent_dir = path.parent().unwrap();
let assets = find_related_assets(parent_dir); section.assets = find_related_assets(parent_dir, config);
if let Some(ref globset) = config.ignored_content_globset {
// `find_related_assets` only scans the immediate directory (it is not recursive) so our
// filtering only needs to work against the file_name component, not the full suffix. If
// `find_related_assets` was changed to also return files in subdirectories, we could
// use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter
// against the remaining path. Note that the current behaviour effectively means that
// the `ignored_content` setting in the config file is limited to single-file glob
// patterns (no "**" patterns).
section.assets = assets
.into_iter()
.filter(|path| match path.file_name() {
None => false,
Some(file) => !globset.is_match(file),
})
.collect();
} else {
section.assets = assets;
}
section.serialized_assets = section.serialize_assets(); section.serialized_assets = section.serialize_assets();
Ok(section) Ok(section)

View file

@ -85,8 +85,7 @@ impl Site {
let content_path = path.join("content"); let content_path = path.join("content");
let static_path = path.join("static"); let static_path = path.join("static");
let imageproc = let imageproc = imageproc::Processor::new(path.to_path_buf(), &config);
imageproc::Processor::new(content_path.clone(), &static_path, &config.base_url);
let output_path = path.join(config.output_dir.clone()); let output_path = path.join(config.output_dir.clone());
let site = Site { let site = Site {
@ -152,9 +151,9 @@ impl Site {
} }
pub fn set_base_url(&mut self, base_url: String) { pub fn set_base_url(&mut self, base_url: String) {
let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (set_base_url)");
imageproc.set_base_url(&base_url);
self.config.base_url = base_url; self.config.base_url = base_url;
let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (set_base_url)");
imageproc.set_base_url(&self.config);
} }
pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) { pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) {

View file

@ -21,11 +21,13 @@ pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> {
vec![site.static_path.clone(), site.output_path.clone(), site.content_path.clone()], vec![site.static_path.clone(), site.output_path.clone(), site.content_path.clone()],
), ),
); );
site.tera site.tera.register_function(
.register_function("resize_image", global_fns::ResizeImage::new(site.imageproc.clone())); "resize_image",
global_fns::ResizeImage::new(site.base_path.clone(), site.imageproc.clone()),
);
site.tera.register_function( site.tera.register_function(
"get_image_metadata", "get_image_metadata",
global_fns::GetImageMeta::new(site.content_path.clone()), global_fns::GetImageMetadata::new(site.base_path.clone()),
); );
site.tera.register_function("load_data", global_fns::LoadData::new(site.base_path.clone())); site.tera.register_function("load_data", global_fns::LoadData::new(site.base_path.clone()));
site.tera.register_function("trans", global_fns::Trans::new(site.config.clone())); site.tera.register_function("trans", global_fns::Trans::new(site.config.clone()));

View file

@ -11,7 +11,9 @@ lazy_static = "1"
toml = "0.5" toml = "0.5"
csv = "1" csv = "1"
image = "0.23" image = "0.23"
serde_json = "1.0" serde = "1"
serde_json = "1"
serde_derive = "1"
sha2 = "0.9" sha2 = "0.9"
url = "2" url = "2"
nom-bibtex = "0.3" nom-bibtex = "0.3"

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -4,16 +4,31 @@ use std::path::PathBuf;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use image::GenericImageView; use image::GenericImageView;
use serde_derive::{Deserialize, Serialize};
use svg_metadata as svg; use svg_metadata as svg;
use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value}; use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value};
#[derive(Debug, Serialize, Deserialize)]
struct ResizeImageResponse {
/// The final URL for that asset
url: String,
/// The path to the static asset generated
static_path: String,
}
#[derive(Debug)] #[derive(Debug)]
pub struct ResizeImage { pub struct ResizeImage {
/// The base path of the Zola site
base_path: PathBuf,
search_paths: [PathBuf; 2],
imageproc: Arc<Mutex<imageproc::Processor>>, imageproc: Arc<Mutex<imageproc::Processor>>,
} }
impl ResizeImage { impl ResizeImage {
pub fn new(imageproc: Arc<Mutex<imageproc::Processor>>) -> Self { pub fn new(base_path: PathBuf, imageproc: Arc<Mutex<imageproc::Processor>>) -> Self {
Self { imageproc } let search_paths =
[base_path.join("static").to_path_buf(), base_path.join("content").to_path_buf()];
Self { base_path, imageproc, search_paths }
} }
} }
@ -22,7 +37,7 @@ static DEFAULT_FMT: &str = "auto";
impl TeraFn for ResizeImage { impl TeraFn for ResizeImage {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> { fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
let path = required_arg!( let mut path = required_arg!(
String, String,
args.get("path"), args.get("path"),
"`resize_image` requires a `path` argument with a string value" "`resize_image` requires a `path` argument with a string value"
@ -53,45 +68,38 @@ impl TeraFn for ResizeImage {
} }
let mut imageproc = self.imageproc.lock().unwrap(); let mut imageproc = self.imageproc.lock().unwrap();
if !imageproc.source_exists(&path) { if path.starts_with("@/") {
path = path.replace("@/", "content/");
}
let mut file_path = self.base_path.join(&path);
let mut file_exists = file_path.exists();
if !file_exists {
// we need to search in both search folders now
for dir in &self.search_paths {
let p = dir.join(&path);
if p.exists() {
file_path = p;
file_exists = true;
break;
}
}
}
if !file_exists {
return Err(format!("`resize_image`: Cannot find path: {}", path).into()); return Err(format!("`resize_image`: Cannot find path: {}", path).into());
} }
let imageop = imageproc::ImageOp::from_args(path, &op, width, height, &format, quality) let imageop =
.map_err(|e| format!("`resize_image`: {}", e))?; imageproc::ImageOp::from_args(path, file_path, &op, width, height, &format, quality)
let url = imageproc.insert(imageop); .map_err(|e| format!("`resize_image`: {}", e))?;
let (static_path, url) = imageproc.insert(imageop);
to_value(url).map_err(|err| err.into()) to_value(ResizeImageResponse {
} static_path: static_path.to_string_lossy().into_owned(),
} url,
})
#[derive(Debug)] .map_err(|err| err.into())
pub struct GetImageMeta {
content_path: PathBuf,
}
impl GetImageMeta {
pub fn new(content_path: PathBuf) -> Self {
Self { content_path }
}
}
impl TeraFn for GetImageMeta {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
let path = required_arg!(
String,
args.get("path"),
"`get_image_metadata` requires a `path` argument with a string value"
);
let src_path = self.content_path.join(&path);
if !src_path.exists() {
return Err(format!("`get_image_metadata`: Cannot find path: {}", path).into());
}
let (height, width) = image_dimensions(&src_path)?;
let mut map = tera::Map::new();
map.insert(String::from("height"), Value::Number(tera::Number::from(height)));
map.insert(String::from("width"), Value::Number(tera::Number::from(width)));
Ok(Value::Object(map))
} }
} }
@ -112,9 +120,163 @@ fn image_dimensions(path: &PathBuf) -> Result<(u32, u32)> {
} }
} }
#[derive(Debug)]
pub struct GetImageMetadata {
/// The base path of the Zola site
base_path: PathBuf,
}
impl GetImageMetadata {
pub fn new(base_path: PathBuf) -> Self {
Self { base_path }
}
}
impl TeraFn for GetImageMetadata {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
let mut path = required_arg!(
String,
args.get("path"),
"`get_image_metadata` requires a `path` argument with a string value"
);
if path.starts_with("@/") {
path = path.replace("@/", "content/");
}
let src_path = self.base_path.join(&path);
if !src_path.exists() {
return Err(format!("`get_image_metadata`: Cannot find path: {}", path).into());
}
let (height, width) = image_dimensions(&src_path)?;
let mut map = tera::Map::new();
map.insert(String::from("height"), Value::Number(tera::Number::from(height)));
map.insert(String::from("width"), Value::Number(tera::Number::from(width)));
Ok(Value::Object(map))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::{GetImageMetadata, ResizeImage};
// TODO use std::collections::HashMap;
use std::fs::{copy, create_dir_all};
use config::Config;
use std::sync::{Arc, Mutex};
use tempfile::{tempdir, TempDir};
use tera::{to_value, Function};
fn create_dir_with_image() -> TempDir {
let dir = tempdir().unwrap();
create_dir_all(dir.path().join("content").join("gallery")).unwrap();
create_dir_all(dir.path().join("static")).unwrap();
copy("gutenberg.jpg", dir.path().join("content").join("gutenberg.jpg")).unwrap();
copy("gutenberg.jpg", dir.path().join("content").join("gallery").join("asset.jpg"))
.unwrap();
copy("gutenberg.jpg", dir.path().join("static").join("gutenberg.jpg")).unwrap();
dir
}
// https://github.com/getzola/zola/issues/788
// https://github.com/getzola/zola/issues/1035
#[test]
fn can_resize_image() {
let dir = create_dir_with_image();
let imageproc = imageproc::Processor::new(dir.path().to_path_buf(), &Config::default());
let static_fn = ResizeImage::new(dir.path().to_path_buf(), Arc::new(Mutex::new(imageproc)));
let mut args = HashMap::new();
args.insert("height".to_string(), to_value(40).unwrap());
args.insert("width".to_string(), to_value(40).unwrap());
// hashing is stable based on filename and params so we can compare with hashes
// 1. resizing an image in static
args.insert("path".to_string(), to_value("static/gutenberg.jpg").unwrap());
let data = static_fn.call(&args).unwrap().as_object().unwrap().clone();
assert_eq!(
data["static_path"],
to_value("static/processed_images/e49f5bd23ec5007c00.jpg").unwrap()
);
assert_eq!(
data["url"],
to_value("http://a-website.com/processed_images/e49f5bd23ec5007c00.jpg").unwrap()
);
// 2. resizing an image in content with a relative path
args.insert("path".to_string(), to_value("content/gutenberg.jpg").unwrap());
let data = static_fn.call(&args).unwrap().as_object().unwrap().clone();
assert_eq!(
data["static_path"],
to_value("static/processed_images/32454a1e0243976c00.jpg").unwrap()
);
assert_eq!(
data["url"],
to_value("http://a-website.com/processed_images/32454a1e0243976c00.jpg").unwrap()
);
// 3. resizing an image in content starting with `@/`
args.insert("path".to_string(), to_value("@/gutenberg.jpg").unwrap());
let data = static_fn.call(&args).unwrap().as_object().unwrap().clone();
assert_eq!(
data["static_path"],
to_value("static/processed_images/32454a1e0243976c00.jpg").unwrap()
);
assert_eq!(
data["url"],
to_value("http://a-website.com/processed_images/32454a1e0243976c00.jpg").unwrap()
);
// 4. resizing an image with a relative path not starting with static or content
args.insert("path".to_string(), to_value("gallery/asset.jpg").unwrap());
let data = static_fn.call(&args).unwrap().as_object().unwrap().clone();
assert_eq!(
data["static_path"],
to_value("static/processed_images/c8aaba7b0593a60b00.jpg").unwrap()
);
assert_eq!(
data["url"],
to_value("http://a-website.com/processed_images/c8aaba7b0593a60b00.jpg").unwrap()
);
// 5. resizing with an absolute path
args.insert("path".to_string(), to_value("/content/gutenberg.jpg").unwrap());
assert!(static_fn.call(&args).is_err());
}
// TODO: consider https://github.com/getzola/zola/issues/1161
#[test]
fn can_get_image_metadata() {
let dir = create_dir_with_image();
let static_fn = GetImageMetadata::new(dir.path().to_path_buf());
// Let's test a few scenarii
let mut args = HashMap::new();
// 1. a call to something in `static` with a relative path
args.insert("path".to_string(), to_value("static/gutenberg.jpg").unwrap());
let data = static_fn.call(&args).unwrap().as_object().unwrap().clone();
assert_eq!(data["height"], to_value(380).unwrap());
assert_eq!(data["width"], to_value(300).unwrap());
// 2. a call to something in `static` with an absolute path is not handled currently
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("/static/gutenberg.jpg").unwrap());
assert!(static_fn.call(&args).is_err());
// 3. a call to something in `content` with a relative path
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("content/gutenberg.jpg").unwrap());
let data = static_fn.call(&args).unwrap().as_object().unwrap().clone();
assert_eq!(data["height"], to_value(380).unwrap());
assert_eq!(data["width"], to_value(300).unwrap());
// 4. a call to something in `content` with a @/ path corresponds to
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("@/gutenberg.jpg").unwrap());
let data = static_fn.call(&args).unwrap().as_object().unwrap().clone();
assert_eq!(data["height"], to_value(380).unwrap());
assert_eq!(data["width"], to_value(300).unwrap());
}
} }

View file

@ -19,7 +19,7 @@ mod load_data;
pub use self::content::{GetPage, GetSection, GetTaxonomy, GetTaxonomyUrl}; pub use self::content::{GetPage, GetSection, GetTaxonomy, GetTaxonomyUrl};
pub use self::i18n::Trans; pub use self::i18n::Trans;
pub use self::images::{GetImageMeta, ResizeImage}; pub use self::images::{GetImageMetadata, ResizeImage};
pub use self::load_data::LoadData; pub use self::load_data::LoadData;
#[derive(Debug)] #[derive(Debug)]

View file

@ -1,5 +1,5 @@
use filetime::{set_file_mtime, FileTime}; use filetime::{set_file_mtime, FileTime};
use std::fs::{copy, create_dir_all, metadata, read_dir, File}; use std::fs::{copy, create_dir_all, metadata, File};
use std::io::prelude::*; use std::io::prelude::*;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::SystemTime; use std::time::SystemTime;
@ -60,27 +60,6 @@ pub fn read_file(path: &Path) -> Result<String> {
Ok(content) Ok(content)
} }
/// Looks into the current folder for the path and see if there's anything that is not a .md
/// file. Those will be copied next to the rendered .html file
pub fn find_related_assets(path: &Path) -> Vec<PathBuf> {
let mut assets = vec![];
for entry in read_dir(path).unwrap().filter_map(std::result::Result::ok) {
let entry_path = entry.path();
if entry_path.is_file() {
match entry_path.extension() {
Some(e) => match e.to_str() {
Some("md") => continue,
_ => assets.push(entry_path.to_path_buf()),
},
None => continue,
}
}
}
assets
}
/// Copy a file but takes into account where to start the copy as /// Copy a file but takes into account where to start the copy as
/// there might be folders we need to create on the way. /// there might be folders we need to create on the way.
pub fn copy_file(src: &Path, dest: &PathBuf, base_path: &PathBuf, hard_link: bool) -> Result<()> { pub fn copy_file(src: &Path, dest: &PathBuf, base_path: &PathBuf, hard_link: bool) -> Result<()> {
@ -204,25 +183,9 @@ mod tests {
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use tempfile::{tempdir, tempdir_in}; use tempfile::tempdir_in;
use super::{copy_file, find_related_assets}; use super::copy_file;
#[test]
fn can_find_related_assets() {
let tmp_dir = tempdir().expect("create temp dir");
File::create(tmp_dir.path().join("index.md")).unwrap();
File::create(tmp_dir.path().join("example.js")).unwrap();
File::create(tmp_dir.path().join("graph.jpg")).unwrap();
File::create(tmp_dir.path().join("fail.png")).unwrap();
let assets = find_related_assets(tmp_dir.path());
assert_eq!(assets.len(), 3);
assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 3);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "example.js").count(), 1);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "graph.jpg").count(), 1);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "fail.png").count(), 1);
}
#[test] #[test]
fn test_copy_file_timestamp_preserved() { fn test_copy_file_timestamp_preserved() {