From 7fb99eaa44f387fadf744354c605f0cfcc582800 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Wed, 12 May 2021 21:07:18 +0200 Subject: [PATCH] Revamp the images template functions --- CHANGELOG.md | 1 + Cargo.lock | 3 + components/imageproc/Cargo.toml | 3 +- components/imageproc/src/lib.rs | 99 +++---- components/library/src/content/mod.rs | 62 +++++ components/library/src/content/page.rs | 30 +-- components/library/src/content/section.rs | 28 +- components/site/src/lib.rs | 7 +- components/site/src/tpls.rs | 8 +- components/templates/Cargo.toml | 4 +- components/templates/gutenberg.jpg | Bin 0 -> 47783 bytes components/templates/src/global_fns/images.rs | 242 +++++++++++++++--- components/templates/src/global_fns/mod.rs | 2 +- components/utils/src/fs.rs | 43 +--- 14 files changed, 336 insertions(+), 196 deletions(-) create mode 100644 components/templates/gutenberg.jpg diff --git a/CHANGELOG.md b/CHANGELOG.md index 65743edb..1c035759 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Breaking - 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 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` diff --git a/Cargo.lock b/Cargo.lock index d7b834f5..6e9b889f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1022,6 +1022,7 @@ dependencies = [ name = "imageproc" version = "0.1.0" dependencies = [ + "config", "errors", "image", "lazy_static", @@ -2592,6 +2593,8 @@ dependencies = [ "nom-bibtex", "rendering", "reqwest", + "serde", + "serde_derive", "serde_json", "sha2", "svg_metadata", diff --git a/components/imageproc/Cargo.toml b/components/imageproc/Cargo.toml index a24e8a34..70a9af1a 100644 --- a/components/imageproc/Cargo.toml +++ b/components/imageproc/Cargo.toml @@ -10,7 +10,8 @@ regex = "1.0" tera = "1" image = "0.23" rayon = "1" -webp="0.1.1" +webp = "0.1.1" errors = { path = "../errors" } utils = { path = "../utils" } +config = { path = "../config" } diff --git a/components/imageproc/src/lib.rs b/components/imageproc/src/lib.rs index 2ce000fe..4e04f033 100644 --- a/components/imageproc/src/lib.rs +++ b/components/imageproc/src/lib.rs @@ -11,10 +11,12 @@ use lazy_static::lazy_static; use rayon::prelude::*; use regex::Regex; +use config::Config; use errors::{Error, Result}; use utils::fs as ufs; static RESIZED_SUBDIR: &str = "processed_images"; +const DEFAULT_Q_JPG: u8 = 75; lazy_static! { pub static ref RESIZED_FILENAME: Regex = @@ -51,14 +53,12 @@ impl ResizeOp { match op { "fit_width" => { 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" => { if height.is_none() { - return Err("op=\"fit_height\" requires a `height` argument" - .to_string() - .into()); + return Err("op=\"fit_height\" requires a `height` argument".into()); } } "scale" | "fit" | "fill" => { @@ -132,8 +132,6 @@ impl Hash for ResizeOp { } } } -const DEFAULT_Q_JPG: u8 = 75; - /// Thumbnail image format #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Format { @@ -215,6 +213,7 @@ impl Hash for Format { #[derive(Debug, PartialEq, Eq)] pub struct ImageOp { source: String, + input_path: PathBuf, op: ResizeOp, format: Format, /// Hash of the above parameters @@ -227,18 +226,9 @@ pub struct 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( source: String, + input_path: PathBuf, op: &str, width: Option, height: Option, @@ -247,18 +237,24 @@ impl ImageOp { ) -> Result { let op = ResizeOp::from_args(op, width, height)?; 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::*; - let src_path = content_path.join(&self.source); - if !ufs::file_stale(&src_path, target_path) { + if !ufs::file_stale(&self.input_path, target_path) { return Ok(()); } - let mut img = image::open(&src_path)?; + let mut img = image::open(&self.input_path)?; let (img_w, img_h) = img.dimensions(); const RESIZE_FILTER: FilterType = FilterType::Lanczos3; @@ -266,8 +262,8 @@ impl ImageOp { let img = match self.op { Scale(w, h) => img.resize_exact(w, h, RESIZE_FILTER), - FitWidth(w) => img.resize(w, u32::max_value(), RESIZE_FILTER), - FitHeight(h) => img.resize(u32::max_value(), h, RESIZE_FILTER), + FitWidth(w) => img.resize(w, u32::MAX, RESIZE_FILTER), + FitHeight(h) => img.resize(u32::MAX, h, RESIZE_FILTER), Fit(w, h) => { if img_w > w || img_h > h { 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`, /// taking care of file stale status based on timestamps and possible hash collisions. #[derive(Debug)] pub struct Processor { - content_path: PathBuf, - resized_path: PathBuf, - resized_url: String, + /// The base path of the Zola site + base_path: PathBuf, + base_url: String, + output_dir: PathBuf, /// 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, /// we need to be aware of and handle collisions ourselves. @@ -345,30 +342,18 @@ pub struct 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 { - content_path, - resized_path: static_path.join(RESIZED_SUBDIR), - resized_url: Self::resized_url(base_url), + output_dir: base_path.join("static").join(RESIZED_SUBDIR), + base_url: config.make_permalink(RESIZED_SUBDIR), + base_path, img_ops: HashMap::new(), img_ops_collisions: Vec::new(), } } - fn resized_url(base_url: &str) -> String { - if base_url.ends_with('/') { - 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 set_base_url(&mut self, config: &Config) { + self.base_url = config.make_permalink(RESIZED_SUBDIR); } pub fn num_img_ops(&self) -> usize { @@ -427,25 +412,25 @@ impl Processor { format!("{:016x}{:02x}.{}", hash, collision_id, format.extension()) } - fn op_url(&self, hash: u64, collision_id: u32, format: Format) -> String { - format!("{}/{}", &self.resized_url, Self::op_filename(hash, collision_id, format)) - } - - pub fn insert(&mut self, img_op: ImageOp) -> String { + /// Adds the given operation to the queue but do not process it immediately. + /// Returns (path in static folder, final URL). + pub fn insert(&mut self, img_op: ImageOp) -> (PathBuf, String) { let hash = img_op.hash; let format = img_op.format; 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<()> { // Do not create folders if they don't exist - if !self.resized_path.exists() { + if !self.output_dir.exists() { return Ok(()); } - ufs::ensure_directory_exists(&self.resized_path)?; - let entries = fs::read_dir(&self.resized_path)?; + ufs::ensure_directory_exists(&self.output_dir)?; + let entries = fs::read_dir(&self.output_dir)?; for entry in entries { let entry_path = entry?.path(); if entry_path.is_file() { @@ -466,15 +451,15 @@ impl Processor { pub fn do_process(&mut self) -> Result<()> { if !self.img_ops.is_empty() { - ufs::ensure_directory_exists(&self.resized_path)?; + ufs::ensure_directory_exists(&self.output_dir)?; } self.img_ops .par_iter() .map(|(hash, op)| { let target = - self.resized_path.join(Self::op_filename(*hash, op.collision_id, op.format)); - op.perform(&self.content_path, &target) + self.output_dir.join(Self::op_filename(*hash, op.collision_id, op.format)); + op.perform(&target) .map_err(|e| Error::chain(format!("Failed to process image: {}", op.source), e)) }) .collect::>() diff --git a/components/library/src/content/mod.rs b/components/library/src/content/mod.rs index dc00271b..161a1ab9 100644 --- a/components/library/src/content/mod.rs +++ b/components/library/src/content/mod.rs @@ -3,11 +3,15 @@ mod page; mod section; mod ser; +use std::fs::read_dir; +use std::path::{Path, PathBuf}; + pub use self::file_info::FileInfo; pub use self::page::Page; pub use self::section::Section; pub use self::ser::{SerializingPage, SerializingSection}; +use config::Config; use rendering::Heading; pub fn has_anchor(headings: &[Heading], anchor: &str) -> bool { @@ -23,9 +27,67 @@ pub fn has_anchor(headings: &[Heading], anchor: &str) -> bool { 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 { + 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)] mod tests { 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] fn can_find_anchor_at_root() { diff --git a/components/library/src/content/page.rs b/components/library/src/content/page.rs index d0937187..9fa4c2a1 100644 --- a/components/library/src/content/page.rs +++ b/components/library/src/content/page.rs @@ -12,14 +12,14 @@ use config::Config; use errors::{Error, Result}; use front_matter::{split_page_content, InsertAnchor, PageFrontMatter}; use rendering::{render_content, Heading, RenderContext}; -use utils::fs::{find_related_assets, read_file}; use utils::site::get_reading_analytics; +use utils::slugs::slugify_paths; use utils::templates::render_template; use crate::content::file_info::FileInfo; -use crate::content::has_anchor; 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! { // Based on https://regex101.com/r/H2n38Z/1/tests @@ -43,7 +43,7 @@ pub struct Page { pub raw_content: String, /// All the non-md files we found next to the .md file pub assets: Vec, - /// 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, /// The HTML rendered of the page pub content: String, @@ -216,27 +216,7 @@ impl Page { if page.file.name == "index" { let parent_dir = path.parent().unwrap(); - let assets = find_related_assets(parent_dir); - - 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.assets = find_related_assets(parent_dir, config); page.serialized_assets = page.serialize_assets(&base_path); } else { page.assets = vec![]; diff --git a/components/library/src/content/section.rs b/components/library/src/content/section.rs index 3d4fb6fd..a2d693ad 100644 --- a/components/library/src/content/section.rs +++ b/components/library/src/content/section.rs @@ -8,13 +8,13 @@ use config::Config; use errors::{Error, Result}; use front_matter::{split_section_content, SectionFrontMatter}; 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::templates::render_template; use crate::content::file_info::FileInfo; -use crate::content::has_anchor; use crate::content::ser::SerializingSection; +use crate::content::{find_related_assets, has_anchor}; use crate::library::Library; // 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, /// All the non-md files we found next to the .md file pub assets: Vec, - /// 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, /// All direct pages of that section pub pages: Vec, @@ -122,27 +122,7 @@ impl Section { let mut section = Section::parse(path, &content, config, base_path)?; let parent_dir = path.parent().unwrap(); - let assets = find_related_assets(parent_dir); - - 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.assets = find_related_assets(parent_dir, config); section.serialized_assets = section.serialize_assets(); Ok(section) diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index a69bf39b..37d8c32c 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -85,8 +85,7 @@ impl Site { let content_path = path.join("content"); let static_path = path.join("static"); - let imageproc = - imageproc::Processor::new(content_path.clone(), &static_path, &config.base_url); + let imageproc = imageproc::Processor::new(path.to_path_buf(), &config); let output_path = path.join(config.output_dir.clone()); let site = Site { @@ -152,9 +151,9 @@ impl Site { } 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; + 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>(&mut self, path: P) { diff --git a/components/site/src/tpls.rs b/components/site/src/tpls.rs index 19c6f7ff..0c3ec9d8 100644 --- a/components/site/src/tpls.rs +++ b/components/site/src/tpls.rs @@ -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()], ), ); - site.tera - .register_function("resize_image", global_fns::ResizeImage::new(site.imageproc.clone())); + site.tera.register_function( + "resize_image", + global_fns::ResizeImage::new(site.base_path.clone(), site.imageproc.clone()), + ); site.tera.register_function( "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("trans", global_fns::Trans::new(site.config.clone())); diff --git a/components/templates/Cargo.toml b/components/templates/Cargo.toml index 8baca786..385c1e97 100644 --- a/components/templates/Cargo.toml +++ b/components/templates/Cargo.toml @@ -11,7 +11,9 @@ lazy_static = "1" toml = "0.5" csv = "1" image = "0.23" -serde_json = "1.0" +serde = "1" +serde_json = "1" +serde_derive = "1" sha2 = "0.9" url = "2" nom-bibtex = "0.3" diff --git a/components/templates/gutenberg.jpg b/components/templates/gutenberg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0b031e9c78bdc01bb9fc56755343eb14919a28a3 GIT binary patch literal 47783 zcmbSyWmFtN*X9fZ3~qto4uN37VQ@llC-~s*8r&gh(7}R3NRZ&}?oMzIFgU^8Ki=Iv z-=4F-cK3GGudc3hpVL*hZ{6qVmxY&g0FIoLtP}tU1Ohx?FTl$(Kmq_lK=@C4HN@A3 zgo1>Gh=_y+1|y?jpkZL3qoJc?V&UOnV&P(;qvO2A!F_{IKtOp9l}10RKOe z070+MAR?h6A)(@9qGRI!Kif+u02c-53uFNSX#fbgKoBnQr58X6005C*z5S2F|I2^~ zuRbD!QBcv)UkB9T01$v65CS6TKVH9%_Ite#K*UADqvjAtexqs(rg6sS3`)pDq5W9Z zMWFWQl#a{9B^VX$Eg=yx3H>_;#`jFzJiL7T5CMr#l2Xz#vT~o*H8i!fb#zV5%q=Xf ztZiJsxVd|HdU=O@4GjyAh>U_ICMBn&eossPk^l2oL19tx@9LV`y84F3rsnRR-oE~U z!J*-)>6zKN`Gv)$jZOI0_Rj9!{=wP##pTuY&F$U&e{cZ-p#KT$zmfePxNu)_As`}x z5W)Y!1w`<8?I2u4Bx(+1JaJX9vGW@m&L9;0j|q8IU8uBNYNrGyE`QM8(s6IlpZy2g z|3LPC2Q2vi3)z1I``@^h02m

