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
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