diff options
Diffstat (limited to 'librsvg-c/src/c_api/handle.rs')
-rw-r--r-- | librsvg-c/src/c_api/handle.rs | 2144 |
1 files changed, 2144 insertions, 0 deletions
diff --git a/librsvg-c/src/c_api/handle.rs b/librsvg-c/src/c_api/handle.rs new file mode 100644 index 00000000..1698cc17 --- /dev/null +++ b/librsvg-c/src/c_api/handle.rs @@ -0,0 +1,2144 @@ +//! Main API for `RsvgHandle`. +//! +//! The C API of librsvg revolves around an `RsvgHandle` GObject class, which is +//! implemented as follows: +//! +//! * [`RsvgHandle`] and [`RsvgHandleClass`] are derivatives of `GObject` and +//! `GObjectClass`. These are coded explicitly, instead of using +//! [`glib::subclass::prelude::InstanceStruct<T>`] and +//! [`glib::subclass::prelude::ClassStruct<T>`], as the structs need need to be kept +//! ABI-compatible with the traditional C API/ABI. +//! +//! * The actual data for a handle (e.g. the `RsvgHandle`'s private data, in GObject +//! parlance) is in [`CHandle`]. +//! +//! * Public C ABI functions are the `#[no_mangle]` functions with an `rsvg_` prefix. +//! +//! The C API is implemented in terms of the Rust API in `librsvg_crate`. In effect, +//! [`RsvgHandle`] is a rather convoluted builder or adapter pattern that translates all the +//! historical idiosyncrasies of the C API into the simple Rust API. + +use std::cell::{Cell, Ref, RefCell, RefMut}; +use std::ffi::{CStr, CString, OsStr}; +use std::fmt; +use std::path::PathBuf; +use std::ptr; +use std::slice; +use std::str; +use std::{f64, i32}; + +use gdk_pixbuf::Pixbuf; +use gio::prelude::*; +use glib::error::ErrorDomain; +use url::Url; + +use glib::subclass::prelude::*; +use glib::translate::*; +use glib::types::instance_of; +use glib::{ffi::gpointer, gobject_ffi}; +use glib::{Bytes, Cast, StaticType, ToValue}; + +use crate::api::{self, CairoRenderer, IntrinsicDimensions, Loader, LoadingError, SvgHandle}; + +use crate::{ + length::RsvgLength, + rsvg_log, + session::Session, + surface_utils::shared_surface::{SharedImageSurface, SurfaceType}, +}; + +use super::dpi::Dpi; +use super::messages::{rsvg_g_critical, rsvg_g_warning}; +use super::pixbuf_utils::{empty_pixbuf, pixbuf_from_surface}; +use super::sizing::LegacySize; + +// The C API exports global variables that contain the library's version number; +// those get autogenerated from `build.rs` and placed in this `version.rs` file. +include!(concat!(env!("OUT_DIR"), "/version.rs")); + +// This is basically the same as api::RenderingError but with extra cases for +// the peculiarities of the C API. +enum RenderingError { + RenderingError(api::RenderingError), + + // The RsvgHandle is created, but hasn't been loaded yet. + HandleIsNotLoaded, +} + +impl<T: Into<api::RenderingError>> From<T> for RenderingError { + fn from(e: T) -> RenderingError { + RenderingError::RenderingError(e.into()) + } +} + +impl fmt::Display for RenderingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + RenderingError::RenderingError(ref e) => e.fmt(f), + RenderingError::HandleIsNotLoaded => write!(f, "SVG data is not loaded into handle"), + } + } +} + +/// Rust version of the `RsvgHandleFlags` enum in C. +#[glib::flags(name = "RsvgHandleFlags")] +pub enum HandleFlags { + #[flags_value(name = "RSVG_HANDLE_FLAGS_NONE", nick = "flags-none")] + NONE = 0, + + #[flags_value(name = "RSVG_HANDLE_FLAG_UNLIMITED", nick = "flag-unlimited")] + UNLIMITED = 1 << 0, + + #[flags_value( + name = "RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA", + nick = "flag-keep-image-data" + )] + KEEP_IMAGE_DATA = 1 << 1, +} + +/// Type alias used to pass flags in the C API functions. +pub type RsvgHandleFlags = u32; + +/// Internal representation of the loading flags, without bitflags. +#[derive(Default, Copy, Clone)] +struct LoadFlags { + unlimited_size: bool, + keep_image_data: bool, +} + +impl From<HandleFlags> for LoadFlags { + fn from(flags: HandleFlags) -> LoadFlags { + LoadFlags { + unlimited_size: flags.contains(HandleFlags::UNLIMITED), + keep_image_data: flags.contains(HandleFlags::KEEP_IMAGE_DATA), + } + } +} + +impl From<LoadFlags> for HandleFlags { + fn from(lflags: LoadFlags) -> HandleFlags { + let mut hflags = HandleFlags::empty(); + + if lflags.unlimited_size { + hflags.insert(HandleFlags::UNLIMITED); + } + + if lflags.keep_image_data { + hflags.insert(HandleFlags::KEEP_IMAGE_DATA); + } + + hflags + } +} + +/// GObject class struct for RsvgHandle. +/// +/// This is not done through [`glib::subclass::prelude::ClassStruct<T>`] because we need +/// to include the `_abi_padding` field for ABI compatibility with the C headers, and +/// `simple::ClassStruct` does not allow that. +#[repr(C)] +pub struct RsvgHandleClass { + // Keep this in sync with rsvg.h:RsvgHandleClass + parent: gobject_ffi::GObjectClass, + + _abi_padding: [gpointer; 15], +} + +unsafe impl ClassStruct for RsvgHandleClass { + type Type = imp::CHandle; +} + +/// GObject instance struct for RsvgHandle. +/// +/// This is not done through [`glib::subclass::prelude::InstanceStruct<T>`] because we need +/// to include the `_abi_padding` field for ABI compatibility with the C headers, and +/// `simple::InstanceStruct` does not allow that. +#[repr(C)] +pub struct RsvgHandle { + // Keep this in sync with rsvg.h:RsvgHandle + parent: gobject_ffi::GObject, + + _abi_padding: [gpointer; 16], +} + +unsafe impl InstanceStruct for RsvgHandle { + type Type = imp::CHandle; +} + +/// State machine for `RsvgHandle`. +/// +/// When an `RsvgHandled` is created it is empty / not loaded yet, and it does not know +/// whether the caller will feed it data gradually with the legacy `write()/close()` API, +/// or whether it will be given a `GInputStream` to read in a blocking fashion. After the +/// handle is loaded (e.g. the SVG document is finished parsing), we make sure that no +/// further loading operations can be done. +#[allow(clippy::large_enum_variant)] +enum LoadState { + /// Just created the CHandle; nothing loaded yet. + Start, + + /// Being loaded using the legacy write()/close() API. + /// + /// We buffer all the data from `write()` calls until the time `close()` is called; + /// then we run the buffer through a decompressor in case this is an SVGZ file. + Loading { buffer: Vec<u8> }, + + /// Loading finished successfully; the document is in the `SvgHandle`. + ClosedOk { handle: SvgHandle }, + + /// Loaded unsuccessfully. + ClosedError, +} + +impl LoadState { + fn set_from_loading_result( + &mut self, + result: Result<SvgHandle, LoadingError>, + ) -> Result<(), LoadingError> { + match result { + Ok(handle) => { + *self = LoadState::ClosedOk { handle }; + Ok(()) + } + + Err(e) => { + *self = LoadState::ClosedError; + Err(e) + } + } + } +} + +impl Default for LoadState { + fn default() -> Self { + Self::Start + } +} + +/// Holds the base URL for loading a handle, and the C-accessible version of it +/// +/// There is a public API to query the base URL, and we need to +/// produce a CString with it. However, that API returns a borrowed +/// *const char, so we need to maintain a long-lived CString along with the +/// internal Url. +#[derive(Default)] +struct BaseUrl { + inner: Option<BaseUrlInner>, +} + +struct BaseUrlInner { + url: Url, + cstring: CString, +} + +impl BaseUrl { + fn set(&mut self, url: Url) { + let cstring = CString::new(url.as_str()).unwrap(); + + self.inner = Some(BaseUrlInner { url, cstring }); + } + + fn get(&self) -> Option<&Url> { + self.inner.as_ref().map(|b| &b.url) + } + + fn get_gfile(&self) -> Option<gio::File> { + self.get().map(|url| gio::File::for_uri(url.as_str())) + } + + fn get_ptr(&self) -> *const libc::c_char { + self.inner + .as_ref() + .map(|b| b.cstring.as_ptr()) + .unwrap_or_else(ptr::null) + } +} + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct RsvgRectangle { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} + +impl From<cairo::Rectangle> for RsvgRectangle { + fn from(r: cairo::Rectangle) -> RsvgRectangle { + RsvgRectangle { + x: r.x(), + y: r.y(), + width: r.width(), + height: r.height(), + } + } +} + +impl From<RsvgRectangle> for cairo::Rectangle { + fn from(r: RsvgRectangle) -> cairo::Rectangle { + cairo::Rectangle::new(r.x, r.y, r.width, r.height) + } +} + +mod imp { + use super::*; + use glib::{ParamSpec, ParamSpecDouble, ParamSpecFlags, ParamSpecInt, ParamSpecString}; + use once_cell::sync::Lazy; + + /// Contains all the interior mutability for a RsvgHandle to be called + /// from the C API. + #[derive(Default)] + pub struct CHandle { + pub(super) inner: RefCell<CHandleInner>, + pub(super) load_state: RefCell<LoadState>, + pub(super) session: Session, + } + + #[derive(Default)] + pub(super) struct CHandleInner { + pub(super) dpi: Dpi, + pub(super) load_flags: LoadFlags, + pub(super) base_url: BaseUrl, + pub(super) size_callback: SizeCallback, + pub(super) is_testing: bool, + } + + #[glib::object_subclass] + impl ObjectSubclass for CHandle { + const NAME: &'static str = "RsvgHandle"; + + type Type = super::CHandle; + + type Instance = RsvgHandle; + type Class = RsvgHandleClass; + } + + impl ObjectImpl for CHandle { + fn properties() -> &'static [ParamSpec] { + static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| { + vec![ + ParamSpecFlags::builder::<HandleFlags>("flags") + .construct_only() + .build(), + ParamSpecDouble::builder("dpi-x").construct().build(), + ParamSpecDouble::builder("dpi-y").construct().build(), + ParamSpecString::builder("base-uri").construct().build(), + ParamSpecInt::builder("width").read_only().build(), + ParamSpecInt::builder("height").read_only().build(), + ParamSpecDouble::builder("em").read_only().build(), + ParamSpecDouble::builder("ex").read_only().build(), + ParamSpecString::builder("title").read_only().build(), + ParamSpecString::builder("desc").read_only().build(), + ParamSpecString::builder("metadata").read_only().build(), + ] + }); + PROPERTIES.as_ref() + } + + fn set_property(&self, id: usize, value: &glib::Value, pspec: &ParamSpec) { + let obj = self.obj(); + match pspec.name() { + "flags" => { + let v: HandleFlags = value.get().expect("flags value has incorrect type"); + obj.set_flags(v); + } + + "dpi-x" => { + let dpi_x: f64 = value.get().expect("dpi-x value has incorrect type"); + obj.set_dpi_x(dpi_x); + } + + "dpi-y" => { + let dpi_y: f64 = value.get().expect("dpi-y value has incorrect type"); + obj.set_dpi_y(dpi_y); + } + + "base-uri" => { + let v: Option<String> = value.get().expect("base-uri value has incorrect type"); + + // rsvg_handle_set_base_uri() expects non-NULL URI strings, + // but the "base-uri" property can be set to NULL due to a missing + // construct-time property. + + if let Some(s) = v { + obj.set_base_url(&s); + } + } + + _ => unreachable!("invalid property id {}", id), + } + } + + fn property(&self, id: usize, pspec: &ParamSpec) -> glib::Value { + let obj = self.obj(); + match pspec.name() { + "flags" => obj.get_flags().to_value(), + "dpi-x" => obj.get_dpi_x().to_value(), + "dpi-y" => obj.get_dpi_y().to_value(), + "base-uri" => obj.get_base_url().to_value(), + "width" => obj.get_dimensions_or_empty().width.to_value(), + "height" => obj.get_dimensions_or_empty().height.to_value(), + "em" => obj.get_dimensions_or_empty().em.to_value(), + "ex" => obj.get_dimensions_or_empty().ex.to_value(), + + // the following three are deprecated + "title" => None::<String>.to_value(), + "desc" => None::<String>.to_value(), + "metadata" => None::<String>.to_value(), + + _ => unreachable!("invalid property id={} for RsvgHandle", id), + } + } + } +} + +glib::wrapper! { + // We don't use subclass:simple::InstanceStruct and ClassStruct + // because we need to maintain the respective _abi_padding of each + // of RsvgHandleClass and RsvgHandle. + pub struct CHandle(ObjectSubclass<imp::CHandle>); +} + +// Keep in sync with tests/src/reference.rs +pub(crate) fn checked_i32(x: f64) -> Result<i32, cairo::Error> { + cast::i32(x).map_err(|_| cairo::Error::InvalidSize) +} + +// Keep in sync with rsvg.h:RsvgPositionData +#[repr(C)] +pub struct RsvgPositionData { + pub x: libc::c_int, + pub y: libc::c_int, +} + +// Keep in sync with rsvg.h:RsvgDimensionData +#[repr(C)] +pub struct RsvgDimensionData { + pub width: libc::c_int, + pub height: libc::c_int, + pub em: f64, + pub ex: f64, +} + +impl RsvgDimensionData { + // This is not #[derive(Default)] to make it clear that it + // shouldn't be the default value for anything; it is actually a + // special case we use to indicate an error to the public API. + pub fn empty() -> RsvgDimensionData { + RsvgDimensionData { + width: 0, + height: 0, + em: 0.0, + ex: 0.0, + } + } +} + +// Keep in sync with rsvg.h:RsvgSizeFunc +pub type RsvgSizeFunc = Option< + unsafe extern "C" fn( + inout_width: *mut libc::c_int, + inout_height: *mut libc::c_int, + user_data: gpointer, + ), +>; + +struct SizeCallback { + size_func: RsvgSizeFunc, + user_data: gpointer, + destroy_notify: glib::ffi::GDestroyNotify, + in_loop: Cell<bool>, +} + +impl SizeCallback { + fn new( + size_func: RsvgSizeFunc, + user_data: gpointer, + destroy_notify: glib::ffi::GDestroyNotify, + ) -> Self { + SizeCallback { + size_func, + user_data, + destroy_notify, + in_loop: Cell::new(false), + } + } + + fn call(&self, width: libc::c_int, height: libc::c_int) -> (libc::c_int, libc::c_int) { + unsafe { + let mut w = width; + let mut h = height; + + if let Some(ref f) = self.size_func { + f(&mut w, &mut h, self.user_data); + }; + + (w, h) + } + } + + fn start_loop(&self) { + assert!(!self.in_loop.get()); + self.in_loop.set(true); + } + + fn end_loop(&self) { + assert!(self.in_loop.get()); + self.in_loop.set(false); + } + + fn get_in_loop(&self) -> bool { + self.in_loop.get() + } +} + +impl Default for SizeCallback { + fn default() -> SizeCallback { + SizeCallback { + size_func: None, + user_data: ptr::null_mut(), + destroy_notify: None, + in_loop: Cell::new(false), + } + } +} + +impl Drop for SizeCallback { + fn drop(&mut self) { + unsafe { + if let Some(ref f) = self.destroy_notify { + f(self.user_data); + }; + } + } +} +pub trait CairoRectangleExt { + fn from_size(width: f64, height: f64) -> Self; +} + +impl CairoRectangleExt for cairo::Rectangle { + fn from_size(width: f64, height: f64) -> Self { + Self::new(0.0, 0.0, width, height) + } +} + +impl CHandle { + fn set_base_url(&self, url: &str) { + let imp = self.imp(); + let session = &imp.session; + let state = imp.load_state.borrow(); + + match *state { + LoadState::Start => (), + _ => { + rsvg_g_critical( + "Please set the base file or URI before loading any data into RsvgHandle", + ); + return; + } + } + + match Url::parse(url) { + Ok(u) => { + rsvg_log!(session, "setting base_uri to \"{}\"", u.as_str()); + let mut inner = imp.inner.borrow_mut(); + inner.base_url.set(u); + } + + Err(e) => { + rsvg_log!( + session, + "not setting base_uri to \"{}\" since it is invalid: {}", + url, + e + ); + } + } + } + + fn set_base_gfile(&self, file: &gio::File) { + self.set_base_url(&file.uri()); + } + + fn get_base_url(&self) -> Option<String> { + let inner = self.imp().inner.borrow(); + inner.base_url.get().map(|url| url.as_str().to_string()) + } + + fn get_base_url_as_ptr(&self) -> *const libc::c_char { + let inner = self.imp().inner.borrow(); + inner.base_url.get_ptr() + } + + fn set_dpi_x(&self, dpi_x: f64) { + let mut inner = self.imp().inner.borrow_mut(); + let dpi = inner.dpi; + inner.dpi = Dpi::new(dpi_x, dpi.y()); + } + + fn set_dpi_y(&self, dpi_y: f64) { + let mut inner = self.imp().inner.borrow_mut(); + let dpi = inner.dpi; + inner.dpi = Dpi::new(dpi.x(), dpi_y); + } + + fn get_dpi_x(&self) -> f64 { + let inner = self.imp().inner.borrow(); + inner.dpi.x() + } + + fn get_dpi_y(&self) -> f64 { + let inner = self.imp().inner.borrow(); + inner.dpi.y() + } + + fn set_flags(&self, flags: HandleFlags) { + let mut inner = self.imp().inner.borrow_mut(); + inner.load_flags = LoadFlags::from(flags); + } + + fn get_flags(&self) -> HandleFlags { + let inner = self.imp().inner.borrow(); + HandleFlags::from(inner.load_flags) + } + + fn set_size_callback( + &self, + size_func: RsvgSizeFunc, + user_data: gpointer, + destroy_notify: glib::ffi::GDestroyNotify, + ) { + let mut inner = self.imp().inner.borrow_mut(); + inner.size_callback = SizeCallback::new(size_func, user_data, destroy_notify); + } + + fn write(&self, buf: &[u8]) { + let mut state = self.imp().load_state.borrow_mut(); + + match *state { + LoadState::Start => { + *state = LoadState::Loading { + buffer: Vec::from(buf), + } + } + + LoadState::Loading { ref mut buffer } => { + buffer.extend_from_slice(buf); + } + + _ => { + rsvg_g_critical("Handle must not be closed in order to write to it"); + } + } + } + + fn close(&self) -> Result<(), LoadingError> { + let imp = self.imp(); + + let inner = imp.inner.borrow(); + let mut state = imp.load_state.borrow_mut(); + + match *state { + LoadState::Start => { + *state = LoadState::ClosedError; + Err(LoadingError::XmlParseError(String::from( + "caller did not write any data", + ))) + } + + LoadState::Loading { ref buffer } => { + let bytes = Bytes::from(buffer); + let stream = gio::MemoryInputStream::from_bytes(&bytes); + + let base_file = inner.base_url.get_gfile(); + self.read_stream(state, &stream.upcast(), base_file.as_ref(), None) + } + + // Closing is idempotent + LoadState::ClosedOk { .. } => Ok(()), + LoadState::ClosedError => Ok(()), + } + } + + fn read_stream_sync( + &self, + stream: &gio::InputStream, + cancellable: Option<&gio::Cancellable>, + ) -> Result<(), LoadingError> { + let imp = self.imp(); + + let state = imp.load_state.borrow_mut(); + let inner = imp.inner.borrow(); + + match *state { + LoadState::Start => { + let base_file = inner.base_url.get_gfile(); + self.read_stream(state, stream, base_file.as_ref(), cancellable) + } + + LoadState::Loading { .. } | LoadState::ClosedOk { .. } | LoadState::ClosedError => { + rsvg_g_critical( + "handle must not be already loaded in order to call \ + rsvg_handle_read_stream_sync()", + ); + Err(LoadingError::Other(String::from("API ordering"))) + } + } + } + + fn read_stream( + &self, + mut load_state: RefMut<'_, LoadState>, + stream: &gio::InputStream, + base_file: Option<&gio::File>, + cancellable: Option<&gio::Cancellable>, + ) -> Result<(), LoadingError> { + let loader = self.make_loader(); + + load_state.set_from_loading_result(loader.read_stream(stream, base_file, cancellable)) + } + + fn get_handle_ref(&self) -> Result<Ref<'_, SvgHandle>, RenderingError> { + let state = self.imp().load_state.borrow(); + + match *state { + LoadState::Start => { + rsvg_g_critical("Handle has not been loaded"); + Err(RenderingError::HandleIsNotLoaded) + } + + LoadState::Loading { .. } => { + rsvg_g_critical("Handle is still loading; call rsvg_handle_close() first"); + Err(RenderingError::HandleIsNotLoaded) + } + + LoadState::ClosedError => { + rsvg_g_critical( + "Handle could not read or parse the SVG; did you check for errors during the \ + loading stage?", + ); + Err(RenderingError::HandleIsNotLoaded) + } + + LoadState::ClosedOk { .. } => Ok(Ref::map(state, |s| match *s { + LoadState::ClosedOk { ref handle } => handle, + _ => unreachable!(), + })), + } + } + + fn make_loader(&self) -> Loader { + let imp = self.imp(); + let inner = imp.inner.borrow(); + let session = imp.session.clone(); + + Loader::new_with_session(session) + .with_unlimited_size(inner.load_flags.unlimited_size) + .keep_image_data(inner.load_flags.keep_image_data) + } + + fn has_sub(&self, id: &str) -> Result<bool, RenderingError> { + let handle = self.get_handle_ref()?; + Ok(handle.has_element_with_id(id)?) + } + + fn get_dimensions_or_empty(&self) -> RsvgDimensionData { + self.get_dimensions_sub(None) + .unwrap_or_else(|_| RsvgDimensionData::empty()) + } + + fn get_dimensions_sub(&self, id: Option<&str>) -> Result<RsvgDimensionData, RenderingError> { + let inner = self.imp().inner.borrow(); + + // This function is probably called from the cairo_render functions, + // or is being erroneously called within the size_func. + // To prevent an infinite loop we are saving the state, and + // returning a meaningless size. + if inner.size_callback.get_in_loop() { + return Ok(RsvgDimensionData { + width: 1, + height: 1, + em: 1.0, + ex: 1.0, + }); + } + + inner.size_callback.start_loop(); + + let res = self + .get_geometry_sub(id) + .and_then(|(ink_r, _)| { + // Keep these in sync with tests/src/reference.rs + let width = checked_i32(ink_r.width().round())?; + let height = checked_i32(ink_r.height().round())?; + + Ok((ink_r, width, height)) + }) + .map(|(ink_r, width, height)| { + let (w, h) = inner.size_callback.call(width, height); + + RsvgDimensionData { + width: w, + height: h, + em: ink_r.width(), + ex: ink_r.height(), + } + }); + + inner.size_callback.end_loop(); + + res + } + + fn get_position_sub(&self, id: Option<&str>) -> Result<RsvgPositionData, RenderingError> { + let inner = self.imp().inner.borrow(); + + if id.is_none() { + return Ok(RsvgPositionData { x: 0, y: 0 }); + } + + self.get_geometry_sub(id) + .and_then(|(ink_r, _)| { + let width = checked_i32(ink_r.width().round())?; + let height = checked_i32(ink_r.height().round())?; + + Ok((ink_r, width, height)) + }) + .and_then(|(ink_r, width, height)| { + inner.size_callback.call(width, height); + + Ok(RsvgPositionData { + x: checked_i32(ink_r.x())?, + y: checked_i32(ink_r.y())?, + }) + }) + } + + fn make_renderer<'a>(&self, handle_ref: &'a Ref<'_, SvgHandle>) -> CairoRenderer<'a> { + let inner = self.imp().inner.borrow(); + + CairoRenderer::new(handle_ref) + .with_dpi(inner.dpi.x(), inner.dpi.y()) + .test_mode(inner.is_testing) + } + + fn get_geometry_sub( + &self, + id: Option<&str>, + ) -> Result<(cairo::Rectangle, cairo::Rectangle), RenderingError> { + let handle = self.get_handle_ref()?; + let renderer = self.make_renderer(&handle); + + Ok(renderer.legacy_layer_geometry(id)?) + } + + fn set_stylesheet(&self, css: &str) -> Result<(), LoadingError> { + match *self.imp().load_state.borrow_mut() { + LoadState::ClosedOk { ref mut handle } => handle.set_stylesheet(css), + + _ => { + rsvg_g_critical( + "handle must already be loaded in order to call \ + rsvg_handle_set_stylesheet()", + ); + Err(LoadingError::Other(String::from("API ordering"))) + } + } + } + + fn render_cairo_sub( + &self, + cr: *mut cairo::ffi::cairo_t, + id: Option<&str>, + ) -> Result<(), RenderingError> { + let dimensions = self.get_dimensions_sub(None)?; + if dimensions.width == 0 || dimensions.height == 0 { + // nothing to render + return Ok(()); + } + + let viewport = cairo::Rectangle::new( + 0.0, + 0.0, + f64::from(dimensions.width), + f64::from(dimensions.height), + ); + + self.render_layer(cr, id, &viewport) + } + + fn get_pixbuf_sub(&self, id: Option<&str>) -> Result<Pixbuf, RenderingError> { + let dimensions = self.get_dimensions_sub(None)?; + + if dimensions.width == 0 || dimensions.height == 0 { + return Ok(empty_pixbuf()?); + } + + let surface = cairo::ImageSurface::create( + cairo::Format::ARgb32, + dimensions.width, + dimensions.height, + )?; + + { + let cr = cairo::Context::new(&surface)?; + let cr_raw = cr.to_raw_none(); + self.render_cairo_sub(cr_raw, id)?; + } + + let surface = SharedImageSurface::wrap(surface, SurfaceType::SRgb)?; + + Ok(pixbuf_from_surface(&surface)?) + } + + fn render_document( + &self, + cr: *mut cairo::ffi::cairo_t, + viewport: &cairo::Rectangle, + ) -> Result<(), RenderingError> { + let cr = check_cairo_context(cr)?; + + let handle = self.get_handle_ref()?; + + let renderer = self.make_renderer(&handle); + Ok(renderer.render_document(&cr, viewport)?) + } + + fn get_geometry_for_layer( + &self, + id: Option<&str>, + viewport: &cairo::Rectangle, + ) -> Result<(RsvgRectangle, RsvgRectangle), RenderingError> { + let handle = self.get_handle_ref()?; + let renderer = self.make_renderer(&handle); + + Ok(renderer + .geometry_for_layer(id, viewport) + .map(|(i, l)| (RsvgRectangle::from(i), RsvgRectangle::from(l)))?) + } + + fn render_layer( + &self, + cr: *mut cairo::ffi::cairo_t, + id: Option<&str>, + viewport: &cairo::Rectangle, + ) -> Result<(), RenderingError> { + let cr = check_cairo_context(cr)?; + + let handle = self.get_handle_ref()?; + + let renderer = self.make_renderer(&handle); + + Ok(renderer.render_layer(&cr, id, viewport)?) + } + + fn get_geometry_for_element( + &self, + id: Option<&str>, + ) -> Result<(RsvgRectangle, RsvgRectangle), RenderingError> { + let handle = self.get_handle_ref()?; + + let renderer = self.make_renderer(&handle); + + Ok(renderer + .geometry_for_element(id) + .map(|(i, l)| (RsvgRectangle::from(i), RsvgRectangle::from(l)))?) + } + + fn render_element( + &self, + cr: *mut cairo::ffi::cairo_t, + id: Option<&str>, + element_viewport: &cairo::Rectangle, + ) -> Result<(), RenderingError> { + let cr = check_cairo_context(cr)?; + + let handle = self.get_handle_ref()?; + + let renderer = self.make_renderer(&handle); + + Ok(renderer.render_element(&cr, id, element_viewport)?) + } + + fn get_intrinsic_dimensions(&self) -> Result<IntrinsicDimensions, RenderingError> { + let handle = self.get_handle_ref()?; + let renderer = self.make_renderer(&handle); + Ok(renderer.intrinsic_dimensions()) + } + + fn get_intrinsic_size_in_pixels(&self) -> Result<Option<(f64, f64)>, RenderingError> { + let handle = self.get_handle_ref()?; + let renderer = self.make_renderer(&handle); + Ok(renderer.intrinsic_size_in_pixels()) + } + + fn set_testing(&self, is_testing: bool) { + let mut inner = self.imp().inner.borrow_mut(); + inner.is_testing = is_testing; + } +} + +fn is_rsvg_handle(obj: *const RsvgHandle) -> bool { + unsafe { instance_of::<CHandle>(obj as *const _) } +} + +fn is_input_stream(obj: *mut gio::ffi::GInputStream) -> bool { + unsafe { instance_of::<gio::InputStream>(obj as *const _) } +} + +fn is_gfile(obj: *const gio::ffi::GFile) -> bool { + unsafe { instance_of::<gio::File>(obj as *const _) } +} + +fn is_cancellable(obj: *mut gio::ffi::GCancellable) -> bool { + unsafe { instance_of::<gio::Cancellable>(obj as *const _) } +} + +fn get_rust_handle(handle: *const RsvgHandle) -> CHandle { + let handle = unsafe { &*handle }; + handle.imp().obj().to_owned() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_type() -> glib::ffi::GType { + CHandle::static_type().into_glib() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_error_get_type() -> glib::ffi::GType { + Error::static_type().into_glib() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_flags_get_type() -> glib::ffi::GType { + HandleFlags::static_type().into_glib() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_set_base_uri( + handle: *const RsvgHandle, + uri: *const libc::c_char, +) { + rsvg_return_if_fail! { + rsvg_handle_set_base_uri; + + is_rsvg_handle(handle), + !uri.is_null(), + } + + let rhandle = get_rust_handle(handle); + + assert!(!uri.is_null()); + let uri: String = from_glib_none(uri); + + rhandle.set_base_url(&uri); +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_set_base_gfile( + handle: *const RsvgHandle, + raw_gfile: *mut gio::ffi::GFile, +) { + rsvg_return_if_fail! { + rsvg_handle_set_base_gfile; + + is_rsvg_handle(handle), + is_gfile(raw_gfile), + } + + let rhandle = get_rust_handle(handle); + + assert!(!raw_gfile.is_null()); + + let file: gio::File = from_glib_none(raw_gfile); + + rhandle.set_base_gfile(&file); +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_base_uri( + handle: *const RsvgHandle, +) -> *const libc::c_char { + rsvg_return_val_if_fail! { + rsvg_handle_get_base_uri => ptr::null(); + + is_rsvg_handle(handle), + } + + let rhandle = get_rust_handle(handle); + + rhandle.get_base_url_as_ptr() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_set_dpi(handle: *const RsvgHandle, dpi: libc::c_double) { + rsvg_return_if_fail! { + rsvg_handle_set_dpi; + + is_rsvg_handle(handle), + } + + let rhandle = get_rust_handle(handle); + rhandle.set_dpi_x(dpi); + rhandle.set_dpi_y(dpi); +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_set_dpi_x_y( + handle: *const RsvgHandle, + dpi_x: libc::c_double, + dpi_y: libc::c_double, +) { + rsvg_return_if_fail! { + rsvg_handle_set_dpi_x_y; + + is_rsvg_handle(handle), + } + + let rhandle = get_rust_handle(handle); + rhandle.set_dpi_x(dpi_x); + rhandle.set_dpi_y(dpi_y); +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_set_size_callback( + handle: *const RsvgHandle, + size_func: RsvgSizeFunc, + user_data: gpointer, + destroy_notify: glib::ffi::GDestroyNotify, +) { + rsvg_return_if_fail! { + rsvg_handle_set_size_callback; + + is_rsvg_handle(handle), + } + + let rhandle = get_rust_handle(handle); + + rhandle.set_size_callback(size_func, user_data, destroy_notify); +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_internal_set_testing( + handle: *const RsvgHandle, + testing: glib::ffi::gboolean, +) { + rsvg_return_if_fail! { + rsvg_handle_internal_set_testing; + + is_rsvg_handle(handle), + } + + let rhandle = get_rust_handle(handle); + + rhandle.set_testing(from_glib(testing)); +} + +trait IntoGError { + type GlibResult; + + fn into_gerror(self, session: &Session, error: *mut *mut glib::ffi::GError) + -> Self::GlibResult; +} + +impl<E: fmt::Display> IntoGError for Result<(), E> { + type GlibResult = glib::ffi::gboolean; + + fn into_gerror( + self, + session: &Session, + error: *mut *mut glib::ffi::GError, + ) -> Self::GlibResult { + match self { + Ok(()) => true.into_glib(), + + Err(e) => { + set_gerror(session, error, 0, &format!("{e}")); + false.into_glib() + } + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_read_stream_sync( + handle: *const RsvgHandle, + stream: *mut gio::ffi::GInputStream, + cancellable: *mut gio::ffi::GCancellable, + error: *mut *mut glib::ffi::GError, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_read_stream_sync => false.into_glib(); + + is_rsvg_handle(handle), + is_input_stream(stream), + cancellable.is_null() || is_cancellable(cancellable), + error.is_null() || (*error).is_null(), + } + + let rhandle = get_rust_handle(handle); + let session = rhandle.imp().session.clone(); + + let stream = gio::InputStream::from_glib_none(stream); + let cancellable: Option<gio::Cancellable> = from_glib_none(cancellable); + + rhandle + .read_stream_sync(&stream, cancellable.as_ref()) + .into_gerror(&session, error) +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_write( + handle: *const RsvgHandle, + buf: *const u8, + count: usize, + error: *mut *mut glib::ffi::GError, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_write => false.into_glib(); + + is_rsvg_handle(handle), + error.is_null() || (*error).is_null(), + !buf.is_null() || count == 0, + } + + let rhandle = get_rust_handle(handle); + let buffer = slice::from_raw_parts(buf, count); + rhandle.write(buffer); + + true.into_glib() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_close( + handle: *const RsvgHandle, + error: *mut *mut glib::ffi::GError, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_close => false.into_glib(); + + is_rsvg_handle(handle), + error.is_null() || (*error).is_null(), + } + + let rhandle = get_rust_handle(handle); + let session = rhandle.imp().session.clone(); + + rhandle.close().into_gerror(&session, error) +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_has_sub( + handle: *const RsvgHandle, + id: *const libc::c_char, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_has_sub => false.into_glib(); + + is_rsvg_handle(handle), + } + + let rhandle = get_rust_handle(handle); + + if id.is_null() { + return false.into_glib(); + } + + let id: String = from_glib_none(id); + rhandle.has_sub(&id).unwrap_or(false).into_glib() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_render_cairo( + handle: *const RsvgHandle, + cr: *mut cairo::ffi::cairo_t, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_render_cairo => false.into_glib(); + + is_rsvg_handle(handle), + !cr.is_null(), + } + + let rhandle = get_rust_handle(handle); + let session = rhandle.imp().session.clone(); + + rhandle + .render_cairo_sub(cr, None) + .into_gerror(&session, ptr::null_mut()) +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_render_cairo_sub( + handle: *const RsvgHandle, + cr: *mut cairo::ffi::cairo_t, + id: *const libc::c_char, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_render_cairo_sub => false.into_glib(); + + is_rsvg_handle(handle), + !cr.is_null(), + } + + let rhandle = get_rust_handle(handle); + let session = rhandle.imp().session.clone(); + + let id: Option<String> = from_glib_none(id); + + rhandle + .render_cairo_sub(cr, id.as_deref()) + .into_gerror(&session, ptr::null_mut()) +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_pixbuf( + handle: *const RsvgHandle, +) -> *mut gdk_pixbuf::ffi::GdkPixbuf { + rsvg_return_val_if_fail! { + rsvg_handle_get_pixbuf => ptr::null_mut(); + + is_rsvg_handle(handle), + } + + let rhandle = get_rust_handle(handle); + + match rhandle.get_pixbuf_sub(None) { + Ok(pixbuf) => pixbuf.to_glib_full(), + Err(e) => { + let session = &rhandle.imp().session; + rsvg_log!(session, "could not render: {}", e); + ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_pixbuf_sub( + handle: *const RsvgHandle, + id: *const libc::c_char, +) -> *mut gdk_pixbuf::ffi::GdkPixbuf { + rsvg_return_val_if_fail! { + rsvg_handle_get_pixbuf_sub => ptr::null_mut(); + + is_rsvg_handle(handle), + } + + let rhandle = get_rust_handle(handle); + let id: Option<String> = from_glib_none(id); + + match rhandle.get_pixbuf_sub(id.as_deref()) { + Ok(pixbuf) => pixbuf.to_glib_full(), + Err(e) => { + let session = &rhandle.imp().session; + rsvg_log!(session, "could not render: {}", e); + ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_dimensions( + handle: *const RsvgHandle, + dimension_data: *mut RsvgDimensionData, +) { + rsvg_handle_get_dimensions_sub(handle, dimension_data, ptr::null()); +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_dimensions_sub( + handle: *const RsvgHandle, + dimension_data: *mut RsvgDimensionData, + id: *const libc::c_char, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_get_dimensions_sub => false.into_glib(); + + is_rsvg_handle(handle), + !dimension_data.is_null(), + } + + let rhandle = get_rust_handle(handle); + + let id: Option<String> = from_glib_none(id); + + match rhandle.get_dimensions_sub(id.as_deref()) { + Ok(dimensions) => { + *dimension_data = dimensions; + true.into_glib() + } + + Err(e) => { + let session = &rhandle.imp().session; + rsvg_log!(session, "could not get dimensions: {}", e); + *dimension_data = RsvgDimensionData::empty(); + false.into_glib() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_position_sub( + handle: *const RsvgHandle, + position_data: *mut RsvgPositionData, + id: *const libc::c_char, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_get_position_sub => false.into_glib(); + + is_rsvg_handle(handle), + !position_data.is_null(), + } + + let rhandle = get_rust_handle(handle); + + let id: Option<String> = from_glib_none(id); + + match rhandle.get_position_sub(id.as_deref()) { + Ok(position) => { + *position_data = position; + true.into_glib() + } + + Err(e) => { + let p = &mut *position_data; + + p.x = 0; + p.y = 0; + + let session = &rhandle.imp().session; + rsvg_log!(session, "could not get position: {}", e); + false.into_glib() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_new() -> *const RsvgHandle { + let obj = glib::Object::new::<CHandle>(); + + obj.to_glib_full() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_new_with_flags(flags: RsvgHandleFlags) -> *const RsvgHandle { + let obj = glib::Object::builder::<CHandle>() + .property("flags", &HandleFlags::from_bits_truncate(flags)) + .build(); + + obj.to_glib_full() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_new_from_file( + filename: *const libc::c_char, + error: *mut *mut glib::ffi::GError, +) -> *const RsvgHandle { + rsvg_return_val_if_fail! { + rsvg_handle_new_from_file => ptr::null(); + + !filename.is_null(), + error.is_null() || (*error).is_null(), + } + + let file = match PathOrUrl::new(filename) { + Ok(p) => p.get_gfile(), + + Err(s) => { + // Here we don't have a handle created yet, so it's fine to create a session + // to log the error message. We'll need to change this when we start logging + // API calls, so that we can log the call to rsvg_handle_new_from_file() and + // then pass *that* session to rsvg_handle_new_from_gfile_sync() below. + let session = Session::default(); + set_gerror(&session, error, 0, &s); + return ptr::null_mut(); + } + }; + + rsvg_handle_new_from_gfile_sync(file.to_glib_none().0, 0, ptr::null_mut(), error) +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_new_from_gfile_sync( + file: *mut gio::ffi::GFile, + flags: RsvgHandleFlags, + cancellable: *mut gio::ffi::GCancellable, + error: *mut *mut glib::ffi::GError, +) -> *const RsvgHandle { + rsvg_return_val_if_fail! { + rsvg_handle_new_from_gfile_sync => ptr::null(); + + is_gfile(file), + cancellable.is_null() || is_cancellable(cancellable), + error.is_null() || (*error).is_null(), + } + + let raw_handle = rsvg_handle_new_with_flags(flags); + + let rhandle = get_rust_handle(raw_handle); + let session = rhandle.imp().session.clone(); + + let file = gio::File::from_glib_none(file); + rhandle.set_base_gfile(&file); + + let cancellable: Option<gio::Cancellable> = from_glib_none(cancellable); + + let res = file + .read(cancellable.as_ref()) + .map_err(LoadingError::from) + .and_then(|stream| rhandle.read_stream_sync(&stream.upcast(), cancellable.as_ref())); + + match res { + Ok(()) => raw_handle, + + Err(e) => { + set_gerror(&session, error, 0, &format!("{e}")); + gobject_ffi::g_object_unref(raw_handle as *mut _); + ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_new_from_stream_sync( + input_stream: *mut gio::ffi::GInputStream, + base_file: *mut gio::ffi::GFile, + flags: RsvgHandleFlags, + cancellable: *mut gio::ffi::GCancellable, + error: *mut *mut glib::ffi::GError, +) -> *const RsvgHandle { + rsvg_return_val_if_fail! { + rsvg_handle_new_from_stream_sync => ptr::null(); + + is_input_stream(input_stream), + base_file.is_null() || is_gfile(base_file), + cancellable.is_null() || is_cancellable(cancellable), + error.is_null() || (*error).is_null(), + } + + let raw_handle = rsvg_handle_new_with_flags(flags); + + let rhandle = get_rust_handle(raw_handle); + let session = rhandle.imp().session.clone(); + + let base_file: Option<gio::File> = from_glib_none(base_file); + if let Some(base_file) = base_file { + rhandle.set_base_gfile(&base_file); + } + + let stream: gio::InputStream = from_glib_none(input_stream); + let cancellable: Option<gio::Cancellable> = from_glib_none(cancellable); + + match rhandle.read_stream_sync(&stream, cancellable.as_ref()) { + Ok(()) => raw_handle, + + Err(e) => { + set_gerror(&session, error, 0, &format!("{e}")); + gobject_ffi::g_object_unref(raw_handle as *mut _); + ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_new_from_data( + data: *const u8, + data_len: usize, + error: *mut *mut glib::ffi::GError, +) -> *const RsvgHandle { + rsvg_return_val_if_fail! { + rsvg_handle_new_from_data => ptr::null(); + + !data.is_null() || data_len == 0, + data_len <= std::isize::MAX as usize, + error.is_null() || (*error).is_null(), + } + + // We create the MemoryInputStream without the gtk-rs binding because of this: + // + // - The binding doesn't provide _new_from_data(). All of the binding's ways to + // put data into a MemoryInputStream involve copying the data buffer. + // + // - We can't use glib::Bytes from the binding either, for the same reason. + // + // - For now, we are using the other C-visible constructor, so we need a raw pointer to the + // stream, anyway. + + assert!(data_len <= std::isize::MAX as usize); + let data_len = data_len as isize; + + let raw_stream = gio::ffi::g_memory_input_stream_new_from_data(data as *mut u8, data_len, None); + + let ret = rsvg_handle_new_from_stream_sync( + raw_stream, + ptr::null_mut(), // base_file + 0, + ptr::null_mut(), // cancellable + error, + ); + + gobject_ffi::g_object_unref(raw_stream as *mut _); + ret +} + +unsafe fn set_out_param<T: Copy>( + out_has_param: *mut glib::ffi::gboolean, + out_param: *mut T, + value: &Option<T>, +) { + let has_value = if let Some(ref v) = *value { + if !out_param.is_null() { + *out_param = *v; + } + + true + } else { + false + }; + + if !out_has_param.is_null() { + *out_has_param = has_value.into_glib(); + } +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_free(handle: *mut RsvgHandle) { + gobject_ffi::g_object_unref(handle as *mut _); +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_set_stylesheet( + handle: *const RsvgHandle, + css: *const u8, + css_len: usize, + error: *mut *mut glib::ffi::GError, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_set_stylesheet => false.into_glib(); + + is_rsvg_handle(handle), + !css.is_null() || (css.is_null() && css_len == 0), + error.is_null() || (*error).is_null(), + } + + let rhandle = get_rust_handle(handle); + let session = rhandle.imp().session.clone(); + + let css = match (css, css_len) { + (p, 0) if p.is_null() => "", + (_, _) => { + let s = slice::from_raw_parts(css, css_len); + match str::from_utf8(s) { + Ok(s) => s, + Err(e) => { + set_gerror(&session, error, 0, &format!("CSS is not valid UTF-8: {e}")); + return false.into_glib(); + } + } + } + }; + + rhandle.set_stylesheet(css).into_gerror(&session, error) +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_intrinsic_dimensions( + handle: *const RsvgHandle, + out_has_width: *mut glib::ffi::gboolean, + out_width: *mut RsvgLength, + out_has_height: *mut glib::ffi::gboolean, + out_height: *mut RsvgLength, + out_has_viewbox: *mut glib::ffi::gboolean, + out_viewbox: *mut RsvgRectangle, +) { + rsvg_return_if_fail! { + rsvg_handle_get_intrinsic_dimensions; + + is_rsvg_handle(handle), + } + + let rhandle = get_rust_handle(handle); + + let d = rhandle + .get_intrinsic_dimensions() + .unwrap_or_else(|_| panic!("API called out of order")); + + let w = d.width; + let h = d.height; + let r = d.vbox.map(RsvgRectangle::from); + + set_out_param(out_has_width, out_width, &Into::into(w)); + set_out_param(out_has_height, out_height, &Into::into(h)); + set_out_param(out_has_viewbox, out_viewbox, &r); +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_intrinsic_size_in_pixels( + handle: *const RsvgHandle, + out_width: *mut f64, + out_height: *mut f64, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_get_intrinsic_size_in_pixels => false.into_glib(); + + is_rsvg_handle(handle), + } + + let rhandle = get_rust_handle(handle); + + let dim = rhandle + .get_intrinsic_size_in_pixels() + .unwrap_or_else(|_| panic!("API called out of order")); + + let (w, h) = dim.unwrap_or((0.0, 0.0)); + + if !out_width.is_null() { + *out_width = w; + } + + if !out_height.is_null() { + *out_height = h; + } + + dim.is_some().into_glib() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_render_document( + handle: *const RsvgHandle, + cr: *mut cairo::ffi::cairo_t, + viewport: *const RsvgRectangle, + error: *mut *mut glib::ffi::GError, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_render_document => false.into_glib(); + + is_rsvg_handle(handle), + !cr.is_null(), + !viewport.is_null(), + error.is_null() || (*error).is_null(), + } + + let rhandle = get_rust_handle(handle); + let session = rhandle.imp().session.clone(); + + rhandle + .render_document(cr, &(*viewport).into()) + .into_gerror(&session, error) +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_geometry_for_layer( + handle: *mut RsvgHandle, + id: *const libc::c_char, + viewport: *const RsvgRectangle, + out_ink_rect: *mut RsvgRectangle, + out_logical_rect: *mut RsvgRectangle, + error: *mut *mut glib::ffi::GError, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_get_geometry_for_layer => false.into_glib(); + + is_rsvg_handle(handle), + !viewport.is_null(), + error.is_null() || (*error).is_null(), + } + + let rhandle = get_rust_handle(handle); + let session = rhandle.imp().session.clone(); + + let id: Option<String> = from_glib_none(id); + + rhandle + .get_geometry_for_layer(id.as_deref(), &(*viewport).into()) + .map(|(ink_rect, logical_rect)| { + if !out_ink_rect.is_null() { + *out_ink_rect = ink_rect; + } + + if !out_logical_rect.is_null() { + *out_logical_rect = logical_rect; + } + }) + .into_gerror(&session, error) +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_render_layer( + handle: *const RsvgHandle, + cr: *mut cairo::ffi::cairo_t, + id: *const libc::c_char, + viewport: *const RsvgRectangle, + error: *mut *mut glib::ffi::GError, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_render_layer => false.into_glib(); + + is_rsvg_handle(handle), + !cr.is_null(), + !viewport.is_null(), + error.is_null() || (*error).is_null(), + } + + let rhandle = get_rust_handle(handle); + let session = rhandle.imp().session.clone(); + + let id: Option<String> = from_glib_none(id); + + rhandle + .render_layer(cr, id.as_deref(), &(*viewport).into()) + .into_gerror(&session, error) +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_geometry_for_element( + handle: *const RsvgHandle, + id: *const libc::c_char, + out_ink_rect: *mut RsvgRectangle, + out_logical_rect: *mut RsvgRectangle, + error: *mut *mut glib::ffi::GError, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_get_geometry_for_element => false.into_glib(); + + is_rsvg_handle(handle), + error.is_null() || (*error).is_null(), + } + + let rhandle = get_rust_handle(handle); + let session = rhandle.imp().session.clone(); + + let id: Option<String> = from_glib_none(id); + + rhandle + .get_geometry_for_element(id.as_deref()) + .map(|(ink_rect, logical_rect)| { + if !out_ink_rect.is_null() { + *out_ink_rect = ink_rect; + } + + if !out_logical_rect.is_null() { + *out_logical_rect = logical_rect; + } + }) + .into_gerror(&session, error) +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_render_element( + handle: *const RsvgHandle, + cr: *mut cairo::ffi::cairo_t, + id: *const libc::c_char, + element_viewport: *const RsvgRectangle, + error: *mut *mut glib::ffi::GError, +) -> glib::ffi::gboolean { + rsvg_return_val_if_fail! { + rsvg_handle_render_element => false.into_glib(); + + is_rsvg_handle(handle), + !cr.is_null(), + !element_viewport.is_null(), + error.is_null() || (*error).is_null(), + } + + let rhandle = get_rust_handle(handle); + let session = rhandle.imp().session.clone(); + + let id: Option<String> = from_glib_none(id); + + rhandle + .render_element(cr, id.as_deref(), &(*element_viewport).into()) + .into_gerror(&session, error) +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_desc(handle: *const RsvgHandle) -> *mut libc::c_char { + rsvg_return_val_if_fail! { + rsvg_handle_get_desc => ptr::null_mut(); + + is_rsvg_handle(handle), + } + + ptr::null_mut() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_metadata(handle: *const RsvgHandle) -> *mut libc::c_char { + rsvg_return_val_if_fail! { + rsvg_handle_get_metadata => ptr::null_mut(); + + is_rsvg_handle(handle), + } + + ptr::null_mut() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_handle_get_title(handle: *const RsvgHandle) -> *mut libc::c_char { + rsvg_return_val_if_fail! { + rsvg_handle_get_title => ptr::null_mut(); + + is_rsvg_handle(handle), + } + + ptr::null_mut() +} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_init() {} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_term() {} + +#[no_mangle] +pub unsafe extern "C" fn rsvg_cleanup() {} + +/// Detects whether a `*const libc::c_char` is a path or a URI +/// +/// `rsvg_handle_new_from_file()` takes a `filename` argument, and advertises +/// that it will detect either a file system path, or a proper URI. It will then use +/// `gio::File::for_path()` or `gio::File::for_uri()` as appropriate. +/// +/// This enum does the magic heuristics to figure this out. +/// +/// The `from_os_str` version is for using the same logic on rsvg-convert's command-line +/// arguments: we want `rsvg-convert http://example.com/foo.svg` to go to a URL, not to a +/// local file with that name. +#[derive(Clone, Debug)] +pub enum PathOrUrl { + Path(PathBuf), + Url(Url), +} + +impl PathOrUrl { + unsafe fn new(s: *const libc::c_char) -> Result<PathOrUrl, String> { + let cstr = CStr::from_ptr(s); + + if cstr.to_bytes().is_empty() { + return Err("invalid empty filename".to_string()); + } + + Ok(cstr + .to_str() + .map_err(|_| ()) + .and_then(Self::try_from_str) + .unwrap_or_else(|_| PathOrUrl::Path(PathBuf::from_glib_none(s)))) + } + + fn try_from_str(s: &str) -> Result<PathOrUrl, ()> { + assert!(!s.is_empty()); + + Url::parse(s).map_err(|_| ()).and_then(|url| { + if url.origin().is_tuple() || url.scheme() == "file" { + Ok(PathOrUrl::Url(url)) + } else { + Ok(PathOrUrl::Path(url.to_file_path()?)) + } + }) + } + + pub fn from_os_str(osstr: &OsStr) -> Result<PathOrUrl, String> { + if osstr.is_empty() { + return Err("invalid empty filename".to_string()); + } + + Ok(osstr + .to_str() + .ok_or(()) + .and_then(Self::try_from_str) + .unwrap_or_else(|_| PathOrUrl::Path(PathBuf::from(osstr.to_os_string())))) + } + + pub fn get_gfile(&self) -> gio::File { + match *self { + PathOrUrl::Path(ref p) => gio::File::for_path(p), + PathOrUrl::Url(ref u) => gio::File::for_uri(u.as_str()), + } + } +} + +impl fmt::Display for PathOrUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + PathOrUrl::Path(ref p) => p.display().fmt(f), + PathOrUrl::Url(ref u) => u.fmt(f), + } + } +} + +fn check_cairo_context(cr: *mut cairo::ffi::cairo_t) -> Result<cairo::Context, RenderingError> { + let status = unsafe { cairo::ffi::cairo_status(cr) }; + + if status == cairo::ffi::STATUS_SUCCESS { + Ok(unsafe { from_glib_none(cr) }) + } else { + let status: cairo::Error = status.into(); + + let msg = format!("cannot render on a cairo_t with a failure status (status={status:?})"); + + rsvg_g_warning(&msg); + + Err(RenderingError::from(status)) + } +} + +pub(crate) fn set_gerror( + session: &Session, + err: *mut *mut glib::ffi::GError, + code: u32, + msg: &str, +) { + unsafe { + // this is RSVG_ERROR_FAILED, the only error code available in RsvgError + assert!(code == 0); + + // Log this, in case the calling program passes a NULL GError, so we can at least + // diagnose things by asking for RSVG_LOG. + // + // See https://gitlab.gnome.org/GNOME/gtk/issues/2294 for an example of code that + // passed a NULL GError and so we had no easy way to see what was wrong. + rsvg_log!(session, "{}", msg); + + glib::ffi::g_set_error_literal( + err, + rsvg_error_quark(), + code as libc::c_int, + msg.to_glib_none().0, + ); + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy, glib::Enum)] +#[repr(u32)] +#[enum_type(name = "RsvgError")] +enum Error { + #[enum_value(name = "RSVG_ERROR_FAILED", nick = "failed")] + // Keep in sync with rsvg.h:RsvgError + Failed = 0, +} + +/// Used as a generic error to translate to glib::Error +/// +/// This type implements `glib::error::ErrorDomain`, so it can be used +/// to obtain the error code while calling `glib::Error::new()`. Unfortunately +/// the public librsvg API does not have detailed error codes yet, so we use +/// this single value as the only possible error code to return. +#[derive(Copy, Clone)] +struct RsvgError; + +impl ErrorDomain for RsvgError { + fn domain() -> glib::Quark { + glib::Quark::from_str("rsvg-error-quark") + } + + fn code(self) -> i32 { + Error::Failed as i32 + } + + fn from(_code: i32) -> Option<Self> { + // We don't have enough information from glib error codes + Some(RsvgError) + } +} + +#[no_mangle] +pub extern "C" fn rsvg_error_quark() -> glib::ffi::GQuark { + RsvgError::domain().into_glib() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn path_or_url_unix() { + unsafe { + match PathOrUrl::new(rsvg_c_str!("/foo/bar")).unwrap() { + PathOrUrl::Path(_) => (), + _ => panic!("unix filename should be a PathOrUrl::Path"), + } + + match PathOrUrl::new(rsvg_c_str!("foo/bar")).unwrap() { + PathOrUrl::Path(_) => (), + _ => panic!("unix filename should be a PathOrUrl::Path"), + } + } + } + + #[test] + fn path_or_url_windows() { + unsafe { + match PathOrUrl::new(rsvg_c_str!("c:/foo/bar")).unwrap() { + PathOrUrl::Path(_) => (), + _ => panic!("windows filename should be a PathOrUrl::Path"), + } + + match PathOrUrl::new(rsvg_c_str!("C:/foo/bar")).unwrap() { + PathOrUrl::Path(_) => (), + _ => panic!("windows filename should be a PathOrUrl::Path"), + } + + match PathOrUrl::new(rsvg_c_str!("c:\\foo\\bar")).unwrap() { + PathOrUrl::Path(_) => (), + _ => panic!("windows filename should be a PathOrUrl::Path"), + } + + match PathOrUrl::new(rsvg_c_str!("C:\\foo\\bar")).unwrap() { + PathOrUrl::Path(_) => (), + _ => panic!("windows filename should be a PathOrUrl::Path"), + } + } + } + + #[test] + fn path_or_url_unix_url() { + unsafe { + match PathOrUrl::new(rsvg_c_str!("file:///foo/bar")).unwrap() { + PathOrUrl::Url(_) => (), + _ => panic!("file:// unix filename should be a PathOrUrl::Url"), + } + } + } + + #[test] + fn path_or_url_windows_url() { + unsafe { + match PathOrUrl::new(rsvg_c_str!("file://c:/foo/bar")).unwrap() { + PathOrUrl::Url(_) => (), + _ => panic!("file:// windows filename should be a PathOrUrl::Url"), + } + + match PathOrUrl::new(rsvg_c_str!("file://C:/foo/bar")).unwrap() { + PathOrUrl::Url(_) => (), + _ => panic!("file:// windows filename should be a PathOrUrl::Url"), + } + } + } + + #[test] + fn path_or_url_empty_str() { + unsafe { + assert!(PathOrUrl::new(rsvg_c_str!("")).is_err()); + } + + assert!(PathOrUrl::from_os_str(OsStr::new("")).is_err()); + } + + #[test] + fn base_url_works() { + let mut u = BaseUrl::default(); + + assert!(u.get().is_none()); + assert_eq!(u.get_ptr(), ptr::null()); + + u.set(Url::parse("file:///example.txt").unwrap()); + + assert_eq!(u.get().unwrap().as_str(), "file:///example.txt"); + + unsafe { + let p = u.get_ptr(); + let cstr = CStr::from_ptr(p); + assert_eq!(cstr.to_str().unwrap(), "file:///example.txt"); + } + } +} |