From e710419a2253655b5afe6e1219dc43de1d9a6461 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Wed, 12 Apr 2023 04:13:30 +0200 Subject: [PATCH] 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: --- Cargo.lock | 69 ++++++++++++++++++++++ Cargo.toml | 4 +- meson.build | 1 + src/decoder/formats/heif.rs | 35 ++++++++++- src/decoder/formats/image_rs_other.rs | 83 ++++++++++++++++++++------- src/util/mod.rs | 17 ++++++ 6 files changed, 186 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15a4fc6..afb6666 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 6408dc7..8292072 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/meson.build b/meson.build index 53e9889..921cbfd 100644 --- a/meson.build +++ b/meson.build @@ -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) diff --git a/src/decoder/formats/heif.rs b/src/decoder/formats/heif.rs index ac41425..45aa4de 100644 --- a/src/decoder/formats/heif.rs +++ b/src/decoder/formats/heif.rs @@ -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>, } 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::(icc_profile, lcms2::PixelFormat::RGB_8)? + .transform_in_place(buffer.as_pixels_mut()); + } else if memory_format == gdk::MemoryFormat::R8g8b8a8 { + util::new_trafo::(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, diff --git a/src/decoder/formats/image_rs_other.rs b/src/decoder/formats/image_rs_other.rs index 987d047..46c8005 100644 --- a/src/decoder/formats/image_rs_other.rs +++ b/src/decoder/formats/image_rs_other.rs @@ -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>, } 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::(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::(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(); ( diff --git a/src/util/mod.rs b/src/util/mod.rs index bfe4777..ebce45d 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -228,3 +228,20 @@ pub fn spawn + 'static>( let ctx = glib::MainContext::default(); ctx.spawn_local(f) } + +pub fn new_trafo( + icc_profile: &[u8], + src_format: lcms2::PixelFormat, +) -> anyhow::Result> { + 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, + )?) +}