From 11c5f3fed4516068e78b1e710be05ac57369fee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Sat, 19 Oct 2024 10:29:36 +0800 Subject: [PATCH] perf: apply rotate in place to images with orientation (#1807) --- Cargo.lock | 51 +++++----------- Cargo.toml | 6 +- yazi-adapter/Cargo.toml | 27 ++++----- yazi-adapter/src/image.rs | 122 +++++++++++++------------------------- 4 files changed, 72 insertions(+), 134 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f33bf34..ea4fc8d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index b56b104c..dd8daff7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/yazi-adapter/Cargo.toml b/yazi-adapter/Cargo.toml index b32ec22a..f3a680ac 100644 --- a/yazi-adapter/Cargo.toml +++ b/yazi-adapter/Cargo.toml @@ -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" ] } diff --git a/yazi-adapter/src/image.rs b/yazi-adapter/src/image.rs index a858ac82..967dda3b 100644 --- a/yazi-adapter/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -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 { - 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 { - // 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>) -> image::ImageReader> { + 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), + } } }