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:
parent
d690c0e2d8
commit
1b2dde79b6
18 changed files with 536 additions and 1416 deletions
155
src/decoder/formats/glycin_proxy.rs
Normal file
155
src/decoder/formats/glycin_proxy.rs
Normal 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(());
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue