1use crate::{VectorGroup, VectorItem, VectorPath, VectorTree};
32
33use motion_core::{Color, FillRule, Path2D, Point, Rect, Size, Transform};
34use std::{
35 str::FromStr,
36 sync::{
37 Arc,
38 atomic::{AtomicBool, AtomicUsize, Ordering},
39 },
40};
41
42mod convert;
43mod filters;
44#[cfg(test)]
45mod tests;
46mod view;
47
48use convert::{group, rect};
49use filters::filter_group;
50
51pub use view::Svg;
52
53const MAX_SOURCE: usize = 8 * 1024 * 1024;
55
56#[derive(Clone, Debug)]
59pub struct SvgAsset {
60 pub view_box: Rect,
62 pub tree: Arc<VectorTree>,
64 diagnostics: Arc<[SvgDiagnostic]>,
65 size: Size,
66}
67
68#[derive(Clone, Debug)]
70pub struct SvgDiagnostic {
71 pub element: Option<Arc<str>>,
73 pub message: Arc<str>,
75}
76
77impl SvgAsset {
78 pub fn from_bytes(bytes: &[u8]) -> motion_core::Result<Self> {
80 Self::from_bytes_with_fonts(bytes, &crate::FontLibrary::new())
81 }
82
83 pub fn from_bytes_with_fonts(
85 bytes: &[u8],
86 fonts: &crate::FontLibrary,
87 ) -> motion_core::Result<Self> {
88 Self::decode(bytes, fonts.database.clone())
89 }
90
91 pub(crate) fn decode(bytes: &[u8], fonts: Arc<fontdb::Database>) -> motion_core::Result<Self> {
92 if bytes.len() > MAX_SOURCE {
93 return Err(unsupported("SVG source exceeds its encoded byte allowance"));
94 }
95 let bytes = bytes.strip_prefix(&[0xef, 0xbb, 0xbf]).unwrap_or(bytes);
96 let text =
97 std::str::from_utf8(bytes).map_err(|error| motion_core::Error::Svg(Box::new(error)))?;
98 let document = usvg::roxmltree::Document::parse_with_options(
99 text,
100 usvg::roxmltree::ParsingOptions {
101 allow_dtd: false,
102 nodes_limit: 100000,
103 ..Default::default()
104 },
105 )
106 .map_err(|error| motion_core::Error::Svg(Box::new(error)))?;
107 for node in document.descendants() {
108 if node.ancestors().count() > 64 {
109 return Err(unsupported("SVG XML exceeds depth 64"));
110 }
111 if node.is_element() {
112 if matches!(
113 node.tag_name().name(),
114 "script"
115 | "foreignObject"
116 | "animate"
117 | "animateMotion"
118 | "animateTransform"
119 | "set"
120 ) {
121 return Err(unsupported(
122 "Only static SVG without scripts or foreign objects is supported",
123 ));
124 }
125 if node.attributes().any(|attribute| {
126 attribute.name() == "href"
127 && !attribute.value().is_empty()
128 && !attribute.value().starts_with('#')
129 && !attribute.value().starts_with("data:")
130 }) {
131 return Err(unsupported("External SVG references are not supported"));
132 }
133 }
134 }
135 let has_text = document.descendants().any(|node| node.has_tag_name("text"));
136 if fonts.faces().next().is_none() && has_text {
137 return Err(unsupported(
138 "SVG text requires an explicitly registered FontLibrary",
139 ));
140 }
141 let failed = AtomicBool::new(false);
142 let pixels = AtomicUsize::new(0);
143 let nesting = AtomicUsize::new(0);
144 let options = usvg::Options {
145 fontdb: fonts,
146 image_href_resolver: usvg::ImageHrefResolver {
147 resolve_string: Box::new(|_, _| {
148 failed.store(true, Ordering::Relaxed);
149 None
150 }),
151 resolve_data: Box::new(|mime, data, options| {
152 let result = (|| {
153 if data.len() > MAX_SOURCE {
154 return None;
155 }
156 if mime == "image/svg+xml" {
157 let depth = nesting.fetch_add(1, Ordering::Relaxed);
158 if depth >= 8 {
159 nesting.fetch_sub(1, Ordering::Relaxed);
160 return None;
161 }
162 let parsed = (|| {
163 let text = std::str::from_utf8(&data).ok()?;
164 let doc = usvg::roxmltree::Document::parse_with_options(
165 text,
166 usvg::roxmltree::ParsingOptions {
167 allow_dtd: false,
168 nodes_limit: 20000,
169 ..Default::default()
170 },
171 )
172 .ok()?;
173 for node in doc.descendants().filter(|node| node.is_element()) {
174 if node.ancestors().count() > 64
175 || matches!(
176 node.tag_name().name(),
177 "script"
178 | "foreignObject"
179 | "animate"
180 | "animateMotion"
181 | "animateTransform"
182 | "set"
183 )
184 || node.attributes().any(|attribute| {
185 attribute.name() == "href"
186 && !attribute.value().is_empty()
187 && !attribute.value().starts_with('#')
188 && !attribute.value().starts_with("data:")
189 })
190 {
191 return None;
192 }
193 }
194 Some(usvg::ImageKind::SVG(
195 usvg::Tree::from_xmltree(&doc, options).ok()?,
196 ))
197 })();
198 nesting.fetch_sub(1, Ordering::Relaxed);
199 return parsed;
200 }
201 let reader = image::ImageReader::new(std::io::Cursor::new(data.as_slice()))
202 .with_guessed_format()
203 .ok()?;
204 let format = reader.format()?;
205
206 use image::ImageDecoder as _;
207 let dimensions = reader.into_decoder().ok()?.dimensions();
208 let count = dimensions.0 as usize * dimensions.1 as usize;
209 pixels
210 .try_update(Ordering::Relaxed, Ordering::Relaxed, |total| {
211 total
212 .checked_add(count)
213 .filter(|total| *total <= 16_777_216)
214 })
215 .ok()?;
216 Some(match format {
217 image::ImageFormat::Png => usvg::ImageKind::PNG(data),
218 image::ImageFormat::Jpeg => usvg::ImageKind::JPEG(data),
219 image::ImageFormat::WebP => usvg::ImageKind::WEBP(data),
220 _ => return None,
221 })
222 })();
223 if result.is_none() {
224 failed.store(true, Ordering::Relaxed);
225 }
226 result
227 }),
228 },
229 ..Default::default()
230 };
231 let parsed = usvg::Tree::from_xmltree(&document, &options)
232 .map_err(|error| motion_core::Error::Svg(Box::new(error)))?;
233 if failed.load(Ordering::Relaxed) {
234 return Err(unsupported(
235 "Invalid, external, oversized or unsupported SVG image resource",
236 ));
237 }
238 let size = Size::new(
239 f64::from(parsed.size().width()),
240 f64::from(parsed.size().height()),
241 );
242 let viewport = size.to_rect();
243 let view_box = if let Some(value) = document.root_element().attribute("viewBox") {
244 let value = svgtypes::ViewBox::from_str(value)
245 .map_err(|error| motion_core::Error::Svg(Box::new(error)))?;
246 Rect::new(value.x, value.y, value.x + value.w, value.y + value.h)
247 } else {
248 viewport
249 };
250 let mut count = 0;
251 let mut diagnostics = Vec::new();
252 let root = group(parsed.root(), 0, &mut count, &mut diagnostics)?;
253 let mut tree = VectorTree::new(viewport, root)?;
254 if let Some(root_id) = document
255 .root_element()
256 .attribute("id")
257 .filter(|id| !id.is_empty())
258 && !tree.ids().contains_key(root_id)
259 {
260 let mut root = tree.root().clone();
261 root.id = Some(root_id.into());
262 tree = VectorTree::new(viewport, root)?;
263 }
264 Ok(Self {
265 view_box,
266 tree: Arc::new(tree),
267 diagnostics: diagnostics.into(),
268 size,
269 })
270 }
271
272 pub fn size(&self) -> Size {
274 self.size
275 }
276
277 pub fn diagnostics(&self) -> &[SvgDiagnostic] {
279 &self.diagnostics
280 }
281}
282
283impl motion_core::Asset for SvgAsset {
284 const KIND: &'static str = "svg";
285
286 fn decode(bytes: &[u8], context: &motion_core::LoadContext) -> motion_core::Result<Self> {
287 if let Some(fonts) = context.fonts() {
288 let fonts = fonts.downcast_ref::<crate::FontLibrary>().ok_or_else(|| {
289 unsupported("SVG received an incompatible frozen font environment")
290 })?;
291 Self::from_bytes_with_fonts(bytes, fonts)
292 } else {
293 Self::from_bytes(bytes)
294 }
295 }
296
297 fn retained_bytes(&self) -> usize {
298 fn paint_bytes(paint: &motion_core::Paint) -> usize {
299 paint.gradient().map_or(0, |gradient| {
300 gradient.stops.len() * std::mem::size_of::<(f64, Color)>()
301 })
302 }
303
304 fn bytes(group: &VectorGroup) -> usize {
305 let mut total = std::mem::size_of::<VectorGroup>()
306 + group
307 .filters
308 .iter()
309 .map(|filter| {
310 std::mem::size_of::<motion_core::Filter>() + filter.heap_bytes() as usize
311 })
312 .sum::<usize>()
313 + group.id.as_ref().map_or(0, |id| id.len())
314 + group.children.len() * std::mem::size_of::<VectorItem>();
315 if let Some(clip) = &group.clip {
316 total += std::mem::size_of_val(clip.path.verbs());
317 }
318 if let Some(mask) = &group.mask {
319 total += bytes(mask.tree.root());
320 }
321 for item in &group.children {
322 total += match item {
323 VectorItem::Group(group) => bytes(group),
324 VectorItem::Path(path) => {
325 std::mem::size_of_val(path.path.verbs())
326 + path.id.as_ref().map_or(0, |id| id.len())
327 + path
328 .fill
329 .as_ref()
330 .map_or(0, |(paint, _)| paint_bytes(paint))
331 + path
332 .stroke
333 .as_ref()
334 .map_or(0, |(_, paint)| paint_bytes(paint))
335 }
336 VectorItem::Image(image) => {
337 image.image.retained_bytes() + image.id.as_ref().map_or(0, |id| id.len())
338 }
339 };
340 }
341 total
342 }
343 bytes(self.tree.root())
344 + self
345 .tree
346 .ids()
347 .iter()
348 .map(|(id, address)| id.len() + address.len() * std::mem::size_of::<usize>())
349 .sum::<usize>()
350 + self
351 .diagnostics
352 .iter()
353 .map(|diagnostic| {
354 std::mem::size_of::<SvgDiagnostic>()
355 + diagnostic.message.len()
356 + diagnostic.element.as_ref().map_or(0, |id| id.len())
357 })
358 .sum::<usize>()
359 }
360}
361
362fn unsupported(message: impl Into<String>) -> motion_core::Error {
363 motion_core::ValidationError::unsupported("svg", "content", message.into()).into()
364}
365
366fn matrix(value: usvg::Transform) -> Transform {
367 Transform::from_coefficients([
368 f64::from(value.sx),
369 f64::from(value.ky),
370 f64::from(value.kx),
371 f64::from(value.sy),
372 f64::from(value.tx),
373 f64::from(value.ty),
374 ])
375}
376
377fn geometry(source: &usvg::tiny_skia_path::Path) -> Result<Path2D, motion_core::ValidationError> {
378 use usvg::tiny_skia_path::PathSegment;
379 let mut path = Path2D::new();
380 let point =
381 |point: usvg::tiny_skia_path::Point| Point::new(f64::from(point.x), f64::from(point.y));
382 for segment in source.segments() {
383 match segment {
384 PathSegment::MoveTo(end) => path.try_move_to(point(end)),
385 PathSegment::LineTo(end) => path.try_line_to(point(end)),
386 PathSegment::QuadTo(control, end) => path.try_quadratic_to(point(control), point(end)),
387 PathSegment::CubicTo(first, second, end) => {
388 path.try_cubic_to(point(first), point(second), point(end))
389 }
390 PathSegment::Close => path.try_close(),
391 }?;
392 }
393 Ok(path)
394}
395
396fn id(value: &str) -> Option<Arc<str>> {
397 (!value.is_empty()).then(|| Arc::from(value))
398}