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()
|
||||
}
|
||||
}
|
||||
158
src/decoder/mod.rs
Normal file
158
src/decoder/mod.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
///! Decodes several image formats
|
||||
mod formats;
|
||||
pub mod tiling;
|
||||
|
||||
use crate::image_metadata::ImageMetadata;
|
||||
use formats::*;
|
||||
use tiling::TilingStoreExt;
|
||||
|
||||
use anyhow::Context;
|
||||
use arc_swap::ArcSwap;
|
||||
use futures::channel::mpsc;
|
||||
use gio::prelude::*;
|
||||
use gtk::graphene;
|
||||
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ImageFormat {
|
||||
ImageRs(image_rs::ImageFormat),
|
||||
// TODO: Add details about contained format
|
||||
Heif,
|
||||
Svg,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ImageFormat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
Self::ImageRs(format) => write!(f, "{:?}", format),
|
||||
Self::Heif => write!(f, "HEIF"),
|
||||
Self::Svg => write!(f, "SVG"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// Renderer requests new tiles
|
||||
///
|
||||
/// This happens for initial loading or when zoom level or viewing area changes.
|
||||
pub struct TileRequest {
|
||||
pub viewport: graphene::Rect,
|
||||
pub zoom: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Signals update to the renderer
|
||||
pub struct UpdateSender {
|
||||
sender: mpsc::UnboundedSender<DecoderUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Signals for renderer
|
||||
pub enum DecoderUpdate {
|
||||
/// Dimensions of image in `TilingSore` available/updated
|
||||
Dimensions,
|
||||
/// Metadata available
|
||||
Metadata(ImageMetadata),
|
||||
/// Image format determined
|
||||
Format(ImageFormat),
|
||||
/// New image data available, redraw
|
||||
Redraw,
|
||||
/// And error occured during decoding
|
||||
Error(anyhow::Error),
|
||||
}
|
||||
|
||||
impl UpdateSender {
|
||||
pub fn send(&self, update: DecoderUpdate) {
|
||||
let result = self.sender.unbounded_send(update);
|
||||
|
||||
if let Err(err) = result {
|
||||
log::error!("Failed to send update: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Send occuring errors to renderer
|
||||
pub fn spawn_error_handled<F>(&self, f: F) -> std::thread::JoinHandle<()>
|
||||
where
|
||||
F: FnOnce() -> Result<(), anyhow::Error> + Send + 'static,
|
||||
{
|
||||
let update_sender = self.clone();
|
||||
std::thread::spawn(move || {
|
||||
if let Err(err) = f() {
|
||||
update_sender.send(DecoderUpdate::Error(err));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Decoder {
|
||||
decoder: FormatDecoder,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum FormatDecoder {
|
||||
ImageRsOther(ImageRsOther),
|
||||
Svg(Svg),
|
||||
Heif(Heif),
|
||||
}
|
||||
|
||||
impl Decoder {
|
||||
/// Get new image decoder
|
||||
///
|
||||
/// The textures will be stored in the passed `TilingStore`.
|
||||
/// The renderer should listen to updates from the returned receiver.
|
||||
pub fn new(
|
||||
file: gio::File,
|
||||
tiles: Arc<ArcSwap<tiling::TilingStore>>,
|
||||
) -> anyhow::Result<(Self, mpsc::UnboundedReceiver<DecoderUpdate>)> {
|
||||
let path = file.path().context("Need a file path")?;
|
||||
let (sender, receiver) = mpsc::unbounded();
|
||||
|
||||
let update_sender = UpdateSender { sender };
|
||||
tiles.set_update_sender(update_sender.clone());
|
||||
|
||||
update_sender.send(DecoderUpdate::Metadata(ImageMetadata::load(&path)));
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let file = std::fs::File::open(&path)?;
|
||||
file.take(64).read_to_end(&mut buf)?;
|
||||
let format = image_rs::guess_format(&buf);
|
||||
|
||||
let decoder = if let Ok(format) = format {
|
||||
match format {
|
||||
image_rs::ImageFormat::Avif => {
|
||||
update_sender.send(DecoderUpdate::Format(ImageFormat::Heif));
|
||||
FormatDecoder::Heif(Heif::new(path, update_sender, tiles))
|
||||
}
|
||||
format => {
|
||||
update_sender.send(DecoderUpdate::Format(ImageFormat::ImageRs(format)));
|
||||
FormatDecoder::ImageRsOther(ImageRsOther::new(path, update_sender, tiles))
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Use mime glib mime type detection
|
||||
else if [Some("svg"), Some("svgz")].contains(&path.extension().and_then(|x| x.to_str())) {
|
||||
update_sender.send(DecoderUpdate::Format(ImageFormat::Svg));
|
||||
FormatDecoder::Svg(Svg::new(path, update_sender, tiles))
|
||||
} else if [Some("heic"), Some("avif")].contains(&path.extension().and_then(|x| x.to_str()))
|
||||
{
|
||||
update_sender.send(DecoderUpdate::Format(ImageFormat::Heif));
|
||||
FormatDecoder::Heif(Heif::new(path, update_sender, tiles))
|
||||
} else {
|
||||
return None.context("unknown image format");
|
||||
};
|
||||
|
||||
Ok((Self { decoder }, receiver))
|
||||
}
|
||||
|
||||
/// Request missing tiles
|
||||
pub fn request(&self, tile_request: TileRequest) {
|
||||
match &self.decoder {
|
||||
FormatDecoder::Svg(svg) => svg.request(tile_request, self).unwrap(),
|
||||
FormatDecoder::Heif(_) => {}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
483
src/decoder/tiling.rs
Normal file
483
src/decoder/tiling.rs
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
///! Tiled renderer
|
||||
///!
|
||||
///! It is not always feasible or desiareble to store the complete decoded
|
||||
///! image in the VRAM. [`TilingStore`] 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, UpdateSender};
|
||||
use crate::deps::*;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
const ZOOM_SIGNIFICANT_DIGETS: i32 = 6;
|
||||
pub const TILE_SIZE: u16 = 4000;
|
||||
|
||||
pub type ZoomLevel = u32;
|
||||
pub type Coordinate = u32;
|
||||
pub type Coordinates = (u32, u32);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// Part of the image
|
||||
pub struct Tile {
|
||||
pub position: Coordinates,
|
||||
pub zoom_level: ZoomLevel,
|
||||
/// Border that gets cut away to avoid scaling artifacts at the sides
|
||||
pub bleed: u8,
|
||||
pub texture: gdk::Texture,
|
||||
}
|
||||
|
||||
impl Tile {
|
||||
pub fn area(&self) -> graphene::Rect {
|
||||
let bleed = self.bleed as f32;
|
||||
// The texture is rendered slightly larger such that bleed can be cut away
|
||||
self.area_with_bleed().inset_r(bleed, bleed)
|
||||
}
|
||||
|
||||
pub fn area_with_bleed(&self) -> graphene::Rect {
|
||||
let width = self.texture.width() as f32;
|
||||
let height = self.texture.height() as f32;
|
||||
let bleed = self.bleed as f32;
|
||||
graphene::Rect::new(
|
||||
self.position.0 as f32 - bleed,
|
||||
self.position.1 as f32 - bleed,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn zoom(&self) -> f32 {
|
||||
level_to_zoom(self.zoom_level) as f32
|
||||
}
|
||||
|
||||
/// Add tile to snapshot
|
||||
///
|
||||
/// This is where parts of images actually land on the screen
|
||||
pub fn add_to_snapshot(&self, snapshot: >k::Snapshot, options: &RenderOptions) {
|
||||
let zoom = self.zoom();
|
||||
let area = self.area().scale(1. / zoom, 1. / zoom);
|
||||
let area_with_bleed = self.area_with_bleed().scale(1. / zoom, 1. / zoom);
|
||||
|
||||
// TODO: do not clip outer bounderies of the image
|
||||
snapshot.push_clip(&area);
|
||||
snapshot.append_color(&gdk::RGBA::new(0.118, 0.118, 0.118, 1.), &area);
|
||||
snapshot.append_scaled_texture(&self.texture, options.scaling_filter, &area_with_bleed);
|
||||
snapshot.pop();
|
||||
}
|
||||
|
||||
fn coordinates(&self) -> Coordinates {
|
||||
self.position
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// Store tiles for image
|
||||
pub struct TilingStore {
|
||||
pub tiles: BTreeMap<ZoomLevel, BTreeMap<Coordinates, Tile>>,
|
||||
/// Tile size without bleed
|
||||
pub tile_size: u16,
|
||||
/// Complete image size
|
||||
pub original_dimensions: Option<Coordinates>,
|
||||
pub update_sender: Option<UpdateSender>,
|
||||
}
|
||||
|
||||
impl Default for TilingStore {
|
||||
fn default() -> Self {
|
||||
Self::new(TILE_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderOptions {
|
||||
pub scaling_filter: gsk::ScalingFilter,
|
||||
}
|
||||
|
||||
impl TilingStore {
|
||||
pub fn new(tile_size: u16) -> Self {
|
||||
TilingStore {
|
||||
tiles: Default::default(),
|
||||
tile_size,
|
||||
original_dimensions: None,
|
||||
update_sender: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Render tiles as image
|
||||
///
|
||||
/// This is what actually draws the image
|
||||
pub fn add_to_snapshot(&self, snapshot: >k::Snapshot, zoom: f64, options: &RenderOptions) {
|
||||
let tiles: Vec<_> = self
|
||||
.iter_rendering_priority(zoom)
|
||||
.flat_map(|(_, tiles)| tiles)
|
||||
.collect();
|
||||
|
||||
log::trace!("Rendering {} tiles", tiles.len());
|
||||
// reverse to put least fitting tiles in the background
|
||||
for (_, tile) in tiles.iter().rev() {
|
||||
tile.add_to_snapshot(snapshot, options);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, tile: Tile) {
|
||||
let map = self.tiles.entry(tile.zoom_level).or_default();
|
||||
map.insert(tile.coordinates(), tile);
|
||||
if let Some(updater) = &self.update_sender {
|
||||
updater.send(DecoderUpdate::Redraw);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_original_dimensions(&mut self, dimensions: Coordinates) {
|
||||
self.original_dimensions = Some(dimensions);
|
||||
if let Some(updater) = &self.update_sender {
|
||||
updater.send(DecoderUpdate::Dimensions);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_update_sender(&mut self, sender: UpdateSender) {
|
||||
self.update_sender = Some(sender);
|
||||
}
|
||||
|
||||
pub fn contains(&self, zoom: f64, coordinates: Coordinates) -> bool {
|
||||
let Some(tile_plane) = self.tiles.get(&zoom_to_level(zoom)) else { return false };
|
||||
|
||||
tile_plane.contains_key(&coordinates)
|
||||
}
|
||||
|
||||
/// Tiles ordered by how good they are suited for the zoom level
|
||||
pub fn iter_rendering_priority(
|
||||
&self,
|
||||
zoom: f64,
|
||||
) -> impl Iterator<Item = (&ZoomLevel, &BTreeMap<Coordinates, Tile>)> {
|
||||
let zoom_level = zoom_to_level(zoom);
|
||||
self.tiles
|
||||
// Tiles at zoom level and with better resolution
|
||||
.range(zoom_level..)
|
||||
// Afterwards try tiles with worse resolution
|
||||
.chain(self.tiles.range(..zoom_level).rev())
|
||||
}
|
||||
|
||||
/// Viewport within zoom coordinates
|
||||
///
|
||||
/// TODO: This is the most buggy function ever
|
||||
pub fn cleanup(&mut self, zoom: f64, viewport: graphene::Rect) {
|
||||
let Some(original_dimensions) = self.original_dimensions else { log::error!("Too-early cleanup: Original dimension not known"); return; };
|
||||
|
||||
let mut kept_tiles = Self::new(self.tile_size);
|
||||
|
||||
// Always work with 100% zoom level for tiles
|
||||
let mut missing_tiles = vec![viewport.scale(1. / zoom as f32, 1. / zoom as f32)];
|
||||
|
||||
// Keep tiles in top layer
|
||||
// TODO: Change the top layer if window size changes
|
||||
if let Some(top_layer) = self.tiles.iter().next() {
|
||||
for tile in top_layer.1.values() {
|
||||
kept_tiles.push(tile.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Put all tiles in correct processing order
|
||||
let stored_tiles = self.iter_rendering_priority(zoom);
|
||||
|
||||
for (zoom_level, tiles) in stored_tiles {
|
||||
let tile_zoom = level_to_zoom(*zoom_level);
|
||||
|
||||
let mut next_missing_tiles = vec![];
|
||||
|
||||
// Tiling for this zoom level and viewport
|
||||
let tiling = Tiling {
|
||||
tile_size: self.tile_size,
|
||||
original_dimensions,
|
||||
zoom: tile_zoom,
|
||||
bleed: 2,
|
||||
};
|
||||
|
||||
for mut tile_instruction in tiling.relevant_tiles(&viewport) {
|
||||
// scale to 100% reference
|
||||
tile_instruction.area = tile_instruction
|
||||
.area
|
||||
.scale(1. / tile_zoom as f32, 1. / tile_zoom as f32);
|
||||
|
||||
// Determine if tile of this zoom level has overlap with a missing tile
|
||||
let mut intersections = missing_tiles
|
||||
.iter()
|
||||
.filter(|missing| tile_instruction.area.is_intersected(missing))
|
||||
.peekable();
|
||||
|
||||
if intersections.peek().is_some() {
|
||||
if let Some(tile) = tiles.get(&tile_instruction.coordinates) {
|
||||
// we have this tile and can provide it
|
||||
kept_tiles.push(tile.clone());
|
||||
} else {
|
||||
for missing in missing_tiles.iter() {
|
||||
if let Some(overlap) = missing.intersection(&tile_instruction.area) {
|
||||
next_missing_tiles.push(overlap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
missing_tiles = next_missing_tiles;
|
||||
}
|
||||
|
||||
kept_tiles.tiles.values().flat_map(|x| x.iter()).count();
|
||||
|
||||
self.tiles = kept_tiles.tiles;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// Abstract definition of a tiling
|
||||
pub struct Tiling {
|
||||
pub tile_size: u16,
|
||||
pub original_dimensions: Coordinates,
|
||||
pub zoom: f64,
|
||||
pub bleed: u8,
|
||||
}
|
||||
|
||||
impl Tiling {
|
||||
pub fn scaled_dimensions(&self) -> Coordinates {
|
||||
let (w, h) = self.original_dimensions;
|
||||
(
|
||||
(w as f64 * self.zoom) as Coordinate,
|
||||
(h as f64 * self.zoom) as Coordinate,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns relevant tiles
|
||||
///
|
||||
/// This gives what tiles are needed for convering the `preload_area`.
|
||||
/// Decoders use the [`TileInstructions`] to know what they have to render.
|
||||
pub fn relevant_tiles(&self, preload_area: &graphene::Rect) -> Vec<TileInstructions> {
|
||||
let tile_size = self.tile_size as f32;
|
||||
|
||||
let x0 = (preload_area.x() / tile_size).floor() * tile_size;
|
||||
let x1 = ((preload_area.x() + preload_area.width()) / tile_size).ceil() * tile_size;
|
||||
let y0 = (preload_area.y() / tile_size).floor() * tile_size;
|
||||
let y1 = ((preload_area.y() + preload_area.height()) / tile_size).ceil() * tile_size;
|
||||
|
||||
let mut tiles = Vec::new();
|
||||
for x in (x0 as u32..x1 as u32).step_by(self.tile_size.into()) {
|
||||
for y in (y0 as u32..y1 as u32).step_by(self.tile_size.into()) {
|
||||
let width = tile_size
|
||||
+ f32::min(
|
||||
0.,
|
||||
self.scaled_dimensions().0 as f32 - (x as f32 + tile_size),
|
||||
);
|
||||
let height = tile_size
|
||||
+ f32::min(
|
||||
0.,
|
||||
self.scaled_dimensions().1 as f32 - (y as f32 + tile_size),
|
||||
);
|
||||
|
||||
let area = graphene::Rect::new(x as f32, y as f32, width, height);
|
||||
let tile = TileInstructions {
|
||||
tiling: *self,
|
||||
area,
|
||||
coordinates: (x, y),
|
||||
};
|
||||
tiles.push(tile);
|
||||
}
|
||||
}
|
||||
|
||||
tiles.sort_by_key(|tile| tile.area.center().distance(&preload_area.center()).0 as u32);
|
||||
|
||||
tiles
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
/// Instruction for decoder to generate this tile
|
||||
pub struct TileInstructions {
|
||||
pub tiling: Tiling,
|
||||
pub area: graphene::Rect,
|
||||
pub coordinates: Coordinates,
|
||||
}
|
||||
|
||||
impl TileInstructions {
|
||||
pub fn area_with_bleed(&self) -> graphene::Rect {
|
||||
let bleed = self.tiling.bleed as f32;
|
||||
self.area.inset_r(-bleed, -bleed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Integer zoom levels are used because they are hashable
|
||||
/// and ignore tiny float calculation errors.
|
||||
pub fn zoom_to_level(zoom: f64) -> u32 {
|
||||
(zoom * 10_f64.powi(ZOOM_SIGNIFICANT_DIGETS)) as u32
|
||||
}
|
||||
|
||||
pub fn level_to_zoom(zoom_level: u32) -> f64 {
|
||||
zoom_level as f64 * 10_f64.powi(-ZOOM_SIGNIFICANT_DIGETS)
|
||||
}
|
||||
|
||||
pub fn zoom_normalize(zoom: f64) -> f64 {
|
||||
level_to_zoom(zoom_to_level(zoom))
|
||||
}
|
||||
|
||||
trait RectExt {
|
||||
fn is_intersected(&self, b: &graphene::Rect) -> bool;
|
||||
}
|
||||
|
||||
impl RectExt for graphene::Rect {
|
||||
fn is_intersected(&self, b: &graphene::Rect) -> bool {
|
||||
self.intersection(b).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TilingStoreExt {
|
||||
fn push(&self, tile: Tile);
|
||||
fn set_original_dimensions(&self, size: Coordinates);
|
||||
fn set_update_sender(&self, sender: UpdateSender);
|
||||
}
|
||||
|
||||
impl TilingStoreExt for ArcSwap<TilingStore> {
|
||||
fn push(&self, tile: Tile) {
|
||||
self.rcu(|tiling_store| {
|
||||
let mut new_store = (**tiling_store).clone();
|
||||
new_store.push(tile.clone());
|
||||
Arc::new(new_store)
|
||||
});
|
||||
}
|
||||
|
||||
fn set_original_dimensions(&self, size: Coordinates) {
|
||||
self.rcu(|tiling_store| {
|
||||
let mut new_store = (**tiling_store).clone();
|
||||
new_store.set_original_dimensions(size);
|
||||
Arc::new(new_store)
|
||||
});
|
||||
}
|
||||
|
||||
fn set_update_sender(&self, sender: UpdateSender) {
|
||||
self.rcu(|tiling_store| {
|
||||
let mut new_store = (**tiling_store).clone();
|
||||
new_store.set_update_sender(sender.clone());
|
||||
Arc::new(new_store)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_tiling_cleanup() {
|
||||
let mut store = TilingStore::new(2);
|
||||
|
||||
let toplevel = Tile {
|
||||
position: (0, 0),
|
||||
zoom_level: zoom_to_level(1.),
|
||||
bleed: 0,
|
||||
texture: gdk::MemoryTexture::new(
|
||||
4,
|
||||
4,
|
||||
gdk::MemoryFormat::R8g8b8,
|
||||
&glib::Bytes::from_static(&[0; 4 * 4 * 3]),
|
||||
4 * 3,
|
||||
)
|
||||
.upcast(),
|
||||
};
|
||||
store.push(toplevel);
|
||||
|
||||
let right_level = Tile {
|
||||
position: (0, 0),
|
||||
zoom_level: zoom_to_level(1.5),
|
||||
bleed: 0,
|
||||
texture: gdk::MemoryTexture::new(
|
||||
2,
|
||||
2,
|
||||
gdk::MemoryFormat::R8g8b8,
|
||||
&glib::Bytes::from_static(&[0; 2 * 2 * 3]),
|
||||
2 * 3,
|
||||
)
|
||||
.upcast(),
|
||||
};
|
||||
store.push(right_level);
|
||||
|
||||
let low_level = Tile {
|
||||
position: (2, 0),
|
||||
zoom_level: zoom_to_level(1.6),
|
||||
bleed: 0,
|
||||
texture: gdk::MemoryTexture::new(
|
||||
2,
|
||||
2,
|
||||
gdk::MemoryFormat::R8g8b8,
|
||||
&glib::Bytes::from_static(&[0; 2 * 2 * 3]),
|
||||
2 * 3,
|
||||
)
|
||||
.upcast(),
|
||||
};
|
||||
store.push(low_level);
|
||||
|
||||
let overlapped = Tile {
|
||||
position: (0, 0),
|
||||
zoom_level: zoom_to_level(1.7),
|
||||
bleed: 0,
|
||||
texture: gdk::MemoryTexture::new(
|
||||
2,
|
||||
2,
|
||||
gdk::MemoryFormat::R8g8b8,
|
||||
&glib::Bytes::from_static(&[0; 2 * 2 * 3]),
|
||||
2 * 3,
|
||||
)
|
||||
.upcast(),
|
||||
};
|
||||
store.push(overlapped);
|
||||
|
||||
store.cleanup(1.5, graphene::Rect::new(0., 0., 4. * 1.5, 4. * 1.5));
|
||||
|
||||
assert!(store.tiles.get(&1000000).unwrap().get(&(0, 0)).is_some());
|
||||
assert!(store.tiles.get(&1500000).unwrap().get(&(0, 0)).is_some());
|
||||
assert!(store.tiles.get(&1600000).unwrap().get(&(2, 0)).is_some());
|
||||
// TODO: This should pass, but it does not
|
||||
//assert!(store.tiles.get(&1700000).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tiling_cleanup_simple() {
|
||||
let mut store = TilingStore::new(2);
|
||||
|
||||
let toplevel = Tile {
|
||||
position: (0, 0),
|
||||
zoom_level: zoom_to_level(1.),
|
||||
bleed: 0,
|
||||
texture: gdk::MemoryTexture::new(
|
||||
4,
|
||||
4,
|
||||
gdk::MemoryFormat::R8g8b8,
|
||||
&glib::Bytes::from_static(&[0; 4 * 4 * 3]),
|
||||
4 * 3,
|
||||
)
|
||||
.upcast(),
|
||||
};
|
||||
store.push(toplevel);
|
||||
|
||||
let detailled = Tile {
|
||||
position: (2, 2),
|
||||
zoom_level: zoom_to_level(2.),
|
||||
bleed: 0,
|
||||
texture: gdk::MemoryTexture::new(
|
||||
4,
|
||||
4,
|
||||
gdk::MemoryFormat::R8g8b8,
|
||||
&glib::Bytes::from_static(&[0; 4 * 4 * 3]),
|
||||
4 * 3,
|
||||
)
|
||||
.upcast(),
|
||||
};
|
||||
store.push(detailled);
|
||||
|
||||
store.cleanup(1.5, graphene::Rect::new(0., 0., 20., 20.));
|
||||
|
||||
assert!(store.tiles.get(&1000000).unwrap().get(&(0, 0)).is_some());
|
||||
assert!(store.tiles.get(&2000000).unwrap().get(&(2, 2)).is_some());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue