perf: apply rotate in place to images with orientation (#1807)

This commit is contained in:
三咲雅 · Misaki Masa 2024-10-19 10:29:36 +08:00 committed by GitHub
parent df78827af5
commit 11c5f3fed4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 72 additions and 134 deletions

51
Cargo.lock generated
View file

@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.89"
version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95"
[[package]]
name = "arbitrary"
@ -288,9 +288,9 @@ dependencies = [
[[package]]
name = "built"
version = "0.7.4"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4"
checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b"
[[package]]
name = "bumpalo"
@ -1084,9 +1084,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.2"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae"
dependencies = [
"bytemuck",
"byteorder-lite",
@ -1106,9 +1106,9 @@ dependencies = [
[[package]]
name = "image-webp"
version = "0.1.3"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904"
checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
dependencies = [
"byteorder-lite",
"quick-error",
@ -1235,15 +1235,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kamadak-exif"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077"
dependencies = [
"mutate_once",
]
[[package]]
name = "kqueue"
version = "1.0.8"
@ -1278,9 +1269,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.159"
version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "libfuzzer-sys"
@ -1484,12 +1475,6 @@ dependencies = [
"syn 2.0.79",
]
[[package]]
name = "mutate_once"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b"
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@ -1833,9 +1818,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.87"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
dependencies = [
"unicode-ident",
]
@ -1980,9 +1965,9 @@ dependencies = [
[[package]]
name = "ravif"
version = "0.11.10"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd"
checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6"
dependencies = [
"avif-serialize",
"imgref",
@ -2066,9 +2051,6 @@ name = "rgb"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
dependencies = [
"bytemuck",
]
[[package]]
name = "rustc-demangle"
@ -2154,9 +2136,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.128"
version = "1.0.131"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
checksum = "67d42a0bd4ac281beff598909bb56a86acaf979b84483e1c79c10dcaf98f8cf3"
dependencies = [
"itoa",
"memchr",
@ -3202,7 +3184,6 @@ dependencies = [
"futures",
"image",
"imagesize",
"kamadak-exif",
"ratatui",
"scopeguard",
"tokio",

View file

@ -11,7 +11,7 @@ strip = true
[workspace.dependencies]
ansi-to-tui = "6.0.0"
anyhow = "1.0.89"
anyhow = "1.0.90"
arc-swap = "1.7.1"
base64 = "0.22.1"
bitflags = "2.6.0"
@ -20,7 +20,7 @@ crossterm = { version = "0.28.1", features = [ "event-stream" ] }
dirs = "5.0.1"
futures = "0.3.31"
globset = "0.4.15"
libc = "0.2.159"
libc = "0.2.161"
md-5 = "0.10.6"
mlua = { version = "0.9.9", features = [ "lua54", "serialize", "macros", "async" ] }
parking_lot = "0.12.3"
@ -28,7 +28,7 @@ ratatui = { version = "0.28.1", features = [ "unstable-rendered-line-info"
regex = "1.11.0"
scopeguard = "1.2.0"
serde = { version = "1.0.210", features = [ "derive" ] }
serde_json = "1.0.128"
serde_json = "1.0.131"
shell-words = "1.1.0"
tokio = { version = "1.40.0", features = [ "full" ] }
tokio-stream = "0.1.16"

View file

@ -14,20 +14,19 @@ yazi-macro = { path = "../yazi-macro", version = "0.3.3" }
yazi-shared = { path = "../yazi-shared", version = "0.3.3" }
# External dependencies
ansi-to-tui = { workspace = true }
anyhow = { workspace = true }
arc-swap = { workspace = true }
base64 = { workspace = true }
color_quant = "1.1.0"
crossterm = { workspace = true }
futures = { workspace = true }
image = { version = "0.25.2", default-features = false, features = [ "rayon", "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "webp" ] }
imagesize = "0.13.0"
kamadak-exif = "0.5.5"
ratatui = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
ansi-to-tui = { workspace = true }
anyhow = { workspace = true }
arc-swap = { workspace = true }
base64 = { workspace = true }
color_quant = "1.1.0"
crossterm = { workspace = true }
futures = { workspace = true }
image = { version = "0.25.4", default-features = false, features = [ "rayon", "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "webp" ] }
imagesize = "0.13.0"
ratatui = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -1,8 +1,7 @@
use std::{fs::File, io::BufReader, path::{Path, PathBuf}};
use std::path::{Path, PathBuf};
use anyhow::Result;
use exif::{In, Tag};
use image::{DynamicImage, ExtendedColorType, ImageEncoder, ImageError, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::{self, FilterType}};
use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation};
use ratatui::layout::Rect;
use yazi_config::{PREVIEW, TASKS};
@ -12,27 +11,18 @@ pub struct Image;
impl Image {
pub async fn precache(path: &Path, cache: PathBuf) -> Result<()> {
let orientation = Self::orientation(path).await?;
let path = path.to_owned();
let mut img = tokio::task::spawn_blocking(move || {
Self::set_limits(image::ImageReader::open(path)?.with_guessed_format()?).decode()
})
.await??;
let (mut w, mut h) = (PREVIEW.max_width, PREVIEW.max_height);
if (5..=8).contains(&orientation) {
(w, h) = (h, w);
}
let (mut img, orientation) = Self::decode_from(path).await?;
let (w, h) = Self::flip_size(orientation, (PREVIEW.max_width, PREVIEW.max_height));
let buf = tokio::task::spawn_blocking(move || {
if img.width() > w || img.height() > h {
img = img.resize(w, h, Self::filter());
}
if orientation != Orientation::NoTransforms {
img.apply_orientation(orientation);
}
let mut buf = Vec::new();
img = Self::rotate(img, orientation);
if img.color().has_alpha() {
let rgba = img.into_rgba8();
PngEncoder::new(&mut buf).write_image(
@ -54,31 +44,26 @@ impl Image {
}
pub(super) async fn downscale(path: &Path, rect: Rect) -> Result<DynamicImage> {
let orientation = Self::orientation(path).await?;
let path = path.to_owned();
let mut img = tokio::task::spawn_blocking(move || {
Self::set_limits(image::ImageReader::open(path)?.with_guessed_format()?).decode()
})
.await??;
let (mut w, mut h) = Self::max_pixel(rect);
if (5..=8).contains(&orientation) {
(w, h) = (h, w);
}
let (mut img, orientation) = Self::decode_from(path).await?;
let (w, h) = Self::flip_size(orientation, Self::max_pixel(rect));
// Fast path.
if img.width() <= w && img.height() <= h && orientation <= 1 {
if img.width() <= w && img.height() <= h && orientation == Orientation::NoTransforms {
return Ok(img);
}
tokio::task::spawn_blocking(move || {
let img = tokio::task::spawn_blocking(move || {
if img.width() > w || img.height() > h {
img = img.resize(w, h, Self::filter())
}
Ok(Self::rotate(img, orientation))
if orientation != Orientation::NoTransforms {
img.apply_orientation(orientation);
}
img
})
.await?
.await?;
Ok(img)
}
pub(super) fn max_pixel(rect: Rect) -> (u32, u32) {
@ -113,53 +98,7 @@ impl Image {
}
}
async fn orientation(path: &Path) -> Result<u8> {
// We don't want to read the orientation of the cached image that has been
// rotated in the `Self::precache()` step.
if path.parent() == Some(&PREVIEW.cache_dir) {
return Ok(0);
}
let path = path.to_owned();
tokio::task::spawn_blocking(move || {
let file = std::fs::File::open(path)?;
let mut reader = std::io::BufReader::new(&file);
let Ok(exif) = exif::Reader::new().read_from_container(&mut reader) else {
return Ok(0);
};
Ok(match exif.get_field(Tag::Orientation, In::PRIMARY) {
Some(orientation) => match orientation.value.get_uint(0) {
Some(v @ 1..=8) => v as u8,
_ => 1,
},
None => 1,
})
})
.await?
}
// https://magnushoff.com/articles/jpeg-orientation/
fn rotate(mut img: DynamicImage, orientation: u8) -> DynamicImage {
let alpha = img.color().has_alpha();
img = match orientation {
2 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)),
3 => DynamicImage::ImageRgba8(imageops::rotate180(&img)),
4 => DynamicImage::ImageRgba8(imageops::flip_vertical(&img)),
5 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&imageops::rotate90(&img))),
6 => DynamicImage::ImageRgba8(imageops::rotate90(&img)),
7 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&imageops::rotate270(&img))),
8 => DynamicImage::ImageRgba8(imageops::rotate270(&img)),
_ => img,
};
if !alpha {
img = DynamicImage::ImageRgb8(img.into_rgb8());
}
img
}
fn set_limits(mut r: image::ImageReader<BufReader<File>>) -> image::ImageReader<BufReader<File>> {
async fn decode_from(path: &Path) -> ImageResult<(DynamicImage, Orientation)> {
let mut limits = Limits::no_limits();
if TASKS.image_alloc > 0 {
limits.max_alloc = Some(TASKS.image_alloc as u64);
@ -170,7 +109,26 @@ impl Image {
if TASKS.image_bound[1] > 0 {
limits.max_image_height = Some(TASKS.image_bound[1] as u32);
}
r.limits(limits);
r
let path = path.to_owned();
tokio::task::spawn_blocking(move || {
let mut reader = ImageReader::open(path)?;
reader.limits(limits);
let mut decoder = reader.with_guessed_format()?.into_decoder()?;
let orientation = decoder.orientation().unwrap_or(Orientation::NoTransforms);
Ok((DynamicImage::from_decoder(decoder)?, orientation))
})
.await
.map_err(|e| ImageError::IoError(e.into()))?
}
fn flip_size(orientation: Orientation, (w, h): (u32, u32)) -> (u32, u32) {
use image::metadata::Orientation::{Rotate90, Rotate90FlipH, Rotate270, Rotate270FlipH};
match orientation {
Rotate90 | Rotate270 | Rotate90FlipH | Rotate270FlipH => (h, w),
_ => (w, h),
}
}
}