Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add optional "filter" type to image resize function #2623

Open
wants to merge 2 commits into
base: next
Choose a base branch
from
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
23 changes: 23 additions & 0 deletions components/imageproc/src/filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use libs::image::imageops::FilterType::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum FilterType {
Lanczos3,
Nearest,
Triangle,
CatmullRom,
Gaussian,
}

impl Into<libs::image::imageops::FilterType> for FilterType {
fn into(self) -> libs::image::imageops::FilterType {
match self {
FilterType::Lanczos3 => Lanczos3,
FilterType::Nearest => Nearest,
FilterType::Gaussian => Gaussian,
FilterType::Triangle => Triangle,
FilterType::CatmullRom => CatmullRom,
}
}
}
1 change: 1 addition & 0 deletions components/imageproc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod filter;
mod format;
mod helpers;
mod meta;
Expand Down
66 changes: 41 additions & 25 deletions components/imageproc/src/ops.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
use errors::{anyhow, Result};

use crate::filter::FilterType;

/// De-serialized & sanitized arguments of `resize_image`
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResizeOperation {
/// A simple scale operation that doesn't take aspect ratio into account
Scale(u32, u32),
Scale(u32, u32, FilterType),
/// Scales the image to a specified width with height computed such
/// that aspect ratio is preserved
FitWidth(u32),
FitWidth(u32, FilterType),
/// Scales the image to a specified height with width computed such
/// that aspect ratio is preserved
FitHeight(u32),
FitHeight(u32, FilterType),
/// If the image is larger than the specified width or height, scales the image such
/// that it fits within the specified width and height preserving aspect ratio.
/// Either dimension may end up being smaller, but never larger than specified.
Fit(u32, u32),
Fit(u32, u32, FilterType),
/// Scales the image such that it fills the specified width and height.
/// Output will always have the exact dimensions specified.
/// The part of the image that doesn't fit in the thumbnail due to differing
/// aspect ratio will be cropped away, if any.
Fill(u32, u32),
Fill(u32, u32, FilterType),
}

