This allows to show images from locations that don't have a representation with path. This also works around not being able to read files in trash:/// Closes #56 Part-of: <https://gitlab.gnome.org/Incubator/loupe/-/merge_requests/174>
224 lines
7.4 KiB
Rust
224 lines
7.4 KiB
Rust
///! Decode using librsvg
|
|
use super::*;
|
|
use crate::decoder::tiling::{self, FrameBufferExt};
|
|
use crate::decoder::TileRequest;
|
|
use crate::deps::*;
|
|
|
|
use anyhow::Context;
|
|
use arc_swap::ArcSwap;
|
|
use gtk::prelude::*;
|
|
|
|
use std::sync::Arc;
|
|
|
|
/// Current librsvg limit on maximum dimensions. See
|
|
/// <https://gitlab.gnome.org/GNOME/librsvg/-/issues/938>
|
|
pub const RSVG_MAX_SIZE: u32 = 32_767;
|
|
|
|
#[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(
|
|
file: gio::File,
|
|
updater: UpdateSender,
|
|
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
|
) -> 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_file(&file, gio::Cancellable::NONE)?;
|
|
let renderer = rsvg::CairoRenderer::new(&handle);
|
|
|
|
let (original_width, original_height) = svg_dimensions(&renderer);
|
|
|
|
let intrisic_dimensions = renderer.intrinsic_dimensions();
|
|
tiles.set_original_dimensions_full(
|
|
(original_width, original_height),
|
|
ImageDimensionDetails::Svg((intrisic_dimensions.width, intrisic_dimensions.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: 0,
|
|
};
|
|
|
|
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
|
|
if total_height > RSVG_MAX_SIZE || total_width > RSVG_MAX_SIZE {
|
|
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()
|
|
}
|
|
}
|