&pY-0z?6)%xV6M&fEcJ=nn(mT<|17yMO?$dw*{( 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%0ARGaEEY-J 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 kY@jx z4Vk;0+A{m6DYcIn&!Dc9XqMLT7$V-0DQLoj+b0L=J5UFayT=(2%C8)mD$BQNKmiI4 z+~>D!t#MLSLg zN6dTUpQ+}6Ig54*H*MV+P$VF721ar3^{-0smyFX)&@UEvU+n?B`)OW2T0-$*bJ%i8 zA5UuY=>YR0QUMA;0F3eeNvpRFIh6B_661%Uqmc-0RP>sMy~08#zs7DB+N=l~0V2c-aXzAdsM;^I49 z&?K%VjX)}@gv%xa4&bmKfE}xux4#xvD9BZripb+>JFs6I_rcqd?^Y()ZtipoQ6mH9 zB$+tr^OMJ=a)Rza*v|+~2WkM?g744Y6q9*_7z)IW2VsiJxzbB9mfv)o9Ij6se}z2C zV|Cg9BclP&Us{OD?#c5N+&SCEXacnUa7L0qT?6EwQdf-e&q2?%OQp^bDo3~!kT6i# z`HuwqWMCY63a2X>nU(q5y9d7m)~ZV69hu>nDf(i7J1A~a`Su2y3yGi44y1)v%9Hqg z#s{_yHt$jUEwH>ken3=hA|$MdT1oLFhZlWq|t89y*$rHRkhv#qvU=@(DsB(j!VJcA{AbnX0I zah^W80Ihp?(O^0d>ZE@2IUHl5`hFE!-pXA{@*xUulyS9t@lTRNJZ|m*B%hZYb@Z$D za(#l;gdi!BCU$P;Zq@_oo^e1L`f^)nF~IOizfzR!TaB5HA&>!I;>!X?>|ke(e(g*7 zC7Mxr`@z3sq|C~svh3qlU(c|TVL%Yw->, } + impl ResizeImage { - pub fn new(imageproc: Arc>) -> Self { - Self { imageproc } + pub fn new(base_path: PathBuf, imageproc: Arc>) -> Self { + 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 { fn call(&self, args: &HashMap) -> Result { - let path = required_arg!( + let mut path = required_arg!( String, args.get("path"), "`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(); - 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()); } - let imageop = imageproc::ImageOp::from_args(path, &op, width, height, &format, quality) - .map_err(|e| format!("`resize_image`: {}", e))?; - let url = imageproc.insert(imageop); + let imageop = + imageproc::ImageOp::from_args(path, file_path, &op, width, height, &format, quality) + .map_err(|e| format!("`resize_image`: {}", e))?; + let (static_path, url) = imageproc.insert(imageop); - to_value(url).map_err(|err| err.into()) - } -} - -#[derive(Debug)] -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) -> Result { - 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)) + to_value(ResizeImageResponse { + static_path: static_path.to_string_lossy().into_owned(), + url, + }) + .map_err(|err| err.into()) } } @@ -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) -> Result { + 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)] 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()); + } } diff --git a/components/templates/src/global_fns/mod.rs b/components/templates/src/global_fns/mod.rs index 12638923..9ee87d11 100644 --- a/components/templates/src/global_fns/mod.rs +++ b/components/templates/src/global_fns/mod.rs @@ -19,7 +19,7 @@ mod load_data; pub use self::content::{GetPage, GetSection, GetTaxonomy, GetTaxonomyUrl}; pub use self::i18n::Trans; -pub use self::images::{GetImageMeta, ResizeImage}; +pub use self::images::{GetImageMetadata, ResizeImage}; pub use self::load_data::LoadData; #[derive(Debug)] diff --git a/components/utils/src/fs.rs b/components/utils/src/fs.rs index afb7a04e..81d07605 100644 --- a/components/utils/src/fs.rs +++ b/components/utils/src/fs.rs @@ -1,5 +1,5 @@ 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::path::{Path, PathBuf}; use std::time::SystemTime; @@ -60,27 +60,6 @@ pub fn read_file(path: &Path) -> Result { 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 { - 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 /// 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<()> { @@ -204,25 +183,9 @@ mod tests { use std::path::PathBuf; use std::str::FromStr; - use tempfile::{tempdir, tempdir_in}; + use tempfile::tempdir_in; - use super::{copy_file, find_related_assets}; - - #[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); - } + use super::copy_file; #[test] fn test_copy_file_timestamp_preserved() {