decoder: Use glycin for image loading expect SVG

This gives us sandboxed image decoding and others the
option to extend Loupe with other image formats.
This commit is contained in:
Sophie Herold 2023-05-23 01:49:40 +02:00
parent d690c0e2d8
commit 1b2dde79b6
18 changed files with 536 additions and 1416 deletions

View file

@ -0,0 +1,155 @@
// Copyright (c) 2023 Sophie Herold
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
//! Decode using glycin
use super::*;
use crate::decoder::tiling::{self, FrameBufferExt};
use crate::deps::*;
use crate::image_metadata::ImageMetadata;
use arc_swap::ArcSwap;
use gtk::prelude::*;
use std::sync::Arc;
pub const FRAME_BUFFER: usize = 3;
#[derive(Debug)]
pub struct Glycin {
cancellable: gio::Cancellable,
next_frame_send: async_channel::Sender<()>,
}
impl Drop for Glycin {
fn drop(&mut self) {
self.cancellable.cancel();
}
}
impl Glycin {
pub fn new(
file: gio::File,
updater: UpdateSender,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) -> Self {
let cancellable = gio::Cancellable::new();
let cancellable_ = cancellable.clone();
let (next_frame_send, next_frame_recv) = async_channel::bounded(2);
glib::MainContext::default().spawn(async move {
let update_sender = updater.clone();
let result: Result<(), glycin::Error> = async move {
let mut image_request = glycin::ImageRequest::new(file);
#[cfg(feature = "disable-glycin-sandbox")]
image_request.sandbox_mechanism(Some(glycin::SandboxMechanism::NotSandboxed));
image_request.cancellable(cancellable_);
let image = image_request.request().await?;
if let Some(exif_raw) = image.info().exif.clone().into() {
updater.send(DecoderUpdate::Metadata(ImageMetadata::from_exif_bytes(
exif_raw,
)));
}
let dimensions = (image.info().width, image.info().height);
tiles.set_original_dimensions(dimensions);
updater.send(DecoderUpdate::Format(ImageFormat::new(
image.mime_type(),
image.format_name(),
)));
let frame = image.next_frame().await?;
if let Some(delay) = frame.delay {
updater.send(DecoderUpdate::Animated);
let position = (0, 0);
let tile = tiling::Tile {
position,
zoom_level: tiling::zoom_to_level(1.),
bleed: 0,
texture: frame.texture,
};
tiles.push_frame(tile, dimensions, delay);
updater.send(DecoderUpdate::Redraw);
loop {
if next_frame_recv.recv().await.is_ok() {
loop {
let frame = image.next_frame().await?;
let position = (0, 0);
let tile = tiling::Tile {
position,
zoom_level: tiling::zoom_to_level(1.),
bleed: 0,
texture: frame.texture,
};
tiles.push_frame(tile, dimensions, delay);
if tiles.n_frames() >= FRAME_BUFFER {
break;
}
}
} else {
log::debug!("Animation handler gone");
return Ok(());
}
}
} else {
let tile = tiling::Tile {
position: (0, 0),
zoom_level: tiling::zoom_to_level(1.),
bleed: 0,
texture: frame.texture,
};
tiles.push(tile);
}
Ok(())
}
.await;
if let Err(err) = result {
if err.unsupported_format().is_some() {
update_sender.send(DecoderUpdate::UnsupportedFormat);
}
update_sender.send(DecoderUpdate::Error(err.into()));
}
});
Self {
cancellable,
next_frame_send,
}
}
pub fn fill_frame_buffer(&self) {
let _result = self.next_frame_send.try_send(());
}
}

View file

