#![warn(missing_docs)]
use std::fmt;
pub use crate::{
accept_language::{AcceptLanguage, Language},
error::{ImplementationLimit, LoadingError},
length::{LengthUnit, RsvgLength as Length},
};
use url::Url;
use std::path::Path;
use std::sync::Arc;
use gio::prelude::*; use gio::Cancellable;
use locale_config::{LanguageRange, Locale};
use crate::{
accept_language::{LanguageTags, UserLanguage},
dpi::Dpi,
error::InternalRenderingError,
handle::{Handle, LoadOptions},
rsvg_log,
session::Session,
url_resolver::UrlResolver,
};
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum RenderingError {
Rendering(String),
LimitExceeded(ImplementationLimit),
IdNotFound,
InvalidId(String),
OutOfMemory(String),
}
impl std::error::Error for RenderingError {}
impl From<cairo::Error> for RenderingError {
fn from(e: cairo::Error) -> RenderingError {
RenderingError::Rendering(format!("{e:?}"))
}
}
impl From<InternalRenderingError> for RenderingError {
fn from(e: InternalRenderingError) -> RenderingError {
match e {
InternalRenderingError::Rendering(s) => RenderingError::Rendering(s),
InternalRenderingError::LimitExceeded(l) => RenderingError::LimitExceeded(l),
InternalRenderingError::InvalidTransform => {
RenderingError::Rendering("invalid transform".to_string())
}
InternalRenderingError::IdNotFound => RenderingError::IdNotFound,
InternalRenderingError::InvalidId(s) => RenderingError::InvalidId(s),
InternalRenderingError::OutOfMemory(s) => RenderingError::OutOfMemory(s),
}
}
}
impl fmt::Display for RenderingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
RenderingError::Rendering(ref s) => write!(f, "rendering error: {s}"),
RenderingError::LimitExceeded(ref l) => write!(f, "{l}"),
RenderingError::IdNotFound => write!(f, "element id not found"),
RenderingError::InvalidId(ref s) => write!(f, "invalid id: {s:?}"),
RenderingError::OutOfMemory(ref s) => write!(f, "out of memory: {s}"),
}
}
}
pub struct Loader {
unlimited_size: bool,
keep_image_data: bool,
session: Session,
}
impl Loader {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
unlimited_size: false,
keep_image_data: false,
session: Session::default(),
}
}
#[cfg(feature = "c-api")]
pub fn new_with_session(session: Session) -> Self {
Self {
unlimited_size: false,
keep_image_data: false,
session,
}
}
pub fn with_unlimited_size(mut self, unlimited: bool) -> Self {
self.unlimited_size = unlimited;
self
}
pub fn keep_image_data(mut self, keep: bool) -> Self {
self.keep_image_data = keep;
self
}
pub fn read_path<P: AsRef<Path>>(self, path: P) -> Result<SvgHandle, LoadingError> {
let file = gio::File::for_path(path);
self.read_file(&file, None::<&Cancellable>)
}
pub fn read_file<F: IsA<gio::File>, P: IsA<Cancellable>>(
self,
file: &F,
cancellable: Option<&P>,
) -> Result<SvgHandle, LoadingError> {
let stream = file.read(cancellable)?;
self.read_stream(&stream, Some(file), cancellable)
}
pub fn read_stream<S: IsA<gio::InputStream>, F: IsA<gio::File>, P: IsA<Cancellable>>(
self,
stream: &S,
base_file: Option<&F>,
cancellable: Option<&P>,
) -> Result<SvgHandle, LoadingError> {
let base_file = base_file.map(|f| f.as_ref());
let base_url = if let Some(base_file) = base_file {
Some(url_from_file(base_file)?)
} else {
None
};
let load_options = LoadOptions::new(UrlResolver::new(base_url))
.with_unlimited_size(self.unlimited_size)
.keep_image_data(self.keep_image_data);
Ok(SvgHandle {
handle: Handle::from_stream(
self.session.clone(),
Arc::new(load_options),
stream.as_ref(),
cancellable.map(|c| c.as_ref()),
)?,
session: self.session,
})
}
}
fn url_from_file(file: &gio::File) -> Result<Url, LoadingError> {
Url::parse(&file.uri()).map_err(|_| LoadingError::BadUrl)
}
pub struct SvgHandle {
session: Session,
pub(crate) handle: Handle,
}
impl SvgHandle {
pub fn has_element_with_id(&self, id: &str) -> Result<bool, RenderingError> {
Ok(self.handle.has_sub(id)?)
}
pub fn set_stylesheet(&mut self, css: &str) -> Result<(), LoadingError> {
self.handle.set_stylesheet(css)
}
}
pub struct CairoRenderer<'a> {
pub(crate) handle: &'a SvgHandle,
pub(crate) dpi: Dpi,
user_language: UserLanguage,
is_testing: bool,
}
const DEFAULT_DPI_X: f64 = 96.0;
const DEFAULT_DPI_Y: f64 = 96.0;
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct IntrinsicDimensions {
pub width: Length,
pub height: Length,
pub vbox: Option<cairo::Rectangle>,
}
fn locale_from_environment() -> Locale {
let mut locale = Locale::invariant();
for name in glib::language_names() {
let name = name.as_str();
if let Ok(range) = LanguageRange::from_unix(name) {
locale.add(&range);
}
}
locale
}
impl UserLanguage {
fn new(language: &Language, session: &Session) -> UserLanguage {
match *language {
Language::FromEnvironment => UserLanguage::LanguageTags(
LanguageTags::from_locale(&locale_from_environment())
.map_err(|s| {
rsvg_log!(session, "could not convert locale to language tags: {}", s);
})
.unwrap_or_else(|_| LanguageTags::empty()),
),
Language::AcceptLanguage(ref a) => UserLanguage::AcceptLanguage(a.clone()),
}
}
}
impl<'a> CairoRenderer<'a> {
pub fn new(handle: &'a SvgHandle) -> Self {
let session = &handle.session;
CairoRenderer {
handle,
dpi: Dpi::new(DEFAULT_DPI_X, DEFAULT_DPI_Y),
user_language: UserLanguage::new(&Language::FromEnvironment, session),
is_testing: false,
}
}
pub fn with_dpi(self, dpi_x: f64, dpi_y: f64) -> Self {
assert!(dpi_x > 0.0);
assert!(dpi_y > 0.0);
CairoRenderer {
dpi: Dpi::new(dpi_x, dpi_y),
..self
}
}
pub fn with_language(self, language: &Language) -> Self {
let user_language = UserLanguage::new(language, &self.handle.session);
CairoRenderer {
user_language,
..self
}
}
pub fn intrinsic_dimensions(&self) -> IntrinsicDimensions {
let d = self.handle.handle.get_intrinsic_dimensions();
IntrinsicDimensions {
width: Into::into(d.width),
height: Into::into(d.height),
vbox: d.vbox.map(|v| cairo::Rectangle::from(*v)),
}
}
pub fn intrinsic_size_in_pixels(&self) -> Option<(f64, f64)> {
let dim = self.intrinsic_dimensions();
let width = dim.width;
let height = dim.height;
if width.unit == LengthUnit::Percent || height.unit == LengthUnit::Percent {
return None;
}
Some(self.handle.handle.width_height_to_user(self.dpi))
}
pub fn render_document(
&self,
cr: &cairo::Context,
viewport: &cairo::Rectangle,
) -> Result<(), RenderingError> {
Ok(self.handle.handle.render_document(
cr,
viewport,
&self.user_language,
self.dpi,
self.is_testing,
)?)
}
pub fn geometry_for_layer(
&self,
id: Option<&str>,
viewport: &cairo::Rectangle,
) -> Result<(cairo::Rectangle, cairo::Rectangle), RenderingError> {
Ok(self.handle.handle.get_geometry_for_layer(
id,
viewport,
&self.user_language,
self.dpi,
self.is_testing,
)?)
}
pub fn render_layer(
&self,
cr: &cairo::Context,
id: Option<&str>,
viewport: &cairo::Rectangle,
) -> Result<(), RenderingError> {
Ok(self.handle.handle.render_layer(
cr,
id,
viewport,
&self.user_language,
self.dpi,
self.is_testing,
)?)
}
pub fn geometry_for_element(
&self,
id: Option<&str>,
) -> Result<(cairo::Rectangle, cairo::Rectangle), RenderingError> {
Ok(self.handle.handle.get_geometry_for_element(
id,
&self.user_language,
self.dpi,
self.is_testing,
)?)
}
pub fn render_element(
&self,
cr: &cairo::Context,
id: Option<&str>,
element_viewport: &cairo::Rectangle,
) -> Result<(), RenderingError> {
Ok(self.handle.handle.render_element(
cr,
id,
element_viewport,
&self.user_language,
self.dpi,
self.is_testing,
)?)
}
#[doc(hidden)]
#[cfg(feature = "c-api")]
pub fn dpi(&self) -> Dpi {
self.dpi
}
#[doc(hidden)]
#[cfg(feature = "c-api")]
pub fn handle(&self) -> &Handle {
&self.handle.handle
}
#[doc(hidden)]
#[cfg(feature = "c-api")]
pub fn test_mode(self, is_testing: bool) -> Self {
CairoRenderer { is_testing, ..self }
}
}