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
2023-03-04 03:14:52 +01:00
|
|
|
///! 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;
|
|
|
|
|
|
2023-03-09 21:27:51 +01:00
|
|
|
/// Current librsvg limit on maximum dimensions. See
|
|
|
|
|
/// <https://gitlab.gnome.org/GNOME/librsvg/-/issues/938>
|
|
|
|
|
pub const RSVG_MAX_SIZE: u32 = 32_767;
|
|
|
|
|
|
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
2023-03-04 03:14:52 +01:00
|
|
|
#[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
|
2023-03-09 21:27:51 +01:00
|
|
|
if total_height > RSVG_MAX_SIZE || total_width > RSVG_MAX_SIZE {
|
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
2023-03-04 03:14:52 +01:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
}
|