librsvg source for verification 2026-05-22

This commit is contained in:
2026-05-22 16:45:08 +08:00
commit 75af7ac721
2138 changed files with 161177 additions and 0 deletions

1
librsvg-c/COPYING.LIB Symbolic link
View File

@@ -0,0 +1 @@
../COPYING.LIB

39
librsvg-c/Cargo.toml Normal file
View File

@@ -0,0 +1,39 @@
[package]
name = "librsvg-c"
version.workspace = true
authors.workspace = true
description.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
edition.workspace = true
rust-version.workspace = true
[dependencies]
cairo-rs = { workspace = true, features = ["v1_16", "png", "pdf", "ps", "svg"] }
cast.workspace = true
float-cmp.workspace = true
gdk-pixbuf = { workspace = true, optional = true }
gio.workspace = true
glib.workspace = true
libc.workspace = true
librsvg = { workspace = true, features = ["capi"] }
rgb = { workspace = true, features = ["argb"] }
url.workspace = true
[build-dependencies]
regex.workspace = true
[features]
avif = ["librsvg/avif"]
capi = ["librsvg/capi"]
pixbuf = ["dep:gdk-pixbuf"]
[package.metadata.capi]
min_version = "0.10.10"
[package.metadata.capi.library]
name = "rsvg_2"
[package.metadata.capi.header]
enabled = false

88
librsvg-c/build.rs Normal file
View File

@@ -0,0 +1,88 @@
use regex::Regex;
use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader, Result, Write};
use std::path::Path;
fn main() {
let version = read_version().expect("could not parse version from meson.build");
write_version_rs(&version);
}
struct Version {
major: String,
minor: String,
micro: String,
}
fn read_version() -> Result<Version> {
{
let output =
Path::new(&env::var("CARGO_MANIFEST_DIR").expect("Manifest directory unknown"))
.parent()
.expect("Invalid manifest path")
.join("meson.build");
let file = File::open(output)?;
// This function reads one of the build configuration files (meson.build) and scans
// it for the package's version number.
//
// The start of meson.build should contain this:
//
// project('librsvg',
// 'rust',
// 'c',
// version: '2.53.0',
// meson_version: '>= 0.59')
//
// This regex looks for the "version" line.
let regex = Regex::new(r#"^\s+version: '(\d+\.\d+\.\d+)'"#).unwrap();
for line in BufReader::new(file).lines() {
match line {
Ok(line) => {
if let Some(caps) = regex.captures(&line) {
let version_str = &caps[1];
let mut components = version_str.split('.');
let major = components.next().unwrap().to_string();
let minor = components.next().unwrap().to_string();
let micro = components.next().unwrap().to_string();
return Ok(Version {
major,
minor,
micro,
});
}
}
Err(e) => return Err(e),
}
}
}
panic!("Version not found in meson.build");
}
fn write_version_rs(version: &Version) {
let output = Path::new(&env::var("OUT_DIR").unwrap()).join("version.rs");
let mut file = File::create(output).expect("open version.rs for writing");
file.write_all(
format!(
r#"
use std::os::raw::c_uint;
#[unsafe(no_mangle)]
pub static rsvg_major_version: c_uint = {};
#[unsafe(no_mangle)]
pub static rsvg_minor_version: c_uint = {};
#[unsafe(no_mangle)]
pub static rsvg_micro_version: c_uint = {};
"#,
version.major, version.minor, version.micro,
)
.as_bytes(),
)
.expect("write version.rs");
}

11
librsvg-c/meson.build Normal file
View File

@@ -0,0 +1,11 @@
librsvg_c_sources = files(
'Cargo.toml',
'build.rs',
'src/dpi.rs',
'src/handle.rs',
'src/lib.rs',
'src/messages.rs',
'src/pixbuf_utils.rs',
'src/sizing.rs',
'tests/legacy_sizing.rs',
)

71
librsvg-c/src/dpi.rs Normal file
View File

@@ -0,0 +1,71 @@
//! Legacy C API for setting a default DPI (dots per inch = DPI).
//!
//! There are two deprecated functions, `rsvg_set_default_dpi` and
//! `rsvg_set_default_dpi_x_y`, which set global values for the default DPI to be used
//! with `RsvgHandle`. In turn, `RsvgHandle` assumes that when its own DPI value is set
//! to `0.0` (which is in fact its default), it will fall back to the global DPI.
//!
//! This is clearly not thread-safe, but it is the legacy behavior.
//!
//! This module encapsulates that behavior so that the `rsvg_internals` crate
//! can always have immutable DPI values as intended.
// This is configurable at runtime
const DEFAULT_DPI_X: f64 = 90.0;
const DEFAULT_DPI_Y: f64 = 90.0;
static mut DPI_X: f64 = DEFAULT_DPI_X;
static mut DPI_Y: f64 = DEFAULT_DPI_Y;
#[derive(Debug, Copy, Clone, Default)]
pub(crate) struct Dpi {
x: f64,
y: f64,
}
impl Dpi {
pub(crate) fn new(x: f64, y: f64) -> Dpi {
Dpi { x, y }
}
pub(crate) fn x(&self) -> f64 {
if self.x <= 0.0 {
unsafe { DPI_X }
} else {
self.x
}
}
pub(crate) fn y(&self) -> f64 {
if self.y <= 0.0 {
unsafe { DPI_Y }
} else {
self.y
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rsvg_set_default_dpi_x_y(dpi_x: libc::c_double, dpi_y: libc::c_double) {
// mutating global statics is unsafe
unsafe {
if dpi_x <= 0.0 {
DPI_X = DEFAULT_DPI_X;
} else {
DPI_X = dpi_x;
}
if dpi_y <= 0.0 {
DPI_Y = DEFAULT_DPI_Y;
} else {
DPI_Y = dpi_y;
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rsvg_set_default_dpi(dpi: libc::c_double) {
unsafe {
rsvg_set_default_dpi_x_y(dpi, dpi);
}
}

2206
librsvg-c/src/handle.rs Normal file

File diff suppressed because it is too large Load Diff

63
librsvg-c/src/lib.rs Normal file
View File

@@ -0,0 +1,63 @@
//! C API for librsvg, based on GObject.
//!
//! The main API is in the [`handle`] module. The other modules have utility functions
//! and the legacy pixbuf-based API in the `pixbuf_utils` module.
#![allow(clippy::missing_safety_doc)]
#[rustfmt::skip]
pub use handle::{
rsvg_error_get_type,
rsvg_handle_close,
rsvg_handle_flags_get_type,
rsvg_handle_get_base_uri,
rsvg_handle_get_dimensions,
rsvg_handle_get_dimensions_sub,
rsvg_handle_get_geometry_for_element,
rsvg_handle_get_geometry_for_layer,
rsvg_handle_get_intrinsic_dimensions,
rsvg_handle_get_intrinsic_size_in_pixels,
rsvg_handle_get_position_sub,
rsvg_handle_has_sub,
rsvg_handle_internal_set_testing,
rsvg_handle_new_from_data,
rsvg_handle_new_from_file,
rsvg_handle_new_from_gfile_sync,
rsvg_handle_new_from_stream_sync,
rsvg_handle_new_with_flags,
rsvg_handle_read_stream_sync,
rsvg_handle_render_cairo_sub,
rsvg_handle_render_element,
rsvg_handle_render_document,
rsvg_handle_render_layer,
rsvg_handle_set_base_gfile,
rsvg_handle_set_base_uri,
rsvg_handle_set_dpi_x_y,
rsvg_handle_set_size_callback,
rsvg_handle_write,
};
#[cfg(feature = "pixbuf")]
pub use handle::rsvg_handle_get_pixbuf_sub;
pub use dpi::{rsvg_set_default_dpi, rsvg_set_default_dpi_x_y};
#[rustfmt::skip]
#[cfg(feature = "pixbuf")]
pub use pixbuf_utils::{
rsvg_pixbuf_from_file,
rsvg_pixbuf_from_file_at_max_size,
rsvg_pixbuf_from_file_at_size,
rsvg_pixbuf_from_file_at_zoom,
rsvg_pixbuf_from_file_at_zoom_with_max,
};
#[macro_use]
mod messages;
mod dpi;
pub mod handle;
#[cfg(feature = "pixbuf")]
pub mod pixbuf_utils;
pub mod sizing;

162
librsvg-c/src/messages.rs Normal file
View File

@@ -0,0 +1,162 @@
//! Logging functions, plus Rust versions of `g_return_if_fail()`.
//!
//! Glib's `g_return_if_fail()`, `g_warning()`, etc. are all C macros, so they cannot be
//! used from Rust. This module defines equivalent functions or macros with an `rsvg_`
//! prefix, to be clear that they should only be used from the implementation of the C API
//! and not from the main Rust code of the library.
use glib::ffi::{G_LOG_LEVEL_CRITICAL, G_LOG_LEVEL_WARNING, GLogField, g_log_structured_array};
use glib::translate::*;
/*
G_LOG_LEVEL_CRITICAL = 1 << 3,
G_LOG_LEVEL_WARNING = 1 << 4,
#define g_critical(...) g_log_structured_standard (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, \
__FILE__, G_STRINGIFY (__LINE__), \
G_STRFUNC, __VA_ARGS__)
#define g_warning(...) g_log_structured_standard (G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, \
__FILE__, G_STRINGIFY (__LINE__), \
G_STRFUNC, __VA_ARGS__)
GLogField fields[] =
{
{ "PRIORITY", log_level_to_priority (log_level), -1 },
{ "CODE_FILE", file, -1 },
{ "CODE_LINE", line, -1 },
{ "CODE_FUNC", func, -1 },
/* Filled in later: */
{ "MESSAGE", NULL, -1 },
/* If @log_domain is %NULL, we will not pass this field: */
{ "GLIB_DOMAIN", log_domain, -1 },
};
g_log_structured_array (log_level, fields, n_fields);
*/
/// Helper function for converting string literals to C char pointers.
#[macro_export]
macro_rules! rsvg_c_str {
($txt:expr) => {
// SAFETY: it's important that the type we pass to `from_bytes_with_nul` is 'static,
// so that the storage behind the returned pointer doesn't get freed while it's still
// being used. We get that by only allowing string literals.
std::ffi::CStr::from_bytes_with_nul(concat!($txt, "\0").as_bytes())
.unwrap()
.as_ptr()
};
}
/// Helper for `rsvg_g_warning` and `rsvg_g_critical`
///
/// This simulates what in C would be a call to the g_warning() or g_critical()
/// macros, but with the underlying function g_log_structured_array().
///
/// If the implementation of g_warning() or g_critical() changes, we'll have
/// to change this function.
fn rsvg_g_log(level: glib::ffi::GLogLevelFlags, msg: &str) {
// stolen from gmessages.c:log_level_to_priority()
let priority = match level {
G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL => c"4".as_ptr(),
_ => unreachable!("please add another log level priority to rsvg_g_log()"),
};
let c_msg = msg.to_glib_none();
let c_char_msg: *const libc::c_char = c_msg.0;
// Glib's g_log_structured_standard() adds a few more fields for the source
// file, line number, etc., but those are not terribly useful without a stack
// trace. So, we'll omit them.
let fields = [
GLogField {
key: c"PRIORITY".as_ptr(),
value: priority.cast(),
length: -1,
},
GLogField {
key: c"MESSAGE".as_ptr(),
value: c_char_msg.cast(),
length: msg.len() as _,
},
// This is the G_LOG_DOMAIN set from the Makefile
GLogField {
key: c"GLIB_DOMAIN".as_ptr(),
value: c"librsvg".as_ptr().cast(),
length: -1,
},
];
unsafe {
g_log_structured_array(level, fields.as_ptr(), fields.len());
}
}
/// Replacement for `g_warning()`.
///
/// Use this to signal an error condition in the following situations:
///
/// * The C API does not have an adequate error code for the error in question (and cannot
/// be changed to have one, for ABI compatibility reasons).
///
/// * Applications using the C API would be prone to ignoring an important error,
/// so it's best to have a warning on the console to at least have a hope of someone
/// noticing the error.
pub(crate) fn rsvg_g_warning(msg: &str) {
rsvg_g_log(glib::ffi::G_LOG_LEVEL_WARNING, msg);
}
/// Replacement for `g_critical()`.
///
/// Use this to signal a programming error from the caller of the C API, like passing
/// incorrect arguments or calling the API out of order. Rust code conventionally panics
/// in such situations, but C/Glib code does not, so it's best to "do nothing", print a
/// critical message, and return. Development versions of GNOME will crash the program
/// if this happens; release versions will ignore the error.
pub(crate) fn rsvg_g_critical(msg: &str) {
rsvg_g_log(glib::ffi::G_LOG_LEVEL_CRITICAL, msg);
}
/// Replacement for `g_return_if_fail()`.
// Once Rust has a function! macro that gives us the current function name, we
// can remove the $func_name argument.
#[macro_export]
macro_rules! rsvg_return_if_fail {
{
$func_name:ident;
$($condition:expr,)+
} => {
$(
if !$condition {
unsafe {
glib::ffi::g_return_if_fail_warning(
c"librsvg".as_ptr(),
rsvg_c_str!(stringify!($func_name)),
rsvg_c_str!(stringify!($condition)),
);
}
return;
}
)+
}
}
/// Replacement for `g_return_val_if_fail()`.
#[macro_export]
macro_rules! rsvg_return_val_if_fail {
{
$func_name:ident => $retval:expr;
$($condition:expr,)+
} => {
$(
if !$condition {
unsafe {
glib::ffi::g_return_if_fail_warning(
c"librsvg".as_ptr(),
rsvg_c_str!(stringify!($func_name)),
rsvg_c_str!(stringify!($condition)),
);
}
return $retval;
}
)+
}
}

View File

@@ -0,0 +1,381 @@
//! Legacy C API for functions that render directly to a `GdkPixbuf`.
//!
//! This is the implementation of the `rsvg_pixbuf_*` family of functions.
#![cfg(feature = "pixbuf")]
use std::path::PathBuf;
use std::ptr;
use gdk_pixbuf::{Colorspace, Pixbuf};
use glib::translate::*;
use rgb::FromSlice;
use super::dpi::Dpi;
use super::handle::{checked_i32, set_gerror};
use super::sizing::LegacySize;
use rsvg::c_api_only::{Pixel, PixelOps, Session, SharedImageSurface, SurfaceType, ToPixel};
use rsvg::{CairoRenderer, Loader, RenderingError};
/// GdkPixbuf's endian-independent RGBA8 pixel layout.
type GdkPixbufRGBA = rgb::RGBA8;
/// Trait to convert pixels in various formats to RGBA, for GdkPixbuf.
///
/// GdkPixbuf unconditionally uses RGBA ordering regardless of endianness,
/// but we need to convert to it from Cairo's endian-dependent 0xaarrggbb.
trait ToGdkPixbufRGBA {
fn to_pixbuf_rgba(&self) -> GdkPixbufRGBA;
}
impl ToGdkPixbufRGBA for Pixel {
#[inline]
fn to_pixbuf_rgba(&self) -> GdkPixbufRGBA {
GdkPixbufRGBA {
r: self.r,
g: self.g,
b: self.b,
a: self.a,
}
}
}
pub fn empty_pixbuf() -> Result<Pixbuf, RenderingError> {
// GdkPixbuf does not allow zero-sized pixbufs, but Cairo allows zero-sized
// surfaces. In this case, return a 1-pixel transparent pixbuf.
let pixbuf = Pixbuf::new(Colorspace::Rgb, true, 8, 1, 1)
.ok_or_else(|| RenderingError::OutOfMemory(String::from("creating a Pixbuf")))?;
pixbuf.put_pixel(0, 0, 0, 0, 0, 0);
Ok(pixbuf)
}
pub fn pixbuf_from_surface(surface: &SharedImageSurface) -> Result<Pixbuf, RenderingError> {
let width = surface.width();
let height = surface.height();
let pixbuf = Pixbuf::new(Colorspace::Rgb, true, 8, width, height)
.ok_or_else(|| RenderingError::OutOfMemory(String::from("creating a Pixbuf")))?;
assert!(pixbuf.colorspace() == Colorspace::Rgb);
assert!(pixbuf.bits_per_sample() == 8);
assert!(pixbuf.n_channels() == 4);
let pixbuf_data = unsafe { pixbuf.pixels() };
let stride = pixbuf.rowstride() as usize;
// We use chunks_mut(), not chunks_exact_mut(), because gdk-pixbuf tends
// to make the last row *not* have the full stride (i.e. it is
// only as wide as the pixels in that row).
pixbuf_data
.chunks_mut(stride)
.take(height as usize)
.map(|row| row.as_rgba_mut())
.zip(surface.rows())
.flat_map(|(dest_row, src_row)| src_row.iter().zip(dest_row.iter_mut()))
.for_each(|(src, dest)| *dest = src.to_pixel().unpremultiply().to_pixbuf_rgba());
Ok(pixbuf)
}
enum SizeKind {
Zoom,
WidthHeight,
WidthHeightMax,
ZoomMax,
}
struct SizeMode {
kind: SizeKind,
x_zoom: f64,
y_zoom: f64,
width: i32,
height: i32,
}
fn get_final_size(in_width: f64, in_height: f64, size_mode: &SizeMode) -> (f64, f64) {
if in_width == 0.0 || in_height == 0.0 {
return (0.0, 0.0);
}
let mut out_width;
let mut out_height;
match size_mode.kind {
SizeKind::Zoom => {
out_width = size_mode.x_zoom * in_width;
out_height = size_mode.y_zoom * in_height;
}
SizeKind::ZoomMax => {
out_width = size_mode.x_zoom * in_width;
out_height = size_mode.y_zoom * in_height;
if out_width > f64::from(size_mode.width) || out_height > f64::from(size_mode.height) {
let zoom_x = f64::from(size_mode.width) / out_width;
let zoom_y = f64::from(size_mode.height) / out_height;
let zoom = zoom_x.min(zoom_y);
out_width *= zoom;
out_height *= zoom;
}
}
SizeKind::WidthHeightMax => {
let zoom_x = f64::from(size_mode.width) / in_width;
let zoom_y = f64::from(size_mode.height) / in_height;
let zoom = zoom_x.min(zoom_y);
out_width = zoom * in_width;
out_height = zoom * in_height;
}
SizeKind::WidthHeight => {
if size_mode.width != -1 {
out_width = f64::from(size_mode.width);
} else {
out_width = in_width;
}
if size_mode.height != -1 {
out_height = f64::from(size_mode.height);
} else {
out_height = in_height;
}
}
}
(out_width, out_height)
}
pub fn render_to_pixbuf_at_size(
renderer: &CairoRenderer<'_>,
document_width: f64,
document_height: f64,
desired_width: f64,
desired_height: f64,
) -> Result<Pixbuf, RenderingError> {
if desired_width == 0.0
|| desired_height == 0.0
|| document_width == 0.0
|| document_height == 0.0
{
return empty_pixbuf();
}
let surface = cairo::ImageSurface::create(
cairo::Format::ARgb32,
checked_i32(desired_width.ceil())?,
checked_i32(desired_height.ceil())?,
)?;
{
let cr = cairo::Context::new(&surface)?;
cr.scale(
desired_width / document_width,
desired_height / document_height,
);
let viewport = cairo::Rectangle::new(0.0, 0.0, document_width, document_height);
// We do it with a cr transform so we can scale non-proportionally.
renderer.render_document(&cr, &viewport)?;
}
let shared_surface = SharedImageSurface::wrap(surface, SurfaceType::SRgb)?;
pixbuf_from_surface(&shared_surface)
}
unsafe fn pixbuf_from_file_with_size_mode(
filename: *const libc::c_char,
size_mode: &SizeMode,
error: *mut *mut glib::ffi::GError,
) -> *mut gdk_pixbuf::ffi::GdkPixbuf {
let path = unsafe { PathBuf::from_glib_none(filename) };
let session = Session::default();
let handle = match Loader::new_with_session(session.clone()).read_path(path) {
Ok(handle) => handle,
Err(e) => {
set_gerror(&session, error, 0, &format!("{e}"));
return ptr::null_mut();
}
};
let dpi = Dpi::default();
let renderer = CairoRenderer::new(&handle).with_dpi(dpi.x(), dpi.y());
let (document_width, document_height) = match renderer.legacy_document_size() {
Ok(dim) => dim,
Err(e) => {
set_gerror(&session, error, 0, &format!("{e}"));
return ptr::null_mut();
}
};
let (desired_width, desired_height) =
get_final_size(document_width, document_height, size_mode);
render_to_pixbuf_at_size(
&renderer,
document_width,
document_height,
desired_width,
desired_height,
)
.map(|pixbuf| pixbuf.to_glib_full())
.unwrap_or_else(|e| {
set_gerror(&session, error, 0, &format!("{e}"));
ptr::null_mut()
})
}
#[unsafe(no_mangle)]
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe extern "C" fn rsvg_pixbuf_from_file(
filename: *const libc::c_char,
error: *mut *mut glib::ffi::GError,
) -> *mut gdk_pixbuf::ffi::GdkPixbuf {
rsvg_return_val_if_fail! {
rsvg_pixbuf_from_file => ptr::null_mut();
!filename.is_null(),
error.is_null() || (*error).is_null(),
}
pixbuf_from_file_with_size_mode(
filename,
&SizeMode {
kind: SizeKind::WidthHeight,
x_zoom: 0.0,
y_zoom: 0.0,
width: -1,
height: -1,
},
error,
)
}
#[unsafe(no_mangle)]
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe extern "C" fn rsvg_pixbuf_from_file_at_size(
filename: *const libc::c_char,
width: libc::c_int,
height: libc::c_int,
error: *mut *mut glib::ffi::GError,
) -> *mut gdk_pixbuf::ffi::GdkPixbuf {
rsvg_return_val_if_fail! {
rsvg_pixbuf_from_file_at_size => ptr::null_mut();
!filename.is_null(),
(width >= 1 && height >= 1) || (width == -1 && height == -1),
error.is_null() || (*error).is_null(),
}
pixbuf_from_file_with_size_mode(
filename,
&SizeMode {
kind: SizeKind::WidthHeight,
x_zoom: 0.0,
y_zoom: 0.0,
width,
height,
},
error,
)
}
#[unsafe(no_mangle)]
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe extern "C" fn rsvg_pixbuf_from_file_at_zoom(
filename: *const libc::c_char,
x_zoom: libc::c_double,
y_zoom: libc::c_double,
error: *mut *mut glib::ffi::GError,
) -> *mut gdk_pixbuf::ffi::GdkPixbuf {
rsvg_return_val_if_fail! {
rsvg_pixbuf_from_file_at_zoom => ptr::null_mut();
!filename.is_null(),
x_zoom > 0.0 && y_zoom > 0.0,
error.is_null() || (*error).is_null(),
}
pixbuf_from_file_with_size_mode(
filename,
&SizeMode {
kind: SizeKind::Zoom,
x_zoom,
y_zoom,
width: 0,
height: 0,
},
error,
)
}
#[unsafe(no_mangle)]
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe extern "C" fn rsvg_pixbuf_from_file_at_zoom_with_max(
filename: *const libc::c_char,
x_zoom: libc::c_double,
y_zoom: libc::c_double,
max_width: libc::c_int,
max_height: libc::c_int,
error: *mut *mut glib::ffi::GError,
) -> *mut gdk_pixbuf::ffi::GdkPixbuf {
rsvg_return_val_if_fail! {
rsvg_pixbuf_from_file_at_zoom_with_max => ptr::null_mut();
!filename.is_null(),
x_zoom > 0.0 && y_zoom > 0.0,
max_width >= 1 && max_height >= 1,
error.is_null() || (*error).is_null(),
}
pixbuf_from_file_with_size_mode(
filename,
&SizeMode {
kind: SizeKind::ZoomMax,
x_zoom,
y_zoom,
width: max_width,
height: max_height,
},
error,
)
}
#[unsafe(no_mangle)]
#[allow(unsafe_op_in_unsafe_fn)]
pub unsafe extern "C" fn rsvg_pixbuf_from_file_at_max_size(
filename: *const libc::c_char,
max_width: libc::c_int,
max_height: libc::c_int,
error: *mut *mut glib::ffi::GError,
) -> *mut gdk_pixbuf::ffi::GdkPixbuf {
rsvg_return_val_if_fail! {
rsvg_pixbuf_from_file_at_max_size => ptr::null_mut();
!filename.is_null(),
max_width >= 1 && max_height >= 1,
error.is_null() || (*error).is_null(),
}
pixbuf_from_file_with_size_mode(
filename,
&SizeMode {
kind: SizeKind::WidthHeightMax,
x_zoom: 0.0,
y_zoom: 0.0,
width: max_width,
height: max_height,
},
error,
)
}

114
librsvg-c/src/sizing.rs Normal file
View File

@@ -0,0 +1,114 @@
//! Compute an SVG document's size with the legacy logic.
//!
//! See the documentation for [`LegacySize`]. The legacy C API functions like
//! `rsvg_handle_render_cairo()` do not take a viewport argument: they do not know how big
//! the caller would like to render the document; instead they compute a "natural size"
//! for the document based on its `width`/`height`/`viewBox` and some heuristics for when
//! they are missing.
//!
//! The new style C functions like `rsvg_handle_render_document()` actually take a
//! viewport, which indicates how big the result should be. This matches the expectations
//! of the web platform, which assumes that all embedded content goes into a viewport.
use float_cmp::approx_eq;
use rsvg::c_api_only::Dpi;
use rsvg::{CairoRenderer, IntrinsicDimensions, RenderingError};
use super::handle::CairoRectangleExt;
/// Extension methods to compute the SVG's size suitable for the legacy C API.
///
/// The legacy C API can compute an SVG document's size from the
/// `width`, `height`, and `viewBox` attributes of the toplevel `<svg>`
/// element. If these are not available, then the size must be computed
/// by actually measuring the geometries of elements in the document.
///
/// See <https://www.w3.org/TR/css-images-3/#sizing-terms> for terminology and logic.
pub trait LegacySize {
fn legacy_document_size(&self) -> Result<(f64, f64), RenderingError> {
let (ink_r, _) = self.legacy_layer_geometry(None)?;
Ok((ink_r.width(), ink_r.height()))
}
fn legacy_layer_geometry(
&self,
id: Option<&str>,
) -> Result<(cairo::Rectangle, cairo::Rectangle), RenderingError>;
}
impl<'a> LegacySize for CairoRenderer<'a> {
fn legacy_layer_geometry(
&self,
id: Option<&str>,
) -> Result<(cairo::Rectangle, cairo::Rectangle), RenderingError> {
match id {
Some(id) => Ok(self.geometry_for_layer(Some(id), &unit_rectangle())?),
None => {
let size_from_intrinsic_dimensions =
self.intrinsic_size_in_pixels().or_else(|| {
size_in_pixels_from_percentage_width_and_height(
self,
&self.intrinsic_dimensions(),
self.dpi(),
)
});
if let Some((w, h)) = size_from_intrinsic_dimensions {
// We have a size directly computed from the <svg> attributes
let rect = cairo::Rectangle::from_size(w, h);
Ok((rect, rect))
} else {
self.geometry_for_layer(None, &unit_rectangle())
}
}
}
}
}
fn unit_rectangle() -> cairo::Rectangle {
cairo::Rectangle::from_size(1.0, 1.0)
}
/// If the width and height are in percentage units, computes a size equal to the
/// `viewBox`'s aspect ratio if it exists, or else returns None.
///
/// For example, a `viewBox="0 0 100 200"` will yield `Some(100.0, 200.0)`.
///
/// Note that this only checks that the width and height are in percentage units, but
/// it actually ignores their values. This is because at the point this function is
/// called, there is no viewport to embed the SVG document in, so those percentage
/// units cannot be resolved against anything in particular. The idea is to return
/// some dimensions with the correct aspect ratio.
fn size_in_pixels_from_percentage_width_and_height(
renderer: &CairoRenderer,
dim: &IntrinsicDimensions,
dpi: Dpi,
) -> Option<(f64, f64)> {
let IntrinsicDimensions {
width,
height,
vbox,
} = *dim;
use rsvg::LengthUnit::*;
// Unwrap or return None if we don't know the aspect ratio -> Let the caller handle it.
let vbox = vbox?;
let (w, h) = renderer.width_height_to_user(dpi);
// Avoid division by zero below. If the viewBox is zero-sized, there's
// not much we can do.
if approx_eq!(f64, vbox.width(), 0.0) || approx_eq!(f64, vbox.height(), 0.0) {
return Some((0.0, 0.0));
}
match (width.unit, height.unit) {
(Percent, Percent) => Some((vbox.width(), vbox.height())),
(_, Percent) => Some((w, w * vbox.height() / vbox.width())),
(Percent, _) => Some((h * vbox.width() / vbox.height(), h)),
(_, _) => unreachable!("should have been called with percentage units"),
}
}

1874
librsvg-c/tests-c/api.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
api_test_sources = files(
'api.c',
'test-utils.c',
'test-utils.h',
)
test_c_args = [
'-DTEST_DATA_DIR=@0@'.format(meson.current_source_dir()),
'-DTEST_SRC_DIR=@0@'.format(meson.current_build_dir()),
'-DTOP_SRC_DIR=@0@'.format(meson.project_source_root()),
]
if pixbuf_dep.found()
test_c_args += ['-DHAVE_PIXBUF']
endif
api_test = executable(
'api',
api_test_sources,
c_args: test_c_args,
dependencies: librsvg_dep,
gnu_symbol_visibility: 'hidden',
install: false,
)
test(
'C API tests',
api_test,
# the following incantation is for glib's gtestutils
timeout: -1,
args: [ '--tap', 'k' ],
env: [
'G_TEST_SRCDIR=@0@'.format(meson.current_source_dir()),
'G_TEST_BUILDDIR=@0@'.format(meson.current_build_dir()),
]
)

View File

@@ -0,0 +1,258 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* vim: set sw=4 sts=4 ts=4 expandtab: */
#include "test-utils.h"
#include <string.h>
#include <pango/pango.h>
#include <pango/pangocairo.h>
#if !PANGO_VERSION_CHECK (1, 44, 0)
# include <hb.h>
#endif
#include <ft2build.h>
#include FT_FREETYPE_H
/* Compare two buffers, returning the number of pixels that are
* different and the maximum difference of any single color channel in
* result_ret.
*
* This function should be rewritten to compare all formats supported by
* cairo_format_t instead of taking a mask as a parameter.
*/
static void
buffer_diff_core (unsigned char *_buf_a,
unsigned char *_buf_b,
unsigned char *_buf_diff,
int width,
int height,
int stride,
guint32 mask,
TestUtilsBufferDiffResult *result_ret)
{
int x, y;
guint32 *row_a, *row_b, *row;
TestUtilsBufferDiffResult result = {0, 0};
guint32 *buf_a = (guint32 *) _buf_a;
guint32 *buf_b = (guint32 *) _buf_b;
guint32 *buf_diff = (guint32 *) _buf_diff;
stride /= sizeof(guint32);
for (y = 0; y < height; y++)
{
row_a = buf_a + y * stride;
row_b = buf_b + y * stride;
row = buf_diff + y * stride;
for (x = 0; x < width; x++)
{
/* check if the pixels are the same */
if ((row_a[x] & mask) != (row_b[x] & mask)) {
int channel;
guint32 diff_pixel = 0;
/* calculate a difference value for all 4 channels */
for (channel = 0; channel < 4; channel++) {
int value_a = (row_a[x] >> (channel*8)) & 0xff;
int value_b = (row_b[x] >> (channel*8)) & 0xff;
unsigned int diff;
diff = abs (value_a - value_b);
if (diff > result.max_diff)
result.max_diff = diff;
diff *= 4; /* emphasize */
if (diff)
diff += 128; /* make sure it's visible */
if (diff > 255)
diff = 255;
diff_pixel |= diff << (channel*8);
}
result.pixels_changed++;
if ((diff_pixel & 0x00ffffff) == 0) {
/* alpha only difference, convert to luminance */
guint8 alpha = diff_pixel >> 24;
diff_pixel = alpha * 0x010101;
}
row[x] = diff_pixel;
} else {
row[x] = 0;
}
row[x] |= 0xff000000; /* Set ALPHA to 100% (opaque) */
}
}
*result_ret = result;
}
void
test_utils_compare_surfaces (cairo_surface_t *surface_a,
cairo_surface_t *surface_b,
cairo_surface_t *surface_diff,
TestUtilsBufferDiffResult *result)
{
/* Here, we run cairo's old buffer_diff algorithm which looks for
* pixel-perfect images.
*/
buffer_diff_core (cairo_image_surface_get_data (surface_a),
cairo_image_surface_get_data (surface_b),
cairo_image_surface_get_data (surface_diff),
cairo_image_surface_get_width (surface_a),
cairo_image_surface_get_height (surface_a),
cairo_image_surface_get_stride (surface_a),
0xffffffff,
result);
if (result->pixels_changed == 0)
return;
g_test_message ("%d pixels differ (with maximum difference of %d) from reference image\n",
result->pixels_changed, result->max_diff);
}
#ifdef HAVE_PIXBUF
/* Copied from gdk_cairo_surface_paint_pixbuf in gdkcairo.c,
* we do not want to depend on GDK
*/
static void
test_utils_cairo_surface_paint_pixbuf (cairo_surface_t *surface,
const GdkPixbuf *pixbuf)
{
gint width, height;
guchar *gdk_pixels, *cairo_pixels;
int gdk_rowstride, cairo_stride;
int n_channels;
int j;
if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS)
return;
/* This function can't just copy any pixbuf to any surface, be
* sure to read the invariants here before calling it */
g_assert (cairo_surface_get_type (surface) == CAIRO_SURFACE_TYPE_IMAGE);
g_assert (cairo_image_surface_get_format (surface) == CAIRO_FORMAT_RGB24 ||
cairo_image_surface_get_format (surface) == CAIRO_FORMAT_ARGB32);
g_assert (cairo_image_surface_get_width (surface) == gdk_pixbuf_get_width (pixbuf));
g_assert (cairo_image_surface_get_height (surface) == gdk_pixbuf_get_height (pixbuf));
cairo_surface_flush (surface);
width = gdk_pixbuf_get_width (pixbuf);
height = gdk_pixbuf_get_height (pixbuf);
gdk_pixels = gdk_pixbuf_get_pixels (pixbuf);
gdk_rowstride = gdk_pixbuf_get_rowstride (pixbuf);
n_channels = gdk_pixbuf_get_n_channels (pixbuf);
cairo_stride = cairo_image_surface_get_stride (surface);
cairo_pixels = cairo_image_surface_get_data (surface);
for (j = height; j; j--)
{
guchar *p = gdk_pixels;
guchar *q = cairo_pixels;
if (n_channels == 3)
{
guchar *end = p + 3 * width;
while (p < end)
{
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
q[0] = p[2];
q[1] = p[1];
q[2] = p[0];
#else
q[1] = p[0];
q[2] = p[1];
q[3] = p[2];
#endif
p += 3;
q += 4;
}
}
else
{
guchar *end = p + 4 * width;
guint t1,t2,t3;
#define MULT(d,c,a,t) G_STMT_START { t = c * a + 0x80; d = ((t >> 8) + t) >> 8; } G_STMT_END
while (p < end)
{
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
MULT(q[0], p[2], p[3], t1);
MULT(q[1], p[1], p[3], t2);
MULT(q[2], p[0], p[3], t3);
q[3] = p[3];
#else
q[0] = p[3];
MULT(q[1], p[0], p[3], t1);
MULT(q[2], p[1], p[3], t2);
MULT(q[3], p[2], p[3], t3);
#endif
p += 4;
q += 4;
}
#undef MULT
}
gdk_pixels += gdk_rowstride;
cairo_pixels += cairo_stride;
}
cairo_surface_mark_dirty (surface);
}
cairo_surface_t *
test_utils_cairo_surface_from_pixbuf (const GdkPixbuf *pixbuf)
{
cairo_surface_t *surface;
g_return_val_if_fail (GDK_IS_PIXBUF (pixbuf), NULL);
g_return_val_if_fail (gdk_pixbuf_get_n_channels (pixbuf) == 4, NULL);
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
gdk_pixbuf_get_width (pixbuf),
gdk_pixbuf_get_height (pixbuf));
test_utils_cairo_surface_paint_pixbuf (surface, pixbuf);
return surface;
}
#endif /* defined(HAVE_PIXBUF) */
static gchar *data_path = NULL;
const gchar *
test_utils_get_test_data_path (void)
{
if (data_path)
return data_path;
data_path = g_test_build_filename (G_TEST_DIST, "../../rsvg/tests/fixtures", NULL);
return data_path;
}
void
test_utils_print_dependency_versions (void)
{
FT_Library ft_lib;
FT_Int ft_major = 0;
FT_Int ft_minor = 0;
FT_Int ft_patch = 0;
FT_Init_FreeType (&ft_lib);
FT_Library_Version (ft_lib, &ft_major, &ft_minor, &ft_patch);
FT_Done_FreeType (ft_lib);
g_test_message ("Cairo version: %s", cairo_version_string ());
g_test_message ("Pango version: %s", pango_version_string ());
g_test_message ("Freetype version: %d.%d.%d", ft_major, ft_minor, ft_patch);
#if PANGO_VERSION_CHECK (1, 44, 0)
g_test_message ("Harfbuzz version: %s", hb_version_string ());
#else
g_test_message ("Not printing Harfbuzz version since Pango is older than 1.44");
#endif
}

View File

@@ -0,0 +1,40 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* vim: set sw=4 sts=4 ts=4 expandtab: */
#ifndef TEST_UTILS_H
#define TEST_UTILS_H
#include <cairo.h>
#include <gio/gio.h>
#ifdef HAVE_PIXBUF
#include <gdk-pixbuf/gdk-pixbuf.h>
#endif
G_BEGIN_DECLS
typedef struct {
unsigned int pixels_changed;
unsigned int max_diff;
} TestUtilsBufferDiffResult;
void test_utils_compare_surfaces (cairo_surface_t *surface_a,
cairo_surface_t *surface_b,
cairo_surface_t *surface_diff,
TestUtilsBufferDiffResult *result);
#ifdef HAVE_PIXBUF
cairo_surface_t *test_utils_cairo_surface_from_pixbuf (const GdkPixbuf *pixbuf);
#endif
typedef gboolean (* AddTestFunc) (GFile *file);
const gchar *test_utils_get_test_data_path (void);
void test_utils_print_dependency_versions (void);
void test_utils_setup_font_map (void);
G_END_DECLS
#endif /* TEST_UTILS_H */

View File

@@ -0,0 +1,193 @@
use librsvg_c::sizing::LegacySize;
use rsvg::{CairoRenderer, Loader, LoadingError, SvgHandle};
fn load_svg(input: &'static [u8]) -> Result<SvgHandle, LoadingError> {
let bytes = glib::Bytes::from_static(input);
let stream = gio::MemoryInputStream::from_bytes(&bytes);
Loader::new().read_stream(&stream, None::<&gio::File>, None::<&gio::Cancellable>)
}
#[test]
fn just_viewbox_uses_viewbox_size() {
let svg = load_svg(
br#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 200"/>
"#,
)
.unwrap();
assert_eq!(
CairoRenderer::new(&svg)
.legacy_layer_geometry(None)
.unwrap(),
(
cairo::Rectangle::new(0.0, 0.0, 100.0, 200.0),
cairo::Rectangle::new(0.0, 0.0, 100.0, 200.0),
)
);
}
#[test]
fn no_intrinsic_size_uses_element_geometries() {
let svg = load_svg(
br#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="20" width="30" height="40" fill="black"/>
</svg>
"#,
)
.unwrap();
assert_eq!(
CairoRenderer::new(&svg)
.legacy_layer_geometry(None)
.unwrap(),
(
cairo::Rectangle::new(10.0, 20.0, 30.0, 40.0),
cairo::Rectangle::new(10.0, 20.0, 30.0, 40.0),
)
);
}
#[test]
fn hundred_percent_width_height_uses_viewbox() {
let svg = load_svg(
br#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100 200"/>
"#,
)
.unwrap();
assert_eq!(
CairoRenderer::new(&svg)
.legacy_layer_geometry(None)
.unwrap(),
(
cairo::Rectangle::new(0.0, 0.0, 100.0, 200.0),
cairo::Rectangle::new(0.0, 0.0, 100.0, 200.0),
)
);
}
#[test]
fn hundred_percent_width_height_no_viewbox_uses_element_geometries() {
let svg = load_svg(
br#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
<rect x="10" y="20" width="30" height="40" fill="black"/>
</svg>
"#,
)
.unwrap();
assert_eq!(
CairoRenderer::new(&svg)
.legacy_layer_geometry(None)
.unwrap(),
(
cairo::Rectangle::new(10.0, 20.0, 30.0, 40.0),
cairo::Rectangle::new(10.0, 20.0, 30.0, 40.0),
)
);
}
#[test]
fn width_and_viewbox_preserves_aspect_ratio() {
let svg = load_svg(
br#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="60" viewBox="0 0 30 40">
<rect x="10" y="20" width="30" height="40" fill="black"/>
</svg>
"#,
)
.unwrap();
// Per the spec, the height property should default to 100%, so the above would end up
// like <svg width="60" height="100%" viewBox="0 0 30 40">
//
// If that were being *rendered* to a viewport, no problem, just use units horizontally
// and 100% of the viewport vertically.
//
// But we are being asked to compute the SVG's natural geometry, so the best we can do
// is to take the aspect ratio defined by the viewBox and apply it to the width.
assert_eq!(
CairoRenderer::new(&svg)
.legacy_layer_geometry(None)
.unwrap(),
(
cairo::Rectangle::new(0.0, 0.0, 60.0, 80.0),
cairo::Rectangle::new(0.0, 0.0, 60.0, 80.0),
)
);
}
#[test]
fn height_and_viewbox_preserves_aspect_ratio() {
let svg = load_svg(
br#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="80" viewBox="0 0 30 40">
<rect x="10" y="20" width="30" height="40" fill="black"/>
</svg>
"#,
)
.unwrap();
// See the comment above in width_and_viewbox_preserves_aspect_ratio(); this
// is equivalent but for the height.
assert_eq!(
CairoRenderer::new(&svg)
.legacy_layer_geometry(None)
.unwrap(),
(
cairo::Rectangle::new(0.0, 0.0, 60.0, 80.0),
cairo::Rectangle::new(0.0, 0.0, 60.0, 80.0),
)
);
}
#[test]
fn zero_width_vbox() {
let svg = load_svg(
br#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="80" viewBox="0 0 0 40">
<rect x="10" y="20" width="30" height="40" fill="black"/>
</svg>
"#,
)
.unwrap();
assert_eq!(
CairoRenderer::new(&svg)
.legacy_layer_geometry(None)
.unwrap(),
(
cairo::Rectangle::new(0.0, 0.0, 0.0, 0.0),
cairo::Rectangle::new(0.0, 0.0, 0.0, 0.0)
)
);
}
#[test]
fn zero_height_vbox() {
let svg = load_svg(
br#"<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="80" viewBox="0 0 30 0">
<rect x="10" y="20" width="30" height="40" fill="black"/>
</svg>
"#,
)
.unwrap();
assert_eq!(
CairoRenderer::new(&svg)
.legacy_layer_geometry(None)
.unwrap(),
(
cairo::Rectangle::new(0.0, 0.0, 0.0, 0.0),
cairo::Rectangle::new(0.0, 0.0, 0.0, 0.0)
)
);
}