impl ResizeOperation {
pub fn from_args(op: &str, width: Option<u32>, height: Option<u32>) -> Result<Self> {
pub fn from_args(
op: &str,
width: Option<u32>,
height: Option<u32>,
filter: &str,
) -> Result<Self> {
use ResizeOperation::*;

// Validate args:
Expand All @@ -46,12 +53,21 @@ impl ResizeOperation {
_ => return Err(anyhow!("Invalid image resize operation: {}", op)),
};

let filter = match filter {
"lanczos3" => FilterType::Lanczos3,
"nearest" => FilterType::Nearest,
"triangle" => FilterType::Triangle,
"catmullrom" => FilterType::CatmullRom,
"gaussian" => FilterType::Gaussian,
_ => return Err(anyhow!("Invalid filter type: {}", filter)),
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we convert directly to the filter type from the image crate and skip our own?

Copy link
Author

Choose a reason for hiding this comment

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

I went with a custom type because FilterType from the image crate is not Hash, so it breaks downstream functionality when the processed image paths are generated.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I've just made a PR to impl it, let's see if it works

Copy link
Collaborator

Choose a reason for hiding this comment

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

i think it's released in the most recent version


Ok(match op {
"scale" => Scale(width.unwrap(), height.unwrap()),
"fit_width" => FitWidth(width.unwrap()),
"fit_height" => FitHeight(height.unwrap()),
"fit" => Fit(width.unwrap(), height.unwrap()),
"fill" => Fill(width.unwrap(), height.unwrap()),
"scale" => Scale(width.unwrap(), height.unwrap(), filter),
"fit_width" => FitWidth(width.unwrap(), filter),
"fit_height" => FitHeight(height.unwrap(), filter),
"fit" => Fit(width.unwrap(), height.unwrap(), filter),
"fill" => Fill(width.unwrap(), height.unwrap(), filter),
_ => unreachable!(),
})
}
Expand All @@ -63,7 +79,7 @@ impl ResizeOperation {
#[derive(Clone, PartialEq, Eq, Hash, Default, Debug)]
pub struct ResizeInstructions {
pub crop_instruction: Option<(u32, u32, u32, u32)>, // x, y, w, h
pub resize_instruction: Option<(u32, u32)>, // w, h
pub resize_instruction: Option<(u32, u32, FilterType)>, // w, h, filter
}

impl ResizeInstructions {
Expand All @@ -73,16 +89,16 @@ impl ResizeInstructions {
let res = ResizeInstructions::default();

match args {
Scale(w, h) => res.resize((w, h)),
FitWidth(w) => {
Scale(w, h, filter) => res.resize((w, h), filter),
FitWidth(w, filter) => {
let h = (orig_h as u64 * w as u64) / orig_w as u64;
res.resize((w, h as u32))
res.resize((w, h as u32), filter)
}
FitHeight(h) => {
FitHeight(h, filter) => {
let w = (orig_w as u64 * h as u64) / orig_h as u64;
res.resize((w as u32, h))
res.resize((w as u32, h), filter)
}
Fit(w, h) => {
Fit(w, h, filter) => {
if orig_w <= w && orig_h <= h {
return res; // ie. no-op
}
Expand All @@ -91,12 +107,12 @@ impl ResizeInstructions {
let orig_h_w = orig_h as u64 * w as u64;

if orig_w_h > orig_h_w {
Self::new(FitWidth(w), (orig_w, orig_h))
Self::new(FitWidth(w, filter), (orig_w, orig_h))
} else {
Self::new(FitHeight(h), (orig_w, orig_h))
Self::new(FitHeight(h, filter), (orig_w, orig_h))
}
}
Fill(w, h) => {
Fill(w, h, filter) => {
const RATIO_EPSILLION: f32 = 0.1;

let factor_w = orig_w as f32 / w as f32;
Expand All @@ -106,7 +122,7 @@ impl ResizeInstructions {
// If the horizontal and vertical factor is very similar,
// that means the aspect is similar enough that there's not much point
// in cropping, so just perform a simple scale in this case.
res.resize((w, h))
res.resize((w, h), filter)
} else {
// We perform the fill such that a crop is performed first
// and then resize_exact can be used, which should be cheaper than
Expand All @@ -123,7 +139,7 @@ impl ResizeInstructions {
((orig_w - crop_w) / 2, 0)
};

res.crop((offset_w, offset_h, crop_w, crop_h)).resize((w, h))
res.crop((offset_w, offset_h, crop_w, crop_h)).resize((w, h), filter)
}
}
}
Expand All @@ -134,8 +150,8 @@ impl ResizeInstructions {
self
}

pub fn resize(mut self, size: (u32, u32)) -> Self {
self.resize_instruction = Some(size);
pub fn resize(mut self, size: (u32, u32), filter: FilterType) -> Self {
self.resize_instruction = Some((size.0, size.1, filter));
self
}
}
12 changes: 8 additions & 4 deletions components/imageproc/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ use config::Config;
use errors::{anyhow, Context, Result};
use libs::ahash::{HashMap, HashSet};
use libs::image::codecs::jpeg::JpegEncoder;
use libs::image::imageops::FilterType;
use libs::image::{EncodableLayout, ImageFormat};
use libs::rayon::prelude::*;
use libs::{image, webp};
use serde::{Deserialize, Serialize};
use utils::fs as ufs;

use crate::filter::FilterType;
use crate::format::Format;
use crate::helpers::get_processed_filename;
use crate::{fix_orientation, ImageMeta, ResizeInstructions, ResizeOperation};
Expand Down Expand Up @@ -46,8 +46,9 @@ impl ImageOp {
Some((x, y, w, h)) => img.crop(x, y, w, h),
None => img,
};

let img = match self.instr.resize_instruction {
Some((w, h)) => img.resize_exact(w, h, FilterType::Lanczos3),
Some((w, h, filter)) => img.resize_exact(w, h, filter.into()),
None => img,
};

Expand Down Expand Up @@ -91,6 +92,8 @@ pub struct EnqueueResponse {
pub orig_width: u32,
/// Original image height
pub orig_height: u32,
/// Resize filter
pub filter: FilterType,
}

impl EnqueueResponse {
Expand All @@ -101,10 +104,11 @@ impl EnqueueResponse {
instr: &ResizeInstructions,
) -> Self {
let static_path = static_path.to_string_lossy().into_owned();
let (width, height) = instr.resize_instruction.unwrap_or(meta.size);
let (width, height, filter) =
instr.resize_instruction.unwrap_or((meta.size.0, meta.size.1, FilterType::Lanczos3));
let (orig_width, orig_height) = meta.size;

Self { url, static_path, width, height, orig_width, orig_height }
Self { url, static_path, width, height, orig_width, orig_height, filter }
}
}

Expand Down
4 changes: 2 additions & 2 deletions components/imageproc/tests/resize_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ fn image_op_test(
let tmpdir = tempfile::tempdir().unwrap().into_path();
let config = Config::parse(CONFIG).unwrap();
let mut proc = Processor::new(tmpdir.clone(), &config);
let resize_op = ResizeOperation::from_args(op, width, height).unwrap();
let resize_op = ResizeOperation::from_args(op, width, height, "gaussian").unwrap();

let resp = proc.enqueue(resize_op, source_img.into(), source_path, format, None).unwrap();
assert_processed_path_matches(&resp.url, "https://example.com/processed_images/", expect_ext);
Expand Down Expand Up @@ -227,7 +227,7 @@ fn resize_and_check(source_img: &str) -> bool {
let tmpdir = tempfile::tempdir().unwrap().into_path();
let config = Config::parse(CONFIG).unwrap();
let mut proc = Processor::new(tmpdir.clone(), &config);
let resize_op = ResizeOperation::from_args("scale", Some(16), Some(16)).unwrap();
let resize_op = ResizeOperation::from_args("scale", Some(16), Some(16), "gaussian").unwrap();

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

Expand Down
24 changes: 15 additions & 9 deletions components/templates/src/global_fns/images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ impl ResizeImage {

static DEFAULT_OP: &str = "fill";
static DEFAULT_FMT: &str = "auto";
static DEFAULT_FILTER: &str = "lanczos3";

impl TeraFn for ResizeImage {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
Expand Down Expand Up @@ -60,7 +61,12 @@ impl TeraFn for ResizeImage {
return Err("`resize_image`: `quality` must be in range 1-100".to_string().into());
}
}
let resize_op = imageproc::ResizeOperation::from_args(&op, width, height)

let filter =
optional_arg!(String, args.get("filter"), "`resize_image`: `filter` must be a string")
.unwrap_or_else(|| DEFAULT_FILTER.to_string());

let resize_op = imageproc::ResizeOperation::from_args(&op, width, height, &filter)
.map_err(|e| format!("`resize_image`: {}", e))?;
let mut imageproc = self.imageproc.lock().unwrap();
let (file_path, unified_path) =
Expand Down Expand Up @@ -193,12 +199,12 @@ mod tests {

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

Expand All @@ -207,12 +213,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.3301b37eed389d2e.jpg").display()))
to_value(&format!("{}", static_path.join("gutenberg.16c7c7c4d4bf998b.jpg").display()))
.unwrap()
);
assert_eq!(
data["url"],
to_value("http://a-website.com/processed_images/gutenberg.3301b37eed389d2e.jpg")
to_value("http://a-website.com/processed_images/gutenberg.16c7c7c4d4bf998b.jpg")
.unwrap()
);

Expand All @@ -231,25 +237,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.d2fde9a750b68471.jpg").display()))
to_value(&format!("{}", static_path.join("asset.08b6a9e588035492.jpg").display()))
.unwrap()
);
assert_eq!(
data["url"],
to_value("http://a-website.com/processed_images/asset.d2fde9a750b68471.jpg").unwrap()
to_value("http://a-website.com/processed_images/asset.08b6a9e588035492.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.9b0d29e07d588b60.jpg").display()))
to_value(&format!("{}", static_path.join("in-theme.38343d88b102bc9b.jpg").display()))
.unwrap()
);
assert_eq!(
data["url"],
to_value("http://a-website.com/processed_images/in-theme.9b0d29e07d588b60.jpg")
to_value("http://a-website.com/processed_images/in-theme.38343d88b102bc9b.jpg")
.unwrap()
);
}
Expand Down
20 changes: 19 additions & 1 deletion docs/content/documentation/content/image-processing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ which is available in template code as well as in shortcodes.
The function usage is as follows:

```jinja2
resize_image(path, width, height, op, format, quality)
resize_image(path, width, height, op, format, quality, filter)
```

### Arguments
Expand Down Expand Up @@ -38,6 +38,14 @@ resize_image(path, width, height, op, format, quality)
The default is `"auto"`, this means that the format is chosen based on input image format.
JPEG is chosen for JPEGs and other lossy formats, and PNG is chosen for PNGs and other lossless formats.
- `quality` (_optional_): JPEG or WebP quality of the resized image, in percent. Only used when encoding JPEGs or WebPs; for JPEG default value is `75`, for WebP default is lossless.
- `filter` (_optional_): Resize filter. This can be one of:
- `"lanczos3"`
- `"nearest"`
- `"triangle"`
- `"catmullrom"`
- `"gaussian"`

The default is `"lanczos3"`, which produces smooth resizing generally free of visible artifacts. The filters are further detailed in a later section.

### Image processing and return value

Expand Down Expand Up @@ -74,6 +82,8 @@ The source for all examples is this 300 pixel × 380 pixel image:

![zola](01-zola.png)

These examples all use the default `"lanczos3"` resize filter.

### **`"scale"`**
Simply scales the image to the specified dimensions (`width` & `height`) irrespective of the aspect ratio.

Expand Down Expand Up @@ -124,6 +134,14 @@ The source for all examples is this 300 pixel × 380 pixel image:
{{ resize_image(path="documentation/content/image-processing/01-zola.png", width=150, height=150, op="fill") }}


## Reize filters

Depending on the image content, different filters may produce better results. Below are all the filters applied to two images. From left to right, the filters are `lanczos3`, `nearest`, `triangle`, `catmullrom`, and `gaussian`. It can be seen that `gaussian` and `triangle` filters tend to have softer results, whereas `lanczos3` and `catmullrom` better preserve small details. `nearest` is a good option for images of a pixel art style, as no interpolation is performed between neighboring pixels.

{{ filters(path="documentation/content/image-processing/01-zola.png") }}

{{ filters(path="documentation/content/image-processing/knight.png") }}

## Using `resize_image` in markdown via shortcodes

`resize_image` is a Zola built-in Tera function (see the [templates](@/documentation/templates/_index.md) chapter),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
8 changes: 8 additions & 0 deletions docs/templates/shortcodes/filters.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div>
{% for filter in ["lanczos3", "nearest", "triangle", "catmullrom", "gaussian"] -%}
{% set image = resize_image(path=path, width=226, height=285, filter=filter) %}
<a href="{{ get_url(path=path) }}" target="_blank">
<img src="{{ image.url }}" />
</a>
{%- endfor %}
</div>