@ -1,278 +0,0 @@
// Copyright (c) 2023 Sophie Herold
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
///! Decode using libheif-rs
use super::*;
use crate::decoder::tiling::{self, FrameBufferExt};
use crate::deps::*;
use crate::image_metadata::ImageMetadata;
use crate::util;
use crate::util::gettext::*;
use crate::util::ToBufRead;
use anyhow::{anyhow, bail, Context};
use arc_swap::ArcSwap;
use gtk::prelude::*;
use libheif_rs::{ColorProfile, ColorSpace, HeifContext, LibHeif, Plane, RgbChroma};
use once_cell::sync::Lazy;
use rgb::AsPixels;
use std::sync::Arc;
#[derive(Debug)]
pub struct Heif {
cancellable: gio::Cancellable,
}
impl Drop for Heif {
fn drop(&mut self) {
self.cancellable.cancel();
}
}
/// Loads plugins on first use
static LIBHEIF: Lazy<LibHeif> = Lazy::new(LibHeif::new);
impl Heif {
pub fn new(
file: gio::File,
updater: UpdateSender,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) -> Self {
log::debug!("Loading HEIF");
let cancellable = gio::Cancellable::new();
let cancellable_ = cancellable.clone();
updater.clone().spawn_error_handled(move || {
let file_size = file
.query_info(
gio::FILE_ATTRIBUTE_STANDARD_SIZE,
gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE,
)
.context(gettext("Failed to read image file information"))?
.size();
let buf_reader = file.to_buf_read(&cancellable)?;
let stream_reader = libheif_rs::StreamReader::new(buf_reader, file_size as u64);
let ctx = HeifContext::read_from_reader(Box::new(stream_reader))
.context(gettext("Failed to decode image"))?;
let handle = ctx.primary_image_handle()?;
let mut metadata = Self::exif(&handle, &file);
// TODO: Later use libheif 1.16 to get info if there is a transformation
metadata.heif_transform = true;
updater.send(DecoderUpdate::Metadata(metadata));
tiles.set_original_dimensions((handle.width(), handle.height()));
let rgb_chroma = if handle.luma_bits_per_pixel() > 8 {
if handle.has_alpha_channel() {
#[cfg(target_endian = "little")]
{
RgbChroma::HdrRgbaLe
}
#[cfg(target_endian = "big")]
{
RgbChroma::HdrRgbaBe
}
} else {
#[cfg(target_endian = "little")]
{
RgbChroma::HdrRgbLe
}
#[cfg(target_endian = "big")]
{
RgbChroma::HdrRgbBe
}
}
} else if handle.has_alpha_channel() {
RgbChroma::Rgba
} else {
RgbChroma::Rgb
};
let image_result = LIBHEIF.decode(&handle, ColorSpace::Rgb(rgb_chroma), None);
let mut image = match image_result {
Err(err)
if matches!(err.sub_code, libheif_rs::HeifErrorSubCode::UnsupportedCodec) =>
{
updater.send(DecoderUpdate::UnsupportedFormat);
bail!(anyhow!(err).context(gettext("Unknown image format")));
}
image => 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
.planes_mut()
.interleaved
.context("Interleaved plane does not exist")?;
let decoded = Decoded {
plane,
has_alpha_channel: handle.has_alpha_channel(),
pre_multiplied_alpha: handle.is_premultiplied_alpha(),
rgb_chroma,
icc_profile,
};
let tile = tiling::Tile {
position: (0, 0),
zoom_level: tiling::zoom_to_level(1.),
bleed: 0,
texture: decoded.into_texture()?,
};
tiles.push(tile);
Ok(())
});
Heif {
cancellable: cancellable_,
}
}
fn exif(handle: &libheif_rs::ImageHandle, file: &gio::File) -> ImageMetadata {
let mut meta_ids = vec![0];
handle.metadata_block_ids(&mut meta_ids, b"Exif");
if let Some(meta_id) = meta_ids.first() {
match handle.metadata(*meta_id) {
Ok(mut exif_bytes) => {
if let Some(skip) = exif_bytes
.get(0..4)
.map(|x| u32::from_be_bytes(x.try_into().unwrap()) as usize)
{
if exif_bytes.len() > skip + 4 {
exif_bytes.drain(0..skip + 4);
return ImageMetadata::from_exif_bytes(exif_bytes);
} else {
log::warn!("EXIF data has far too few bytes in {}", file.uri());
}
} else {
log::warn!("EXIF data has far too few bytes in {}", file.uri());
}
}
Err(err) => {
log::warn!("Unable to decode EXIF data for {}: {err}", file.uri());
}
}
}
ImageMetadata::default()
}
}
pub struct Decoded<'a> {
plane: Plane<&'a mut [u8]>,
has_alpha_channel: bool,
pre_multiplied_alpha: bool,
rgb_chroma: RgbChroma,
icc_profile: Option<Vec<u8>>,
}
impl<'a> Decoded<'a> {
pub fn into_texture(self) -> anyhow::Result<gdk::Texture> {
let memory_format = match self.rgb_chroma {
RgbChroma::HdrRgbBe
| RgbChroma::HdrRgbaBe
| RgbChroma::HdrRgbLe
| RgbChroma::HdrRgbaLe => {
if let Ok(transmuted) =
safe_transmute::transmute_many_pedantic_mut::<u16>(self.plane.data)
{
// Scale HDR pixels to 16bit (they are usually 10bit or 12bit)
for pixel in transmuted.iter_mut() {
*pixel <<= 16 - self.plane.bits_per_pixel;
}
} else {
log::error!("Could not transform HDR (16bit) data to u16");
}
if self.has_alpha_channel {
if self.pre_multiplied_alpha {
gdk::MemoryFormat::R16g16b16a16Premultiplied
} else {
gdk::MemoryFormat::R16g16b16a16
}
} else {
gdk::MemoryFormat::R16g16b16
}
}
RgbChroma::Rgb | RgbChroma::Rgba => {
if self.has_alpha_channel {
if self.pre_multiplied_alpha {
gdk::MemoryFormat::R8g8b8a8Premultiplied
} else {
gdk::MemoryFormat::R8g8b8a8
}
} else {
gdk::MemoryFormat::R8g8b8
}
}
RgbChroma::C444 => unreachable!(),
};
let mut buffer = self.plane.data.to_vec();
if let Some(icc_profile) = &self.icc_profile {
if memory_format == gdk::MemoryFormat::R8g8b8 {
util::appply_icc_profile::<rgb::RGB8>(
icc_profile,
lcms2::PixelFormat::RGB_8,
buffer.as_pixels_mut(),
);
} else if memory_format == gdk::MemoryFormat::R8g8b8a8 {
util::appply_icc_profile::<rgb::RGBA8>(
icc_profile,
lcms2::PixelFormat::RGBA_8,
buffer.as_pixels_mut(),
);
}
}
let bytes = glib::Bytes::from_owned(buffer);
let tex = gdk::MemoryTexture::new(
self.plane.width as i32,
self.plane.height as i32,
memory_format,
&bytes,
self.plane.stride,
);
Ok(tex.upcast())
}
}

