Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions components/imageproc/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::borrow::Cow;
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::Path;

use errors::{Context, Result};

use crate::format::Format;
use crate::ResizeOperation;
use libs::image::DynamicImage;
Expand Down Expand Up @@ -46,17 +49,16 @@ fn get_orientation(raw_metadata: Option<Vec<u8>>) -> Option<u32> {
}
}

/// We only use the input_path to get the file stem.
/// Hashing the resolved `input_path` would include the absolute path to the image
/// with all filesystem components.
pub fn get_processed_filename(
input_path: &Path,
input_src: &str,
op: &ResizeOperation,
format: &Format,
) -> String {
) -> Result<String> {
let mut hasher = DefaultHasher::new();
hasher.write(input_src.as_ref());
hasher.write(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does that change perf significantly? I can imagine reading a whole site of images is going to be much more computationally expensive than filenames

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it doesn't make sense to read the file multiple times, once for the filename and once to actually operate on it

Copy link
Contributor Author

@lpulley lpulley Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I've been thinking about how to approach this. We'd essentially want to read each input once (at most), and for each operation to hold a reference or key to the in-memory contents of the input for processing.

I'll push something for that if I figure out a good solution.

&fs::read(input_path)
.with_context(|| format!("Failed to read file {}", input_path.display()))?,
);
op.hash(&mut hasher);
format.hash(&mut hasher);
let hash = hasher.finish();
Expand All @@ -65,5 +67,5 @@ pub fn get_processed_filename(
.map(|s| s.to_string_lossy())
.unwrap_or_else(|| Cow::Borrowed("unknown"));

format!("{}.{:016x}.{}", filename, hash, format.extension())
Ok(format!("{}.{:016x}.{}", filename, hash, format.extension()))
}
3 changes: 1 addition & 2 deletions components/imageproc/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ impl Processor {
pub fn enqueue(
&mut self,
op: ResizeOperation,
input_src: String,
input_path: PathBuf,
format: &str,
quality: Option<u8>,
Expand All @@ -186,7 +185,7 @@ impl Processor {
// We get the output format
let format = Format::from_args(meta.is_lossy(), format, quality, speed)?;
// Now we have all the data we need to generate the output filename and the response
let filename = get_processed_filename(&input_path, &input_src, &op, &format);
let filename = get_processed_filename(&input_path, &op, &format)?;
let url = format!("{}{}", self.base_url, filename);
let static_path = Path::new("static").join(RESIZED_SUBDIR).join(&filename);
let output_path = self.output_dir.join(&filename);
Expand Down
5 changes: 2 additions & 3 deletions components/imageproc/tests/resize_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ fn image_op_test(
let mut proc = Processor::new(tmpdir.clone(), &config);
let resize_op = ResizeOperation::from_args(op, width, height).unwrap();

let resp =
proc.enqueue(resize_op, source_img.into(), source_path, format, quality, speed).unwrap();
let resp = proc.enqueue(resize_op, source_path, format, quality, speed).unwrap();
assert_processed_path_matches(&resp.url, "https://example.com/processed_images/", expect_ext);
assert_processed_path_matches(&resp.static_path, PROCESSED_PREFIX.as_str(), expect_ext);
assert_eq!(resp.width, expect_width);
Expand Down Expand Up @@ -587,7 +586,7 @@ fn resize_and_check(source_img: &str) -> bool {
let mut proc = Processor::new(tmpdir.clone(), &config);
let resize_op = ResizeOperation::from_args("scale", Some(16), Some(16)).unwrap();

let resp = proc.enqueue(resize_op, source_img.into(), source_path, "jpg", None, None).unwrap();
let resp = proc.enqueue(resize_op, source_path, "jpg", None, None).unwrap();

proc.do_process().unwrap();
let processed_path = PathBuf::from(&resp.static_path);
Expand Down
4 changes: 2 additions & 2 deletions components/templates/src/global_fns/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ impl TeraFn for GetUrl {
&self.output_path,
)
.map_err(|e| format!("`get_url`: {}", e))?
.and_then(|(p, _)| fs::File::open(p).ok())
.and_then(|p| fs::File::open(p).ok())
.and_then(|mut f| {
let mut contents = Vec::new();
f.read_to_end(&mut contents).ok()?;
Expand Down Expand Up @@ -194,7 +194,7 @@ impl TeraFn for GetHash {
match search_for_file(&self.base_path, &path_v, &self.theme, &self.output_path)
.map_err(|e| format!("`get_hash`: {}", e))?
{
Some((f, _)) => f,
Some(f) => f,
None => {
return Err(format!("`get_hash`: Cannot find file: {}", path_v).into());
}
Expand Down
5 changes: 2 additions & 3 deletions components/templates/src/global_fns/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@ use utils::fs::is_path_in_directory;
/// 5. base_path + themes + {current_theme} + static + path
/// A path starting with @/ will replace it with `content/` and a path starting with `/` will have
/// it removed.
/// It also returns the unified path so it can be used as unique hash for a given file.
/// It will error if the file is not contained in the Zola directory.
pub fn search_for_file(
base_path: &Path,
path: &str,
theme: &Option<String>,
output_path: &Path,
) -> Result<Option<(PathBuf, String)>> {
) -> Result<Option<PathBuf>> {
let mut search_paths =
vec![base_path.join("static"), base_path.join("content"), base_path.join(output_path)];
if let Some(t) = theme {
Expand Down Expand Up @@ -52,7 +51,7 @@ pub fn search_for_file(
}

if file_exists {
Ok(Some((file_path, actual_path.into_owned())))
Ok(Some(file_path))
} else {
Ok(None)
}
Expand Down
49 changes: 24 additions & 25 deletions components/templates/src/global_fns/images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl TeraFn for ResizeImage {
let resize_op = imageproc::ResizeOperation::from_args(&op, width, height)
.map_err(|e| format!("`resize_image`: {}", e))?;
let mut imageproc = self.imageproc.lock().unwrap();
let (file_path, unified_path) =
let file_path =
match search_for_file(&self.base_path, &path, &self.theme, &self.output_path)
.map_err(|e| format!("`resize_image`: {}", e))?
{
Expand All @@ -71,7 +71,7 @@ impl TeraFn for ResizeImage {
};

let response = imageproc
.enqueue(resize_op, unified_path, file_path, &format, quality, speed)
.enqueue(resize_op, file_path, &format, quality, speed)
.map_err(|e| format!("`resize_image`: {}", e))?;

to_value(response).map_err(Into::into)
Expand All @@ -83,7 +83,7 @@ pub struct GetImageMetadata {
/// The base path of the Zola site
base_path: PathBuf,
theme: Option<String>,
result_cache: Arc<Mutex<HashMap<String, Value>>>,
result_cache: Arc<Mutex<HashMap<PathBuf, Value>>>,
output_path: PathBuf,
}

Expand All @@ -107,28 +107,27 @@ impl TeraFn for GetImageMetadata {
)
.unwrap_or(false);

let (src_path, unified_path) =
match search_for_file(&self.base_path, &path, &self.theme, &self.output_path)
.map_err(|e| format!("`get_image_metadata`: {}", e))?
{
Some((f, p)) => (f, p),
None => {
if allow_missing {
return Ok(Value::Null);
}
return Err(format!("`get_image_metadata`: Cannot find path: {}", path).into());
let src_path = match search_for_file(&self.base_path, &path, &self.theme, &self.output_path)
.map_err(|e| format!("`get_image_metadata`: {}", e))?
{
Some(f) => f,
None => {
if allow_missing {
return Ok(Value::Null);
}
};
return Err(format!("`get_image_metadata`: Cannot find path: {}", path).into());
}
};

let mut cache = self.result_cache.lock().expect("result cache lock");
if let Some(cached_result) = cache.get(&unified_path) {
if let Some(cached_result) = cache.get(&src_path) {
return Ok(cached_result.clone());
}

let response = imageproc::read_image_metadata(src_path)
let response = imageproc::read_image_metadata(&src_path)
.map_err(|e| format!("`resize_image`: {}", e))?;
let out = to_value(response).unwrap();
cache.insert(unified_path, out.clone());
cache.insert(src_path, out.clone());

Ok(out)
}
Expand Down Expand Up @@ -190,12 +189,12 @@ mod tests {

assert_eq!(
data["static_path"],
to_value(&format!("{}", static_path.join("gutenberg.e99218b5a3185c99.jpg").display()))
to_value(&format!("{}", static_path.join("gutenberg.9786ef7a62f75bc4.jpg").display()))
.unwrap()
);
assert_eq!(
data["url"],
to_value("http://a-website.com/processed_images/gutenberg.e99218b5a3185c99.jpg")
to_value("http://a-website.com/processed_images/gutenberg.9786ef7a62f75bc4.jpg")
.unwrap()
);

Expand All @@ -204,12 +203,12 @@ mod tests {
let data = static_fn.call(&args).unwrap().as_object().unwrap().clone();
assert_eq!(
data["static_path"],
to_value(&format!("{}", static_path.join("gutenberg.155d032b1aae0133.jpg").display()))
to_value(&format!("{}", static_path.join("gutenberg.9786ef7a62f75bc4.jpg").display()))
.unwrap()
);
assert_eq!(
data["url"],
to_value("http://a-website.com/processed_images/gutenberg.155d032b1aae0133.jpg")
to_value("http://a-website.com/processed_images/gutenberg.9786ef7a62f75bc4.jpg")
.unwrap()
);

Expand All @@ -228,25 +227,25 @@ mod tests {
let data = static_fn.call(&args).unwrap().as_object().unwrap().clone();
assert_eq!(
data["static_path"],
to_value(&format!("{}", static_path.join("asset.160461e0d0be19e6.jpg").display()))
to_value(&format!("{}", static_path.join("asset.9786ef7a62f75bc4.jpg").display()))
.unwrap()
);
assert_eq!(
data["url"],
to_value("http://a-website.com/processed_images/asset.160461e0d0be19e6.jpg").unwrap()
to_value("http://a-website.com/processed_images/asset.9786ef7a62f75bc4.jpg").unwrap()
);

// 6. Looking up a file in the theme
args.insert("path".to_string(), to_value("in-theme.jpg").unwrap());
let data = static_fn.call(&args).unwrap().as_object().unwrap().clone();
assert_eq!(
data["static_path"],
to_value(&format!("{}", static_path.join("in-theme.d8f4f6eef30de1b2.jpg").display()))
to_value(&format!("{}", static_path.join("in-theme.9786ef7a62f75bc4.jpg").display()))
.unwrap()
);
assert_eq!(
data["url"],
to_value("http://a-website.com/processed_images/in-theme.d8f4f6eef30de1b2.jpg")
to_value("http://a-website.com/processed_images/in-theme.9786ef7a62f75bc4.jpg")
.unwrap()
);
}
Expand Down
2 changes: 1 addition & 1 deletion components/templates/src/global_fns/load_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ impl DataSource {
return match search_for_file(base_path, &path, theme, output_path)
.map_err(|e| format!("`load_data`: {}", e))?
{
Some((f, _)) => Ok(Some(DataSource::Path(f))),
Some(f) => Ok(Some(DataSource::Path(f))),
None => Ok(None),
};
}
Expand Down
4 changes: 2 additions & 2 deletions docs/content/documentation/content/image-processing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ Zola performs image processing during the build process and places the resized i
static/processed_images/
```

The filename of each resized image is a hash of the function arguments,
The filename of each resized image is a hash of the input file contents and the other `resize_image` arguments,
which means that once an image is resized in a certain way, it will be stored in the above directory and will not
need to be resized again during subsequent builds (unless the image itself, the dimensions, or other arguments have changed).
need to be resized again during subsequent builds (unless the input file contents or other `resize_image` arguments have changed).

The function returns an object with the following schema:

Expand Down