1use crate::ImageAsset;
4#[cfg(feature = "text")]
5use motion_core::LoadContext;
6use motion_core::{Asset, Assets, Handle};
7#[cfg(test)]
8use std::sync::Arc;
9
10pub trait AssetsExt {
12 fn asset<A: Asset>(&self, path: &str) -> Handle<A>;
14
15 fn image(&self, path: &str) -> Handle<ImageAsset> {
17 self.asset(path)
18 }
19
20 #[cfg(feature = "svg")]
22 fn svg(&self, path: &str) -> Handle<crate::SvgAsset> {
23 self.asset(path)
24 }
25
26 #[cfg(feature = "image")]
28 fn animated_image(&self, path: &str) -> Handle<crate::AnimatedImageAsset> {
29 self.asset(path)
30 }
31
32 #[cfg(feature = "text")]
34 fn font(&self, path: &str) -> Handle<crate::FontFace> {
35 self.asset(path)
36 }
37}
38
39impl AssetsExt for Assets {
40 fn asset<A: Asset>(&self, path: &str) -> Handle<A> {
41 self.load(path)
42 }
43}
44
45#[cfg(feature = "text")]
46impl Asset for crate::FontFace {
47 const KIND: &'static str = "font";
48
49 fn decode(bytes: &[u8], _: &LoadContext) -> motion_core::Result<Self> {
50 Ok(Self::from_bytes(bytes.to_vec(), 0)?)
51 }
52
53 fn retained_bytes(&self) -> usize {
54 self.byte_len()
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[cfg(feature = "svg")]
63 #[test]
64 fn source_drawables_use_the_same_owner_as_the_scene_callback() -> motion_core::Result {
65 use motion_core::{Node, NodeModifiers, Size, Source};
66 use motion_render::{Composition, RenderContext};
67 let assets = Assets::new(".")?;
68 assets.preload(
69 "logo.png",
70 Arc::new(ImageAsset::from_rgba8(2, 2, vec![255; 16])?),
71 )?;
72 let svg = assets.load::<crate::SvgAsset>(Source::Bytes {
73 key: "parsed-svg".into(),
74 data: Arc::from(&br##"<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2"><rect width="2" height="2" fill="#00ff00"/></svg>"##[..]),
75 }).get()?;
76 assets.preload("diagram.svg", svg)?;
77 let composition =
78 Composition::new(8, 4).render_context(RenderContext::new().with_assets(assets.clone()));
79 let frame = composition.render_frame(0, Size::new(8.0, 4.0), |context| {
80 assert!(context.assets().pool().same_pool(assets.pool()));
81 for node in [
82 Node::from(crate::Image::open("logo.png")),
83 Node::from(crate::Svg::open("diagram.svg")),
84 ] {
85 assert_eq!(
86 context.measure(&node).unwrap(),
87 motion_core::Rect::new(0.0, 0.0, 2.0, 2.0)
88 );
89 }
90 Node::group((
91 crate::Image::open("logo.png"),
92 crate::Svg::open("diagram.svg").translate(4.0, 0.0),
93 ))
94 })?;
95 assert_eq!(frame.pixels()[0], 0xffffff);
96 assert_eq!(frame.pixels()[4], 0x00ff00);
97 let override_assets = Assets::new(".")?;
98 override_assets.preload(
99 "logo.png",
100 Arc::new(ImageAsset::from_rgba8(1, 1, vec![255, 0, 0, 255])?),
101 )?;
102 let frame = composition
103 .resources(override_assets.clone())
104 .render_frame(0, Size::new(8.0, 4.0), |context| {
105 assert!(context.assets().pool().same_pool(override_assets.pool()));
106 crate::Image::open("logo.png").into()
107 })?;
108 assert_eq!(frame.pixels()[0], 0xff0000);
109 Ok(())
110 }
111
112 #[test]
113 fn source_image_missing_files_follow_strict_and_placeholder_policies() -> motion_core::Result {
114 use motion_core::Size;
115 use motion_render::Composition;
116 let directory = tempfile::tempdir()?;
117 let assets = Assets::new(directory.path())?;
118 let composition = Composition::new(2, 2).resources(assets);
119 let build = |_: &motion_core::SceneContext<'_>| {
120 crate::Image::open("absent.png").size(2.0, 2.0).into()
121 };
122 let error = composition
123 .render_frame(0, Size::new(2.0, 2.0), build)
124 .err()
125 .unwrap();
126 assert_eq!(error.kind(), motion_core::ErrorKind::Asset);
127 assert_eq!(error.frame(), Some(0));
128 let frame =
129 composition
130 .allow_missing_assets(true)
131 .render_frame(0, Size::new(2.0, 2.0), build)?;
132 assert_eq!(frame.pixels()[0], 0xff00ff);
133 Ok(())
134 }
135
136 #[cfg(feature = "text")]
137 #[test]
138 fn invalid_font_does_not_change_resources() {
139 let assets = Assets::default();
140 assert!(crate::FontFace::from_bytes(Vec::new(), 0).is_err());
141 assert_eq!(assets.pool().retained_bytes(), 0);
142 }
143
144 #[test]
145 fn identifier_limit_bounds_tiny_shared_assets() -> motion_core::Result {
146 let pixel = Arc::new(ImageAsset::from_rgba8(1, 1, vec![255; 4])?);
147 let bytes = pixel.retained_bytes();
148 let assets = Assets::new(".")?;
149 for index in 0..4096 {
150 assets.preload(format!("pixel-{index}"), pixel.clone())?;
151 }
152 assert!(assets.preload("overflow", pixel).is_err());
153 assert_eq!(assets.pool().retained_bytes(), 4096 * bytes);
154 Ok(())
155 }
156
157 #[test]
158 fn preloads_respect_allowance_and_snapshot_lifetimes() -> motion_core::Result {
159 let image = Arc::new(ImageAsset::from_rgba8(2, 2, vec![255; 16])?);
160 let bytes = image.retained_bytes();
161 let pool = motion_core::ResourcePool::with_limits(motion_core::CacheLimits {
162 entries: 4096,
163 bytes,
164 })?;
165 let assets = Assets::new(".")?.with_pool(pool.clone());
166 assets.preload("sun", image.clone())?;
167 assert_eq!(pool.retained_bytes(), bytes);
168 assert!(assets.preload("sun", image.clone()).is_err());
169 assert!(assets.preload("moon", image.clone()).is_err());
170 assert!(Arc::ptr_eq(&image, &assets.image("sun").get()?));
171 let snapshot = assets.snapshot();
172 assert!(assets.remove::<ImageAsset>("sun"));
173 assert_eq!(pool.retained_bytes(), bytes);
174 assert!(Arc::ptr_eq(&image, &snapshot.image("sun").get()?));
175 drop(snapshot);
176 assert_eq!(pool.retained_bytes(), 0);
177 assert!(assets.remove::<ImageAsset>("moon"));
178 assets.preload("moon", image)?;
179 Ok(())
180 }
181}