View file

@ -1,393 +0,0 @@
// Copyright (c) 2023 Sophie Herold
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
///! Decode using image-rs
use super::*;
use crate::decoder::tiling::{self, FrameBufferExt};
use crate::deps::*;
use crate::image_metadata::ImageMetadata;
use crate::util;
use crate::util::gettext::*;
use crate::util::{BufReadSeek, ToBufRead};
use anyhow::Context;
use arc_swap::ArcSwap;
use gtk::prelude::*;
use image_rs::{AnimationDecoder, ImageDecoder};
use rgb::AsPixels;
use std::sync::Arc;
#[derive(Debug)]
pub struct ImageRsOther {
thread_handle: Option<std::thread::JoinHandle<()>>,
cancellable: gio::Cancellable,
}
impl Drop for ImageRsOther {
fn drop(&mut self) {
if let Some(handle) = &self.thread_handle {
self.cancellable.cancel();
handle.thread().unpark();
}
}
}
impl ImageRsOther {
fn reader(
file: &gio::File,
format: image_rs::ImageFormat,
cancellable: &gio::Cancellable,
) -> anyhow::Result<image_rs::io::Reader<Box<dyn BufReadSeek>>> {
let buf_read = file.to_buf_read(cancellable)?;
let mut reader = image_rs::io::Reader::new(buf_read);
reader.set_format(format);
Ok(reader)
}
pub fn new(
file: gio::File,
format: image_rs::ImageFormat,
updater: UpdateSender,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) -> Self {
let cancellable = gio::Cancellable::new();
let thread_handle = match format {
image_rs::ImageFormat::Gif
| image_rs::ImageFormat::WebP
| image_rs::ImageFormat::Png => Some(Self::new_animated(
file,
format,
updater,
tiles,
cancellable.clone(),
)),
_ => {
Self::new_static(file, format, updater, tiles, cancellable.clone());
None
}
};
ImageRsOther {
thread_handle,
cancellable,
}
}
fn new_static(
file: gio::File,
format: image_rs::ImageFormat,
updater: UpdateSender,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
cancellable: gio::Cancellable,
) {
updater.clone().spawn_error_handled(move || {
updater.send(DecoderUpdate::Metadata(ImageMetadata::load(&file)));
let reader = Self::reader(&file, format, &cancellable)?;
let dimensions = reader
.into_dimensions()
.context("Failed to read image dimensions")?;
tiles.set_original_dimensions(dimensions);
let (icc_profile, dynamic_image) = match format {
image_rs::ImageFormat::Jpeg => {
let reader = file.to_buf_read(&cancellable)?;
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(&cancellable)?;
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, &cancellable)?;
reader.limits(image_rs::io::Limits::no_limits());
let dynamic_image =
reader.decode().context(gettext("Failed to decode image"))?;
(None, dynamic_image)
}
};
let decoded_image = Decoded {
dynamic_image,
icc_profile,
};
let tile = tiling::Tile {
position: (0, 0),
zoom_level: tiling::zoom_to_level(1.),
bleed: 0,
texture: decoded_image.into_texture()?,
};
tiles.push(tile);
Ok(())
});
}
fn new_animated(
file: gio::File,
format: image_rs::ImageFormat,
updater: UpdateSender,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
cancellable: gio::Cancellable,
) -> std::thread::JoinHandle<()> {
updater.clone().spawn_error_handled(move || {
updater.send(DecoderUpdate::Metadata(ImageMetadata::load(&file)));
let mut nth_frame = 1;
// We are currently decoding for each repetition of the animation
// TODO: Check if/how that can be solved differently
loop {
let buf_reader = file.to_buf_read(&cancellable)?;
let (frames, animated_format) = match format {
image_rs::ImageFormat::Gif => (
image_rs::codecs::gif::GifDecoder::new(buf_reader)?.into_frames(),
ImageFormat::AnimatedGif,
),
image_rs::ImageFormat::WebP => {
let decoder = image_rs::codecs::webp::WebPDecoder::new(buf_reader)?;
if decoder.has_animation() {
(decoder.into_frames(), ImageFormat::AnimatedWebP)
} else {
// Static WebP images need a different decoder
Self::new_static(file, format, updater, tiles, cancellable);
return Ok(());
}
}
image_rs::ImageFormat::Png => {
let decoder = image_rs::codecs::png::PngDecoder::new(buf_reader)?;
if decoder.is_apng() {
(decoder.apng().into_frames(), ImageFormat::AnimatedPng)
} else {
// Static PNG images need a different decoder
Self::new_static(file, format, updater, tiles, cancellable);
return Ok(());
}
}
_ => todo!(),
};
for frame in frames {
match frame {
Err(err) => {
log::warn!("Skipping frame: {err}");
}
Ok(frame) => {
if nth_frame == 2 {
// We have at least two frames so this is animated
updater.send(DecoderUpdate::Format(animated_format));
nth_frame = 3;
}
let (delay_num, delay_den) = frame.delay().numer_denom_ms();
let delay = std::time::Duration::from_micros(f64::round(
delay_num as f64 * 1000. / delay_den as f64,
)
as u64);
let position = (frame.left(), frame.top());
let rgb_image = frame.into_buffer();
let dimensions = (
rgb_image.width() + position.0,
rgb_image.height() + position.1,
);
let dynamic_image = image_rs::DynamicImage::from(rgb_image);
let decoded_image = Decoded {
dynamic_image,
icc_profile: None,
};
let tile = tiling::Tile {
position,
zoom_level: tiling::zoom_to_level(1.),
bleed: 0,
texture: decoded_image.into_texture()?,
};
tiles.push_frame(tile, dimensions, delay);
if nth_frame == 1 {
updater.send(DecoderUpdate::Dimensions(Default::default()));
updater.send(DecoderUpdate::Redraw);
nth_frame = 2;
}
}
}
if tiles.n_frames() >= 3 {
std::thread::park();
}
if cancellable.is_cancelled() {
log::debug!("Terminating decoder thread.");
return Ok(());
}
}
}
})
}
pub fn fill_frame_buffer(&self) {
if let Some(handle) = &self.thread_handle {
handle.thread().unpark();
} else {
log::error!("Trying to wake up one-shot decoder");
}
}
}
pub struct Decoded {
dynamic_image: image_rs::DynamicImage,
icc_profile: Option<Vec<u8>>,
}
impl Decoded {
pub fn into_texture(self) -> anyhow::Result<gdk::Texture> {
let (n_bytes, memory_format, layout, data) = match self.dynamic_image {
img @ image_rs::DynamicImage::ImageLuma8(_) => {
let buffer = img.to_rgb8();
(
1,
gdk::MemoryFormat::R8g8b8,
buffer.sample_layout(),
buffer.into_raw(),
)
}
img @ image_rs::DynamicImage::ImageLumaA8(_) => {
let buffer = img.to_rgba8();
(
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::appply_icc_profile::<rgb::RGB8>(
icc_profile,
lcms2::PixelFormat::RGB_8,
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::appply_icc_profile::<rgb::RGBA8>(
icc_profile,
lcms2::PixelFormat::RGBA_8,
buffer.as_pixels_mut(),
);
}
(
1,
gdk::MemoryFormat::R8g8b8a8,
buffer.sample_layout(),
buffer.into_raw(),
)
}
img @ image_rs::DynamicImage::ImageLuma16(_) => {
let buffer = img.to_rgb16();
(
2,
gdk::MemoryFormat::R16g16b16,
buffer.sample_layout(),
image_rs::DynamicImage::from(buffer).into_bytes(),
)
}
img @ image_rs::DynamicImage::ImageLumaA16(_) => {
let buffer = img.to_rgba16();
(
2,
gdk::MemoryFormat::R16g16b16a16,
buffer.sample_layout(),
image_rs::DynamicImage::from(buffer).into_bytes(),
)
}
image_rs::DynamicImage::ImageRgb16(buffer) => (
2,
gdk::MemoryFormat::R16g16b16,
buffer.sample_layout(),
image_rs::DynamicImage::from(buffer).into_bytes(),
),
image_rs::DynamicImage::ImageRgba16(buffer) => (
2,
gdk::MemoryFormat::R16g16b16a16,
buffer.sample_layout(),
image_rs::DynamicImage::from(buffer).into_bytes(),
),
image_rs::DynamicImage::ImageRgb32F(buffer) => (
4,
gdk::MemoryFormat::R32g32b32Float,
buffer.sample_layout(),
image_rs::DynamicImage::from(buffer).into_bytes(),
),
image_rs::DynamicImage::ImageRgba32F(buffer) => (
4,
gdk::MemoryFormat::R32g32b32a32Float,
buffer.sample_layout(),
image_rs::DynamicImage::from(buffer).into_bytes(),
),
img => {
let buffer = img.to_rgba16();
(
2,
gdk::MemoryFormat::R16g16b16a16,
buffer.sample_layout(),
safe_transmute::transmute_vec(buffer.into_raw())
.context("Failed to transform into 16 bit texture")?,
)
}
};
let bytes = glib::Bytes::from_owned(data);
let texture = gdk::MemoryTexture::new(
layout.width as i32,
layout.height as i32,
memory_format,
&bytes,
layout.height_stride * n_bytes,
)
.upcast();
Ok(texture)
}
}

View file

@ -15,66 +15,40 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
mod heif;
mod image_rs_other;
mod glycin_proxy;
mod svg;
pub use heif::Heif;
pub use image_rs_other::ImageRsOther;
pub use glycin_proxy::Glycin;
pub use svg::{Svg, RSVG_MAX_SIZE};
use super::{DecoderUpdate, UpdateSender};
use crate::util::gettext::*;
#[derive(Clone, Copy, Debug)]
pub enum ImageFormat {
ImageRs(image_rs::ImageFormat),
AnimatedGif,
AnimatedWebP,
AnimatedPng,
// TODO: Add details about contained format
Heif,
Svg,
#[derive(Clone, Debug)]
pub struct ImageFormat {
mime_type: glycin::MimeType,
name: String,
}
impl ImageFormat {
pub fn is_animated(&self) -> bool {
matches!(
self,
ImageFormat::AnimatedGif | ImageFormat::AnimatedWebP | ImageFormat::AnimatedPng
)
pub fn new(mime_type: glycin::MimeType, name: String) -> Self {
Self { mime_type, name }
}
pub fn is_svg(&self) -> bool {
matches!(self, Self::Svg)
matches!(
self.mime_type.as_str(),
"image/svg+xml" | "image/svg+xml-compressed"
)
}
pub fn is_potentially_transparent(&self) -> bool {
!matches!(
self,
Self::ImageRs(image_rs::ImageFormat::Bmp) | Self::ImageRs(image_rs::ImageFormat::Jpeg)
)
!matches!(self.mime_type.as_str(), "image/bmp" | "image/jpeg")
}
}
impl std::fmt::Display for ImageFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
Self::ImageRs(format) => {
let debug = format!("{:?}", format);
if debug.len() <= 4 {
// uppercase abbreviations
write!(f, "{}", debug.to_uppercase())
} else {
write!(f, "{}", debug)
}
}
Self::AnimatedGif => write!(f, "{}", gettext("Animated GIF")),
Self::AnimatedWebP => write!(f, "{}", gettext("Animated WebP")),
Self::AnimatedPng => write!(f, "{}", gettext("Animated PNG")),
Self::Heif => write!(f, "HEIF Container"),
Self::Svg => write!(f, "SVG"),
}
write!(f, "{}", self.name)
}
}

