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
|
|
@ -15,7 +15,7 @@ flatpak:
|
||||||
variables:
|
variables:
|
||||||
# We are running tests manually
|
# We are running tests manually
|
||||||
RUN_TESTS: "0"
|
RUN_TESTS: "0"
|
||||||
CONFIG_OPTS: "-Dprofile=ci"
|
CONFIG_OPTS: "-Dprofile=ci -Ddisable-glycin-sandbox=false"
|
||||||
|
|
||||||
flatpak@aarch64:
|
flatpak@aarch64:
|
||||||
extends: ".flatpak@aarch64"
|
extends: ".flatpak@aarch64"
|
||||||
|
|
@ -23,7 +23,7 @@ flatpak@aarch64:
|
||||||
interruptible: true
|
interruptible: true
|
||||||
variables:
|
variables:
|
||||||
RUN_TESTS: "0"
|
RUN_TESTS: "0"
|
||||||
CONFIG_OPTS: "-Dprofile=ci"
|
CONFIG_OPTS: "-Dprofile=ci -Ddisable-glycin-sandbox=false"
|
||||||
|
|
||||||
cargo-fmt:
|
cargo-fmt:
|
||||||
image: "rust:slim"
|
image: "rust:slim"
|
||||||
|
|
|
||||||
795
Cargo.lock
generated
795
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
|
@ -9,26 +9,26 @@ license = "GPL-3.0-or-later"
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
|
[features]
|
||||||
|
disable-glycin-sandbox = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { version = "1.0.66", features = ["backtrace"] }
|
anyhow = { version = "1.0.66", features = ["backtrace"] }
|
||||||
arc-swap = "1.6.0"
|
arc-swap = "1.6.0"
|
||||||
|
async-channel = "1.8.0"
|
||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
gvdb-macros = "0.1.6"
|
gvdb-macros = "0.1.6"
|
||||||
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"
|
|
||||||
url = "2.3.1"
|
url = "2.3.1"
|
||||||
|
|
||||||
# decoders
|
# decoders
|
||||||
image-rs = { package = "image", version = "0.24.6" }
|
librsvg = { git = "https://gitlab.gnome.org/GNOME/librsvg.git", rev = "51c80502b8474ec10c315da3559a049bdf4fe8f1" }
|
||||||
libheif-rs = "0.19.0"
|
glycin = { git = "https://gitlab.gnome.org/sophie-h/glycin.git" }
|
||||||
librsvg = { git = "https://gitlab.gnome.org/GNOME/librsvg.git" }
|
|
||||||
|
|
||||||
[dependencies.ashpd]
|
[dependencies.ashpd]
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
|
||||||
29
README.md
29
README.md
|
|
@ -18,38 +18,15 @@ Bundles will not be automatically updated after installation.
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Fast GPU accelerated image rendering with tiled rendering for SVGs
|
- Fast GPU accelerated image rendering with tiled rendering for SVGs
|
||||||
|
- Extendable and sandboxed (expect SVG) image decoding with
|
||||||
|
- Support for more than 15 image formats by default
|
||||||
- Extensive support for touchpad and touchscreen gestures
|
- Extensive support for touchpad and touchscreen gestures
|
||||||
- Build-in support for more than 15 image formats
|
|
||||||
- Accessible presentation of the most important metadata
|
- Accessible presentation of the most important metadata
|
||||||
- Sleek but powerful interface developed in conjunction with GNOME Human Interface Guidelines
|
- Sleek but powerful interface developed in conjunction with GNOME Human Interface Guidelines
|
||||||
|
|
||||||
## Supported Formats
|
## Supported Formats
|
||||||
|
|
||||||
| Format | View | Optional | ICC | CICP | EXIF | XMP | Animation | Library |
|
Loupe uses [glycin](https://gitlab.gnome.org/sophie-h/glycin) for loading images. You can check [glycin's README](https://gitlab.gnome.org/sophie-h/glycin#supported-image-formats) for more details about the formats supported by the default loaders. However, glycin supports adding loaders for additional formats. Therefore, the supported formats on your system may vary and might be changed by installing or removing glycin loaders.
|
||||||
|----------|------|----------|-----|------|------|-----|-----------|----------------------------|
|
|
||||||
| AVIF | ✔ | ✔ | ✔ | ✔ | ✔ | ✘ | ✘ | libheif-rs + libheif (C++) |
|
|
||||||
| BMP | ✔ | | ✘ | — | — | — | — | image-rs |
|
|
||||||
| DDS | ✔ | | — | — | — | — | — | image-rs |
|
|
||||||
| farbfeld | ✔ | | — | — | — | — | — | image-rs |
|
|
||||||
| QOI | ✔ | | — | — | — | — | — | image-rs |
|
|
||||||
| GIF | ✔ | | ✘ * | — | — | ✘ | ✔ | image-rs |
|
|
||||||
| HEIC | ✔ | ✔ | ✔ | ✔ | ✔ | ✘ | ✘ | libheif-rs + libheif (C++) |
|
|
||||||
| ICO | ✔ | | — | — | — | — | — | image-rs |
|
|
||||||
| JPEG | ✔ | | ✔ | — | ✔ | ✘ | — | image-rs |
|
|
||||||
| OpenEXR | ✔ | | — | — | — | — | — | image-rs |
|
|
||||||
| PNG | ✔ | | ✔ | ✘ | ✔ | ✘ | ✔ | image-rs |
|
|
||||||
| PNM | ✔ | | — | — | — | — | — | image-rs |
|
|
||||||
| SVG | ✔ | | ✘ | — | — | ✘ * | — | librsvg |
|
|
||||||
| TGA | ✔ | | — | — | — | — | — | image-rs |
|
|
||||||
| TIFF | ✔ | | ✘ | — | ✔ | ✘ | — | image-rs |
|
|
||||||
| WEBP | ✔ | | — | — | ✔ | ✘ | ✔ | image-rs |
|
|
||||||
|
|
||||||
| Symbol | Meaning |
|
|
||||||
|--------|------------------------------------------------|
|
|
||||||
| * | Unclear if used in practice, needs research |
|
|
||||||
| ✘ | Supported by format but not implemented yet |
|
|
||||||
| ? | Unclear if supported by format, needs research |
|
|
||||||
| ✔ | Supported |
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
],
|
],
|
||||||
"command" : "loupe",
|
"command" : "loupe",
|
||||||
"finish-args" : [
|
"finish-args" : [
|
||||||
"--share=network",
|
|
||||||
"--share=ipc",
|
"--share=ipc",
|
||||||
"--socket=fallback-x11",
|
"--socket=fallback-x11",
|
||||||
"--socket=wayland",
|
"--socket=wayland",
|
||||||
|
|
@ -120,8 +119,8 @@
|
||||||
"tag" : "v1.0.11"
|
"tag" : "v1.0.11"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name" : "libheif",
|
"name" : "libheif",
|
||||||
"buildsystem" : "cmake",
|
"buildsystem" : "cmake",
|
||||||
"config-opts": [
|
"config-opts": [
|
||||||
|
|
@ -146,7 +145,18 @@
|
||||||
{
|
{
|
||||||
"type" : "git",
|
"type" : "git",
|
||||||
"url" : "https://github.com/strukturag/libheif.git",
|
"url" : "https://github.com/strukturag/libheif.git",
|
||||||
"tag" : "v1.15.1"
|
"tag" : "v1.16.2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name" : "glycin",
|
||||||
|
"buildsystem" : "meson",
|
||||||
|
"sources" : [
|
||||||
|
{
|
||||||
|
"type" : "git",
|
||||||
|
"url" : "https://gitlab.gnome.org/sophie-h/glycin.git",
|
||||||
|
"commit" : "2e3f9add120a4913e559729d361d6d7e5d010f2b"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -154,7 +164,8 @@
|
||||||
"name" : "loupe",
|
"name" : "loupe",
|
||||||
"buildsystem" : "meson",
|
"buildsystem" : "meson",
|
||||||
"config-opts": [
|
"config-opts": [
|
||||||
"-Dprofile=development"
|
"-Dprofile=development",
|
||||||
|
"-Ddisable-glycin-sandbox=true"
|
||||||
],
|
],
|
||||||
"sources" : [
|
"sources" : [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,8 @@ option(
|
||||||
],
|
],
|
||||||
value: 'release'
|
value: 'release'
|
||||||
)
|
)
|
||||||
|
option(
|
||||||
|
'disable-glycin-sandbox',
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Disable sandboxing of image loaders in glycin. Only intended for development purposes.',
|
||||||
|
)
|
||||||
|
|
@ -7,8 +7,7 @@ data/org.gnome.Loupe.desktop.in.in
|
||||||
data/org.gnome.Loupe.metainfo.xml.in.in
|
data/org.gnome.Loupe.metainfo.xml.in.in
|
||||||
data/resources/gtk/help-overlay.ui
|
data/resources/gtk/help-overlay.ui
|
||||||
src/application.rs
|
src/application.rs
|
||||||
src/decoder/formats/heif.rs
|
src/decoder/formats/glycin_proxy.rs
|
||||||
src/decoder/formats/image_rs_other.rs
|
|
||||||
src/decoder/formats/mod.rs
|
src/decoder/formats/mod.rs
|
||||||
src/decoder/formats/svg.rs
|
src/decoder/formats/svg.rs
|
||||||
src/decoder/mod.rs
|
src/decoder/mod.rs
|
||||||
|
|
|
||||||
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
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
mod heif;
|
mod glycin_proxy;
|
||||||
mod image_rs_other;
|
|
||||||
mod svg;
|
mod svg;
|
||||||
|
|
||||||
pub use heif::Heif;
|
pub use glycin_proxy::Glycin;
|
||||||
pub use image_rs_other::ImageRsOther;
|
|
||||||
pub use svg::{Svg, RSVG_MAX_SIZE};
|
pub use svg::{Svg, RSVG_MAX_SIZE};
|
||||||
|
|
||||||
use super::{DecoderUpdate, UpdateSender};
|
use super::{DecoderUpdate, UpdateSender};
|
||||||
use crate::util::gettext::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ImageFormat {
|
pub struct ImageFormat {
|
||||||
ImageRs(image_rs::ImageFormat),
|
mime_type: glycin::MimeType,
|
||||||
AnimatedGif,
|
name: String,
|
||||||
AnimatedWebP,
|
|
||||||
AnimatedPng,
|
|
||||||
// TODO: Add details about contained format
|
|
||||||
Heif,
|
|
||||||
Svg,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageFormat {
|
impl ImageFormat {
|
||||||
pub fn is_animated(&self) -> bool {
|
pub fn new(mime_type: glycin::MimeType, name: String) -> Self {
|
||||||
matches!(
|
Self { mime_type, name }
|
||||||
self,
|
|
||||||
ImageFormat::AnimatedGif | ImageFormat::AnimatedWebP | ImageFormat::AnimatedPng
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_svg(&self) -> bool {
|
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 {
|
pub fn is_potentially_transparent(&self) -> bool {
|
||||||
!matches!(
|
!matches!(self.mime_type.as_str(), "image/bmp" | "image/jpeg")
|
||||||
self,
|
|
||||||
Self::ImageRs(image_rs::ImageFormat::Bmp) | Self::ImageRs(image_rs::ImageFormat::Jpeg)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ImageFormat {
|
impl std::fmt::Display for ImageFormat {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
match self {
|
write!(f, "{}", self.name)
|
||||||
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"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
///! Decode using librsvg
|
//! Decode using librsvg
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::decoder::tiling::{self, FrameBufferExt};
|
use crate::decoder::tiling::{self, FrameBufferExt};
|
||||||
use crate::decoder::TileRequest;
|
use crate::decoder::TileRequest;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
///! Decodes several image formats
|
//! Decodes several image formats
|
||||||
pub mod formats;
|
pub mod formats;
|
||||||
pub mod tiling;
|
pub mod tiling;
|
||||||
|
|
||||||
|
|
@ -23,17 +23,14 @@ pub use formats::ImageDimensionDetails;
|
||||||
|
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
use crate::image_metadata::ImageMetadata;
|
use crate::image_metadata::ImageMetadata;
|
||||||
use crate::util::gettext::*;
|
|
||||||
use crate::util::ToBufRead;
|
|
||||||
use formats::*;
|
use formats::*;
|
||||||
use tiling::FrameBufferExt;
|
use tiling::FrameBufferExt;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::Result;
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use futures::channel::mpsc;
|
use futures::channel::mpsc;
|
||||||
use gio::prelude::*;
|
use gio::prelude::*;
|
||||||
|
|
||||||
use std::io::Read;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use formats::{ImageFormat, RSVG_MAX_SIZE};
|
pub use formats::{ImageFormat, RSVG_MAX_SIZE};
|
||||||
|
|
@ -67,6 +64,8 @@ pub enum DecoderUpdate {
|
||||||
UnsupportedFormat,
|
UnsupportedFormat,
|
||||||
/// New image data available, redraw
|
/// New image data available, redraw
|
||||||
Redraw,
|
Redraw,
|
||||||
|
/// Start animation
|
||||||
|
Animated,
|
||||||
/// And error occured during decoding
|
/// And error occured during decoding
|
||||||
Error(anyhow::Error),
|
Error(anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
@ -102,9 +101,8 @@ pub struct Decoder {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum FormatDecoder {
|
enum FormatDecoder {
|
||||||
ImageRsOther(ImageRsOther),
|
Glycin(Glycin),
|
||||||
Svg(Svg),
|
Svg(Svg),
|
||||||
Heif(Heif),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Decoder {
|
impl Decoder {
|
||||||
|
|
@ -115,31 +113,16 @@ impl Decoder {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
file: gio::File,
|
file: gio::File,
|
||||||
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
||||||
) -> (Result<Self, ()>, mpsc::UnboundedReceiver<DecoderUpdate>) {
|
) -> (Self, mpsc::UnboundedReceiver<DecoderUpdate>) {
|
||||||
let (sender, receiver) = mpsc::unbounded();
|
let (sender, receiver) = mpsc::unbounded();
|
||||||
|
|
||||||
let update_sender = UpdateSender { sender };
|
let update_sender = UpdateSender { sender };
|
||||||
tiles.set_update_sender(update_sender.clone());
|
tiles.set_update_sender(update_sender.clone());
|
||||||
|
|
||||||
let format_decoder = gio::spawn_blocking(
|
let format_decoder = Self::format_decoder(update_sender.clone(), file, tiles);
|
||||||
glib::clone!(@strong update_sender => move || Self::format_decoder(
|
let decoder = Self {
|
||||||
update_sender,
|
decoder: format_decoder,
|
||||||
file,
|
update_sender,
|
||||||
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(())
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
(decoder, receiver)
|
(decoder, receiver)
|
||||||
|
|
@ -149,33 +132,12 @@ impl Decoder {
|
||||||
update_sender: UpdateSender,
|
update_sender: UpdateSender,
|
||||||
file: gio::File,
|
file: gio::File,
|
||||||
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
||||||
) -> anyhow::Result<FormatDecoder> {
|
) -> FormatDecoder {
|
||||||
let format = Self::guess_format(&file)?;
|
if let Ok(file_info) = file.query_info(
|
||||||
|
gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
|
||||||
if let Some(format) = format {
|
gio::FileQueryInfoFlags::NONE,
|
||||||
match format {
|
gio::Cancellable::NONE,
|
||||||
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")?;
|
|
||||||
if let Some(mime_type) = file_info
|
if let Some(mime_type) = file_info
|
||||||
.content_type()
|
.content_type()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -185,62 +147,32 @@ impl Decoder {
|
||||||
// - image/svg+xml
|
// - image/svg+xml
|
||||||
// - image/svg+xml-compressed
|
// - image/svg+xml-compressed
|
||||||
if mime_type.split('+').next() == Some("image/svg") {
|
if mime_type.split('+').next() == Some("image/svg") {
|
||||||
update_sender.send(DecoderUpdate::Format(ImageFormat::Svg));
|
update_sender.send(DecoderUpdate::Format(ImageFormat::new(
|
||||||
return Ok(FormatDecoder::Svg(Svg::new(file, update_sender, tiles)));
|
mime_type.into(),
|
||||||
} else if ["image/avif", "image/heif", "image/heic"].contains(&mime_type.as_str()) {
|
"SVG".into(),
|
||||||
update_sender.send(DecoderUpdate::Format(ImageFormat::Heif));
|
)));
|
||||||
return Ok(FormatDecoder::Heif(Heif::new(file, update_sender, tiles)));
|
return FormatDecoder::Svg(Svg::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::UnsupportedFormat);
|
FormatDecoder::Glycin(Glycin::new(file, update_sender, tiles))
|
||||||
bail!(gettext("Unknown image format"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request missing tiles
|
/// Request missing tiles
|
||||||
pub fn request(&self, tile_request: TileRequest) {
|
pub fn request(&self, tile_request: TileRequest) {
|
||||||
match &self.decoder {
|
if let FormatDecoder::Svg(svg) = &self.decoder {
|
||||||
FormatDecoder::Svg(svg) => {
|
if let Err(err) = svg.request(tile_request) {
|
||||||
if let Err(err) = svg.request(tile_request) {
|
self.update_sender.send(DecoderUpdate::Error(err));
|
||||||
self.update_sender.send(DecoderUpdate::Error(err));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FormatDecoder::Heif(_) => {}
|
}
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_frame_buffer(&self) {
|
pub fn fill_frame_buffer(&self) {
|
||||||
if let FormatDecoder::ImageRsOther(decoder) = &self.decoder {
|
if let FormatDecoder::Glycin(decoder) = &self.decoder {
|
||||||
decoder.fill_frame_buffer();
|
decoder.fill_frame_buffer();
|
||||||
} else {
|
} else {
|
||||||
log::error!("Trying to fill frame buffer for decoder without animation support");
|
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
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
///! Tiled renderer
|
//! Tiled renderer
|
||||||
///!
|
//!
|
||||||
///! It is not always feasible or desiareble to store the complete decoded
|
//! It is not always feasible or desiareble to store the complete decoded
|
||||||
///! image in the VRAM. [`TiledImage`] allows to compose the parts of
|
//! image in the VRAM. [`TiledImage`] allows to compose the parts of
|
||||||
///! the image currently viewed, out of smaller [`Tiles`].
|
//! the image currently viewed, out of smaller [`Tiles`].
|
||||||
///!
|
//!
|
||||||
///! This is especially important for SVGs where every the image has to
|
//! 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
|
//! 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
|
//! showing large JPEGs etc where the complete decoded image would not
|
||||||
///! even fit in the VRAM.
|
//! even fit in the VRAM.
|
||||||
use super::{DecoderUpdate, ImageDimensionDetails, UpdateSender};
|
use super::{DecoderUpdate, ImageDimensionDetails, UpdateSender};
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ else
|
||||||
message('Building in release mode')
|
message('Building in release mode')
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if get_option('disable-glycin-sandbox')
|
||||||
|
cargo_options += [ '--features', 'disable-glycin-sandbox' ]
|
||||||
|
endif
|
||||||
|
|
||||||
cargo_env = { 'CARGO_HOME': build_root / 'cargo-home' }
|
cargo_env = { 'CARGO_HOME': build_root / 'cargo-home' }
|
||||||
|
|
||||||
cargo_build = custom_target(
|
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();
|
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).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 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
|
// Looks better in SVG animations and avoids rendering issues
|
||||||
gsk::ScalingFilter::Linear
|
gsk::ScalingFilter::Linear
|
||||||
} else if applicable_zoom < 1. {
|
} else if applicable_zoom < 1. {
|
||||||
|
|
@ -770,7 +770,7 @@ impl LpImage {
|
||||||
// Reset background color for reloads
|
// Reset background color for reloads
|
||||||
self.set_background_color(None);
|
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();
|
let weak_obj = self.downgrade();
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
|
|
@ -782,10 +782,7 @@ impl LpImage {
|
||||||
log::debug!("Stopped listening to decoder since sender is gone");
|
log::debug!("Stopped listening to decoder since sender is gone");
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Ok(decoder) = decoder_res {
|
self.imp().decoder.replace(Some(Arc::new(decoder)));
|
||||||
let decoder = Arc::new(decoder);
|
|
||||||
self.imp().decoder.replace(Some(decoder));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called when decoder sends update
|
/// Called when decoder sends update
|
||||||
|
|
@ -836,13 +833,11 @@ impl LpImage {
|
||||||
DecoderUpdate::Format(format) => {
|
DecoderUpdate::Format(format) => {
|
||||||
imp.format.replace(Some(format));
|
imp.format.replace(Some(format));
|
||||||
self.notify("format-name");
|
self.notify("format-name");
|
||||||
|
}
|
||||||
// Do the animation part for animated formats
|
DecoderUpdate::Animated => {
|
||||||
if format.is_animated() {
|
let callback_id = self
|
||||||
let callback_id = self
|
|
||||||
.add_tick_callback(glib::clone!(@weak self as obj => @default-return glib::Continue(false), move |_, clock| obj.tick_callback(clock)));
|
.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 => {
|
DecoderUpdate::UnsupportedFormat => {
|
||||||
if !self.is_unsupported() {
|
if !self.is_unsupported() {
|
||||||
|
|
@ -1160,7 +1155,7 @@ impl LpImage {
|
||||||
|
|
||||||
/// Maximal zoom allowed for this image
|
/// Maximal zoom allowed for this image
|
||||||
fn max_zoom(&self) -> f64 {
|
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();
|
let (width, height) = self.original_dimensions();
|
||||||
// Avoid division by 0
|
// Avoid division by 0
|
||||||
let long_side = f64::max(1., i32::max(width, height) as f64);
|
let long_side = f64::max(1., i32::max(width, height) as f64);
|
||||||
|
|
@ -1684,12 +1679,12 @@ impl LpImage {
|
||||||
|
|
||||||
/// Image format
|
/// Image format
|
||||||
pub fn format(&self) -> Option<decoder::ImageFormat> {
|
pub fn format(&self) -> Option<decoder::ImageFormat> {
|
||||||
*self.imp().format.borrow()
|
self.imp().format.borrow().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Image format displayable name
|
/// Image format displayable name
|
||||||
pub fn format_name(&self) -> Option<String> {
|
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
|
/// Returns decoding error if one occured
|
||||||
|
|
|
||||||
|
|
@ -418,7 +418,7 @@ impl LpPrint {
|
||||||
let texture_scale = self.user_width() / orig_width as f64;
|
let texture_scale = self.user_width() / orig_width as f64;
|
||||||
let cairo_scale = cairo_dpi / self.dpi();
|
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
|
// Render SVG to exact needed sizes
|
||||||
// TODO: This should be async
|
// TODO: This should be async
|
||||||
decoder::formats::Svg::render_print(
|
decoder::formats::Svg::render_print(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue