diff --git a/.github/workflows/draft.yml b/.github/workflows/draft.yml index 9272ef9d..4df42b5b 100644 --- a/.github/workflows/draft.yml +++ b/.github/workflows/draft.yml @@ -230,6 +230,7 @@ jobs: echo 'NIGHTLY_BODY<> $GITHUB_ENV echo "From commit: ${GITHUB_SHA:0:8}" >> $GITHUB_ENV echo "Generated on: $(date -u +"%Y-%m-%d %H:%M") UTC" >> $GITHUB_ENV + echo "Nightly changelog: https://github.com/sxyazi/yazi/blob/main/CHANGELOG.md#unreleased" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV - uses: actions/checkout@v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 406e1e08..b98f3b34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): ### Added - Support VFS for preset previewers that rely on external commands ([#3477]) +- Support 8-bit images in RGB, CIELAB, and GRAY color spaces ([#3358]) ### Fixed @@ -1559,6 +1560,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3290]: https://github.com/sxyazi/yazi/pull/3290 [#3313]: https://github.com/sxyazi/yazi/pull/3313 [#3317]: https://github.com/sxyazi/yazi/pull/3317 +[#3358]: https://github.com/sxyazi/yazi/pull/3358 [#3360]: https://github.com/sxyazi/yazi/pull/3360 [#3361]: https://github.com/sxyazi/yazi/pull/3361 [#3364]: https://github.com/sxyazi/yazi/pull/3364 diff --git a/Cargo.lock b/Cargo.lock index e3005b52..0ef16332 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -834,7 +834,7 @@ dependencies = [ "parking_lot", "rustix 0.38.44", "serde", - "signal-hook", + "signal-hook 0.3.18", "signal-hook-mio", "winapi", ] @@ -856,7 +856,7 @@ dependencies = [ "parking_lot", "rustix 1.1.3", "serde", - "signal-hook", + "signal-hook 0.3.18", "signal-hook-mio", "winapi", ] @@ -983,8 +983,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -1001,13 +1011,37 @@ dependencies = [ "syn 2.0.112", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.112", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.112", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.112", ] @@ -1102,7 +1136,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.112", @@ -1915,11 +1949,11 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" dependencies = [ - "darling", + "darling 0.23.0", "indoc", "proc-macro2", "quote", @@ -3918,6 +3952,16 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a37d01603c37b5466f808de79f845c7116049b0579adb70a6b7d47c1fa3a952" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-mio" version = "0.2.5" @@ -3926,7 +3970,7 @@ checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", - "signal-hook", + "signal-hook 0.3.18", ] [[package]] @@ -3941,13 +3985,13 @@ dependencies = [ [[package]] name = "signal-hook-tokio" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e" +checksum = "e513e435a8898a0002270f29d0a708b7879708fb5c4d00e46983ca2d2d378cf0" dependencies = [ "futures-core", "libc", - "signal-hook", + "signal-hook 0.4.1", "tokio", ] @@ -4227,7 +4271,7 @@ dependencies = [ "phf", "serde", "sha2 0.10.9", - "signal-hook", + "signal-hook 0.3.18", "siphasher", "terminfo", "termios", @@ -5360,6 +5404,7 @@ dependencies = [ "base64", "crossterm 0.29.0", "image", + "moxcms", "palette", "quantette", "ratatui", @@ -5888,9 +5933,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac060176f7020d62c3bcc1cdbcec619d54f48b07ad1963a3f80ce7a0c17755f" +checksum = "de9211a9f64b825911bdf0240f58b7a8dac217fe260fc61f080a07f61372fbd5" [[package]] name = "zune-core" diff --git a/yazi-adapter/Cargo.toml b/yazi-adapter/Cargo.toml index af045c6e..a94c409a 100644 --- a/yazi-adapter/Cargo.toml +++ b/yazi-adapter/Cargo.toml @@ -24,6 +24,7 @@ anyhow = { workspace = true } base64 = { workspace = true } crossterm = { workspace = true } image = { version = "0.25.9", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "webp" ] } +moxcms = "0.7.11" palette = { version = "0.7.6", default-features = false } quantette = { version = "0.5.1", default-features = false } ratatui = { workspace = true } diff --git a/yazi-adapter/src/icc.rs b/yazi-adapter/src/icc.rs new file mode 100644 index 00000000..6b3f0541 --- /dev/null +++ b/yazi-adapter/src/icc.rs @@ -0,0 +1,69 @@ +use anyhow::Context; +use image::{ColorType, DynamicImage, GrayAlphaImage, GrayImage, ImageDecoder, RgbImage, RgbaImage, metadata::Cicp}; +use moxcms::{CicpColorPrimaries, ColorProfile, DataColorSpace, Layout, TransferCharacteristics, TransformOptions}; + +pub(super) struct Icc; +impl Icc { + pub(super) fn transform(mut decoder: impl ImageDecoder) -> anyhow::Result { + if let Some(layout) = Self::color_type_to_layout(decoder.color_type()) + && let Some(icc) = decoder.icc_profile().unwrap_or_default() + && let Ok(profile) = ColorProfile::new_from_slice(&icc) + && Self::requires_transform(&profile) + { + let mut buf = vec![0u8; decoder.total_bytes() as usize]; + let (w, h) = decoder.dimensions(); + decoder.read_image(&mut buf)?; + + let transformer = profile + // TODO: Use `create_transform_in_place_nbit` in the next minor version of moxcms. + .create_transform_8bit(layout, &ColorProfile::new_srgb(), layout, TransformOptions::default()) + .context("cannot make a profile transformer")?; + + let mut converted = vec![0u8; buf.len()]; + transformer.transform(&buf, &mut converted).context("cannot transform image")?; + + let mut image: DynamicImage = match layout { + Layout::Gray => { + GrayImage::from_raw(w, h, converted).context("cannot load transformed image")?.into() + } + Layout::GrayAlpha => { + GrayAlphaImage::from_raw(w, h, converted).context("cannot load transformed image")?.into() + } + Layout::Rgb => { + RgbImage::from_raw(w, h, converted).context("cannot load transformed image")?.into() + } + Layout::Rgba => { + RgbaImage::from_raw(w, h, converted).context("cannot load transformed image")?.into() + } + _ => unreachable!(), + }; + + image.set_rgb_primaries(Cicp::SRGB.primaries); + image.set_transfer_function(Cicp::SRGB.transfer); + Ok(image) + } else { + Ok(DynamicImage::from_decoder(decoder)?) + } + } + + fn color_type_to_layout(color_type: ColorType) -> Option { + match color_type { + ColorType::L8 => Some(Layout::Gray), + ColorType::La8 => Some(Layout::GrayAlpha), + ColorType::Rgb8 => Some(Layout::Rgb), + ColorType::Rgba8 => Some(Layout::Rgba), + _ => None, + } + } + + fn requires_transform(profile: &ColorProfile) -> bool { + if profile.color_space == DataColorSpace::Cmyk { + return false; + } + + profile.cicp.is_none_or(|c| { + c.color_primaries != CicpColorPrimaries::Bt709 + || c.transfer_characteristics != TransferCharacteristics::Srgb + }) + } +} diff --git a/yazi-adapter/src/image.rs b/yazi-adapter/src/image.rs index 0eabe67e..35b8a52a 100644 --- a/yazi-adapter/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -1,18 +1,18 @@ use std::path::{Path, PathBuf}; use anyhow::Result; -use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation}; +use image::{DynamicImage, ImageDecoder, ImageError, ImageReader, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation}; use ratatui::layout::Rect; use yazi_config::YAZI; use yazi_fs::provider::{Provider, local::Local}; -use crate::Dimension; +use crate::{Dimension, Icc}; pub struct Image; impl Image { pub async fn precache(src: PathBuf, cache: &Path) -> Result<()> { - let (mut img, orientation, icc) = Self::decode_from(src).await?; + let (mut img, orientation) = Self::decode_from(src).await?; let (w, h) = Self::flip_size(orientation, (YAZI.preview.max_width, YAZI.preview.max_height)); let buf = tokio::task::spawn_blocking(move || { @@ -25,14 +25,11 @@ impl Image { let mut buf = Vec::new(); if img.color().has_alpha() { - let rgba = img.into_rgba8(); - let mut encoder = PngEncoder::new(&mut buf); - icc.map(|b| encoder.set_icc_profile(b)); - encoder.write_image(&rgba, rgba.width(), rgba.height(), ExtendedColorType::Rgba8)?; + let encoder = PngEncoder::new(&mut buf); + img.write_with_encoder(encoder)?; } else { - let mut encoder = JpegEncoder::new_with_quality(&mut buf, YAZI.preview.image_quality); - icc.map(|b| encoder.set_icc_profile(b)); - encoder.encode_image(&img.into_rgb8())?; + let encoder = JpegEncoder::new_with_quality(&mut buf, YAZI.preview.image_quality); + img.write_with_encoder(encoder)?; } Ok::<_, ImageError>(buf) @@ -43,7 +40,7 @@ impl Image { } pub(super) async fn downscale(path: PathBuf, rect: Rect) -> Result { - let (mut img, orientation, _) = Self::decode_from(path).await?; + let (mut img, orientation) = Self::decode_from(path).await?; let (w, h) = Self::flip_size(orientation, Self::max_pixel(rect)); // Fast path. @@ -96,7 +93,7 @@ impl Image { } } - async fn decode_from(path: PathBuf) -> ImageResult<(DynamicImage, Orientation, Option>)> { + async fn decode_from(path: PathBuf) -> Result<(DynamicImage, Orientation)> { let mut limits = Limits::no_limits(); if YAZI.tasks.image_alloc > 0 { limits.max_alloc = Some(YAZI.tasks.image_alloc as u64); @@ -114,9 +111,7 @@ impl Image { let mut decoder = reader.with_guessed_format()?.into_decoder()?; let orientation = decoder.orientation().unwrap_or(Orientation::NoTransforms); - let icc = decoder.icc_profile().unwrap_or_default(); - - Ok((DynamicImage::from_decoder(decoder)?, orientation, icc)) + Ok((Icc::transform(decoder)?, orientation)) }) .await .map_err(|e| ImageError::IoError(e.into()))? diff --git a/yazi-adapter/src/lib.rs b/yazi-adapter/src/lib.rs index 29d87ad3..4333d583 100644 --- a/yazi-adapter/src/lib.rs +++ b/yazi-adapter/src/lib.rs @@ -1,6 +1,6 @@ yazi_macro::mod_pub!(drivers); -yazi_macro::mod_flat!(adapter brand dimension emulator image info mux unknown); +yazi_macro::mod_flat!(adapter brand dimension emulator icc image info mux unknown); use yazi_shared::{RoCell, SyncCell, in_wsl}; diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index bfa2f30b..2013ec76 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -64,7 +64,7 @@ tracing-subscriber = { version = "0.3.22", features = [ "env-filter" ] } [target."cfg(unix)".dependencies] libc = { workspace = true } -signal-hook-tokio = { version = "0.3.1", features = [ "futures-v0_3" ] } +signal-hook-tokio = { version = "0.4.0", features = [ "futures-v0_3" ] } [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }