1use std::cmp::Ordering;
16use std::collections::HashMap;
17use std::rc::Rc;
18
19use crate::engine::html::xml::XmlElement;
20use crate::engine::layouter::types::{Color, ColorScheme, Gradient};
21use crate::engine::renderer_model::{AffineTransform, FillRule, Path};
22use crate::engine::svg::{
23 parse_use_ref, resolve_fill_rule, ClipPath, PaintState, SvgStroke, ViewBox, element_shape,
24 numeric_presentation_value, parse_len, parse_transform, parse_view_box,
25 viewbox_meet_transform, with_alpha,
26};
27use crate::engine::tree::NodeRef;
28
29const MAX_RASTER_DIMENSION: u32 = 8192;
31const MAX_RASTER_PIXELS: u64 = 33_554_432;
34const MAX_SVG_DEPTH: usize = 128;
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct RasterResult {
41 pub width: u32,
42 pub height: u32,
43 pub rgba: Vec<u8>,
44}
45
46pub fn rasterize_from_bytes(
52 svg: &[u8],
53 target_size: Option<(u32, u32)>,
54) -> Result<RasterResult, String> {
55 let markup =
56 std::str::from_utf8(svg).map_err(|error| format!("SVG is not valid UTF-8: {error}"))?;
57 rasterize(markup, target_size)
58}
59
60pub fn rasterize(markup: &str, target: Option<(u32, u32)>) -> Result<RasterResult, String> {
62 let tree = crate::engine::html::xml::parse(markup)?;
63 let root = tree.root.borrow();
64 let root_el = &root.value;
65 if root_el.name != "svg" {
66 return Err(format!(
67 "not an SVG document (root element is <{}>)",
68 root_el.name
69 ));
70 }
71
72 let view_box = root_el.attr("viewBox").map(parse_view_box);
73 let (vw, vh) = match target {
74 Some((w, h)) => (w as f32, h as f32),
75 None => (
76 viewport_len(root_el, "width", view_box, 300.0),
77 viewport_len(root_el, "height", view_box, 150.0),
78 ),
79 };
80 if !vw.is_finite() || !vh.is_finite() {
81 return Err("SVG viewport size is not finite".to_string());
82 }
83 let width = vw.clamp(1.0, MAX_RASTER_DIMENSION as f32) as u32;
84 let height = vh.clamp(1.0, MAX_RASTER_DIMENSION as f32) as u32;
85 if u64::from(width) * u64::from(height) > MAX_RASTER_PIXELS {
86 return Err(format!(
87 "SVG viewport {width}x{height} exceeds the rasterization limit"
88 ));
89 }
90 let buffer_len = (width as usize)
91 .checked_mul(height as usize)
92 .and_then(|pixels| pixels.checked_mul(4))
93 .ok_or_else(|| "SVG viewport buffer size overflowed".to_string())?;
94
95 let base = viewbox_meet_transform(view_box.unwrap_or((0.0, 0.0, 0.0, 0.0)), vw, vh);
96 let gradients = HashMap::new();
97 let mut id_map: HashMap<String, NodeRef<XmlElement>> = HashMap::new();
98 collect_tree_id_map(&tree.root, &mut id_map);
99 let mut clip_paths: HashMap<String, ClipPath> = HashMap::new();
100 collect_tree_clip_paths(&tree.root, &mut clip_paths);
101 let mut rgba = vec![0u8; buffer_len];
102 let mut use_stack: Vec<usize> = Vec::new();
103 for child in root.children() {
104 process_element(
105 child,
106 base,
107 1.0,
108 PaintState::new(),
109 &gradients,
110 &id_map,
111 &clip_paths,
112 &mut use_stack,
113 width,
114 height,
115 &mut rgba,
116 0,
117 )?;
118 }
119 Ok(RasterResult {
120 width,
121 height,
122 rgba,
123 })
124}
125
126fn collect_tree_id_map(
130 root: &NodeRef<XmlElement>,
131 out: &mut HashMap<String, NodeRef<XmlElement>>,
132) {
133 let el = root.borrow();
134 if let Some(id_value) = el.value.attr("id")
135 && !id_value.is_empty()
136 {
137 out.entry(id_value.to_owned()).or_insert(Rc::clone(root));
138 }
139 drop(el);
140 for child in root.borrow().children() {
141 collect_tree_id_map(child, out);
142 }
143}
144
145fn collect_tree_clip_paths(
149 root: &NodeRef<XmlElement>,
150 out: &mut HashMap<String, ClipPath>,
151) {
152 let el = root.borrow();
153 if el.value.name == "clipPath"
154 && let Some(clip_id) = el.value.attr("id").filter(|id| !id.is_empty())
155 {
156 let transform = match el.value.attr("transform") {
157 Some(value) => parse_transform(value).unwrap_or(AffineTransform::identity()),
158 None => AffineTransform::identity(),
159 };
160 let rule = resolve_fill_rule(&el.value);
161 let mut path = Path::new();
162 collect_tree_clip_path_shapes(root, &transform, &mut path);
163 if !path.commands().is_empty() {
164 out.entry(clip_id.to_owned()).or_insert(ClipPath { path, rule });
165 }
166 }
167 drop(el);
168 for child in root.borrow().children() {
169 collect_tree_clip_paths(child, out);
170 }
171}
172
173fn collect_tree_clip_path_shapes(
176 root: &NodeRef<XmlElement>,
177 transform: &AffineTransform,
178 path: &mut Path,
179) {
180 let el = root.borrow();
181 if let Some(shape_path) = element_shape(&el.value) {
182 let transformed = crate::engine::renderer_model::transform_path(&shape_path, transform);
183 for subpath in transformed.subpaths() {
184 if let Some(&first) = subpath.first() {
185 path.move_to(first.0, first.1);
186 for &(x, y) in &subpath[1..] {
187 path.line_to(x, y);
188 }
189 path.close();
190 }
191 }
192 }
193 drop(el);
194 for child in root.borrow().children() {
195 collect_tree_clip_path_shapes(child, transform, path);
196 }
197}
198
199fn viewport_len(el: &XmlElement, attr: &str, view_box: Option<ViewBox>, fallback: f32) -> f32 {
203 if let Some(value) = el.attr(attr).and_then(parse_len) {
204 return value.max(1.0).ceil();
205 }
206 match view_box {
207 Some((_, _, w, _)) if attr == "width" => w.max(1.0).ceil(),
208 Some((_, _, _, h)) if attr == "height" => h.max(1.0).ceil(),
209 _ => fallback.max(1.0).ceil(),
210 }
211}
212
213#[allow(clippy::too_many_arguments)]
224fn process_element(
225 node: &NodeRef<XmlElement>,
226 inherited: AffineTransform,
227 opacity: f32,
228 mut paint: PaintState,
229 gradients: &HashMap<String, Gradient>,
230 id_map: &HashMap<String, NodeRef<XmlElement>>,
231 clip_paths: &HashMap<String, ClipPath>,
232 use_stack: &mut Vec<usize>,
233 width: u32,
234 height: u32,
235 rgba: &mut [u8],
236 depth: usize,
237) -> Result<(), String> {
238 if depth > MAX_SVG_DEPTH {
239 return Err(format!("SVG nesting exceeds {MAX_SVG_DEPTH} levels"));
240 }
241 let el = node.borrow();
242 let name = el.value.name.clone();
243 let eff = match el.value.attr("transform") {
244 Some(value) => inherited.then(&parse_transform(value).map_err(|e| format!("{name}: {e}"))?),
245 None => inherited,
246 };
247 let element_opacity = numeric_presentation_value(&el.value, "opacity").unwrap_or(1.0);
248 let opacity = opacity * element_opacity.clamp(0.0, 1.0);
249 paint.apply(&el.value, ColorScheme::Light, Color::default(), gradients);
250
251 match name.as_str() {
252 "g" | "a" | "svg" | "symbol" => {
253 for child in el.children() {
254 process_element(
255 child,
256 eff,
257 opacity,
258 paint.clone(),
259 gradients,
260 id_map,
261 clip_paths,
262 use_stack,
263 width,
264 height,
265 rgba,
266 depth + 1,
267 )?;
268 }
269 }
270 "use" => {
271 let href = el
274 .value
275 .attr("href")
276 .or_else(|| el.value.attr("xlink:href"));
277 let target_node = href
278 .and_then(parse_use_ref)
279 .and_then(|frag| id_map.get(&frag).cloned());
280 if let Some(target) = target_node {
281 let target_ptr = Rc::as_ptr(&target) as usize;
282 if !use_stack.contains(&target_ptr) {
283 let x = el.value.attr("x").and_then(parse_len).unwrap_or(0.0);
284 let y = el.value.attr("y").and_then(parse_len).unwrap_or(0.0);
285 let use_transform = eff.then(&AffineTransform::translate(x, y));
286 use_stack.push(target_ptr);
287 process_element(
288 &target,
289 use_transform,
290 opacity,
291 paint.clone(),
292 gradients,
293 id_map,
294 clip_paths,
295 use_stack,
296 width,
297 height,
298 rgba,
299 depth + 1,
300 )?;
301 use_stack.pop();
302 }
303 }
304 }
305 "path" | "rect" | "circle" | "ellipse" | "polygon" | "polyline" | "line" => {
306 let Some(path) = element_shape(&el.value) else {
307 return Ok(());
308 };
309 if path.commands().is_empty() {
310 return Ok(());
311 }
312 let fill = solid_fallback_paint(&paint, opacity);
313 let stroke = paint.stroke.clone().map(|mut stroke| {
314 if stroke.gradient.is_some() {
315 stroke.gradient = None;
317 stroke.color = with_alpha(stroke.color, opacity * paint.stroke_opacity);
318 } else {
319 stroke.color = with_alpha(stroke.color, opacity * paint.stroke_opacity);
320 }
321 stroke
322 });
323 let clip_mask = el
324 .value
325 .attr("clip-path")
326 .and_then(parse_use_ref)
327 .and_then(|frag| clip_paths.get(&frag))
328 .map(|clip| {
329 let mut buf = vec![0u8; (width as usize) * (height as usize) * 4];
330 let transformed =
331 crate::engine::renderer_model::transform_path(&clip.path, &eff);
332 fill_path_into(
333 &mut buf,
334 width,
335 height,
336 &transformed,
337 Color(255, 255, 255, 255),
338 clip.rule,
339 None,
340 );
341 buf
342 });
343 render_shape(
344 &path,
345 eff,
346 fill,
347 paint.rule,
348 stroke.as_ref(),
349 clip_mask.as_deref(),
350 width,
351 height,
352 rgba,
353 );
354 }
355 _ => {}
356 }
357 Ok(())
358}
359
360fn solid_fallback_paint(paint: &PaintState, opacity: f32) -> Color {
363 if let Some(fill) = paint.fill {
364 return with_alpha(fill, opacity * paint.fill_opacity);
365 }
366 if let Some(gradient) = &paint.fill_gradient
367 && let Some(last) = gradient.stops.last()
368 {
369 return with_alpha(last.color, opacity * paint.fill_opacity);
370 }
371 Color(0, 0, 0, 255)
372}
373
374#[allow(clippy::too_many_arguments)]
382fn render_shape(
383 path: &Path,
384 eff: AffineTransform,
385 fill: Color,
386 rule: FillRule,
387 stroke: Option<&SvgStroke>,
388 clip: Option<&[u8]>,
389 width: u32,
390 height: u32,
391 rgba: &mut [u8],
392) {
393 let transformed = crate::engine::renderer_model::transform_path(path, &eff);
394 fill_path_into(rgba, width, height, &transformed, fill, rule, clip);
395 if let Some(stroke) = stroke {
396 let outline = crate::engine::renderer_model::stroke_path(&transformed, &stroke.to_stroke());
397 fill_path_into(
398 rgba,
399 width,
400 height,
401 &outline,
402 stroke.color,
403 FillRule::NonZero,
404 clip,
405 );
406 }
407}
408
409const AA_SUBSAMPLES: u32 = 4;
411
412pub(crate) fn fill_path_into(
425 rgba: &mut [u8],
426 width: u32,
427 height: u32,
428 path: &Path,
429 fill: Color,
430 rule: FillRule,
431 clip: Option<&[u8]>,
432) {
433 if fill.3 == 0 || width == 0 || height == 0 {
434 return;
435 }
436 let height_i = height as i64;
437
438 let rings: Vec<Vec<(f32, f32)>> = path
439 .subpaths()
440 .into_iter()
441 .filter(|ring| ring.len() >= 3)
442 .collect();
443 if rings.is_empty() {
444 return;
445 }
446
447 let mut min_y = f32::INFINITY;
448 let mut max_y = f32::NEG_INFINITY;
449 for ring in &rings {
450 for &(_, y) in ring {
451 min_y = min_y.min(y);
452 max_y = max_y.max(y);
453 }
454 }
455
456 let row_start = min_y.max(0.0).floor() as i64;
457 let row_end = (max_y.min(height as f32).ceil() as i64).clamp(0, height_i);
458
459 for row in row_start..row_end {
460 let row_offset = (row as usize) * (width as usize);
461 let mut coverage = vec![0u16; width as usize];
462 for sub in 0..AA_SUBSAMPLES {
463 let yc = (row as f32) + (sub as f32 + 0.5) / AA_SUBSAMPLES as f32;
464 for (prev_x, x) in inside_spans(&rings, yc, rule) {
465 mark_columns(&mut coverage, prev_x, x);
466 }
467 }
468 for (column, hits) in coverage.iter().enumerate() {
469 if *hits == 0 {
470 continue;
471 }
472 let covers = *hits as f32 / AA_SUBSAMPLES as f32;
473 let mut alpha = (fill.3 as f32 * covers).round() as u8;
474 if let Some(clip_mask) = clip {
475 let clip_alpha = clip_mask[(row_offset + column) * 4 + 3] as f32 / 255.0;
476 alpha = (alpha as f32 * clip_alpha).round() as u8;
477 }
478 blend_pixel(
479 rgba,
480 row_offset + column,
481 Color(fill.0, fill.1, fill.2, alpha),
482 );
483 }
484 }
485}
486
487fn inside_spans(
490 rings: &[Vec<(f32, f32)>],
491 yc: f32,
492 rule: FillRule,
493) -> impl Iterator<Item = (f32, f32)> + '_ {
494 let mut edges: Vec<(f32, f32)> = Vec::new();
495 for ring in rings {
496 let last = ring.len() - 1;
497 for i in 0..ring.len() {
498 let (x0, y0) = ring[i];
499 let (x1, y1) = ring[if i == last { 0 } else { i + 1 }];
500 let (lo, hi) = if y0 < y1 { (y0, y1) } else { (y1, y0) };
501 if yc < lo || yc >= hi {
504 continue;
505 }
506 let t = (yc - y0) / (y1 - y0);
507 let x = x0 + t * (x1 - x0);
508 let direction = if y1 > y0 { 1.0 } else { -1.0 };
509 edges.push((x, direction));
510 }
511 }
512 edges.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
513
514 Spans {
515 edges,
516 rule,
517 prev_x: f32::NEG_INFINITY,
518 winding: 0.0,
519 parity: false,
520 }
521}
522
523struct Spans {
527 edges: Vec<(f32, f32)>,
528 rule: FillRule,
529 prev_x: f32,
530 winding: f32,
531 parity: bool,
532}
533
534impl Iterator for Spans {
535 type Item = (f32, f32);
536
537 fn next(&mut self) -> Option<(f32, f32)> {
538 while let Some((x, direction)) = self.edges.first().copied() {
539 self.edges.remove(0);
540 let span = match self.rule {
541 FillRule::NonZero => {
542 let span = if self.winding != 0.0 && x > self.prev_x {
543 Some((self.prev_x, x))
544 } else {
545 None
546 };
547 self.winding += direction;
548 span
549 }
550 FillRule::EvenOdd => {
551 let span = if self.parity && x > self.prev_x {
552 Some((self.prev_x, x))
553 } else {
554 None
555 };
556 self.parity = !self.parity;
557 span
558 }
559 };
560 self.prev_x = x;
561 if let Some(span) = span {
562 return Some(span);
563 }
564 }
565 None
566 }
567}
568
569fn mark_columns(coverage: &mut [u16], prev_x: f32, x: f32) {
572 let width = coverage.len() as i64;
573 let c0 = (((prev_x - 0.5).ceil().max(0.0)) as i64).min(width);
574 let c1 = ((x - 0.5).ceil().max(0.0) as i64).min(width);
575 for c in c0.max(0)..c1 {
576 coverage[c as usize] += 1;
577 }
578}
579
580fn blend_pixel(rgba: &mut [u8], index: usize, src: Color) {
583 let idx = index * 4;
584 let sa = src.3 as f32 / 255.0;
585 let da = rgba[idx + 3] as f32 / 255.0;
586 let out_alpha = sa + da * (1.0 - sa);
587 if out_alpha <= 0.0 {
588 return;
589 }
590 let rgba_f32 = [
591 src.0 as f32 / 255.0,
592 src.1 as f32 / 255.0,
593 src.2 as f32 / 255.0,
594 ];
595 for channel in 0..3 {
596 let s = rgba_f32[channel];
597 let d = rgba[idx + channel] as f32 / 255.0;
598 let composited = (s * sa + d * da * (1.0 - sa)) / out_alpha;
599 rgba[idx + channel] = (composited * 255.0).round() as u8;
600 }
601 rgba[idx + 3] = (out_alpha * 255.0).round() as u8;
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 #[test]
609 fn rasterizes_fill_attribute_rectangle() {
610 let result = rasterize(
611 r##"<svg width="4" height="3"><rect x="0" y="0" width="4" height="3" fill="#ff0000"/></svg>"##,
612 None,
613 )
614 .expect("rasterizes");
615 assert_eq!((result.width, result.height), (4, 3));
616 assert!(
618 result
619 .rgba
620 .as_chunks::<4>()
621 .0
622 .iter()
623 .all(|p| *p == [255, 0, 0, 255])
624 );
625 }
626
627 #[test]
628 fn rasterizes_style_fill_with_transform_and_viewbox() {
629 let result = rasterize(
632 r##"<svg width="20" height="10" viewBox="0 0 20 10">
633 <g transform="translate(2, 2)">
634 <path d="M 2 8 L 18 5 L 2 2 Z" style="fill:#00ff00"/>
635 </g>
636 </svg>"##,
637 None,
638 )
639 .expect("rasterizes");
640 assert_eq!((result.width, result.height), (20, 10));
641 let edge = result.rgba[(5 * 20 + 10) * 4..(5 * 20 + 10) * 4 + 4].to_vec();
646 assert_eq!(edge, [0, 255, 0, 191]);
647 let interior = result.rgba[(5 * 20 + 5) * 4..(5 * 20 + 5) * 4 + 4].to_vec();
648 assert_eq!(interior, [0, 255, 0, 255]);
649 assert_eq!(&result.rgba[0..4], &[0, 0, 0, 0]);
650 }
651
652 #[test]
653 fn viewbox_meet_centers_letterboxed_content() {
654 let result = rasterize(
657 r##"<svg width="20" height="10" viewBox="0 0 10 10"><rect x="0" y="0" width="10" height="10" fill="#ff0000"/></svg>"##,
658 None,
659 )
660 .expect("rasterizes");
661 assert_eq!(&result.rgba[0..4], &[0, 0, 0, 0]);
662 assert_eq!(&result.rgba[40..44], &[255, 0, 0, 255]);
663 }
664
665 #[test]
666 fn preserves_transparent_regions_outside_shapes() {
667 let result = rasterize(
668 r##"<svg width="3" height="3"><rect x="1" y="1" width="1" height="1" fill="#ffffff"/></svg>"##,
669 None,
670 )
671 .expect("rasterizes");
672 assert_eq!(&result.rgba[0..4], &[0, 0, 0, 0]);
673 assert_eq!(&result.rgba[16..20], &[255, 255, 255, 255]);
674 }
675
676 #[test]
677 fn non_svg_root_is_rejected() {
678 assert!(rasterize("<html></html>", None).is_err());
679 assert!(rasterize("<svg", None).is_err());
680 }
681
682 #[test]
683 fn huge_intrinsic_viewport_is_clamped_or_rejected() {
684 assert!(rasterize(r#"<svg width="1e300" height="10"></svg>"#, None).is_err());
687
688 if let Ok(result) = rasterize(r#"<svg width="100000" height="100000"></svg>"#, None) {
691 assert!(result.width <= MAX_RASTER_DIMENSION);
692 assert!(result.height <= MAX_RASTER_DIMENSION);
693 assert!(u64::from(result.width) * u64::from(result.height) <= MAX_RASTER_PIXELS);
694 }
695 }
696 #[test]
697 fn deeply_nested_elements_are_rejected() {
698 let mut svg = String::from("<svg width=\"10\" height=\"10\">");
699 for _ in 0..(MAX_SVG_DEPTH + 8) {
700 svg.push_str("<g>");
701 }
702 for _ in 0..(MAX_SVG_DEPTH + 8) {
703 svg.push_str("</g>");
704 }
705 svg.push_str("</svg>");
706 assert!(rasterize(&svg, None).is_err());
707 }
708
709 #[test]
710 fn rectangle_bounds_are_axis_aligned_after_scale() {
711 let result = rasterize(
712 r##"<svg width="10" height="10"><rect x="2" y="1" width="3" height="2" fill="#0000ff"/></svg>"##,
713 None,
714 )
715 .expect("rasterizes");
716 let px = |x: usize, y: usize| &result.rgba[(y * 10 + x) * 4..(y * 10 + x) * 4 + 4];
717 assert_eq!(px(2, 1), &[0, 0, 255, 255]);
719 assert_eq!(px(1, 1), &[0, 0, 0, 0]);
721 assert_eq!(px(5, 1), &[0, 0, 0, 0]);
722 assert_eq!(px(2, 0), &[0, 0, 0, 0]);
723 }
724
725 #[test]
726 fn even_odd_rule_punches_a_hole_between_overlapping_rings() {
727 let rings = r##"<svg width="10" height="10">
730 <path d="M1 1 L7 1 L7 7 L1 7 Z M3 3 L9 3 L9 9 L3 9 Z" fill="#ff0000"/>
731 </svg>"##;
732 let even_odd = rasterize(
733 r##"<svg width="10" height="10">
734 <path d="M1 1 L7 1 L7 7 L1 7 Z M3 3 L9 3 L9 9 L3 9 Z" fill="#ff0000" fill-rule="evenodd"/>
735 </svg>"##,
736 None,
737 )
738 .expect("rasterizes even-odd");
739 let nonzero = rasterize(rings, None).expect("rasterizes nonzero");
740 fn px(r: &RasterResult, x: usize, y: usize) -> &[u8] {
741 &r.rgba[(y * 10 + x) * 4..(y * 10 + x) * 4 + 4]
742 }
743 assert_eq!(px(&even_odd, 5, 5), &[0, 0, 0, 0]);
745 assert_eq!(px(&nonzero, 5, 5), &[255, 0, 0, 255]);
746 assert_eq!(px(&even_odd, 2, 2), &[255, 0, 0, 255]);
748 assert_eq!(px(&nonzero, 2, 2), &[255, 0, 0, 255]);
749 }
750
751 #[test]
752 fn anti_aliases_pixels_straddling_a_horizontal_edge() {
753 let result = rasterize(
756 r##"<svg width="2" height="3"><rect x="0" y="0.5" width="2" height="2" fill="#ff0000"/></svg>"##,
757 None,
758 )
759 .expect("rasterizes");
760 let px = |row: usize| result.rgba[row * 8..row * 8 + 4].to_vec();
761 assert_eq!(px(0), [255, 0, 0, 128]);
762 assert_eq!(px(1), [255, 0, 0, 255]);
763 assert_eq!(px(2), [255, 0, 0, 128]);
764 }
765
766 #[test]
767 fn opacity_attribute_multiplies_fill_alpha() {
768 let result = rasterize(
769 r##"<svg width="2" height="2"><rect x="0" y="0" width="2" height="2" fill="#ff0000" opacity="0.5"/></svg>"##,
770 None,
771 )
772 .expect("rasterizes");
773 assert_eq!(&result.rgba[0..4], &[255, 0, 0, 128]);
774 }
775
776 #[test]
777 fn group_opacity_inherits_into_children() {
778 let result = rasterize(
779 r##"<svg width="2" height="2"><g opacity="0.5"><rect x="0" y="0" width="2" height="2" fill="#ff0000"/></g></svg>"##,
780 None,
781 )
782 .expect("rasterizes");
783 assert_eq!(&result.rgba[0..4], &[255, 0, 0, 128]);
784 }
785
786 #[test]
787 fn fill_opacity_compounds_with_element_opacity() {
788 let result = rasterize(
789 r##"<svg width="2" height="2"><rect x="0" y="0" width="2" height="2" fill="#ff0000" opacity="0.5" fill-opacity="0.5"/></svg>"##,
790 None,
791 )
792 .expect("rasterizes");
793 assert_eq!(&result.rgba[0..4], &[255, 0, 0, 64]);
795 }
796
797 #[test]
798 fn current_color_falls_back_to_black_for_standalone_documents() {
799 let result = rasterize(
800 r##"<svg width="2" height="2"><rect x="0" y="0" width="2" height="2" fill="currentColor"/></svg>"##,
801 None,
802 )
803 .expect("rasterizes");
804 assert_eq!(&result.rgba[0..4], &[0, 0, 0, 255]);
805 }
806
807 #[test]
808 fn stroke_width_zero_and_stroke_none_suppress_the_line() {
809 let width_zero = rasterize(
812 r##"<svg width="3" height="3"><path d="M0 1.5 L3 1.5" stroke="#ff0000" stroke-width="0"/></svg>"##,
813 None,
814 )
815 .expect("rasterizes");
816 assert!(width_zero.rgba.chunks(4).all(|p| p[3] == 0));
817 let none = rasterize(
818 r##"<svg width="3" height="3"><path d="M0 1.5 L3 1.5" stroke="none" stroke-width="2"/></svg>"##,
819 None,
820 )
821 .expect("rasterizes");
822 assert!(none.rgba.chunks(4).all(|p| p[3] == 0));
823 let no_declaration = rasterize(
824 r##"<svg width="3" height="3"><path d="M0 1.5 L3 1.5"/></svg>"##,
825 None,
826 )
827 .expect("rasterizes");
828 assert!(no_declaration.rgba.chunks(4).all(|p| p[3] == 0));
829 }
830
831 #[test]
832 fn stroke_rasterizes_as_an_antialiased_band_around_the_line() {
833 let result = rasterize(
834 r##"<svg width="6" height="4"><path d="M0 1.5 L6 1.5" stroke="#ff0000" stroke-width="2" fill="none"/></svg>"##,
835 None,
836 )
837 .expect("rasterizes");
838 let px = |x: usize, y: usize| &result.rgba[(y * 6 + x) * 4..(y * 6 + x) * 4 + 4];
839 assert_eq!(px(2, 1), &[255, 0, 0, 255]);
842 assert_eq!(px(2, 0), &[255, 0, 0, 128]);
843 assert_eq!(px(2, 2), &[255, 0, 0, 128]);
844 assert_eq!(px(2, 3), &[0, 0, 0, 0]);
845 assert_eq!(px(0, 3), &[0, 0, 0, 0]);
847 }
848
849 #[test]
850 fn round_cap_bulges_past_the_endpoint_where_butt_does_not() {
851 let round = rasterize(
852 r##"<svg width="8" height="5"><path d="M0.5 2.5 L3.5 2.5" stroke="#ff0000" stroke-width="4" stroke-linecap="round" fill="none"/></svg>"##,
853 None,
854 )
855 .expect("rasterizes round");
856 let butt = rasterize(
857 r##"<svg width="8" height="5"><path d="M0.5 2.5 L3.5 2.5" stroke="#ff0000" stroke-width="4" fill="none"/></svg>"##,
858 None,
859 )
860 .expect("rasterizes butt");
861 fn px_at(rgba: &[u8], width: usize, x: usize, y: usize) -> &[u8] {
862 &rgba[(y * width + x) * 4..(y * width + x) * 4 + 4]
863 }
864
865 assert_eq!(px_at(&round.rgba, 8, 4, 2), &[255, 0, 0, 255]);
868 assert_eq!(px_at(&butt.rgba, 8, 4, 2), &[0, 0, 0, 0]);
869 assert_eq!(px_at(&round.rgba, 8, 6, 2), &[0, 0, 0, 0]);
871 }
872
873 #[test]
874 fn dasharray_splits_the_stroke_into_on_and_off_phases() {
875 let result = rasterize(
876 r##"<svg width="8" height="3"><path d="M0 1.5 L8 1.5" stroke="#ff0000" stroke-width="2" stroke-dasharray="3 3" fill="none"/></svg>"##,
877 None,
878 )
879 .expect("rasterizes");
880 let px = |x: usize, y: usize| &result.rgba[(y * 8 + x) * 4..(y * 8 + x) * 4 + 4];
881 assert_eq!(px(1, 1), &[255, 0, 0, 255]);
882 assert_eq!(px(4, 1), &[0, 0, 0, 0]);
883 assert_eq!(px(6, 1), &[255, 0, 0, 255]);
884 }
885
886 #[test]
887 fn stroke_parameters_inherit_through_a_group() {
888 let result = rasterize(
889 r##"<svg width="4" height="3"><g stroke="#00ff00" stroke-width="2"><path d="M0 1.5 L4 1.5" fill="none"/></g></svg>"##,
890 None,
891 )
892 .expect("rasterizes");
893 let px = |x: usize, y: usize| &result.rgba[(y * 4 + x) * 4..(y * 4 + x) * 4 + 4];
894 assert_eq!(px(1, 1), &[0, 255, 0, 255]);
895 assert_eq!(px(1, 0), &[0, 255, 0, 128]);
896 }
897
898 #[test]
899 fn parse_len_handles_units_and_exponents() {
900 assert_eq!(parse_len("10px"), Some(10.0));
901 assert_eq!(parse_len(" 1.5e1em "), Some(15.0));
902 assert_eq!(parse_len("-2e1"), Some(-20.0));
903 assert_eq!(parse_len("100%"), Some(100.0));
904 assert_eq!(parse_len("nope"), None);
905 }
906}