Skip to content

Commit

Permalink
Procimage refactor (#2086)
Browse files Browse the repository at this point in the history
* Refactor image proc

Closes #2066

* Add colocated_path to shortcodes

Closes #1793
  • Loading branch information
Keats committed Feb 16, 2023
1 parent 29cb3fc commit 8ba6c1c
Show file tree
Hide file tree
Showing 42 changed files with 658 additions and 677 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This will error if 2 values are set
- Remove built-ins shortcodes
- Having a file called `index.md` in a folder with a `_index.md` is now an error
- Ignore temp files from vim/emacs/macos/etc as well as files without extensions when getting colocated assets
- Now integrates the file stem of the original file into the processed images filename: {stem}.{hash}.{extension}

### Other

Expand All @@ -31,7 +32,7 @@ This will error if 2 values are set
- Enable locale date formatting for the Tera `date` filter
- Cachebust fingerprint is now only 20 chars long
- Add `text` alias for plain text highlighting (before, only `txt` was used)

- Adds a new field to `page`: `colocated_path` that points to the folder of the current file being rendered if it's a colocated folder. None otherwise.

## 0.16.1 (2022-08-14)

Expand Down
13 changes: 13 additions & 0 deletions components/content/src/file_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ pub struct FileInfo {
pub name: String,
/// The .md path, starting from the content directory, with `/` slashes
pub relative: String,
/// The path from the content directory to the colocated directory. Ends with a `/` when set.
/// Only filled if it is a colocated directory, None otherwise.
pub colocated_path: Option<String>,
/// Path of the directory containing the .md file
pub parent: PathBuf,
/// Path of the grand parent directory for that file. Only used in sections to find subsections.
Expand All @@ -63,11 +66,17 @@ impl FileInfo {
} else {
format!("{}.md", name)
};
let mut colocated_path = None;

// If we have a folder with an asset, don't consider it as a component
// Splitting on `.` as we might have a language so it isn't *only* index but also index.fr
// etc
if !components.is_empty() && name.split('.').collect::<Vec<_>>()[0] == "index" {
colocated_path = Some({
let mut val = components.join("/");
val.push('/');
val
});
components.pop();
// also set parent_path to grandparent instead
parent = parent.parent().unwrap().to_path_buf();
Expand All @@ -83,6 +92,7 @@ impl FileInfo {
name,
components,
relative,
colocated_path,
}
}

Expand All @@ -108,6 +118,7 @@ impl FileInfo {
name,
components,
relative,
colocated_path: None,
}
}

Expand Down Expand Up @@ -171,6 +182,7 @@ mod tests {
&PathBuf::new(),
);
assert_eq!(file.components, ["posts".to_string(), "tutorials".to_string()]);
assert_eq!(file.colocated_path, Some("posts/tutorials/python/".to_string()));
}

#[test]
Expand Down Expand Up @@ -211,6 +223,7 @@ mod tests {
&PathBuf::new(),
);
assert_eq!(file.components, ["posts".to_string(), "tutorials".to_string()]);
assert_eq!(file.colocated_path, Some("posts/tutorials/python/".to_string()));
let res = file.find_language("en", &["fr"]);
assert!(res.is_ok());
assert_eq!(res.unwrap(), "fr");
Expand Down
4 changes: 4 additions & 0 deletions components/content/src/ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ fn find_backlinks<'a>(relative_path: &str, library: &'a Library) -> Vec<BackLink
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct SerializingPage<'a> {
relative_path: &'a str,
colocated_path: &'a Option<String>,
content: &'a str,
permalink: &'a str,
slug: &'a str,
Expand Down Expand Up @@ -104,6 +105,7 @@ impl<'a> SerializingPage<'a> {

Self {
relative_path: &page.file.relative,
colocated_path: &page.file.colocated_path,
ancestors: &page.ancestors,
content: &page.content,
permalink: &page.permalink,
Expand Down Expand Up @@ -137,6 +139,7 @@ impl<'a> SerializingPage<'a> {
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct SerializingSection<'a> {
relative_path: &'a str,
colocated_path: &'a Option<String>,
content: &'a str,
permalink: &'a str,
draft: bool,
Expand Down Expand Up @@ -198,6 +201,7 @@ impl<'a> SerializingSection<'a> {

Self {
relative_path: &section.file.relative,
colocated_path: &section.file.colocated_path,
ancestors: &section.ancestors,
draft: section.meta.draft,
content: &section.content,
Expand Down
66 changes: 66 additions & 0 deletions components/imageproc/src/format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use errors::{anyhow, Result};
use std::hash::{Hash, Hasher};

const DEFAULT_Q_JPG: u8 = 75;

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

impl Format {
pub fn from_args(is_lossy: bool, format: &str, quality: 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)
}
}
"jpeg" | "jpg" => Ok(Jpeg(jpg_quality)),
"png" => Ok(Png),
"webp" => Ok(WebP(quality)),
_ => Err(anyhow!("Invalid image format: {}", format)),
}
}

pub fn extension(&self) -> &str {
// Kept in sync with RESIZED_FILENAME and op_filename
use Format::*;

match *self {
Png => "png",
Jpeg(_) => "jpg",
WebP(_) => "webp",
}
}
}

#[allow(clippy::derive_hash_xor_eq)]
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,
};

hasher.write_u16(q);
hasher.write(self.extension().as_bytes());
}
}
55 changes: 55 additions & 0 deletions components/imageproc/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use std::borrow::Cow;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::Path;

use crate::format::Format;
use crate::ResizeOperation;
use libs::image::DynamicImage;

/// Apply image rotation based on EXIF data
/// Returns `None` if no transformation is needed
pub fn fix_orientation(img: &DynamicImage, path: &Path) -> Option<DynamicImage> {
let file = std::fs::File::open(path).ok()?;
let mut buf_reader = std::io::BufReader::new(&file);
let exif_reader = exif::Reader::new();
let exif = exif_reader.read_from_container(&mut buf_reader).ok()?;
let orientation =
exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)?.value.get_uint(0)?;
match orientation {
// Values are taken from the page 30 of
// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf
// For more details check http://sylvana.net/jpegcrop/exif_orientation.html
1 => None,
2 => Some(img.fliph()),
3 => Some(img.rotate180()),
4 => Some(img.flipv()),
5 => Some(img.fliph().rotate270()),
6 => Some(img.rotate90()),
7 => Some(img.fliph().rotate90()),
8 => Some(img.rotate270()),
_ => None,
}
}

/// 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 {
let mut hasher = DefaultHasher::new();
hasher.write(input_src.as_ref());
op.hash(&mut hasher);
format.hash(&mut hasher);
let hash = hasher.finish();
let filename = input_path
.file_stem()
.map(|s| s.to_string_lossy())
.unwrap_or_else(|| Cow::Borrowed("unknown"));

format!("{}.{:016x}.{}", filename, hash, format.extension())
}
Loading

0 comments on commit 8ba6c1c

Please sign in to comment.