decoder: Use image-rs, libheif-rs, librsvg
This adds proper support for a lot of image formats including SVGs. It will be the basis for many features to come. The technical core is an asynchronous renderer that can arrange textures in smaller tiles which might be generated on demand. Closes #34, #40, #41, #49 Basis for fixing the following issues #7, #27, #36, #43, #68, #71
This commit is contained in:
parent
4d4bc4bb38
commit
f80ec7db56
20 changed files with 3023 additions and 370 deletions
148
src/decoder/formats/heif.rs
Normal file
148
src/decoder/formats/heif.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
///! Decode using libheif-rs
|
||||
use super::*;
|
||||
use crate::decoder::tiling::{self, TilingStoreExt};
|
||||
use crate::deps::*;
|
||||
use crate::i18n::*;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use anyhow::Context;
|
||||
use arc_swap::ArcSwap;
|
||||
use libheif_rs::{ColorSpace, HeifContext, Plane, RgbChroma};
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Heif;
|
||||
|
||||
impl Heif {
|
||||
pub fn new(
|
||||
path: PathBuf,
|
||||
updater: UpdateSender,
|
||||
tiles: Arc<ArcSwap<tiling::TilingStore>>,
|
||||
) -> Self {
|
||||
log::debug!("Loading HEIF");
|
||||
updater.spawn_error_handled(move || {
|
||||
let ctx = HeifContext::read_from_file(&path.display().to_string())
|
||||
.context(i18n("Failed to read image"))?;
|
||||
let handle = ctx.primary_image_handle()?;
|
||||
|
||||
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 mut image = handle
|
||||
.decode(ColorSpace::Rgb(rgb_chroma), None)
|
||||
.context(i18n("Failed to decode image"))?;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Decoded<'a> {
|
||||
plane: Plane<&'a mut [u8]>,
|
||||
has_alpha_channel: bool,
|
||||
pre_multiplied_alpha: bool,
|
||||
rgb_chroma: RgbChroma,
|
||||
}
|
||||
|
||||
impl<'a> Decoded<'a> {
|
||||
pub fn into_texture(self) -> gdk::Texture {
|
||||
let memory_format = match self.rgb_chroma {
|
||||
RgbChroma::HdrRgbBe
|
||||
| RgbChroma::HdrRgbaBe
|
||||
| RgbChroma::HdrRgbLe
|
||||
| RgbChroma::HdrRgbaLe => {
|
||||
let transmuted: &mut [u16] =
|
||||
safe_transmute::transmute_many_pedantic_mut(self.plane.data).unwrap();
|
||||
|
||||
// Scale HDR pixels to 16bit (they are usually 10bit or 12bit)
|
||||
for pixel in transmuted.iter_mut() {
|
||||
*pixel <<= 16 - self.plane.bits_per_pixel;
|
||||
}
|
||||
|
||||
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 bytes = glib::Bytes::from_owned(self.plane.data.to_vec());
|
||||
|
||||
let tex = gdk::MemoryTexture::new(
|
||||
self.plane.width as i32,
|
||||
self.plane.height as i32,
|
||||
memory_format,
|
||||
&bytes,
|
||||
self.plane.stride,
|
||||
);
|
||||
|
||||
tex.upcast()
|
||||
}
|
||||
}
|
||||
156
src/decoder/formats/image_rs_other.rs
Normal file
156
src/decoder/formats/image_rs_other.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
///! Decode using image-rs
|
||||
use super::*;
|
||||
use crate::decoder::tiling::{self, TilingStoreExt};
|
||||
use crate::deps::*;
|
||||
|
||||
use anyhow::Context;
|
||||
use arc_swap::ArcSwap;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ImageRsOther;
|
||||
|
||||
impl ImageRsOther {
|
||||
pub fn new(
|
||||
path: PathBuf,
|
||||
updater: UpdateSender,
|
||||
tiles: Arc<ArcSwap<tiling::TilingStore>>,
|
||||
) -> Self {
|
||||
updater.spawn_error_handled(move || {
|
||||
let dimensions = image_rs::image_dimensions(&path).context("Failed to read image")?;
|
||||
|
||||
tiles.set_original_dimensions(dimensions);
|
||||
|
||||
let mut reader = image_rs::io::Reader::open(&path).context("Failed to open image")?;
|
||||
|
||||
// TODO: Set something huge?
|
||||
reader.limits(image_rs::io::Limits::no_limits());
|
||||
|
||||
let reader = reader
|
||||
.with_guessed_format()
|
||||
.context("Failed to open image")?;
|
||||
let dynamic_image = reader.decode().context("Failed to decode image")?;
|
||||
|
||||
let decoded_image = Decoded { dynamic_image };
|
||||
|
||||
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(())
|
||||
});
|
||||
|
||||
ImageRsOther
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Decoded {
|
||||
dynamic_image: image_rs::DynamicImage,
|
||||
}
|
||||
|
||||
impl Decoded {
|
||||
pub fn into_texture(self) -> 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(buffer) => (
|
||||
1,
|
||||
gdk::MemoryFormat::R8g8b8,
|
||||
buffer.sample_layout(),
|
||||
buffer.into_raw(),
|
||||
),
|
||||
image_rs::DynamicImage::ImageRgba8(buffer) => (
|
||||
1,
|
||||
gdk::MemoryFormat::R8g8b8a8,
|
||||
buffer.sample_layout(),
|
||||
buffer.into_raw(),
|
||||
),
|
||||
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()).unwrap(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let bytes = glib::Bytes::from_owned(data);
|
||||
|
||||
gdk::MemoryTexture::new(
|
||||
layout.width as i32,
|
||||
layout.height as i32,
|
||||
memory_format,
|
||||
&bytes,
|
||||
layout.height_stride * n_bytes,
|
||||
)
|
||||
.upcast()
|
||||
}
|
||||
}
|
||||
9
src/decoder/formats/mod.rs
Normal file
9
src/decoder/formats/mod.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
mod heif;
|
||||
mod image_rs_other;
|
||||
mod svg;
|
||||
|
||||
pub use heif::Heif;
|
||||
pub use image_rs_other::ImageRsOther;
|
||||
pub use svg::Svg;
|
||||
|
||||
use super::{Decoder, UpdateSender};
|
||||
219
src/decoder/formats/svg.rs
Normal file
219
src/decoder/formats/svg.rs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
///! Decode using librsvg
|
||||
use super::*;
|
||||
use crate::decoder::tiling::{self, TilingStoreExt};
|
||||
use crate::decoder::TileRequest;
|
||||
use crate::deps::*;
|
||||
|
||||
use anyhow::Context;
|
||||
use arc_swap::ArcSwap;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Svg {
|
||||
thread_handle: std::thread::JoinHandle<()>,
|
||||
current_request: Arc<std::sync::RwLock<Request>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
enum Request {
|
||||
#[default]
|
||||
None,
|
||||
Exit,
|
||||
Tile(TileRequest),
|
||||
}
|
||||
|
||||
impl Request {
|
||||
fn is_waiting(&self) -> bool {
|
||||
!matches!(self, Self::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Svg {
|
||||
fn drop(&mut self) {
|
||||
let mut request = self.current_request.write().unwrap();
|
||||
*request = Request::Exit;
|
||||
self.thread_handle.thread().unpark();
|
||||
}
|
||||
}
|
||||
|
||||
impl Svg {
|
||||
pub fn new(
|
||||
path: PathBuf,
|
||||
updater: UpdateSender,
|
||||
tiles: Arc<ArcSwap<tiling::TilingStore>>,
|
||||
) -> Self {
|
||||
let current_request: Arc<std::sync::RwLock<Request>> = Default::default();
|
||||
let request_store = current_request.clone();
|
||||
|
||||
let thread_handle = updater.spawn_error_handled(move || {
|
||||
let handle = rsvg::Loader::new().read_path(path)?;
|
||||
let renderer = rsvg::CairoRenderer::new(&handle);
|
||||
|
||||
let (original_width, original_height) = svg_dimensions(&renderer);
|
||||
|
||||
tiles.set_original_dimensions((original_width, original_height));
|
||||
|
||||
loop {
|
||||
let tile_request = {
|
||||
let mut request = request_store.write().unwrap();
|
||||
let value = request.clone();
|
||||
*request = Request::None;
|
||||
value
|
||||
};
|
||||
|
||||
match tile_request {
|
||||
Request::None => {
|
||||
std::thread::park();
|
||||
}
|
||||
Request::Exit => {
|
||||
log::debug!("Terminating decoder thread.");
|
||||
break;
|
||||
}
|
||||
Request::Tile(tile_request) => {
|
||||
let tiling = tiling::Tiling {
|
||||
tile_size: tiling::TILE_SIZE,
|
||||
original_dimensions: (original_width, original_height),
|
||||
zoom: tile_request.zoom,
|
||||
bleed: 2,
|
||||
};
|
||||
|
||||
let relevant_tiles = tiling.relevant_tiles(&tile_request.viewport);
|
||||
|
||||
for tile_instructions in relevant_tiles {
|
||||
if request_store.read().unwrap().is_waiting() {
|
||||
break;
|
||||
}
|
||||
|
||||
if tiles
|
||||
.load()
|
||||
.contains(tile_request.zoom, tile_instructions.coordinates)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let area = tile_instructions.area_with_bleed();
|
||||
let surface = cairo::ImageSurface::create(
|
||||
cairo::Format::ARgb32,
|
||||
area.width() as i32,
|
||||
area.height() as i32,
|
||||
)?;
|
||||
|
||||
let context = cairo::Context::new(surface.clone())?;
|
||||
let (total_width, total_height) =
|
||||
tile_instructions.tiling.scaled_dimensions();
|
||||
|
||||
// librsvg does not currently support larger images
|
||||
//
|
||||
// TODO: We are only creating tiles, maybe this check in librsvg is wrong?
|
||||
if total_height > 32_767 || total_width > 32_767 {
|
||||
continue;
|
||||
}
|
||||
|
||||
renderer
|
||||
.render_document(
|
||||
&context,
|
||||
&cairo::Rectangle::new(
|
||||
-area.x() as f64,
|
||||
-area.y() as f64,
|
||||
total_width as f64,
|
||||
total_height as f64,
|
||||
),
|
||||
)
|
||||
.context("Failed to render image")?;
|
||||
drop(context);
|
||||
|
||||
let decoded_image = Decoded { surface };
|
||||
|
||||
let tile = tiling::Tile {
|
||||
position: (
|
||||
tile_instructions.area.x() as u32,
|
||||
tile_instructions.area.y() as u32,
|
||||
),
|
||||
zoom_level: tiling::zoom_to_level(tile_instructions.tiling.zoom),
|
||||
bleed: 2,
|
||||
texture: decoded_image.into_texture(),
|
||||
};
|
||||
|
||||
tiles.push(tile.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Self {
|
||||
thread_handle,
|
||||
current_request,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request(&self, request: TileRequest, _abstract_decoder: &Decoder) -> anyhow::Result<()> {
|
||||
let mut current_request = self.current_request.write().unwrap();
|
||||
*current_request = Request::Tile(request);
|
||||
self.thread_handle.thread().unpark();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn svg_dimensions(renderer: &rsvg::CairoRenderer) -> (u32, u32) {
|
||||
if let Some((width, height)) = renderer.intrinsic_size_in_pixels() {
|
||||
(width.ceil() as u32, height.ceil() as u32)
|
||||
} else {
|
||||
match renderer.intrinsic_dimensions() {
|
||||
rsvg::IntrinsicDimensions {
|
||||
width:
|
||||
rsvg::Length {
|
||||
length: width,
|
||||
unit: rsvg::LengthUnit::Percent,
|
||||
},
|
||||
height:
|
||||
rsvg::Length {
|
||||
length: height,
|
||||
unit: rsvg::LengthUnit::Percent,
|
||||
},
|
||||
vbox: Some(vbox),
|
||||
} => (
|
||||
(width * vbox.width()).ceil() as u32,
|
||||
(height * vbox.height()).ceil() as u32,
|
||||
),
|
||||
dimensions => {
|
||||
log::warn!("Failed to parse SVG dimensions: {dimensions:?}");
|
||||
(300, 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Decoded {
|
||||
surface: cairo::ImageSurface,
|
||||
}
|
||||
|
||||
impl Decoded {
|
||||
pub fn into_texture(self) -> gdk::Texture {
|
||||
let memory_format = {
|
||||
#[cfg(target_endian = "little")]
|
||||
{
|
||||
gdk::MemoryFormat::B8g8r8a8
|
||||
}
|
||||
|
||||
#[cfg(target_endian = "big")]
|
||||
{
|
||||
gdk::MemoryFormat::A8r8g8b8
|
||||
}
|
||||
};
|
||||
|
||||
let width = self.surface.width();
|
||||
let height = self.surface.height();
|
||||
let stride = self.surface.stride() as usize;
|
||||
|
||||
let bytes = glib::Bytes::from_owned(self.surface.take_data().unwrap().to_vec());
|
||||
|
||||
gdk::MemoryTexture::new(width, height, memory_format, &bytes, stride).upcast()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue