#![cfg(feature = "threads")]
#![allow(clippy::cast_precision_loss)]
extern crate alloc;
use alloc::vec;
use thorvg::*;
#[test]
fn test_init_and_version() {
let _engine = Thorvg::init(0).expect("Failed to init ThorVG");
let (major, _minor, _micro, version_str) = Thorvg::version().expect("Failed to get version");
assert!(major >= 1);
assert!(!version_str.is_empty());
}
#[test]
fn test_init_signature_with_threads() {
let engine = Thorvg::init(0).expect("init(0) should succeed");
drop(engine);
let _engine = Thorvg::init(2).expect("init(2) should succeed");
}
#[test]
fn test_canvas_create_destroy() {
let engine = Thorvg::init(0).unwrap();
let canvas = engine.sw_canvas(EngineOption::Default);
assert!(canvas.is_ok());
}
#[test]
fn test_canvas_trait_generic_dispatch() {
fn finalise<C: Canvas>(c: &mut C) -> Result<()> {
c.draw(true)?;
c.sync()
}
let engine = Thorvg::init(0).unwrap();
let mut canvas = engine.sw_canvas(EngineOption::Default).unwrap();
let mut buffer = vec![0u32; 50 * 50];
unsafe { canvas.set_target(&mut buffer, 50, 50, 50, ColorSpace::ABGR8888) }.unwrap();
let mut shape = engine.shape().unwrap();
shape.append_rect(Rect::new(0.0, 0.0, 25.0, 25.0)).unwrap();
shape.set_fill_color(Rgba::new(0, 255, 0, 255)).unwrap();
canvas.add(shape).unwrap();
finalise(&mut canvas).unwrap();
assert!(buffer.iter().any(|&px| px != 0));
}
#[test]
fn test_engine_option_round_trip() {
assert_eq!(EngineOption::default(), EngineOption::Default);
let engine = Thorvg::init(0).unwrap();
for opt in [
EngineOption::None,
EngineOption::Default,
EngineOption::SmartRender,
EngineOption::Aliased,
] {
assert!(engine.sw_canvas(opt).is_ok(), "sw_canvas rejected {opt:?}");
}
}
#[test]
fn test_canvas_draw_shape() {
let engine = Thorvg::init(0).unwrap();
let mut canvas = engine.sw_canvas(EngineOption::Default).unwrap();
let (width, height) = (100u32, 100u32);
let mut buffer = vec![0u32; (width * height) as usize];
unsafe { canvas.set_target(&mut buffer, width, width, height, ColorSpace::ABGR8888) }.unwrap();
let mut shape = engine.shape().unwrap();
shape
.append_rect(Rect::new(10.0, 10.0, 50.0, 50.0))
.unwrap();
shape.set_fill_color(Rgba::new(255, 0, 0, 255)).unwrap();
canvas.add(shape).unwrap(); canvas.draw(true).unwrap();
canvas.sync().unwrap();
assert!(
buffer.iter().any(|&px| px != 0),
"Expected non-empty render output"
);
}
#[test]
fn test_canvas_clear_all() {
let engine = Thorvg::init(0).unwrap();
let mut canvas = engine.sw_canvas(EngineOption::Default).unwrap();
let mut buffer = vec![0u32; 100 * 100];
unsafe { canvas.set_target(&mut buffer, 100, 100, 100, ColorSpace::ABGR8888) }.unwrap();
for _ in 0..5 {
let mut s = engine.shape().unwrap();
s.append_rect(Rect::new(0.0, 0.0, 10.0, 10.0)).unwrap();
s.set_fill_color(Rgba::new(255, 0, 0, 255)).unwrap();
canvas.add(s).unwrap();
}
canvas.clear().unwrap();
let mut s = engine.shape().unwrap();
s.append_rect(Rect::new(0.0, 0.0, 50.0, 50.0)).unwrap();
s.set_fill_color(Rgba::new(0, 255, 0, 255)).unwrap();
canvas.add(s).unwrap();
canvas.draw(true).unwrap();
canvas.sync().unwrap();
}
#[test]
fn test_sw_canvas_set_target_rejects_stride_below_width() {
let engine = Thorvg::init(0).unwrap();
let mut canvas = engine.sw_canvas(EngineOption::Default).unwrap();
let mut buffer = vec![0u32; 100 * 100];
let err = unsafe { canvas.set_target(&mut buffer, 50, 100, 100, ColorSpace::ABGR8888) }
.expect_err("stride < width must be rejected");
assert_eq!(err, Error::InvalidArguments);
}
#[test]
fn test_sw_canvas_set_target_rejects_undersized_buffer() {
let engine = Thorvg::init(0).unwrap();
let mut canvas = engine.sw_canvas(EngineOption::Default).unwrap();
let mut buffer = vec![0u32; 10]; let err = unsafe { canvas.set_target(&mut buffer, 100, 100, 100, ColorSpace::ABGR8888) }
.expect_err("undersized buffer must be rejected");
assert_eq!(err, Error::InvalidArguments);
}
#[test]
fn test_sw_canvas_set_target_rejects_stride_height_overflow() {
let engine = Thorvg::init(0).unwrap();
let mut canvas = engine.sw_canvas(EngineOption::Default).unwrap();
let mut buffer = vec![0u32; 100];
let err = unsafe {
canvas.set_target(
&mut buffer,
u32::MAX,
u32::MAX,
u32::MAX,
ColorSpace::ABGR8888,
)
}
.expect_err("u32 overflow on stride*height must be rejected");
assert_eq!(err, Error::InvalidArguments);
}
#[test]
fn test_shape_ownership_transfer_to_canvas() {
let engine = Thorvg::init(0).unwrap();
let mut canvas = engine.sw_canvas(EngineOption::Default).unwrap();
let mut buffer = vec![0u32; 100 * 100];
unsafe { canvas.set_target(&mut buffer, 100, 100, 100, ColorSpace::ABGR8888) }.unwrap();
let mut shape = engine.shape().unwrap();
shape.append_rect(Rect::new(0.0, 0.0, 50.0, 50.0)).unwrap();
shape.set_fill_color(Rgba::new(255, 0, 0, 255)).unwrap();
canvas.add(shape).unwrap();
}
#[test]
fn test_shape_ownership_transfer_to_scene() {
let engine = Thorvg::init(0).unwrap();
let mut scene = engine.scene().unwrap();
let mut s1 = engine.shape().unwrap();
s1.append_rect(Rect::new(0.0, 0.0, 10.0, 10.0)).unwrap();
s1.set_fill_color(Rgba::new(255, 0, 0, 255)).unwrap();
let mut s2 = engine.shape().unwrap();
s2.append_circle(Circle::new(50.0, 50.0, 20.0)).unwrap();
s2.set_fill_color(Rgba::new(0, 0, 255, 255)).unwrap();
scene.add(s1).unwrap();
scene.add(s2).unwrap();
}
#[test]
fn test_shape_not_transferred_is_freed() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
shape
.append_rect(Rect::new(0.0, 0.0, 100.0, 100.0))
.unwrap();
shape.set_fill_color(Rgba::new(255, 255, 0, 255)).unwrap();
shape.set_stroke_width(3.0).unwrap();
shape.set_stroke_color(Rgba::new(0, 0, 0, 255)).unwrap();
}
#[test]
fn test_gradient_ownership_transfer_to_shape() {
let engine = Thorvg::init(0).unwrap();
let mut grad = engine.linear_gradient().unwrap();
grad.set_bounds(0.0, 0.0, 100.0, 100.0).unwrap();
grad.set_color_stops(&[
ColorStop { offset: 0.0, color: Rgba::new(255, 0, 0, 255) },
ColorStop { offset: 1.0, color: Rgba::new(0, 0, 255, 255) },
])
.unwrap();
let mut shape = engine.shape().unwrap();
shape
.append_rect(Rect::new(0.0, 0.0, 100.0, 100.0))
.unwrap();
shape.set_linear_gradient(grad).unwrap();
}
#[test]
fn test_gradient_not_transferred_is_freed() {
let engine = Thorvg::init(0).unwrap();
let mut grad = engine.radial_gradient().unwrap();
grad.set_radial(50.0, 50.0, 30.0, 50.0, 50.0, 0.0).unwrap();
grad.set_color_stops(&[
ColorStop { offset: 0.0, color: Rgba::new(255, 255, 255, 255) },
ColorStop { offset: 1.0, color: Rgba::new(0, 0, 0, 255) },
])
.unwrap();
}
#[test]
fn test_gradient_duplicate() {
let engine = Thorvg::init(0).unwrap();
let mut grad = engine.linear_gradient().unwrap();
grad.set_bounds(0.0, 0.0, 200.0, 200.0).unwrap();
grad.set_color_stops(&[
ColorStop { offset: 0.0, color: Rgba::new(255, 0, 0, 255) },
ColorStop { offset: 0.5, color: Rgba::new(0, 255, 0, 255) },
ColorStop { offset: 1.0, color: Rgba::new(0, 0, 255, 255) },
])
.unwrap();
let dup = grad.duplicate();
assert!(dup.is_some());
let dup = dup.unwrap();
let (x1, y1, x2, y2) = dup.bounds().unwrap();
assert!((x1 - 0.0).abs() < f32::EPSILON);
assert!((y1 - 0.0).abs() < f32::EPSILON);
assert!((x2 - 200.0).abs() < f32::EPSILON);
assert!((y2 - 200.0).abs() < f32::EPSILON);
}
#[test]
fn test_scene_nested_drop() {
let engine = Thorvg::init(0).unwrap();
let mut canvas = engine.sw_canvas(EngineOption::Default).unwrap();
let mut buffer = vec![0u32; 200 * 200];
unsafe { canvas.set_target(&mut buffer, 200, 200, 200, ColorSpace::ABGR8888) }.unwrap();
let mut scene = engine.scene().unwrap();
for i in 0..10 {
let mut s = engine.shape().unwrap();
s.append_circle(Circle::new(i as f32 * 20.0, 50.0, 10.0))
.unwrap();
s.set_fill_color(Rgba::new(255, 0, 0, 255)).unwrap();
scene.add(s).unwrap();
}
canvas.add(scene).unwrap();
canvas.draw(true).unwrap();
canvas.sync().unwrap();
}
#[test]
fn test_scene_clear_and_reuse() {
let engine = Thorvg::init(0).unwrap();
let mut scene = engine.scene().unwrap();
for _ in 0..5 {
let mut s = engine.shape().unwrap();
s.append_rect(Rect::new(0.0, 0.0, 10.0, 10.0)).unwrap();
scene.add(s).unwrap();
}
scene.clear().unwrap();
let mut s = engine.shape().unwrap();
s.append_rect(Rect::new(0.0, 0.0, 50.0, 50.0)).unwrap();
scene.add(s).unwrap();
}
#[test]
fn test_shape_fill_color_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
shape.set_fill_color(Rgba::new(100, 150, 200, 255)).unwrap();
assert_eq!(shape.fill_color().unwrap(), Rgba::new(100, 150, 200, 255));
}
#[test]
fn test_shape_stroke_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
shape.append_circle(Circle::new(50.0, 50.0, 30.0)).unwrap();
shape.set_stroke_width(3.0).unwrap();
shape.set_stroke_color(Rgba::new(0, 255, 0, 255)).unwrap();
shape.set_stroke_cap(StrokeCap::Round).unwrap();
shape.set_stroke_join(StrokeJoin::Bevel).unwrap();
shape.set_stroke_miterlimit(8.0).unwrap();
assert!((shape.stroke_width().unwrap() - 3.0).abs() < f32::EPSILON);
assert_eq!(shape.stroke_color().unwrap(), Rgba::new(0, 255, 0, 255));
assert_eq!(shape.stroke_cap().unwrap(), StrokeCap::Round);
assert_eq!(shape.stroke_join().unwrap(), StrokeJoin::Bevel);
assert!((shape.stroke_miterlimit().unwrap() - 8.0).abs() < f32::EPSILON);
}
#[test]
fn test_shape_fill_rule_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
assert_eq!(shape.fill_rule().unwrap(), FillRule::NonZero);
shape.set_fill_rule(FillRule::EvenOdd).unwrap();
assert_eq!(shape.fill_rule().unwrap(), FillRule::EvenOdd);
}
#[test]
fn test_shape_set_paint_order_accepts_both_orders() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
shape.set_paint_order(PaintOrder::StrokeThenFill).unwrap();
shape.set_paint_order(PaintOrder::FillThenStroke).unwrap();
}
#[test]
fn test_paint_opacity_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
shape.set_opacity(128).unwrap();
assert_eq!(shape.opacity().unwrap(), 128);
}
#[test]
fn test_paint_visibility_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
assert!(shape.visible());
shape.set_visible(false).unwrap();
assert!(!shape.visible());
shape.set_visible(true).unwrap();
assert!(shape.visible());
}
#[test]
fn test_paint_transform_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
let m = Matrix {
e11: 2.0,
e12: 0.5,
e13: 10.0,
e21: 0.0,
e22: 3.0,
e23: 20.0,
e31: 0.0,
e32: 0.0,
e33: 1.0,
};
shape.set_transform(&m).unwrap();
let got = shape.transform().unwrap();
assert!((got.e11 - 2.0).abs() < f32::EPSILON);
assert!((got.e12 - 0.5).abs() < f32::EPSILON);
assert!((got.e13 - 10.0).abs() < f32::EPSILON);
assert!((got.e22 - 3.0).abs() < f32::EPSILON);
assert!((got.e23 - 20.0).abs() < f32::EPSILON);
}
fn assert_matrix_close(got: &Matrix, want: &Matrix) {
const TOL: f32 = 1e-4;
for (g, w, name) in [
(got.e11, want.e11, "e11"),
(got.e12, want.e12, "e12"),
(got.e13, want.e13, "e13"),
(got.e21, want.e21, "e21"),
(got.e22, want.e22, "e22"),
(got.e23, want.e23, "e23"),
(got.e31, want.e31, "e31"),
(got.e32, want.e32, "e32"),
(got.e33, want.e33, "e33"),
] {
assert!((g - w).abs() < TOL, "{name}: got {g}, want {w}");
}
}
#[test]
fn test_matrix_default_is_identity() {
assert_eq!(Matrix::default(), Matrix::IDENTITY);
}
#[test]
fn test_matrix_translate_matches_thorvg() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
shape.translate(10.0, 20.0).unwrap();
let thorvg_m = shape.transform().unwrap();
assert_matrix_close(&Matrix::default().translate(10.0, 20.0), &thorvg_m);
}
#[test]
fn test_matrix_scale_matches_thorvg() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
shape.scale(2.5).unwrap();
let thorvg_m = shape.transform().unwrap();
assert_matrix_close(&Matrix::default().scale(2.5, 2.5), &thorvg_m);
}
#[test]
fn test_matrix_rotate_matches_thorvg() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
shape.rotate(30.0).unwrap();
let thorvg_m = shape.transform().unwrap();
assert_matrix_close(&Matrix::default().rotate(30.0), &thorvg_m);
}
#[test]
fn test_matrix_scale_then_rotate_matches_thorvg() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
shape.scale(1.5).unwrap();
shape.rotate(40.0).unwrap();
let thorvg_m = shape.transform().unwrap();
assert_matrix_close(&Matrix::default().scale(1.5, 1.5).rotate(40.0), &thorvg_m);
}
#[test]
fn test_matrix_combinator_roundtrips_through_thorvg() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
let m = Matrix::default()
.translate(5.0, -3.0)
.rotate(25.0)
.scale(2.0, 0.5);
shape.set_transform(&m).unwrap();
assert_matrix_close(&shape.transform().unwrap(), &m);
}
#[test]
fn test_paint_id_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
shape.set_id(42).unwrap();
assert_eq!(shape.id(), 42);
}
#[test]
fn test_paint_type() {
let engine = Thorvg::init(0).unwrap();
let shape = engine.shape().unwrap();
assert_eq!(shape.paint_type().unwrap(), PaintType::Shape);
let scene = engine.scene().unwrap();
assert_eq!(scene.paint_type().unwrap(), PaintType::Scene);
let picture = engine.picture().unwrap();
assert_eq!(picture.paint_type().unwrap(), PaintType::Picture);
let text = engine.text().unwrap();
assert_eq!(text.paint_type().unwrap(), PaintType::Text);
}
#[test]
fn test_gradient_type_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let linear = engine.linear_gradient().unwrap();
assert_eq!(linear.gradient_type().unwrap(), PaintType::LinearGradient);
let radial = engine.radial_gradient().unwrap();
assert_eq!(radial.gradient_type().unwrap(), PaintType::RadialGradient);
}
#[test]
fn test_linear_gradient_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let mut grad = engine.linear_gradient().unwrap();
grad.set_bounds(10.0, 20.0, 300.0, 400.0).unwrap();
let (x1, y1, x2, y2) = grad.bounds().unwrap();
assert!((x1 - 10.0).abs() < f32::EPSILON);
assert!((y1 - 20.0).abs() < f32::EPSILON);
assert!((x2 - 300.0).abs() < f32::EPSILON);
assert!((y2 - 400.0).abs() < f32::EPSILON);
}
#[test]
fn test_radial_gradient_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let mut grad = engine.radial_gradient().unwrap();
grad.set_radial(100.0, 120.0, 50.0, 10.0, 20.0, 5.0)
.unwrap();
let (cx, cy, r, fx, fy, fr) = grad.radial().unwrap();
assert!((cx - 100.0).abs() < f32::EPSILON);
assert!((cy - 120.0).abs() < f32::EPSILON);
assert!((r - 50.0).abs() < f32::EPSILON);
assert!((fx - 10.0).abs() < f32::EPSILON);
assert!((fy - 20.0).abs() < f32::EPSILON);
assert!((fr - 5.0).abs() < f32::EPSILON);
}
#[test]
fn test_gradient_spread_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let mut grad = engine.linear_gradient().unwrap();
assert_eq!(grad.spread().unwrap(), FillSpread::Pad);
grad.set_spread(FillSpread::Reflect).unwrap();
assert_eq!(grad.spread().unwrap(), FillSpread::Reflect);
grad.set_spread(FillSpread::Repeat).unwrap();
assert_eq!(grad.spread().unwrap(), FillSpread::Repeat);
}
#[test]
fn test_gradient_color_stops_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let mut grad = engine.linear_gradient().unwrap();
let stops = [
ColorStop { offset: 0.0, color: Rgba::new(10, 20, 30, 40) },
ColorStop { offset: 0.5, color: Rgba::new(100, 110, 120, 130) },
ColorStop { offset: 1.0, color: Rgba::new(200, 210, 220, 230) },
];
grad.set_color_stops(&stops).unwrap();
let got = grad.color_stops().unwrap();
assert_eq!(got.len(), 3);
for (a, b) in stops.iter().zip(got.iter()) {
assert!((a.offset - b.offset).abs() < f32::EPSILON);
assert_eq!(a.color, b.color);
}
}
#[test]
fn test_stroke_dash_roundtrip() {
let engine = Thorvg::init(0).unwrap();
let mut shape = engine.shape().unwrap();
shape.set_stroke_width(2.0).unwrap();
shape.set_stroke_dash(&[10.0, 5.0, 3.0], 2.5).unwrap();
let (pattern, offset) = shape.stroke_dash().unwrap();
assert_eq!(pattern.len(), 3);
assert!((pattern[0] - 10.0).abs() < f32::EPSILON);
assert!((pattern[1] - 5.0).abs() < f32::EPSILON);
assert!((pattern[2] - 3.0).abs() < f32::EPSILON);
assert!((offset - 2.5).abs() < f32::EPSILON);
}
#[test]
fn test_path_segments_size_hint_tracks_remaining_commands() {
let path = Path {
commands: vec![PathCommand::MoveTo, PathCommand::LineTo, PathCommand::Close],
points: vec![Point::new(0.0, 0.0), Point::new(10.0, 10.0)],
};
let mut it = path.segments();
assert_eq!(it.size_hint(), (0, Some(3)));
it.next();
assert_eq!(it.size_hint(), (0, Some(2)));
assert_eq!(path.segments().count(), 3);
}
#[test]
fn test_picture_create_destroy() {
let engine = Thorvg::init(0).unwrap();
let _pic = engine.picture().unwrap();
}
#[test]
fn test_picture_load_svg_from_memory() {
let engine = Thorvg::init(0).unwrap();
let svg = b"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><rect width=\"100\" height=\"100\" fill=\"red\"/></svg>";
let mut pic = engine.picture().unwrap();
pic.load_data(svg, MimeType::Svg, None).unwrap();
let (w, h) = pic.size().unwrap();
assert!(w > 0.0);
assert!(h > 0.0);
}
#[test]
fn test_animation_create_destroy() {
let engine = Thorvg::init(0).unwrap();
let _anim = engine.animation().unwrap();
}
#[test]
fn test_saver_create_destroy() {
let engine = Thorvg::init(0).unwrap();
let _saver = engine.saver().unwrap();
}
#[test]
fn test_saver_save_animation_ownership_transfer() {
let engine = Thorvg::init(0).unwrap();
let mut saver = engine.saver().unwrap();
let anim = engine.animation().unwrap();
let r = saver.save_animation_to_str(anim, "/tmp/thorvg-rs-test.gif", 100, 30);
assert!(r.is_err()); }
#[test]
fn test_text_get_text_round_trips_set_text() {
let engine = Thorvg::init(0).unwrap();
let mut text = engine.text().unwrap();
assert_eq!(text.text(), None);
text.set_text("Hello, ThorVG").unwrap();
assert_eq!(text.text().as_deref(), Some("Hello, ThorVG"));
}
#[test]
fn test_text_set_color_and_outline_accept_rgb() {
let engine = Thorvg::init(0).unwrap();
let mut text = engine.text().unwrap();
text.set_color(Rgb::new(255, 128, 0)).unwrap();
text.set_outline(2.0, Rgb::new(0, 0, 0)).unwrap();
}
#[test]
fn test_glyph_metrics_iter_walks_every_glyph_with_byte_ranges() {
static FONT: &[u8] = include_bytes!("../../thorvg-sys/thorvg/test/resources/Arial.ttf");
let engine = Thorvg::init(0).unwrap();
engine.load_font_data_static("Arial", FONT, None).unwrap();
let mut text = engine.text().unwrap();
text.set_font("Arial").unwrap();
text.set_size(24.0).unwrap();
let s = "Aé1";
let items: alloc::vec::Vec<_> = text
.glyph_metrics_iter(s)
.unwrap()
.map(|r| r.unwrap())
.collect();
assert_eq!(items.len(), s.chars().count());
let mut cursor = 0;
for (metrics, range) in &items {
assert_eq!(range.start, cursor, "ranges must be contiguous");
assert!(range.end > range.start, "each glyph consumes >= 1 byte");
assert_eq!(s[range.clone()].chars().count(), 1);
assert!(metrics.advance >= 0.0);
cursor = range.end;
}
assert_eq!(cursor, s.len(), "ranges must cover the whole string");
let first = text.glyph_metrics("A").unwrap();
assert_eq!(first.advance, items[0].0.advance);
}
#[test]
fn test_glyph_metrics_iter_rejects_interior_nul() {
let engine = Thorvg::init(0).unwrap();
let text = engine.text().unwrap();
assert!(text.glyph_metrics_iter("a\0b").is_err());
}
#[test]
fn test_load_font_data_owned_buffer_is_safe() {
let engine = Thorvg::init(0).unwrap();
let font_bytes: alloc::vec::Vec<u8> =
include_bytes!("../../thorvg-sys/thorvg/test/resources/Arial.ttf").to_vec();
engine
.load_font_data("Arial-Owned", &font_bytes, None)
.unwrap();
drop(font_bytes); }
#[test]
fn test_load_font_data_static_zero_copy() {
static FONT: &[u8] = include_bytes!("../../thorvg-sys/thorvg/test/resources/Arial.ttf");
let engine = Thorvg::init(0).unwrap();
engine
.load_font_data_static("Arial-Static", FONT, None)
.unwrap();
}
#[test]
fn test_accessor_create_destroy() {
let engine = Thorvg::init(0).unwrap();
let _acc = engine.accessor().unwrap();
}
#[test]
fn test_accessor_generate_id() {
let _engine = Thorvg::init(0).unwrap();
let id = Accessor::generate_id("test_layer");
assert!(id.is_some());
let id2 = Accessor::generate_id("test_layer");
assert_eq!(id, id2); let id3 = Accessor::generate_id("other_layer");
assert_ne!(id, id3); }
#[test]
fn test_accessor_for_each_borrowed_views() {
let engine = Thorvg::init(0).unwrap();
let mut acc = engine.accessor().unwrap();
let mut parent = engine.scene().unwrap();
let mut child = engine.shape().unwrap();
child.append_rect(Rect::new(0.0, 0.0, 10.0, 10.0)).unwrap();
parent.add(child).unwrap();
let mut visited: alloc::vec::Vec<PaintType> = alloc::vec::Vec::new();
acc.for_each(&parent, |view, paint| {
let _ = view.get_name(0);
visited.push(paint.paint_type().unwrap_or(PaintType::Undefined));
true
})
.unwrap();
assert!(visited.contains(&PaintType::Scene));
assert!(visited.contains(&PaintType::Shape));
}
#[test]
fn test_invalid_picture_load() {
let engine = Thorvg::init(0).unwrap();
let mut pic = engine.picture().unwrap();
let result = pic.load_from_str("/nonexistent/path.svg");
assert!(result.is_err());
}
#[test]
fn test_shape_stroke_color_without_stroke() {
let engine = Thorvg::init(0).unwrap();
let shape = engine.shape().unwrap();
let result = shape.stroke_color();
assert!(result.is_err());
}
#[test]
fn test_clip_lifecycle() {
let engine = Thorvg::init(0).unwrap();
let mut canvas = engine.sw_canvas(EngineOption::Default).unwrap();
let mut buffer = vec![0u32; 100 * 100];
unsafe { canvas.set_target(&mut buffer, 100, 100, 100, ColorSpace::ABGR8888) }.unwrap();
let mut shape = engine.shape().unwrap();
shape
.append_rect(Rect::new(0.0, 0.0, 100.0, 100.0))
.unwrap();
shape.set_fill_color(Rgba::new(255, 0, 0, 255)).unwrap();
let mut clipper = engine.shape().unwrap();
clipper
.append_circle(Circle::new(50.0, 50.0, 30.0))
.unwrap();
shape.set_clip(clipper).unwrap();
canvas.add(shape).unwrap();
canvas.draw(true).unwrap();
canvas.sync().unwrap();
}
#[test]
fn test_asset_resolver_install_replace_clear() {
use alloc::sync::Arc;
use core::sync::atomic::{AtomicUsize, Ordering};
let engine = Thorvg::init(0).unwrap();
let mut pic = engine.picture().unwrap();
let calls = Arc::new(AtomicUsize::new(0));
let calls_clone = Arc::clone(&calls);
pic.set_asset_resolver(move |_src| {
calls_clone.fetch_add(1, Ordering::SeqCst);
None })
.unwrap();
let calls_clone = Arc::clone(&calls);
pic.set_asset_resolver(move |_src| {
calls_clone.fetch_add(10, Ordering::SeqCst);
None
})
.unwrap();
pic.clear_asset_resolver().unwrap();
drop(pic);
assert_eq!(calls.load(Ordering::SeqCst), 0);
}
const LOTTIE_RESOLVER_JSON: &[u8] =
include_bytes!("../../thorvg-sys/thorvg/test/resources/resolver.json");
#[test]
fn test_asset_resolver_invoked_during_lottie_render() {
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
let engine = Thorvg::init(0).unwrap();
let mut lottie = engine.lottie_animation().unwrap();
let calls = Arc::new(AtomicUsize::new(0));
let seen_srcs: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let calls_clone = Arc::clone(&calls);
let seen_clone = Arc::clone(&seen_srcs);
lottie
.picture_mut()
.set_asset_resolver(move |src| {
calls_clone.fetch_add(1, Ordering::SeqCst);
seen_clone.lock().unwrap().push(String::from(src));
None })
.unwrap();
lottie.load_data(LOTTIE_RESOLVER_JSON).unwrap();
let total = lottie.total_frame().unwrap();
let _ = lottie.set_frame(total * 0.5);
let mut canvas = engine.sw_canvas(EngineOption::Default).unwrap();
let mut buffer = vec![0u32; 800 * 800];
unsafe { canvas.set_target(&mut buffer, 800, 800, 800, ColorSpace::ABGR8888) }.unwrap();
let _ = canvas.draw(true);
let _ = canvas.sync();
assert!(
calls.load(Ordering::SeqCst) >= 1,
"resolver trampoline should have been called at least once"
);
let seen = seen_srcs.lock().unwrap().clone();
assert!(
seen.iter().any(|s| s.ends_with("logo.png")),
"resolver should have seen the image asset path, got: {seen:?}"
);
}
#[test]
fn test_asset_resolver_returns_bytes_path() {
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::sync::atomic::{AtomicUsize, Ordering};
const LOGO_PNG: &[u8] = include_bytes!("assets/logo.png");
let engine = Thorvg::init(0).unwrap();
let mut lottie = engine.lottie_animation().unwrap();
let calls = Arc::new(AtomicUsize::new(0));
let calls_clone = Arc::clone(&calls);
lottie
.picture_mut()
.set_asset_resolver(move |_src| {
calls_clone.fetch_add(1, Ordering::SeqCst);
Some((Vec::from(LOGO_PNG), MimeType::Png))
})
.unwrap();
lottie.load_data(LOTTIE_RESOLVER_JSON).unwrap();
let total = lottie.total_frame().unwrap();
let _ = lottie.set_frame(total * 0.5);
assert!(
calls.load(Ordering::SeqCst) >= 1,
"resolver trampoline (Some branch) should have been called"
);
}
#[cfg(feature = "std")]
#[test]
fn test_asset_resolver_panic_is_caught() {
let engine = Thorvg::init(0).unwrap();
let mut lottie = engine.lottie_animation().unwrap();
lottie
.picture_mut()
.set_asset_resolver(|_src| -> Option<(alloc::vec::Vec<u8>, MimeType)> {
panic!("intentional panic from test resolver");
})
.unwrap();
lottie.load_data(LOTTIE_RESOLVER_JSON).unwrap();
let total = lottie.total_frame().unwrap();
let _ = lottie.set_frame(total * 0.5);
}
#[test]
fn test_audio_resolver_install_replace_clear() {
use alloc::sync::Arc;
use core::sync::atomic::{AtomicUsize, Ordering};
let engine = Thorvg::init(0).unwrap();
let mut lottie = engine.lottie_animation().unwrap();
assert_eq!(
lottie.set_audio_resolver(|_info: &AudioInfo| {}),
Err(Error::InsufficientCondition)
);
lottie.load_data(LOTTIE_RESOLVER_JSON).unwrap();
let calls = Arc::new(AtomicUsize::new(0));
let calls_clone = Arc::clone(&calls);
lottie
.set_audio_resolver(move |_info: &AudioInfo| {
calls_clone.fetch_add(1, Ordering::SeqCst);
})
.unwrap();
let calls_clone = Arc::clone(&calls);
lottie
.set_audio_resolver(move |_info: &AudioInfo| {
calls_clone.fetch_add(10, Ordering::SeqCst);
})
.unwrap();
lottie.clear_audio_resolver().unwrap();
drop(lottie);
assert_eq!(calls.load(Ordering::SeqCst), 0);
}
const LOTTIE_AUDIO_JSON: &[u8] = include_bytes!("assets/audio_layer.json");
#[test]
fn test_audio_resolver_invoked_for_audio_layer() {
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use std::sync::Mutex;
#[derive(Clone)]
struct Snap {
active: bool,
embedded: bool,
size: u32,
mime: Option<String>,
data: Option<Vec<u8>>,
source: Option<String>,
}
let engine = Thorvg::init(0).unwrap();
let mut lottie = engine.lottie_animation().unwrap();
lottie.load_data(LOTTIE_AUDIO_JSON).unwrap();
let snaps: Arc<Mutex<Vec<Snap>>> = Arc::new(Mutex::new(Vec::new()));
let snaps_c = Arc::clone(&snaps);
lottie
.set_audio_resolver(move |info: &AudioInfo| {
snaps_c.lock().unwrap().push(Snap {
active: info.is_active(),
embedded: info.is_embedded(),
size: info.size(),
mime: info.mime_type().map(String::from),
data: info.embedded_data().map(<[u8]>::to_vec),
source: info.source().map(String::from),
});
})
.unwrap();
let _ = lottie.set_frame(30.0);
let _ = lottie.set_frame(90.0);
let _ = lottie.set_frame(30.0);
let got = snaps.lock().unwrap().clone();
assert!(!got.is_empty(), "audio resolver never fired");
assert!(
got.iter().any(|s| s.active
&& s.embedded
&& s.size == 3
&& s.mime.as_deref() == Some("mpeg")
&& s.data.as_deref() == Some(b"ABC")
&& s.source.is_none()),
"expected an active embedded-audio callback, got {} calls",
got.len()
);
assert!(
got.iter().any(|s| !s.active),
"expected a deactivation callback when stepping past the out-point"
);
}
#[test]
fn test_audio_resolver_panic_is_caught() {
let engine = Thorvg::init(0).unwrap();
let mut lottie = engine.lottie_animation().unwrap();
lottie.load_data(LOTTIE_AUDIO_JSON).unwrap();
lottie
.set_audio_resolver(|_info: &AudioInfo| {
panic!("intentional panic from test audio resolver");
})
.unwrap();
let _ = lottie.set_frame(30.0);
assert!(lottie.total_frame().unwrap() > 0.0);
}
#[test]
fn test_mask_getter_round_trip() {
let engine = Thorvg::init(0).unwrap();
let mut paint = engine.shape().unwrap();
paint
.append_rect(Rect::new(0.0, 0.0, 100.0, 100.0))
.unwrap();
assert!(paint.mask().is_none(), "no mask before set_mask");
let mut mask_shape = engine.shape().unwrap();
mask_shape
.append_circle(Circle::new(50.0, 50.0, 30.0))
.unwrap();
paint.set_mask(mask_shape, MaskMethod::Alpha).unwrap();
let (got, method) = paint.mask().expect("mask should be set");
assert_eq!(method, MaskMethod::Alpha);
assert_eq!(got.paint_type().unwrap(), PaintType::Shape);
}
#[test]
fn test_full_pipeline_scene_with_effects() {
let engine = Thorvg::init(0).unwrap();
let mut canvas = engine.sw_canvas(EngineOption::Default).unwrap();
let mut buffer = vec![0u32; 200 * 200];
unsafe { canvas.set_target(&mut buffer, 200, 200, 200, ColorSpace::ABGR8888) }.unwrap();
let mut scene = engine.scene().unwrap();
let mut s1 = engine.shape().unwrap();
s1.append_rect(Rect::new(10.0, 10.0, 80.0, 80.0).corner_radius(5.0))
.unwrap();
s1.set_fill_color(Rgba::new(255, 0, 0, 255)).unwrap();
scene.add(s1).unwrap();
let mut s2 = engine.shape().unwrap();
s2.append_circle(Circle::new(120.0, 50.0, 30.0)).unwrap();
s2.set_fill_color(Rgba::new(0, 0, 255, 255)).unwrap();
scene.add(s2).unwrap();
scene
.add_gaussian_blur_effect(GaussianBlur {
sigma: 2.0,
direction: BlurDirection::Both,
border: BlurBorder::Duplicate,
quality: 50,
})
.unwrap();
scene.translate(10.0, 10.0).unwrap();
canvas.add(scene).unwrap();
canvas.draw(true).unwrap();
canvas.sync().unwrap();
assert!(buffer.iter().any(|&px| px != 0));
}