1mod document;
24mod markup;
25mod path_data;
26mod render_cost;
27
28use std::io::Read as _;
29use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
30use std::sync::{Arc, Mutex, PoisonError};
31
32use alpha2_time::MediaTime;
33use resvg::tiny_skia::{Pixmap, Transform};
34use resvg::usvg::{
35 self, FontResolver, ImageHrefResolver, ImageKind, Node, Options, Tree, roxmltree,
36};
37
38use alpha2_model::{MAX_SVG_DOCUMENT_BYTES, ProviderSource};
39
40use crate::api::{
41 Diagnostic, ExtensionError, Format, FramePixels, ProbeReport, ResourceResolver, Timing,
42 VisualFrame, VisualProvider, VisualSource,
43};
44use crate::budget::{check_canvas, check_document_size, check_image_pixels};
45
46pub struct SvgProvider;
48
49impl VisualProvider for SvgProvider {
50 fn name(&self) -> &'static str {
51 "svg"
52 }
53
54 fn probe(&self, bytes: &[u8]) -> Option<Result<ProbeReport, ExtensionError>> {
55 if !looks_like_svg(bytes) {
56 return None;
57 }
58 Some(
59 check_document_size(&ProviderSource::Svg, bytes, bytes.len() as u64)
60 .and_then(|()| parse(bytes, &crate::api::NoResources))
61 .map(|(_, report)| report),
62 )
63 }
64
65 fn open(
66 &self,
67 bytes: Arc<[u8]>,
68 _source: &ProviderSource,
69 resolver: &dyn ResourceResolver,
70 ) -> Result<Box<dyn VisualSource>, ExtensionError> {
71 if !looks_like_svg(&bytes) {
72 return Err(ExtensionError::Mismatch("not an SVG document".into()));
73 }
74 check_document_size(&ProviderSource::Svg, &bytes, bytes.len() as u64)?;
75 let (tree, report) = parse(&bytes, resolver)?;
76 Ok(Box::new(SvgSource {
77 tree,
78 report,
79 last: None,
80 }))
81 }
82}
83
84fn looks_like_svg(bytes: &[u8]) -> bool {
89 let mut rest = bytes.strip_prefix(b"\xef\xbb\xbf").unwrap_or(bytes);
90 loop {
91 rest = rest.trim_ascii_start();
92 let skipped = if let Some(after) = rest.strip_prefix(b"<?") {
93 after_closing(after, b"?>")
94 } else if let Some(after) = rest.strip_prefix(b"<!--") {
95 after_closing(after, b"-->")
96 } else if let Some(after) = rest.strip_prefix(b"<!DOCTYPE") {
97 after_doctype(after)
98 } else {
99 return rest.strip_prefix(b"<svg").is_some_and(|after| {
100 after
101 .first()
102 .is_some_and(|next| next.is_ascii_whitespace() || matches!(next, b'>' | b'/'))
103 });
104 };
105 let Some(after) = skipped else {
106 return false;
107 };
108 rest = after;
109 }
110}
111
112fn after_closing<'a>(bytes: &'a [u8], closing: &[u8]) -> Option<&'a [u8]> {
114 let at = bytes
115 .windows(closing.len())
116 .position(|window| window == closing)?;
117 Some(&bytes[at + closing.len()..])
118}
119
120fn after_doctype(mut bytes: &[u8]) -> Option<&[u8]> {
125 let mut subset = false;
126 loop {
127 if let Some(after) = bytes.strip_prefix(b"<!--") {
128 bytes = after_closing(after, b"-->")?;
129 } else if let Some(after) = bytes.strip_prefix(b"<?") {
130 bytes = after_closing(after, b"?>")?;
131 } else {
132 let (&first, after) = bytes.split_first()?;
133 bytes = match first {
134 b'"' | b'\'' => after_closing(after, &[first])?,
135 b'[' if !subset => {
136 subset = true;
137 after
138 }
139 b']' if subset => {
140 subset = false;
141 after
142 }
143 b'>' if !subset => return Some(after),
144 _ => after,
145 };
146 }
147 }
148}
149
150pub(crate) const MAX_NESTING: usize = 256;
158
159pub(crate) const MAX_READING: u64 = 1 << 27;
165
166struct Reading {
170 text: AtomicUsize,
173 steps: AtomicU64,
175 nesting: AtomicUsize,
177 refused: Mutex<Option<ExtensionError>>,
180}
181
182impl Reading {
183 fn new() -> Self {
184 Self {
185 text: AtomicUsize::new(MAX_SVG_DOCUMENT_BYTES),
186 steps: AtomicU64::new(MAX_READING),
187 nesting: AtomicUsize::new(0),
188 refused: Mutex::new(None),
189 }
190 }
191
192 fn refuse(&self, error: ExtensionError) {
193 self.refused
194 .lock()
195 .unwrap_or_else(PoisonError::into_inner)
196 .get_or_insert(error);
197 }
198
199 fn refusal(&self) -> Option<ExtensionError> {
200 self.refused
201 .lock()
202 .unwrap_or_else(PoisonError::into_inner)
203 .take()
204 }
205
206 fn take_text(&self, length: usize) -> Result<(), ExtensionError> {
208 let left = self.text.load(Ordering::Relaxed);
209 let left = left.checked_sub(length).ok_or_else(too_much_text)?;
210 self.text.store(left, Ordering::Relaxed);
211 Ok(())
212 }
213}
214
215fn too_much_text() -> ExtensionError {
216 ExtensionError::Budget(format!(
217 "the SVG and the SVG images it draws are more than {MAX_SVG_DOCUMENT_BYTES} bytes of text together"
218 ))
219}
220
221fn read_document(
226 text: &[u8],
227 options: &Options,
228 reading: &Reading,
229) -> Result<Tree, ExtensionError> {
230 let malformed = |error: usvg::Error| ExtensionError::Malformed(error.to_string());
231 reading.take_text(text.len())?;
232 let text = std::str::from_utf8(text).map_err(|_| malformed(usvg::Error::NotAnUtf8Str))?;
233 let base = reading.nesting.load(Ordering::Relaxed);
234 let limit = MAX_NESTING.saturating_sub(base);
235 markup::nesting(text, limit)?;
236 let xml = roxmltree::Document::parse_with_options(
237 text,
238 roxmltree::ParsingOptions {
239 allow_dtd: true,
240 ..roxmltree::ParsingOptions::default()
241 },
242 )
243 .map_err(|error| malformed(usvg::Error::ParsingFailed(error)))?;
244 let steps = reading.steps.load(Ordering::Relaxed);
245 let conversion = document::conversion(&xml, steps, limit)?;
246 reading
247 .steps
248 .store(steps - conversion.steps, Ordering::Relaxed);
249 reading
250 .nesting
251 .store(base + conversion.depth, Ordering::Relaxed);
252 let tree = Tree::from_xmltree(&xml, options);
253 reading.nesting.store(base, Ordering::Relaxed);
254 tree.map_err(malformed)
255}
256
257fn parse(
261 bytes: &[u8],
262 resolver: &dyn ResourceResolver,
263) -> Result<(Tree, ProbeReport), ExtensionError> {
264 let registered_fonts = resolver.fonts();
265 let missing_fonts = Mutex::new(Vec::<String>::new());
266 let image_reports = Mutex::new(Vec::<Diagnostic>::new());
268 let reading = Reading::new();
269 let options = Options {
270 fontdb: Arc::new(usvg::fontdb::Database::new()),
272 font_resolver: font_resolver(®istered_fonts, &missing_fonts),
273 image_href_resolver: image_resolver(resolver, &image_reports, &reading),
274 ..Options::default()
275 };
276 let tree = read_document(bytes, &options, &reading);
277 drop(options);
278 if let Some(refusal) = reading.refusal() {
279 return Err(refusal);
280 }
281 let tree = tree?;
282 let size = tree.size().to_int_size();
283 check_canvas(size.width(), size.height())?;
284 check_image_pixels(image_pixels(tree.root()))?;
285 render_cost::check(&tree, size.width(), size.height())?;
286 let mut diagnostics = Vec::new();
287 let missing_fonts = missing_fonts
288 .into_inner()
289 .unwrap_or_else(PoisonError::into_inner);
290 if !missing_fonts.is_empty() {
291 diagnostics.push(Diagnostic::MissingFont {
292 families: missing_fonts.join(", "),
293 });
294 }
295 for diagnostic in image_reports
296 .into_inner()
297 .unwrap_or_else(PoisonError::into_inner)
298 {
299 if !diagnostics.contains(&diagnostic) {
300 diagnostics.push(diagnostic);
301 }
302 }
303 let report = ProbeReport {
304 format: Format::Svg,
305 width: size.width(),
306 height: size.height(),
307 timing: Timing::Static,
308 loop_count: None,
309 items: Vec::new(),
310 item: None,
311 color: None,
312 diagnostics,
313 };
314 Ok((tree, report))
315}
316
317fn font_resolver<'a>(
326 registered: &'a [Arc<[u8]>],
327 missing: &'a Mutex<Vec<String>>,
328) -> FontResolver<'a> {
329 let select_font = FontResolver::default_font_selector();
330 FontResolver {
331 select_font: Box::new(move |font, fontdb| {
332 if fontdb.is_empty() {
333 let fontdb = Arc::make_mut(fontdb);
334 for font in registered {
335 fontdb
338 .load_font_source(usvg::fontdb::Source::Binary(Arc::new(Arc::clone(font))));
339 }
340 }
341 let found = select_font(font, fontdb);
342 if found.is_none() {
343 let mut missing = missing.lock().unwrap_or_else(PoisonError::into_inner);
344 for family in font.families() {
345 let family = match family {
346 usvg::FontFamily::Named(name) => name.clone(),
347 generic => generic.to_string(),
348 };
349 if !missing.contains(&family) {
350 missing.push(family);
351 }
352 }
353 }
354 found
355 }),
356 select_fallback: FontResolver::default_fallback_selector(),
357 }
358}
359
360fn image_resolver<'a>(
365 resolver: &'a dyn ResourceResolver,
366 reports: &'a Mutex<Vec<Diagnostic>>,
367 reading: &'a Reading,
368) -> ImageHrefResolver<'a> {
369 let default_data = ImageHrefResolver::default_data_resolver();
370 let report = move |diagnostic| {
371 reports
372 .lock()
373 .unwrap_or_else(PoisonError::into_inner)
374 .push(diagnostic);
375 };
376 ImageHrefResolver {
377 resolve_data: Box::new(move |mime, data, options| {
379 let svg = mime == "image/svg+xml"
382 || (mime == "text/plain" && imagesize::image_type(&data).is_err());
383 if svg {
384 return nested_svg(&data, options, reading);
385 }
386 let kind = default_data(mime, data, options)?;
387 within_canvas(kind, || format!("data:{mime}"), reports)
388 }),
389 resolve_string: Box::new(move |href, options| {
390 let unreadable = |reason: &str| {
391 report(Diagnostic::UnreadableImage {
392 href: href.to_owned(),
393 reason: reason.to_owned(),
394 });
395 None
396 };
397 let Ok(bytes) = resolver.bytes(href) else {
398 report(Diagnostic::MissingImage {
399 href: href.to_owned(),
400 });
401 return None;
402 };
403 let data = || Arc::new(bytes.to_vec());
404 let raster = match registered_image(&bytes) {
405 Some(Registered::Png) => ImageKind::PNG(data()),
406 Some(Registered::Jpeg) => ImageKind::JPEG(data()),
407 Some(Registered::Gif) => ImageKind::GIF(data()),
408 Some(Registered::Webp) => ImageKind::WEBP(data()),
409 Some(Registered::Svg) => {
412 return nested_svg(&bytes, options, reading)
413 .or_else(|| unreadable("the SVG document does not parse"));
414 }
415 None => return unreadable("not a PNG, JPEG, GIF, WebP, or SVG image"),
416 };
417 within_canvas(raster, || href.to_owned(), reports)
418 }),
419 }
420}
421
422fn nested_svg(data: &[u8], options: &Options, reading: &Reading) -> Option<ImageKind> {
427 let unpacked;
428 let text = if data.starts_with(&[0x1f, 0x8b]) {
429 let left = reading.text.load(Ordering::Relaxed);
431 let mut text = Vec::new();
432 let read = flate2::read::GzDecoder::new(data)
433 .take(u64::try_from(left).unwrap_or(u64::MAX).saturating_add(1))
434 .read_to_end(&mut text);
435 if read.is_err() {
436 return None;
437 }
438 if text.len() > left {
439 reading.refuse(too_much_text());
440 return None;
441 }
442 unpacked = text;
443 &unpacked[..]
444 } else {
445 data
446 };
447 let nested = Options {
448 resources_dir: None,
449 dpi: options.dpi,
450 font_size: options.font_size,
451 languages: options.languages.clone(),
452 shape_rendering: options.shape_rendering,
453 text_rendering: options.text_rendering,
454 image_rendering: options.image_rendering,
455 default_size: options.default_size,
456 image_href_resolver: ImageHrefResolver {
457 resolve_data: Box::new(|mime, data, nested| {
458 (options.image_href_resolver.resolve_data)(mime, data, nested)
459 }),
460 resolve_string: Box::new(|_, _| None),
461 },
462 fontdb: options.fontdb.clone(),
463 font_resolver: FontResolver {
464 select_font: Box::new(|font, fontdb| (options.font_resolver.select_font)(font, fontdb)),
465 select_fallback: Box::new(|c, used, fontdb| {
466 (options.font_resolver.select_fallback)(c, used, fontdb)
467 }),
468 },
469 ..Options::default()
470 };
471 match read_document(text, &nested, reading) {
472 Ok(tree) => Some(ImageKind::SVG(tree)),
473 Err(error @ ExtensionError::Budget(_)) => {
474 reading.refuse(error);
475 None
476 }
477 Err(_) => None,
478 }
479}
480
481fn within_canvas(
485 kind: ImageKind,
486 href: impl FnOnce() -> String,
487 reports: &Mutex<Vec<Diagnostic>>,
488) -> Option<ImageKind> {
489 let (ImageKind::JPEG(data)
490 | ImageKind::PNG(data)
491 | ImageKind::GIF(data)
492 | ImageKind::WEBP(data)) = &kind
493 else {
494 return Some(kind);
495 };
496 let declared = imagesize::blob_size(data).ok()?;
498 let side = |value: usize| u32::try_from(value).unwrap_or(u32::MAX);
499 if check_canvas(side(declared.width), side(declared.height)).is_err() {
500 reports
501 .lock()
502 .unwrap_or_else(PoisonError::into_inner)
503 .push(Diagnostic::MissingImage { href: href() });
504 return None;
505 }
506 Some(kind)
507}
508
509fn image_pixels(group: &usvg::Group) -> u64 {
514 let mut pixels = 0_u64;
515 for node in group.children() {
516 match node {
517 Node::Group(inner) => pixels = pixels.saturating_add(image_pixels(inner)),
518 Node::Image(image) => pixels = pixels.saturating_add(raster_pixels(image.kind())),
519 Node::Path(_) | Node::Text(_) => {}
520 }
521 node.subroots(|root| pixels = pixels.saturating_add(image_pixels(root)));
523 }
524 pixels
525}
526
527fn raster_pixels(kind: &ImageKind) -> u64 {
530 match kind {
531 ImageKind::JPEG(data)
532 | ImageKind::PNG(data)
533 | ImageKind::GIF(data)
534 | ImageKind::WEBP(data) => imagesize::blob_size(data).map_or(0, |size| {
535 u64::try_from(size.width)
536 .unwrap_or(u64::MAX)
537 .saturating_mul(u64::try_from(size.height).unwrap_or(u64::MAX))
538 }),
539 ImageKind::SVG(_) => 0,
540 }
541}
542
543enum Registered {
545 Png,
546 Jpeg,
547 Gif,
548 Webp,
549 Svg,
551}
552
553fn registered_image(bytes: &[u8]) -> Option<Registered> {
556 if bytes.starts_with(b"\x89PNG") {
557 Some(Registered::Png)
558 } else if bytes.starts_with(&[0xff, 0xd8]) {
559 Some(Registered::Jpeg)
560 } else if bytes.starts_with(b"GIF8") {
561 Some(Registered::Gif)
562 } else if bytes.len() > 12 && &bytes[..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
563 Some(Registered::Webp)
564 } else if looks_like_svg(bytes) || bytes.starts_with(&[0x1f, 0x8b]) {
565 Some(Registered::Svg)
566 } else {
567 None
568 }
569}
570
571struct SvgSource {
573 tree: Tree,
574 report: ProbeReport,
575 last: Option<VisualFrame>,
577}
578
579impl VisualSource for SvgSource {
580 fn report(&self) -> &ProbeReport {
581 &self.report
582 }
583
584 fn render(&mut self, _time: MediaTime, fit: (u32, u32)) -> Result<VisualFrame, ExtensionError> {
585 let size = self.tree.size();
586 let (width, height) = fitted(size.width(), size.height(), fit);
587 if let Some(last) = self
588 .last
589 .as_ref()
590 .filter(|last| (last.width, last.height) == (width, height))
591 {
592 return Ok(last.clone());
593 }
594 render_cost::check(&self.tree, width, height)?;
595 let mut pixmap = Pixmap::new(width, height).ok_or_else(|| {
596 ExtensionError::Budget(format!("cannot rasterize at {width}x{height}"))
597 })?;
598 #[allow(clippy::cast_precision_loss)]
600 let transform =
601 Transform::from_scale(width as f32 / size.width(), height as f32 / size.height());
602 resvg::render(&self.tree, transform, &mut pixmap.as_mut());
603 let frame = VisualFrame {
604 width,
605 height,
606 pixels: FramePixels::Rgba8(pixmap.take().into()),
607 index: 0,
608 start: MediaTime::from_ticks(i64::MIN),
609 end: None,
610 diagnostics: Vec::new(),
611 };
612 self.last = Some(frame.clone());
613 Ok(frame)
614 }
615
616 fn resident_bytes(&self) -> usize {
617 self.last.as_ref().map_or(0, |last| last.pixels.byte_len())
618 }
619}
620
621#[allow(
625 clippy::cast_precision_loss,
626 clippy::cast_possible_truncation,
627 clippy::cast_sign_loss
628)]
629fn fitted(width: f32, height: f32, fit: (u32, u32)) -> (u32, u32) {
630 if fit.0 == 0 || fit.1 == 0 {
631 return (
632 width.round().max(1.0) as u32,
633 height.round().max(1.0) as u32,
634 );
635 }
636 let scale = (fit.0 as f32 / width).min(fit.1 as f32 / height);
637 (
638 (width * scale).round().clamp(1.0, fit.0 as f32) as u32,
639 (height * scale).round().clamp(1.0, fit.1 as f32) as u32,
640 )
641}