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
3 changes: 3 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
matrix:
target:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu
- x86_64-pc-windows-msvc
- x86_64-apple-darwin
Expand All @@ -25,6 +26,8 @@ jobs:
target: x86_64-pc-windows-msvc
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- os: ubuntu-22.04
target: x86_64-unknown-linux-musl
- os: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
- os: macos-13
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
matrix:
target:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu
- x86_64-pc-windows-msvc
- x86_64-apple-darwin
Expand All @@ -32,6 +33,8 @@ jobs:
target: x86_64-pc-windows-msvc
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- os: ubuntu-22.04
target: x86_64-unknown-linux-musl
- os: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
- os: macos-13
Expand All @@ -46,9 +49,22 @@ jobs:
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rustup_toolchain }}
targets: ${{ matrix.target }}

- name: Install Zig and cargo-zigbuild
run: |
curl -sSfL https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz | tar -xJf -
export PATH=$PWD/zig-linux-x86_64-0.14.0:$PATH
cargo install --locked cargo-zigbuild
if: ${{ endsWith(matrix.target, 'linux-musl') }}

- name: Cargo build
run: cargo build --release --target ${{ matrix.target }}
if: ${{ ! endsWith(matrix.target, 'linux-musl') }}

- name: Cargo zigbuild
run: cargo zigbuild --release --target ${{ matrix.target }}
if: ${{ endsWith(matrix.target, 'linux-musl') }}

- name: Show version
run: ./target/${{ matrix.target }}/release/zola --version
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 0.21.0 (unreleased)


## 0.20.0 (2025-02-14)

- Add `name` annotation for codeblock
Expand Down
2 changes: 1 addition & 1 deletion components/content/src/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ impl Library {
#[cfg(test)]
mod tests {
use super::*;
use crate::FileInfo;
use crate::{FileInfo, SortBy};
use config::{LanguageOptions, TaxonomyConfig};
use std::collections::HashMap;
use utils::slugs::SlugifyStrategy;
Expand Down
2 changes: 2 additions & 0 deletions components/content/src/sorting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub fn sort_pages(pages: &[&Page], sort_by: SortBy) -> (Vec<PathBuf>, Vec<PathBu
SortBy::Title | SortBy::TitleBytes => page.meta.title.is_some(),
SortBy::Weight => page.meta.weight.is_some(),
SortBy::Slug => true,
SortBy::Permalink => true,
SortBy::None => unreachable!(),
});

Expand All @@ -34,6 +35,7 @@ pub fn sort_pages(pages: &[&Page], sort_by: SortBy) -> (Vec<PathBuf>, Vec<PathBu
}
SortBy::Weight => a.meta.weight.unwrap().cmp(&b.meta.weight.unwrap()),
SortBy::Slug => natural_lexical_cmp(&a.slug, &b.slug),
SortBy::Permalink => a.permalink.cmp(&b.permalink),
SortBy::None => unreachable!(),
};

Expand Down
2 changes: 2 additions & 0 deletions components/content/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub enum SortBy {
Weight,
/// Sort by slug
Slug,
/// Sort by permalink
Permalink,
/// No sorting
None,
}
118 changes: 84 additions & 34 deletions components/imageproc/src/format.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,84 @@
use errors::{anyhow, Result};
use std::hash::{Hash, Hasher};

const DEFAULT_Q_JPG: u8 = 75;
const QUALITY_MIN_JPEG: u8 = 1;
const QUALITY_MAX_JPEG: u8 = 100;
const QUALITY_MIN_WEBP: u8 = 0;
const QUALITY_MAX_WEBP: u8 = 100;
const QUALITY_MIN_AVIF: u8 = 1;
const QUALITY_MAX_AVIF: u8 = 100;
const SPEED_MIN_AVIF: u8 = 1;
const SPEED_MAX_AVIF: u8 = 10;

const DEFAULT_QUALITY_JPEG: u8 = 75;
const DEFAULT_QUALITY_AVIF: u8 = 80;
const DEFAULT_SPEED_AVIF: u8 = 5;

/// Thumbnail image format
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
/// JPEG, The `u8` argument is JPEG quality (in percent).
Jpeg(u8),
/// PNG
Jpeg { quality: u8 },
Png,
/// WebP, The `u8` argument is WebP quality (in percent), None meaning lossless.
WebP(Option<u8>),
/// AVIF, The `u8` argument is AVIF quality (in percent), None meaning lossless.
Avif(Option<u8>),
WebP { quality: Option<u8> }, // 'None' means lossless
Avif { quality: u8, speed: u8 },
}