View file

@ -15,7 +15,7 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
///! Decode using librsvg
//! Decode using librsvg
use super::*;
use crate::decoder::tiling::{self, FrameBufferExt};
use crate::decoder::TileRequest;

View file

@ -15,7 +15,7 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
///! Decodes several image formats
//! Decodes several image formats
pub mod formats;
pub mod tiling;
@ -23,17 +23,14 @@ pub use formats::ImageDimensionDetails;
use crate::deps::*;
use crate::image_metadata::ImageMetadata;
use crate::util::gettext::*;
use crate::util::ToBufRead;
use formats::*;
use tiling::FrameBufferExt;
use anyhow::{anyhow, bail, Context, Result};
use anyhow::Result;
use arc_swap::ArcSwap;
use futures::channel::mpsc;
use gio::prelude::*;
use std::io::Read;
use std::sync::Arc;
pub use formats::{ImageFormat, RSVG_MAX_SIZE};
@ -67,6 +64,8 @@ pub enum DecoderUpdate {
UnsupportedFormat,
/// New image data available, redraw
Redraw,
/// Start animation
Animated,
/// And error occured during decoding
Error(anyhow::Error),
}
@ -102,9 +101,8 @@ pub struct Decoder {
#[derive(Debug)]
enum FormatDecoder {
ImageRsOther(ImageRsOther),
Glycin(Glycin),
Svg(Svg),
Heif(Heif),
}
impl Decoder {
@ -115,31 +113,16 @@ impl Decoder {
pub async fn new(
file: gio::File,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) -> (Result<Self, ()>, mpsc::UnboundedReceiver<DecoderUpdate>) {
) -> (Self, mpsc::UnboundedReceiver<DecoderUpdate>) {
let (sender, receiver) = mpsc::unbounded();
let update_sender = UpdateSender { sender };
tiles.set_update_sender(update_sender.clone());
let format_decoder = gio::spawn_blocking(
glib::clone!(@strong update_sender => move || Self::format_decoder(
update_sender,
file,
tiles
)),
)
.await
.map_err(|_| anyhow!("Constructing the FormatDecoder failed unexpectedly"));
let decoder = match format_decoder {
Ok(Ok(decoder)) => Ok(Self {
decoder,
update_sender,
}),
Err(err) | Ok(Err(err)) => {
update_sender.send(DecoderUpdate::Error(err));
Err(())
}
let format_decoder = Self::format_decoder(update_sender.clone(), file, tiles);
let decoder = Self {
decoder: format_decoder,
update_sender,
};
(decoder, receiver)
@ -149,33 +132,12 @@ impl Decoder {
update_sender: UpdateSender,
file: gio::File,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) -> anyhow::Result<FormatDecoder> {
let format = Self::guess_format(&file)?;
if let Some(format) = format {
match format {
image_rs::ImageFormat::Avif => {
update_sender.send(DecoderUpdate::Format(ImageFormat::Heif));
return Ok(FormatDecoder::Heif(Heif::new(file, update_sender, tiles)));
}
format => {
update_sender.send(DecoderUpdate::Format(ImageFormat::ImageRs(format)));
return Ok(FormatDecoder::ImageRsOther(ImageRsOther::new(
file,
format,
update_sender,
tiles,
)));
}
}
} else {
let file_info = file
.query_info(
gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE,
)
.context("Could not read content type information")?;
) -> FormatDecoder {
if let Ok(file_info) = file.query_info(
gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE,
) {
if let Some(mime_type) = file_info
.content_type()
.as_ref()
@ -185,62 +147,32 @@ impl Decoder {
// - image/svg+xml
// - image/svg+xml-compressed
if mime_type.split('+').next() == Some("image/svg") {
update_sender.send(DecoderUpdate::Format(ImageFormat::Svg));
return Ok(FormatDecoder::Svg(Svg::new(file, update_sender, tiles)));
} else if ["image/avif", "image/heif", "image/heic"].contains(&mime_type.as_str()) {
update_sender.send(DecoderUpdate::Format(ImageFormat::Heif));
return Ok(FormatDecoder::Heif(Heif::new(file, update_sender, tiles)));
} else {
update_sender.send(DecoderUpdate::UnsupportedFormat);
bail!(gettext_f("Unknown image format: {}", &[mime_type.as_str()]));
update_sender.send(DecoderUpdate::Format(ImageFormat::new(
mime_type.into(),
"SVG".into(),
)));
return FormatDecoder::Svg(Svg::new(file, update_sender, tiles));
}
}
}
update_sender.send(DecoderUpdate::UnsupportedFormat);
bail!(gettext("Unknown image format"))
FormatDecoder::Glycin(Glycin::new(file, update_sender, tiles))
}
/// Request missing tiles
pub fn request(&self, tile_request: TileRequest) {
match &self.decoder {
FormatDecoder::Svg(svg) => {
if let Err(err) = svg.request(tile_request) {
self.update_sender.send(DecoderUpdate::Error(err));
}
if let FormatDecoder::Svg(svg) = &self.decoder {
if let Err(err) = svg.request(tile_request) {
self.update_sender.send(DecoderUpdate::Error(err));
}
FormatDecoder::Heif(_) => {}
_ => {}
};
}
}
pub fn fill_frame_buffer(&self) {
if let FormatDecoder::ImageRsOther(decoder) = &self.decoder {
if let FormatDecoder::Glycin(decoder) = &self.decoder {
decoder.fill_frame_buffer();
} else {
log::error!("Trying to fill frame buffer for decoder without animation support");
}
}
fn guess_format(file: &gio::File) -> anyhow::Result<Option<image_rs::ImageFormat>> {
let mut buf = Vec::new();
// TODO: allow to cancel
let fs_file = file
.to_buf_read(&gio::Cancellable::new())
.context(gettext("Could not open image"))?;
fs_file
.take(64)
.read_to_end(&mut buf)
.context(gettext("Could not read image"))?;
// Try magic bytes first and than file name extension
if let Ok(format) = image_rs::guess_format(&buf) {
Ok(Some(format))
} else if let Some(basename) = file.basename() {
Ok(image_rs::ImageFormat::from_path(basename).ok())
} else {
Ok(None)
}
}
}

View file

@ -15,16 +15,16 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
///! Tiled renderer
///!
///! It is not always feasible or desiareble to store the complete decoded
///! image in the VRAM. [`TiledImage`] allows to compose the parts of
///! the image currently viewed, out of smaller [`Tiles`].
///!
///! This is especially important for SVGs where every the image has to
///! be re-genearted for each zoom level. It can also be used to allow
///! showing large JPEGs etc where the complete decoded image would not
///! even fit in the VRAM.
//! Tiled renderer
//!
//! It is not always feasible or desiareble to store the complete decoded
//! image in the VRAM. [`TiledImage`] allows to compose the parts of
//! the image currently viewed, out of smaller [`Tiles`].
//!
//! This is especially important for SVGs where every the image has to
//! be re-genearted for each zoom level. It can also be used to allow
//! showing large JPEGs etc where the complete decoded image would not
//! even fit in the VRAM.
use super::{DecoderUpdate, ImageDimensionDetails, UpdateSender};
use crate::deps::*;

View file

@ -27,6 +27,10 @@ else
message('Building in release mode')
endif
if get_option('disable-glycin-sandbox')
cargo_options += [ '--features', 'disable-glycin-sandbox' ]
endif
cargo_env = { 'CARGO_HOME': build_root / 'cargo-home' }
cargo_build = custom_target(

View file

@ -246,31 +246,3 @@ 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).context("Invalid ICC profile")?;
let target_profile = lcms2::Profile::new_srgb();
Ok(lcms2::Transform::new(
&src_profile,
src_format,
&target_profile,
src_format,
lcms2::Intent::Perceptual,
)?)
}
pub fn appply_icc_profile<F: Copy>(
icc_profile: &[u8],
src_format: lcms2::PixelFormat,
buf: &mut [F],
) {
match new_trafo(icc_profile, src_format) {
Ok(trafo) => trafo.transform_in_place(buf),
Err(err) => log::warn!("Failed to apply ICC profile: {err}"),
}
}

View file

@ -608,7 +608,7 @@ mod imp {
let applicable_zoom = widget.applicable_zoom();
let scaling_filter = if self.format.borrow().map_or(false, |x| x.is_svg()) {
let scaling_filter = if self.format.borrow().as_ref().map_or(false, |x| x.is_svg()) {
// Looks better in SVG animations and avoids rendering issues
gsk::ScalingFilter::Linear
} else if applicable_zoom < 1. {
@ -770,7 +770,7 @@ impl LpImage {
// Reset background color for reloads
self.set_background_color(None);
let (decoder_res, mut decoder_update) = Decoder::new(file.clone(), tiles.clone()).await;
let (decoder, mut decoder_update) = Decoder::new(file.clone(), tiles.clone()).await;
let weak_obj = self.downgrade();
spawn(async move {
@ -782,10 +782,7 @@ impl LpImage {
log::debug!("Stopped listening to decoder since sender is gone");
});
if let Ok(decoder) = decoder_res {
let decoder = Arc::new(decoder);
self.imp().decoder.replace(Some(decoder));
}
self.imp().decoder.replace(Some(Arc::new(decoder)));
}
/// Called when decoder sends update
@ -836,13 +833,11 @@ impl LpImage {
DecoderUpdate::Format(format) => {
imp.format.replace(Some(format));
self.notify("format-name");
// Do the animation part for animated formats
if format.is_animated() {
let callback_id = self
}
DecoderUpdate::Animated => {
let callback_id = self
.add_tick_callback(glib::clone!(@weak self as obj => @default-return glib::Continue(false), move |_, clock| obj.tick_callback(clock)));
imp.tick_callback.replace(Some(callback_id));
}
imp.tick_callback.replace(Some(callback_id));
}
DecoderUpdate::UnsupportedFormat => {
if !self.is_unsupported() {
@ -1160,7 +1155,7 @@ impl LpImage {
/// Maximal zoom allowed for this image
fn max_zoom(&self) -> f64 {
if let Some(decoder::ImageFormat::Svg) = self.format() {
if self.format().map_or(false, |x| x.is_svg()) {
let (width, height) = self.original_dimensions();
// Avoid division by 0
let long_side = f64::max(1., i32::max(width, height) as f64);
@ -1684,12 +1679,12 @@ impl LpImage {
/// Image format
pub fn format(&self) -> Option<decoder::ImageFormat> {
*self.imp().format.borrow()
self.imp().format.borrow().clone()
}
/// Image format displayable name
pub fn format_name(&self) -> Option<String> {
self.imp().format.borrow().map(|x| x.to_string())
self.imp().format.borrow().as_ref().map(|x| x.to_string())
}
/// Returns decoding error if one occured

View file

@ -418,7 +418,7 @@ impl LpPrint {
let texture_scale = self.user_width() / orig_width as f64;
let cairo_scale = cairo_dpi / self.dpi();
let cairo_surface = if let Some(decoder::ImageFormat::Svg) = image.format() {
let cairo_surface = if image.format().map_or(false, |x| x.is_svg()) {
// Render SVG to exact needed sizes
// TODO: This should be async
decoder::formats::Svg::render_print(