decoder: Add basic color profile support

This only covers 8bit RGB(A) JPEG, PNG, and HEIF.
Should only be a temporary solution until GTK supports
color profiles.

Closes #36

Part-of: <https://gitlab.gnome.org/Incubator/loupe/-/merge_requests/172>
This commit is contained in:
Sophie Herold 2023-04-12 04:13:30 +02:00
parent 3b6cc8c404
commit e710419a22
6 changed files with 186 additions and 23 deletions

69
Cargo.lock generated
View file

@ -397,6 +397,9 @@ name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
dependencies = [
"jobserver",
]
[[package]]
name = "cfg-expr"
@ -755,6 +758,12 @@ dependencies = [
"dtoa",
]
[[package]]
name = "dunce"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c"
[[package]]
name = "either"
version = "1.8.1"
@ -973,6 +982,33 @@ dependencies = [
"spin",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared",
]
[[package]]
name = "foreign-types-macros"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
]
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "form_urlencoded"
version = "1.1.0"
@ -1677,6 +1713,15 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "jobserver"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
dependencies = [
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.3.0"
@ -1725,6 +1770,28 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lcms2"
version = "5.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d50a4d00d909da0ec023ab34ea09bf3b3b79b5a36b3e0da3a305112edaf150"
dependencies = [
"foreign-types",
"lcms2-sys",
]
[[package]]
name = "lcms2-sys"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7abb8aac1a07fd8cdffaa25461a3873a7bf7164cd20b849d44a183a1348d682f"
dependencies = [
"cc",
"dunce",
"libc",
"pkg-config",
]
[[package]]
name = "lebe"
version = "0.5.2"
@ -1911,12 +1978,14 @@ dependencies = [
"image",
"indexmap",
"kamadak-exif",
"lcms2",
"libadwaita",
"libgweather",
"libheif-rs",
"librsvg",
"log",
"once_cell",
"rgb",
"safe-transmute",
"url",
]

View file

@ -16,16 +16,18 @@ env_logger = "0.10.0"
futures = "0.3.25"
indexmap = "1.9.2"
kamadak-exif = "0.5.5"
lcms2 = "5.6.0"
libgweather = "4.2.0"
log = "0.4.17"
once_cell = "1.16.0"
rgb = "0.8.36"
safe-transmute = "0.11.2"
url = "2.3.1"
# decoders
image-rs = { package = "image", version = "0.24.6" }
libheif-rs = "0.19.0"
librsvg = { git = "https://gitlab.gnome.org/GNOME/librsvg.git", rev = "2.56.0" }
url = "2.3.1"
[dependencies.ashpd]
version = "0.4.0"

View file

@ -12,6 +12,7 @@ dependency('gtk4', version: '>=4.11.1')
dependency('libadwaita-1', version: '>=1.3.0')
dependency('gweather4', version: '>=4.0.0')
dependency('libheif', version: '>=1.14.2')
dependency('lcms2', version: '>=2.12.0')
cargo = find_program('cargo', required: true)

View file

@ -2,14 +2,16 @@
use super::*;
use crate::decoder::tiling::{self, FrameBufferExt};
use crate::deps::*;
use crate::util;
use crate::util::gettext::*;
use crate::util::ToBufRead;
use anyhow::Context;
use arc_swap::ArcSwap;
use gtk::prelude::*;
use libheif_rs::{ColorSpace, HeifContext, LibHeif, Plane, RgbChroma};
use libheif_rs::{ColorProfile, ColorSpace, HeifContext, LibHeif, Plane, RgbChroma};
use once_cell::sync::Lazy;
use rgb::AsPixels;
use std::sync::Arc;
@ -73,6 +75,21 @@ impl Heif {
.decode(&handle, ColorSpace::Rgb(rgb_chroma), None)
.context(gettext("Failed to decode image"))?;
let icc_profile = if let Some(profile) = handle.color_profile_raw() {
if [
libheif_rs::color_profile_types::R_ICC,
libheif_rs::color_profile_types::PROF,
]
.contains(&profile.profile_type())
{
Some(profile.data)
} else {
None
}
} else {
None
};
let plane = image
.planes_mut()
.interleaved
@ -83,6 +100,7 @@ impl Heif {
has_alpha_channel: handle.has_alpha_channel(),
pre_multiplied_alpha: handle.is_premultiplied_alpha(),
rgb_chroma,
icc_profile,
};
let tile = tiling::Tile {
@ -106,6 +124,7 @@ pub struct Decoded<'a> {
has_alpha_channel: bool,
pre_multiplied_alpha: bool,
rgb_chroma: RgbChroma,
icc_profile: Option<Vec<u8>>,
}
impl<'a> Decoded<'a> {
@ -150,7 +169,19 @@ impl<'a> Decoded<'a> {
RgbChroma::C444 => unreachable!(),
};
let bytes = glib::Bytes::from_owned(self.plane.data.to_vec());
let mut buffer = self.plane.data.to_vec();
if let Some(icc_profile) = &self.icc_profile {
if memory_format == gdk::MemoryFormat::R8g8b8 {
util::new_trafo::<rgb::RGB8>(icc_profile, lcms2::PixelFormat::RGB_8)?
.transform_in_place(buffer.as_pixels_mut());
} else if memory_format == gdk::MemoryFormat::R8g8b8a8 {
util::new_trafo::<rgb::RGBA8>(icc_profile, lcms2::PixelFormat::RGBA_8)?
.transform_in_place(buffer.as_pixels_mut());
}
}
let bytes = glib::Bytes::from_owned(buffer);
let tex = gdk::MemoryTexture::new(
self.plane.width as i32,

View file

@ -2,13 +2,15 @@
use super::*;
use crate::decoder::tiling::{self, FrameBufferExt};
use crate::deps::*;
use crate::util;
use crate::util::gettext::*;
use crate::util::{BufReadSeek, ToBufRead};
use anyhow::{bail, Context};
use arc_swap::ArcSwap;
use gtk::prelude::*;
use image_rs::AnimationDecoder;
use image_rs::{AnimationDecoder, ImageDecoder};
use rgb::AsPixels;
use std::sync::Arc;
@ -87,13 +89,38 @@ impl ImageRsOther {
tiles.set_original_dimensions(dimensions);
let mut reader = Self::reader(&file, format)?;
let (icc_profile, dynamic_image) = match format {
image_rs::ImageFormat::Jpeg => {
let reader = file.to_buf_read()?;
let mut decoder = image_rs::codecs::jpeg::JpegDecoder::new(reader)?;
let _ = decoder.set_limits(image_rs::io::Limits::no_limits());
let icc_profile = decoder.icc_profile();
let dynamic_image = image_rs::DynamicImage::from_decoder(decoder)
.context(gettext("Failed to decode image"))?;
(icc_profile, dynamic_image)
}
image_rs::ImageFormat::Png => {
let reader = file.to_buf_read()?;
let mut decoder = image_rs::codecs::png::PngDecoder::new(reader)?;
let _ = decoder.set_limits(image_rs::io::Limits::no_limits());
let icc_profile = decoder.icc_profile();
let dynamic_image = image_rs::DynamicImage::from_decoder(decoder)
.context(gettext("Failed to decode image"))?;
(icc_profile, dynamic_image)
}
_ => {
let mut reader = Self::reader(&file, format)?;
reader.limits(image_rs::io::Limits::no_limits());
let dynamic_image =
reader.decode().context(gettext("Failed to decode image"))?;
(None, dynamic_image)
}
};
reader.limits(image_rs::io::Limits::no_limits());
let dynamic_image = reader.decode().context(gettext("Failed to decode image"))?;
let decoded_image = Decoded { dynamic_image };
let decoded_image = Decoded {
dynamic_image,
icc_profile,
};
let tile = tiling::Tile {
position: (0, 0),
@ -177,7 +204,10 @@ impl ImageRsOther {
let dynamic_image = image_rs::DynamicImage::from(rgb_image);
let decoded_image = Decoded { dynamic_image };
let decoded_image = Decoded {
dynamic_image,
icc_profile: None,
};
let tile = tiling::Tile {
position,
@ -224,6 +254,7 @@ impl ImageRsOther {
pub struct Decoded {
dynamic_image: image_rs::DynamicImage,
icc_profile: Option<Vec<u8>>,
}
impl Decoded {
@ -247,18 +278,30 @@ impl Decoded {
buffer.into_raw(),
)
}
image_rs::DynamicImage::ImageRgb8(buffer) => (
1,
gdk::MemoryFormat::R8g8b8,
buffer.sample_layout(),
buffer.into_raw(),
),
image_rs::DynamicImage::ImageRgba8(buffer) => (
1,
gdk::MemoryFormat::R8g8b8a8,
buffer.sample_layout(),
buffer.into_raw(),
),
image_rs::DynamicImage::ImageRgb8(mut buffer) => {
if let Some(icc_profile) = &self.icc_profile {
util::new_trafo::<rgb::RGB8>(icc_profile, lcms2::PixelFormat::RGB_8)?
.transform_in_place(buffer.as_pixels_mut());
}
(
1,
gdk::MemoryFormat::R8g8b8,
buffer.sample_layout(),
buffer.into_raw(),
)
}
image_rs::DynamicImage::ImageRgba8(mut buffer) => {
if let Some(icc_profile) = &self.icc_profile {
util::new_trafo::<rgb::RGBA8>(icc_profile, lcms2::PixelFormat::RGBA_8)?
.transform_in_place(buffer.as_pixels_mut());
}
(
1,
gdk::MemoryFormat::R8g8b8a8,
buffer.sample_layout(),
buffer.into_raw(),
)
}
img @ image_rs::DynamicImage::ImageLuma16(_) => {
let buffer = img.to_rgb16();
(

View file

@ -228,3 +228,20 @@ pub fn spawn<R: 'static, F: std::future::Future<Output = R> + 'static>(
let ctx = glib::MainContext::default();
ctx.spawn_local(f)
}
pub fn new_trafo<F: Copy>(
icc_profile: &[u8],
src_format: lcms2::PixelFormat,
) -> anyhow::Result<lcms2::Transform<F, F>> {
let src_profile = lcms2::Profile::new_icc(icc_profile)?;
let target_profile = lcms2::Profile::new_srgb();
Ok(lcms2::Transform::new(
&src_profile,
src_format,
&target_profile,
src_format,
lcms2::Intent::Perceptual,
)?)
}