From 7e752df78a2c08d6754cc19c354c4df74d5e123d 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: Mon, 8 Jan 2024 11:31:22 +0800 Subject: [PATCH] feat: image orientation support (#488) --- Cargo.lock | 16 ++++++++ yazi-adaptor/Cargo.toml | 17 ++++---- yazi-adaptor/src/image.rs | 81 ++++++++++++++++++++++++++++++++++----- 3 files changed, 96 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a48c9349..f475a4ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -988,6 +988,15 @@ 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" @@ -1195,6 +1204,12 @@ dependencies = [ "syn 2.0.46", ] +[[package]] +name = "mutate_once" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" + [[package]] name = "nom" version = "7.1.3" @@ -2627,6 +2642,7 @@ dependencies = [ "color_quant", "image", "imagesize", + "kamadak-exif", "ratatui", "tokio", "tracing", diff --git a/yazi-adaptor/Cargo.toml b/yazi-adaptor/Cargo.toml index 687d7b3a..169956d9 100644 --- a/yazi-adaptor/Cargo.toml +++ b/yazi-adaptor/Cargo.toml @@ -13,14 +13,15 @@ yazi-config = { path = "../yazi-config", version = "0.1.5" } yazi-shared = { path = "../yazi-shared", version = "0.1.5" } # External dependencies -anyhow = "^1" -arc-swap = "^1" -base64 = "^0" -color_quant = "^1" -image = "^0" -imagesize = "^0" -ratatui = "^0" -tokio = { version = "^1", features = [ "parking_lot", "io-util", "process" ] } +anyhow = "^1" +arc-swap = "^1" +base64 = "^0" +color_quant = "^1" +image = "^0" +imagesize = "^0" +kamadak-exif = "0" +ratatui = "^0" +tokio = { version = "^1", features = [ "parking_lot", "io-util", "process" ] } # Logging tracing = { version = "^0", features = [ "max_level_debug", "release_max_level_warn" ] } diff --git a/yazi-adaptor/src/image.rs b/yazi-adaptor/src/image.rs index d5366353..14b35d83 100644 --- a/yazi-adaptor/src/image.rs +++ b/yazi-adaptor/src/image.rs @@ -1,7 +1,8 @@ -use std::{fs::File, io::BufReader, path::{Path, PathBuf}}; +use std::{fs::File, io::BufReader, ops::RangeBounds, path::{Path, PathBuf}}; use anyhow::Result; -use image::{imageops::FilterType, io::Limits, DynamicImage, ImageFormat}; +use exif::{In, Tag}; +use image::{imageops::{self, FilterType}, io::Limits, DynamicImage, ImageFormat}; use ratatui::layout::Rect; use yazi_config::{PREVIEW, TASKS}; use yazi_shared::term::Term; @@ -10,18 +11,25 @@ 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::io::Reader::open(path)?.with_guessed_format()?).decode() }) .await??; + let (mut w, mut h) = (PREVIEW.max_width, PREVIEW.max_height); tokio::task::spawn_blocking(move || { - let (w, h) = (PREVIEW.max_width, PREVIEW.max_height); + if (5..=8).contains(&orientation) { + (w, h) = (h, w); + } + if img.width() > w || img.height() > h { img = img.resize(w, h, FilterType::Triangle); } + img = Self::rotate(img, orientation); Ok(match img { DynamicImage::ImageRgb8(buf) => buf.save_with_format(cache, ImageFormat::Jpeg), DynamicImage::ImageRgba8(buf) => buf.save_with_format(cache, ImageFormat::Jpeg), @@ -32,19 +40,25 @@ impl Image { } pub(super) async fn downscale(path: &Path, rect: Rect) -> Result { + let orientation = Self::orientation(path).await?; + let path = path.to_owned(); - let img = tokio::task::spawn_blocking(move || { + let mut img = tokio::task::spawn_blocking(move || { Self::set_limits(image::io::Reader::open(path)?.with_guessed_format()?).decode() }) .await??; - let (w, h) = Self::max_size(rect); + let (mut w, mut h) = Self::max_size(rect); tokio::task::spawn_blocking(move || { - Ok(if img.width() > w || img.height() > h { - img.resize(w, h, FilterType::Triangle) - } else { - img - }) + if (5..=8).contains(&orientation) { + (w, h) = (h, w); + } + + if img.width() > w || img.height() > h { + img = img.resize(w, h, FilterType::Triangle) + } + + Ok(Self::rotate(img, orientation)) }) .await? } @@ -58,6 +72,53 @@ impl Image { .unwrap_or((PREVIEW.max_width, PREVIEW.max_height)) } + async fn orientation(path: &Path) -> Result { + 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 rgba = img.color().has_alpha(); + if orientation == 2 { + img = DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)); + } else if orientation == 3 { + img = DynamicImage::ImageRgba8(imageops::rotate180(&img)); + } else if orientation == 4 { + img = DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)); + } else if orientation == 5 { + img = DynamicImage::ImageRgba8(imageops::rotate90(&img)); + img = DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)); + } else if orientation == 6 { + img = DynamicImage::ImageRgba8(imageops::rotate90(&img)); + } else if orientation == 7 { + img = DynamicImage::ImageRgba8(imageops::rotate270(&img)); + img = DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)); + } else if orientation == 8 { + img = DynamicImage::ImageRgba8(imageops::rotate270(&img)); + } + if !rgba { + img = DynamicImage::ImageRgb8(img.into_rgb8()); + } + img + } + fn set_limits(mut r: image::io::Reader>) -> image::io::Reader> { let mut limits = Limits::no_limits(); if TASKS.image_alloc > 0 {