Skip to content

Commit ae5e3fc

Browse files
committed
Fix preview of images with non srgb icc profile
1 parent c725c91 commit ae5e3fc

File tree

3 files changed

+72
-18
lines changed

3 files changed

+72
-18
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

yazi-adapter/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ ratatui = { workspace = true }
2727
scopeguard = { workspace = true }
2828
tokio = { workspace = true }
2929
tracing = { workspace = true }
30+
qcms = "0.3.0"
3031

3132
[target.'cfg(target_os = "macos")'.dependencies]
3233
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

yazi-adapter/src/image.rs

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
use std::path::{Path, PathBuf};
22

3-
use anyhow::Result;
4-
use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation};
3+
use anyhow::{Context, Result};
4+
use image::{
5+
DynamicImage, ImageBuffer, ImageDecoder, ImageError, ImageReader, Limits, RgbImage, RgbaImage,
6+
codecs::{jpeg::JpegEncoder, png::PngEncoder},
7+
imageops::FilterType,
8+
metadata::{Cicp, Orientation},
9+
};
10+
use qcms::{Profile, Transform};
511
use ratatui::layout::Rect;
612
use yazi_config::YAZI;
713
use yazi_fs::provider::{Provider, local::Local};
@@ -12,7 +18,7 @@ pub struct Image;
1218

1319
impl Image {
1420
pub async fn precache(src: PathBuf, cache: &Path) -> Result<()> {
15-
let (mut img, orientation, icc) = Self::decode_from(src).await?;
21+
let (mut img, orientation) = Self::decode_from(src).await?;
1622
let (w, h) = Self::flip_size(orientation, (YAZI.preview.max_width, YAZI.preview.max_height));
1723

1824
let buf = tokio::task::spawn_blocking(move || {
@@ -25,14 +31,11 @@ impl Image {
2531

2632
let mut buf = Vec::new();
2733
if img.color().has_alpha() {
28-
let rgba = img.into_rgba8();
29-
let mut encoder = PngEncoder::new(&mut buf);
30-
icc.map(|b| encoder.set_icc_profile(b));
31-
encoder.write_image(&rgba, rgba.width(), rgba.height(), ExtendedColorType::Rgba8)?;
34+
let encoder = PngEncoder::new(&mut buf);
35+
img.write_with_encoder(encoder)?
3236
} else {
33-
let mut encoder = JpegEncoder::new_with_quality(&mut buf, YAZI.preview.image_quality);
34-
icc.map(|b| encoder.set_icc_profile(b));
35-
encoder.encode_image(&img.into_rgb8())?;
37+
let encoder = JpegEncoder::new_with_quality(&mut buf, YAZI.preview.image_quality);
38+
img.write_with_encoder(encoder)?
3639
}
3740

3841
Ok::<_, ImageError>(buf)
@@ -43,7 +46,7 @@ impl Image {
4346
}
4447

4548
pub(super) async fn downscale(path: PathBuf, rect: Rect) -> Result<DynamicImage> {
46-
let (mut img, orientation, _) = Self::decode_from(path).await?;
49+
let (mut img, orientation) = Self::decode_from(path).await?;
4750
let (w, h) = Self::flip_size(orientation, Self::max_pixel(rect));
4851

4952
// Fast path.
@@ -77,9 +80,9 @@ impl Image {
7780
pub(super) fn pixel_area(size: (u32, u32), rect: Rect) -> Rect {
7881
Dimension::cell_size()
7982
.map(|(cw, ch)| Rect {
80-
x: rect.x,
81-
y: rect.y,
82-
width: (size.0 as f64 / cw).ceil() as u16,
83+
x: rect.x,
84+
y: rect.y,
85+
width: (size.0 as f64 / cw).ceil() as u16,
8386
height: (size.1 as f64 / ch).ceil() as u16,
8487
})
8588
.unwrap_or(rect)
@@ -96,7 +99,52 @@ impl Image {
9699
}
97100
}
98101

99-
async fn decode_from(path: PathBuf) -> ImageResult<(DynamicImage, Orientation, Option<Vec<u8>>)> {
102+
fn _decode(mut decoder: impl ImageDecoder) -> Result<DynamicImage> {
103+
let data_type = match decoder.color_type() {
104+
image::ColorType::L8 => Some(qcms::DataType::Gray8),
105+
image::ColorType::La8 => Some(qcms::DataType::GrayA8),
106+
image::ColorType::Rgb8 => Some(qcms::DataType::RGB8),
107+
image::ColorType::Rgba8 => Some(qcms::DataType::RGBA8),
108+
_ => None,
109+
};
110+
let icc = decoder.icc_profile().unwrap_or_default();
111+
112+
if let Some(dt) = data_type
113+
&& let Some(icc_profile) = icc
114+
&& let Some(input) = Profile::new_from_slice(&icc_profile, false)
115+
&& !input.is_sRGB()
116+
{
117+
let mut data = vec![0u8; decoder.total_bytes() as usize];
118+
let (w, h) = decoder.dimensions();
119+
let has_alpha = decoder.color_type().has_alpha();
120+
let mut output = Profile::new_sRGB();
121+
output.precache_output_transform();
122+
decoder.read_image(&mut data)?;
123+
124+
let xfm = Transform::new(&input, &output, dt, qcms::Intent::default())
125+
.context("Couldn't make a profile transformer")?;
126+
127+
xfm.apply(&mut data);
128+
129+
let mut image = if has_alpha {
130+
let buf =
131+
RgbaImage::from_raw(w, h, data).context("Couldn't read the transformed image data")?;
132+
DynamicImage::ImageRgba8(buf)
133+
} else {
134+
let buf =
135+
RgbImage::from_raw(w, h, data).context("Couldn't read the transformed image data")?;
136+
DynamicImage::ImageRgb8(buf)
137+
};
138+
139+
image.set_rgb_primaries(Cicp::SRGB.primaries);
140+
image.set_transfer_function(Cicp::SRGB.transfer);
141+
Ok(image)
142+
} else {
143+
Ok(DynamicImage::from_decoder(decoder)?)
144+
}
145+
}
146+
147+
async fn decode_from(path: PathBuf) -> Result<(DynamicImage, Orientation)> {
100148
let mut limits = Limits::no_limits();
101149
if YAZI.tasks.image_alloc > 0 {
102150
limits.max_alloc = Some(YAZI.tasks.image_alloc as u64);
@@ -114,9 +162,7 @@ impl Image {
114162

115163
let mut decoder = reader.with_guessed_format()?.into_decoder()?;
116164
let orientation = decoder.orientation().unwrap_or(Orientation::NoTransforms);
117-
let icc = decoder.icc_profile().unwrap_or_default();
118-
119-
Ok((DynamicImage::from_decoder(decoder)?, orientation, icc))
165+
Ok((Self::_decode(decoder)?, orientation))
120166
})
121167
.await
122168
.map_err(|e| ImageError::IoError(e.into()))?

0 commit comments

Comments
 (0)