Support 8-bit images in RGB, CIELAB, and GRAY color spaces (#3358)

Co-authored-by: sxyazi <sxyazi@gmail.com>
This commit is contained in:
leiserfg 2026-01-02 15:57:06 +01:00 committed by GitHub
parent 0000141700
commit a6fcbb04df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 146 additions and 33 deletions

View file

@ -230,6 +230,7 @@ jobs:
echo 'NIGHTLY_BODY<<EOF' >> $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

View file

@ -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

77
Cargo.lock generated
View file

@ -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"

View file

@ -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 }

69
yazi-adapter/src/icc.rs Normal file
View file

@ -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<DynamicImage> {
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<Layout> {
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
})
}
}

View file

@ -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<DynamicImage> {
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<Vec<u8>>)> {
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()))?

View file

@ -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};

View file

@ -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" ] }