impl Format {
pub fn from_args(is_lossy: bool, format: &str, quality: Option<u8>) -> Result<Format> {
pub fn from_args(
is_lossy: bool,
format: &str,
quality: Option<u8>,
speed: Option<u8>,
) -> Result<Format> {
use Format::*;
if let Some(quality) = quality {
assert!(quality > 0 && quality <= 100, "Quality must be within the range [1; 100]");
}
let jpg_quality = quality.unwrap_or(DEFAULT_Q_JPG);
match format {
"auto" => {
if is_lossy {
Ok(Jpeg(jpg_quality))
} else {
Ok(Png)
let format_from_auto = match (format, is_lossy) {
("auto", true) => "jpeg",
("auto", false) => "png",
(other_format, _) => other_format,
};
match format_from_auto {
"jpeg" | "jpg" => match quality.unwrap_or(DEFAULT_QUALITY_JPEG) {
valid_quality @ QUALITY_MIN_JPEG..=QUALITY_MAX_JPEG => {
Ok(Jpeg { quality: valid_quality })
}
}
"jpeg" | "jpg" => Ok(Jpeg(jpg_quality)),
invalid_quality => Err(anyhow!(
"Quality for JPEG must be between {} and {} (inclusive); {} is not valid",
QUALITY_MIN_JPEG,
QUALITY_MAX_JPEG,
invalid_quality
)),
},
"png" => Ok(Png),
"webp" => Ok(WebP(quality)),
"avif" => Ok(Avif(quality)),
"webp" => match quality {
Some(QUALITY_MIN_WEBP..=QUALITY_MAX_WEBP) | None => Ok(WebP { quality }),
Some(invalid_quality) => Err(anyhow!(
"Quality for WebP must be between {} and {} (inclusive); {} is not valid",
QUALITY_MIN_WEBP,
QUALITY_MAX_WEBP,
invalid_quality
)),
},
"avif" => {
let q = match quality.unwrap_or(DEFAULT_QUALITY_AVIF) {
valid_quality @ QUALITY_MIN_AVIF..=QUALITY_MAX_AVIF => Ok(valid_quality),
invalid_quality => Err(anyhow!(
"Quality for AVIF must be between {} and {} (inclusive); {} is not valid",
QUALITY_MIN_AVIF,
QUALITY_MAX_AVIF,
invalid_quality
)),
}?;
let s = match speed.unwrap_or(DEFAULT_SPEED_AVIF) {
valid_speed @ SPEED_MIN_AVIF..=SPEED_MAX_AVIF => Ok(valid_speed),
invalid_speed => Err(anyhow!(
"Speed for AVIF must be between {} and {} (inclusive); {} is not valid",
SPEED_MIN_AVIF,
SPEED_MAX_AVIF,
invalid_speed
)),
}?;
Ok(Avif { quality: q, speed: s })
}
_ => Err(anyhow!("Invalid image format: {}", format)),
}
}
Expand All @@ -45,9 +89,9 @@ impl Format {

match *self {
Png => "png",
Jpeg(_) => "jpg",
WebP(_) => "webp",
Avif(_) => "avif",
Jpeg { .. } => "jpg",
WebP { .. } => "webp",
Avif { .. } => "avif",
}
}
}
Expand All @@ -57,16 +101,22 @@ impl Hash for Format {
fn hash<H: Hasher>(&self, hasher: &mut H) {
use Format::*;

let q = match *self {
Png => 0,
Jpeg(q) => 1001 + q as u16,
WebP(None) => 2000,
WebP(Some(q)) => 2001 + q as u16,
Avif(None) => 3000,
Avif(Some(q)) => 3001 + q as u16,
let quality: i16 = match *self {
Png => -1,
Jpeg { quality } => quality.into(),
WebP { quality: None } => -1,
WebP { quality: Some(quality) } => quality.into(),
Avif { quality, .. } => quality.into(),
};
let speed: i16 = match *self {
Png => -1,
Jpeg { .. } => -1,
WebP { .. } => -1,
Avif { speed, .. } => speed.into(),
};

hasher.write_u16(q);
hasher.write(self.extension().as_bytes());
hasher.write_i16(quality);
hasher.write_i16(speed);
}
}
23 changes: 10 additions & 13 deletions components/imageproc/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use libs::image::codecs::avif::AvifEncoder;
use libs::image::codecs::jpeg::JpegEncoder;
use libs::image::imageops::FilterType;
use libs::image::GenericImageView;
use libs::image::{EncodableLayout, ExtendedColorType, ImageEncoder, ImageFormat};
use libs::image::{EncodableLayout, ImageEncoder, ImageFormat};
use libs::rayon::prelude::*;
use libs::{image, webp};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -60,31 +60,27 @@ impl ImageOp {
Format::Png => {
img.write_to(&mut buffered_f, ImageFormat::Png)?;
}
Format::Jpeg(q) => {
let mut encoder = JpegEncoder::new_with_quality(&mut buffered_f, q);
Format::Jpeg { quality } => {
let mut encoder = JpegEncoder::new_with_quality(&mut buffered_f, quality);
encoder.encode_image(&img)?;
}
Format::WebP(q) => {
Format::WebP { quality } => {
let encoder = webp::Encoder::from_image(&img)
.map_err(|_| anyhow!("Unable to load this kind of image with webp"))?;
let memory = match q {
let memory = match quality {
Some(q) => encoder.encode(q as f32),
None => encoder.encode_lossless(),
};
buffered_f.write_all(memory.as_bytes())?;
}
Format::Avif(q) => {
Format::Avif { quality, speed } => {
let mut avif: Vec<u8> = Vec::new();
let color_type = match img.color().has_alpha() {
true => ExtendedColorType::Rgba8,
false => ExtendedColorType::Rgb8,
};
let encoder = AvifEncoder::new_with_speed_quality(&mut avif, 10, q.unwrap_or(70));
let encoder = AvifEncoder::new_with_speed_quality(&mut avif, speed, quality);
encoder.write_image(
&img.as_bytes(),
img.dimensions().0,
img.dimensions().1,
color_type,
img.color().into(),
)?;
buffered_f.write_all(&avif.as_bytes())?;
}
Expand Down Expand Up @@ -162,6 +158,7 @@ impl Processor {
input_path: PathBuf,
format: &str,
quality: Option<u8>,
speed: Option<u8>,
) -> Result<EnqueueResponse> {
// First we load metadata from the cache if possible, otherwise from the file itself
if !self.meta_cache.contains_key(&input_path) {
Expand All @@ -172,7 +169,7 @@ impl Processor {
// We will have inserted it just above
let meta = &self.meta_cache[&input_path];
// We get the output format
let format = Format::from_args(meta.is_lossy(), format, quality)?;
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 url = format!("{}{}", self.base_url, filename);
Expand Down
Loading