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:
parent
3b6cc8c404
commit
e710419a22
6 changed files with 186 additions and 23 deletions
69
Cargo.lock
generated
69
Cargo.lock
generated
|
|
@ -397,6 +397,9 @@ name = "cc"
|
||||||
version = "1.0.79"
|
version = "1.0.79"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
||||||
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-expr"
|
name = "cfg-expr"
|
||||||
|
|
@ -755,6 +758,12 @@ dependencies = [
|
||||||
"dtoa",
|
"dtoa",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dunce"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
|
|
@ -973,6 +982,33 @@ dependencies = [
|
||||||
"spin",
|
"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]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
@ -1677,6 +1713,15 @@ version = "1.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
|
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jpeg-decoder"
|
name = "jpeg-decoder"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -1725,6 +1770,28 @@ version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
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]]
|
[[package]]
|
||||||
name = "lebe"
|
name = "lebe"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
|
@ -1911,12 +1978,14 @@ dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"kamadak-exif",
|
"kamadak-exif",
|
||||||
|
"lcms2",
|
||||||
"libadwaita",
|
"libadwaita",
|
||||||
"libgweather",
|
"libgweather",
|
||||||
"libheif-rs",
|
"libheif-rs",
|
||||||
"librsvg",
|
"librsvg",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"rgb",
|
||||||
"safe-transmute",
|
"safe-transmute",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,18 @@ env_logger = "0.10.0"
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
indexmap = "1.9.2"
|
indexmap = "1.9.2"
|
||||||
kamadak-exif = "0.5.5"
|
kamadak-exif = "0.5.5"
|
||||||
|
lcms2 = "5.6.0"
|
||||||
libgweather = "4.2.0"
|
libgweather = "4.2.0"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
once_cell = "1.16.0"
|
once_cell = "1.16.0"
|
||||||
|
rgb = "0.8.36"
|
||||||
safe-transmute = "0.11.2"
|
safe-transmute = "0.11.2"
|
||||||
|
url = "2.3.1"
|
||||||
|
|
||||||
# decoders
|
# decoders
|
||||||
image-rs = { package = "image", version = "0.24.6" }
|
image-rs = { package = "image", version = "0.24.6" }
|
||||||
libheif-rs = "0.19.0"
|
libheif-rs = "0.19.0"
|
||||||
librsvg = { git = "https://gitlab.gnome.org/GNOME/librsvg.git", rev = "2.56.0" }
|
librsvg = { git = "https://gitlab.gnome.org/GNOME/librsvg.git", rev = "2.56.0" }
|
||||||
url = "2.3.1"
|
|
||||||
|
|
||||||
[dependencies.ashpd]
|
[dependencies.ashpd]
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ dependency('gtk4', version: '>=4.11.1')
|
||||||
dependency('libadwaita-1', version: '>=1.3.0')
|
dependency('libadwaita-1', version: '>=1.3.0')
|
||||||
dependency('gweather4', version: '>=4.0.0')
|
dependency('gweather4', version: '>=4.0.0')
|
||||||
dependency('libheif', version: '>=1.14.2')
|
dependency('libheif', version: '>=1.14.2')
|
||||||
|
dependency('lcms2', version: '>=2.12.0')
|
||||||
|
|
||||||
cargo = find_program('cargo', required: true)
|
cargo = find_program('cargo', required: true)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,16 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::decoder::tiling::{self, FrameBufferExt};
|
use crate::decoder::tiling::{self, FrameBufferExt};
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
|
use crate::util;
|
||||||
use crate::util::gettext::*;
|
use crate::util::gettext::*;
|
||||||
use crate::util::ToBufRead;
|
use crate::util::ToBufRead;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use gtk::prelude::*;
|
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 once_cell::sync::Lazy;
|
||||||
|
use rgb::AsPixels;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -73,6 +75,21 @@ impl Heif {
|
||||||
.decode(&handle, ColorSpace::Rgb(rgb_chroma), None)
|
.decode(&handle, ColorSpace::Rgb(rgb_chroma), None)
|
||||||
.context(gettext("Failed to decode image"))?;
|
.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
|
let plane = image
|
||||||
.planes_mut()
|
.planes_mut()
|
||||||
.interleaved
|
.interleaved
|
||||||
|
|
@ -83,6 +100,7 @@ impl Heif {
|
||||||
has_alpha_channel: handle.has_alpha_channel(),
|
has_alpha_channel: handle.has_alpha_channel(),
|
||||||
pre_multiplied_alpha: handle.is_premultiplied_alpha(),
|
pre_multiplied_alpha: handle.is_premultiplied_alpha(),
|
||||||
rgb_chroma,
|
rgb_chroma,
|
||||||
|
icc_profile,
|
||||||
};
|
};
|
||||||
|
|
||||||
let tile = tiling::Tile {
|
let tile = tiling::Tile {
|
||||||
|
|
@ -106,6 +124,7 @@ pub struct Decoded<'a> {
|
||||||
has_alpha_channel: bool,
|
has_alpha_channel: bool,
|
||||||
pre_multiplied_alpha: bool,
|
pre_multiplied_alpha: bool,
|
||||||
rgb_chroma: RgbChroma,
|
rgb_chroma: RgbChroma,
|
||||||
|
icc_profile: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Decoded<'a> {
|
impl<'a> Decoded<'a> {
|
||||||
|
|
@ -150,7 +169,19 @@ impl<'a> Decoded<'a> {
|
||||||
RgbChroma::C444 => unreachable!(),
|
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(
|
let tex = gdk::MemoryTexture::new(
|
||||||
self.plane.width as i32,
|
self.plane.width as i32,
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,15 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::decoder::tiling::{self, FrameBufferExt};
|
use crate::decoder::tiling::{self, FrameBufferExt};
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
|
use crate::util;
|
||||||
use crate::util::gettext::*;
|
use crate::util::gettext::*;
|
||||||
use crate::util::{BufReadSeek, ToBufRead};
|
use crate::util::{BufReadSeek, ToBufRead};
|
||||||
|
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use image_rs::AnimationDecoder;
|
use image_rs::{AnimationDecoder, ImageDecoder};
|
||||||
|
use rgb::AsPixels;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -87,13 +89,38 @@ impl ImageRsOther {
|
||||||
|
|
||||||
tiles.set_original_dimensions(dimensions);
|
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 decoded_image = Decoded {
|
||||||
|
dynamic_image,
|
||||||
let dynamic_image = reader.decode().context(gettext("Failed to decode image"))?;
|
icc_profile,
|
||||||
|
};
|
||||||
let decoded_image = Decoded { dynamic_image };
|
|
||||||
|
|
||||||
let tile = tiling::Tile {
|
let tile = tiling::Tile {
|
||||||
position: (0, 0),
|
position: (0, 0),
|
||||||
|
|
@ -177,7 +204,10 @@ impl ImageRsOther {
|
||||||
|
|
||||||
let dynamic_image = image_rs::DynamicImage::from(rgb_image);
|
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 {
|
let tile = tiling::Tile {
|
||||||
position,
|
position,
|
||||||
|
|
@ -224,6 +254,7 @@ impl ImageRsOther {
|
||||||
|
|
||||||
pub struct Decoded {
|
pub struct Decoded {
|
||||||
dynamic_image: image_rs::DynamicImage,
|
dynamic_image: image_rs::DynamicImage,
|
||||||
|
icc_profile: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Decoded {
|
impl Decoded {
|
||||||
|
|
@ -247,18 +278,30 @@ impl Decoded {
|
||||||
buffer.into_raw(),
|
buffer.into_raw(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
image_rs::DynamicImage::ImageRgb8(buffer) => (
|
image_rs::DynamicImage::ImageRgb8(mut buffer) => {
|
||||||
1,
|
if let Some(icc_profile) = &self.icc_profile {
|
||||||
gdk::MemoryFormat::R8g8b8,
|
util::new_trafo::<rgb::RGB8>(icc_profile, lcms2::PixelFormat::RGB_8)?
|
||||||
buffer.sample_layout(),
|
.transform_in_place(buffer.as_pixels_mut());
|
||||||
buffer.into_raw(),
|
}
|
||||||
),
|
(
|
||||||
image_rs::DynamicImage::ImageRgba8(buffer) => (
|
1,
|
||||||
1,
|
gdk::MemoryFormat::R8g8b8,
|
||||||
gdk::MemoryFormat::R8g8b8a8,
|
buffer.sample_layout(),
|
||||||
buffer.sample_layout(),
|
buffer.into_raw(),
|
||||||
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(_) => {
|
img @ image_rs::DynamicImage::ImageLuma16(_) => {
|
||||||
let buffer = img.to_rgb16();
|
let buffer = img.to_rgb16();
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -228,3 +228,20 @@ pub fn spawn<R: 'static, F: std::future::Future<Output = R> + 'static>(
|
||||||
let ctx = glib::MainContext::default();
|
let ctx = glib::MainContext::default();
|
||||||
ctx.spawn_local(f)
|
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,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue