decoder: Use glycin for decoding SVGs as well

Part-of: <https://gitlab.gnome.org/GNOME/loupe/-/merge_requests/267>
This commit is contained in:
Sophie Herold 2023-08-04 13:20:41 +02:00
parent 0aff4ef350
commit a027513447
9 changed files with 140 additions and 978 deletions

754
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,8 +16,10 @@ disable-glycin-sandbox = []
anyhow = { version = "1.0.66", features = ["backtrace"] } anyhow = { version = "1.0.66", features = ["backtrace"] }
arc-swap = "1.6.0" arc-swap = "1.6.0"
async-channel = "1.8.0" async-channel = "1.8.0"
async-std = "1.12.0"
env_logger = "0.10.0" env_logger = "0.10.0"
futures = "0.3.25" futures = "0.3.25"
glycin = "0.1.0-beta.1"
gvdb-macros = "0.1.6" gvdb-macros = "0.1.6"
indexmap = "2.0.0" indexmap = "2.0.0"
kamadak-exif = "0.5.5" kamadak-exif = "0.5.5"
@ -26,10 +28,6 @@ log = "0.4.17"
once_cell = "1.16.0" once_cell = "1.16.0"
url = "2.3.1" url = "2.3.1"
# decoders
rsvg = { git = "https://gitlab.gnome.org/GNOME/librsvg.git", rev = "81b58cc1368ab10f6de3439c8b1276c40b8d4286" }
glycin = "0.1.0-beta.0"
[dependencies.ashpd] [dependencies.ashpd]
version = "0.5.0" version = "0.5.0"
features = ["gtk4"] features = ["gtk4"]

View file

@ -156,7 +156,7 @@
{ {
"type" : "git", "type" : "git",
"url" : "https://gitlab.gnome.org/sophie-h/glycin.git", "url" : "https://gitlab.gnome.org/sophie-h/glycin.git",
"commit" : "897032e50f38370881fb2eb07c3425084c188eff" "tag" : "0.1.beta"
} }
] ]
}, },

View file

@ -51,10 +51,7 @@ impl Glycin {
let (next_frame_send, next_frame_recv) = async_channel::bounded(2); let (next_frame_send, next_frame_recv) = async_channel::bounded(2);
glib::MainContext::default().spawn(async move { updater.clone().spawn_error_handled(async move {
let update_sender = updater.clone();
let result: Result<(), glycin::Error> = async move {
let mut image_request = glycin::ImageRequest::new(file); let mut image_request = glycin::ImageRequest::new(file);
#[cfg(feature = "disable-glycin-sandbox")] #[cfg(feature = "disable-glycin-sandbox")]
@ -132,15 +129,6 @@ impl Glycin {
} }
Ok(()) Ok(())
}
.await;
if let Err(err) = result {
if err.unsupported_format().is_some() {
update_sender.send(DecoderUpdate::UnsupportedFormat);
}
update_sender.send(DecoderUpdate::Error(err.into()));
}
}); });
Self { Self {

View file

@ -54,7 +54,7 @@ impl std::fmt::Display for ImageFormat {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub enum ImageDimensionDetails { pub enum ImageDimensionDetails {
Svg((rsvg::Length, rsvg::Length)), Svg(String),
#[default] #[default]
None, None,
} }

View file

@ -23,6 +23,7 @@ use crate::deps::*;
use anyhow::Context; use anyhow::Context;
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use async_channel as mpsc;
use gtk::prelude::*; use gtk::prelude::*;
use std::sync::Arc; use std::sync::Arc;
@ -33,7 +34,7 @@ pub const RSVG_MAX_SIZE: u32 = 32_767;
#[derive(Debug)] #[derive(Debug)]
pub struct Svg { pub struct Svg {
thread_handle: std::thread::JoinHandle<()>, wakeup: mpsc::Sender<()>,
current_request: Arc<std::sync::RwLock<Request>>, current_request: Arc<std::sync::RwLock<Request>>,
cancellable: gio::Cancellable, cancellable: gio::Cancellable,
} }
@ -54,7 +55,7 @@ impl Request {
impl Drop for Svg { impl Drop for Svg {
fn drop(&mut self) { fn drop(&mut self) {
self.cancellable.cancel(); self.cancellable.cancel();
self.thread_handle.thread().unpark(); let _ = self.wakeup.try_send(());
} }
} }
@ -68,22 +69,37 @@ impl Svg {
let request_store = current_request.clone(); let request_store = current_request.clone();
let cancellable = gio::Cancellable::new(); let cancellable = gio::Cancellable::new();
let cancellable_ = cancellable.clone(); let cancellable_ = cancellable.clone();
let (wakeup, wakeup_resv) = mpsc::unbounded();
let thread_handle = updater.spawn_error_handled(move || { updater.clone().spawn_error_handled(async move {
let handle = rsvg::Loader::new().read_file(&file, Some(&cancellable))?; let mut image_request = glycin::ImageRequest::new(file);
let renderer = rsvg::CairoRenderer::new(&handle);
let (original_width, original_height) = svg_dimensions(&renderer); #[cfg(feature = "disable-glycin-sandbox")]
image_request.sandbox_mechanism(Some(glycin::SandboxMechanism::NotSandboxed));
image_request.cancellable(cancellable.clone());
let image = image_request.request().await?;
let dimensions = if let Some(string) = image.info().dimensions_text.as_ref() {
ImageDimensionDetails::Svg(string.to_string())
} else {
ImageDimensionDetails::None
};
let intrisic_dimensions = renderer.intrinsic_dimensions();
tiles.set_original_dimensions_full( tiles.set_original_dimensions_full(
(original_width, original_height), (image.info().width, image.info().height),
ImageDimensionDetails::Svg((intrisic_dimensions.width, intrisic_dimensions.height)), dimensions,
); );
updater.send(DecoderUpdate::Format(ImageFormat::new(
image.mime_type(),
image.format_name(),
)));
loop { loop {
let tile_request = { let tile_request = {
let mut request = request_store.write().ok().context("RwLock is poisoned")?; let mut request = request_store.write().unwrap();
let value = request.clone(); let value = request.clone();
*request = Request::None; *request = Request::None;
value value
@ -96,7 +112,10 @@ impl Svg {
match tile_request { match tile_request {
Request::None => { Request::None => {
std::thread::park(); let result = wakeup_resv.recv().await;
if result.is_err() {
return Ok(());
}
} }
Request::Tile(tile_request) => { Request::Tile(tile_request) => {
@ -106,12 +125,7 @@ impl Svg {
let relevant_tiles = tiling.relevant_tiles(&tile_request.area); let relevant_tiles = tiling.relevant_tiles(&tile_request.area);
for tile_instructions in relevant_tiles { for tile_instructions in relevant_tiles {
if request_store if request_store.read().ok().unwrap().is_waiting() {
.read()
.ok()
.context("RwLock is poisoned")?
.is_waiting()
{
break; break;
} }
@ -122,44 +136,27 @@ impl Svg {
continue; 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)?;
let (total_width, total_height) = let (total_width, total_height) =
tile_instructions.tiling.scaled_dimensions(); tile_instructions.tiling.scaled_dimensions();
let area = tile_instructions.area_with_bleed();
// librsvg does not currently support larger images let frame_request = glycin::FrameRequest::new()
if total_height > RSVG_MAX_SIZE || total_width > RSVG_MAX_SIZE { .scale(total_width, total_height)
continue; .clip(
} area.x() as u32,
area.y() as u32,
area.width() as u32,
area.height() as u32,
);
renderer let frame = image.specific_frame(frame_request).await?;
.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 position = ( let position = (
tile_instructions.area.x() as u32, tile_instructions.area.x() as u32,
tile_instructions.area.y() as u32, tile_instructions.area.y() as u32,
); );
let texture = decoded_image.into_texture()?;
tiles.push_tile(tiling, position, texture); tiles.push_tile(tiling, position, frame.texture);
} }
} }
} }
@ -169,7 +166,7 @@ impl Svg {
}); });
Self { Self {
thread_handle, wakeup,
current_request, current_request,
cancellable: cancellable_, cancellable: cancellable_,
} }
@ -182,90 +179,22 @@ impl Svg {
.ok() .ok()
.context("RwLock is poisoned")?; .context("RwLock is poisoned")?;
*current_request = Request::Tile(request); *current_request = Request::Tile(request);
self.thread_handle.thread().unpark(); self.wakeup.try_send(())?;
Ok(()) Ok(())
} }
pub fn render_print( pub fn render_print(file: &gio::File, width: i32, height: i32) -> anyhow::Result<gdk::Texture> {
file: &gio::File, #[allow(unused_mut)]
width: i32, let mut image_request = glycin::ImageRequest::new(file.clone());
height: i32,
) -> anyhow::Result<cairo::ImageSurface> {
let handle = rsvg::Loader::new().read_file(file, gio::Cancellable::NONE)?;
let renderer = rsvg::CairoRenderer::new(&handle);
let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height)?;
let context = cairo::Context::new(&surface)?;
renderer
.render_document(
&context,
&cairo::Rectangle::new(0., 0., width as f64, height as f64),
)
.context("Failed to render image")?;
drop(context);
Ok(surface) #[cfg(feature = "disable-glycin-sandbox")]
} image_request.sandbox_mechanism(Some(glycin::SandboxMechanism::NotSandboxed));
}
pub fn svg_dimensions(renderer: &rsvg::CairoRenderer) -> (u32, u32) { let image = async_std::task::block_on(image_request.request())?;
if let Some((width, height)) = renderer.intrinsic_size_in_pixels() { let frame_request = glycin::FrameRequest::new().scale(width as u32, height as u32);
(width.ceil() as u32, height.ceil() as u32) let frame = async_std::task::block_on(image.specific_frame(frame_request))?;
} 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 { Ok(frame.texture)
surface: cairo::ImageSurface,
}
impl Decoded {
pub fn into_texture(self) -> anyhow::Result<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()
.context("Cairo surface already taken")?
.to_vec(),
);
Ok(gdk::MemoryTexture::new(width, height, memory_format, &bytes, stride).upcast())
} }
} }

View file

@ -80,14 +80,21 @@ impl UpdateSender {
} }
/// Send occuring errors to renderer /// Send occuring errors to renderer
pub fn spawn_error_handled<F>(&self, f: F) -> std::thread::JoinHandle<()> pub fn spawn_error_handled<F>(&self, f: F) -> glib::JoinHandle<()>
where where
F: FnOnce() -> Result<(), anyhow::Error> + Send + 'static, F: std::future::Future<Output = Result<(), glycin::Error>> + Send + 'static,
{ {
let update_sender = self.clone(); let update_sender = self.clone();
std::thread::spawn(move || { glib::MainContext::default().spawn(async move {
if let Err(err) = f() { let update_sender = update_sender.clone();
update_sender.send(DecoderUpdate::Error(err));
let result: Result<(), glycin::Error> = f.await;
if let Err(err) = result {
if err.unsupported_format().is_some() {
update_sender.send(DecoderUpdate::UnsupportedFormat);
}
update_sender.send(DecoderUpdate::Error(err.into()));
} }
}) })
} }

View file

@ -847,7 +847,7 @@ impl LpPrint {
let texture_scale = self.user_width() / orig_width as f64; let texture_scale = self.user_width() / orig_width as f64;
let cairo_scale = cairo_dpi / self.dpi(); let cairo_scale = cairo_dpi / self.dpi();
let cairo_surface = if image.format().map_or(false, |x| x.is_svg()) { let texture = if image.format().map_or(false, |x| x.is_svg()) {
// Render SVG to exact needed sizes // Render SVG to exact needed sizes
// TODO: This should be async // TODO: This should be async
decoder::formats::Svg::render_print( decoder::formats::Svg::render_print(
@ -857,8 +857,10 @@ impl LpPrint {
) )
.unwrap() .unwrap()
} else { } else {
let texture = image.print_data(texture_scale).unwrap(); image.print_data(texture_scale).unwrap()
};
let cairo_surface = {
let mut downloader = gdk::TextureDownloader::new(&texture); let mut downloader = gdk::TextureDownloader::new(&texture);
downloader.set_format(gdk::MemoryFormat::B8g8r8a8Premultiplied); downloader.set_format(gdk::MemoryFormat::B8g8r8a8Premultiplied);

View file

@ -306,31 +306,7 @@ impl LpPropertiesView {
fn image_size(&self, _image_size: Option<glib::Variant>) -> String { fn image_size(&self, _image_size: Option<glib::Variant>) -> String {
if let Some(image) = self.image() { if let Some(image) = self.image() {
match image.dimension_details() { match image.dimension_details() {
ImageDimensionDetails::Svg((width, height)) ImageDimensionDetails::Svg(string) => string,
if width.unit != rsvg::LengthUnit::Px
&& height.unit != rsvg::LengthUnit::Px =>
{
// Percent is not stored as percentile
let width_factor = if width.unit == rsvg::LengthUnit::Percent {
100.
} else {
1.
};
let height_factor = if height.unit == rsvg::LengthUnit::Percent {
100.
} else {
1.
};
// Only show two digits
let width_n = (width.length * width_factor * 100.).round() / 100.;
let height_n = (height.length * height_factor * 100.).round() / 100.;
let width_unit = width.unit;
let height_unit = height.unit;
format!("{width_n}\u{202F}{width_unit} \u{D7} {height_n}\u{202F}{height_unit}")
}
_ => { _ => {
let (width, height) = image.image_size(); let (width, height) = image.image_size();
if width > 0 && height > 0 { if width > 0 && height > 0 {