1use crate::scene::ColorLinPremul;
2use std::collections::{HashMap, VecDeque};
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6static SYSTEM_FONTDB: std::sync::LazyLock<Arc<usvg::fontdb::Database>> =
15 std::sync::LazyLock::new(|| {
16 let mut db = usvg::fontdb::Database::new();
17 db.load_system_fonts();
18 Arc::new(db)
19 });
20
21fn builtin_svg_bytes(_path: &Path) -> Option<&'static [u8]> {
26 None
27}
28
29#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct SvgStyle {
32 pub fill: Option<ColorLinPremul>,
34 pub stroke: Option<ColorLinPremul>,
36 pub stroke_width: Option<f32>,
38}
39
40impl SvgStyle {
41 pub fn new() -> Self {
42 Self {
43 fill: None,
44 stroke: None,
45 stroke_width: None,
46 }
47 }
48
49 pub fn with_stroke(mut self, color: ColorLinPremul) -> Self {
50 self.stroke = Some(color);
51 self
52 }
53
54 pub fn with_fill(mut self, color: ColorLinPremul) -> Self {
55 self.fill = Some(color);
56 self
57 }
58
59 pub fn with_stroke_width(mut self, width: f32) -> Self {
60 self.stroke_width = Some(width);
61 self
62 }
63}
64
65impl Default for SvgStyle {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
73struct SvgStyleKey {
74 fill: Option<[u8; 4]>,
75 stroke: Option<[u8; 4]>,
76 stroke_width_bits: Option<u32>,
77}
78
79impl From<SvgStyle> for SvgStyleKey {
80 fn from(style: SvgStyle) -> Self {
81 Self {
82 fill: style.fill.map(|c| {
83 let rgba = c.to_srgba_u8();
84 [rgba[0], rgba[1], rgba[2], rgba[3]]
85 }),
86 stroke: style.stroke.map(|c| {
87 let rgba = c.to_srgba_u8();
88 [rgba[0], rgba[1], rgba[2], rgba[3]]
89 }),
90 stroke_width_bits: style.stroke_width.map(|w| w.to_bits()),
91 }
92 }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
98pub enum ScaleBucket {
99 X025, X05, X075, X1, X125, X15, X2, X25, X3, X4, X5, X6, X8, }
113
114impl ScaleBucket {
115 pub fn from_scale(s: f32) -> Self {
116 if s < 0.375 {
118 ScaleBucket::X025
119 } else if s < 0.625 {
120 ScaleBucket::X05
121 } else if s < 0.875 {
122 ScaleBucket::X075
123 } else if s < 1.125 {
124 ScaleBucket::X1
125 } else if s < 1.375 {
126 ScaleBucket::X125
127 } else if s < 1.75 {
128 ScaleBucket::X15
129 } else if s < 2.25 {
130 ScaleBucket::X2
131 } else if s < 2.75 {
132 ScaleBucket::X25
133 } else if s < 3.5 {
134 ScaleBucket::X3
135 } else if s < 4.5 {
136 ScaleBucket::X4
137 } else if s < 5.5 {
138 ScaleBucket::X5
139 } else if s < 7.0 {
140 ScaleBucket::X6
141 } else {
142 ScaleBucket::X8
143 }
144 }
145
146 pub fn as_f32(self) -> f32 {
147 match self {
148 ScaleBucket::X025 => 0.25,
149 ScaleBucket::X05 => 0.5,
150 ScaleBucket::X075 => 0.75,
151 ScaleBucket::X1 => 1.0,
152 ScaleBucket::X125 => 1.25,
153 ScaleBucket::X15 => 1.5,
154 ScaleBucket::X2 => 2.0,
155 ScaleBucket::X25 => 2.5,
156 ScaleBucket::X3 => 3.0,
157 ScaleBucket::X4 => 4.0,
158 ScaleBucket::X5 => 5.0,
159 ScaleBucket::X6 => 6.0,
160 ScaleBucket::X8 => 8.0,
161 }
162 }
163}
164
165#[derive(Clone, Debug, Eq, PartialEq, Hash)]
166struct CacheKey {
167 path: PathBuf,
168 scale: ScaleBucket,
169 style: SvgStyleKey,
170}
171
172struct CacheEntry {
173 tex: std::sync::Arc<wgpu::Texture>,
174 width: u32,
175 height: u32,
176 last_tick: u64,
177 bytes: usize,
178}
179
180pub struct SvgRasterCache {
186 device: Arc<wgpu::Device>,
187 map: HashMap<CacheKey, CacheEntry>,
189 lru: VecDeque<CacheKey>,
190 current_tick: u64,
191 max_bytes: usize,
193 total_bytes: usize,
194 max_tex_size: u32,
195}
196
197impl SvgRasterCache {
198 pub fn new(device: Arc<wgpu::Device>) -> Self {
199 let max_bytes = 128 * 1024 * 1024;
201 let limits = device.limits();
202 let max_tex_size = limits.max_texture_dimension_2d;
203 Self {
204 device,
205 map: HashMap::new(),
206 lru: VecDeque::new(),
207 current_tick: 0,
208 max_bytes,
209 total_bytes: 0,
210 max_tex_size,
211 }
212 }
213
214 pub fn set_max_bytes(&mut self, bytes: usize) {
215 self.max_bytes = bytes;
216 self.evict_if_needed();
217 }
218
219 fn touch(&mut self, key: &CacheKey) {
220 self.current_tick = self.current_tick.wrapping_add(1);
221 if let Some(entry) = self.map.get_mut(key) {
222 entry.last_tick = self.current_tick;
223 }
224 if let Some(pos) = self.lru.iter().position(|k| k == key) {
226 let k = self.lru.remove(pos).unwrap();
227 self.lru.push_back(k);
228 }
229 }
230
231 fn insert(&mut self, key: CacheKey, entry: CacheEntry) {
232 self.current_tick = self.current_tick.wrapping_add(1);
233 self.total_bytes += entry.bytes;
234 self.map.insert(key.clone(), entry);
235 self.lru.push_back(key);
236 self.evict_if_needed();
237 }
238
239 fn evict_if_needed(&mut self) {
240 while self.total_bytes > self.max_bytes {
241 if let Some(old_key) = self.lru.pop_front() {
242 if let Some(entry) = self.map.remove(&old_key) {
243 self.total_bytes = self.total_bytes.saturating_sub(entry.bytes);
244 }
246 } else {
247 break;
248 }
249 }
250 }
251
252 pub fn get_or_rasterize(
256 &mut self,
257 path: &Path,
258 scale: f32,
259 style: SvgStyle,
260 queue: &wgpu::Queue,
261 ) -> Option<(std::sync::Arc<wgpu::Texture>, u32, u32)> {
262 let scale_b = ScaleBucket::from_scale(scale);
263 let style_key = SvgStyleKey::from(style);
264 let key = CacheKey {
265 path: path.to_path_buf(),
266 scale: scale_b,
267 style: style_key,
268 };
269 if self.map.contains_key(&key) {
270 self.touch(&key);
271 let e = self.map.get(&key).unwrap();
272 return Some((e.tex.clone(), e.width, e.height));
273 }
274
275 let mut data: Vec<u8> = if path.exists() {
279 std::fs::read(path).ok()?
280 } else if let Some(bytes) = builtin_svg_bytes(path) {
281 bytes.to_vec()
282 } else {
283 std::fs::read(path).ok()?
284 };
285
286 if style.fill.is_some() || style.stroke.is_some() || style.stroke_width.is_some() {
288 data = apply_style_overrides_to_xml(&data, style)?;
289 }
290
291 let mut opt = usvg::Options::default();
292 opt.resources_dir = path.parent().map(|p| p.to_path_buf());
293 opt.fontdb = SYSTEM_FONTDB.clone();
294 let tree = usvg::Tree::from_data(&data, &opt).ok()?;
295 let size = tree.size().to_int_size();
296 let (w0, h0): (u32, u32) = (size.width().max(1), size.height().max(1));
297 let s = scale_b.as_f32();
298 let w = ((w0 as f32) * s).round() as u32;
299 let h = ((h0 as f32) * s).round() as u32;
300 if w == 0 || h == 0 {
301 return None;
302 }
303 if w > self.max_tex_size || h > self.max_tex_size {
304 return None;
305 }
306
307 let mut pixmap = tiny_skia::Pixmap::new(w, h)?;
308 let mut pm = pixmap.as_mut();
309 let ts = tiny_skia::Transform::from_scale(s, s);
310 resvg::render(&tree, ts, &mut pm);
311
312 let rgba = pixmap.take();
313 let tex = self.device.create_texture(&wgpu::TextureDescriptor {
314 label: Some("svg-raster"),
315 size: wgpu::Extent3d {
316 width: w,
317 height: h,
318 depth_or_array_layers: 1,
319 },
320 mip_level_count: 1,
321 sample_count: 1,
322 dimension: wgpu::TextureDimension::D2,
323 format: wgpu::TextureFormat::Rgba8UnormSrgb,
324 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
325 view_formats: &[],
326 });
327 queue.write_texture(
328 wgpu::ImageCopyTexture {
329 texture: &tex,
330 mip_level: 0,
331 origin: wgpu::Origin3d::ZERO,
332 aspect: wgpu::TextureAspect::All,
333 },
334 &rgba,
335 wgpu::ImageDataLayout {
336 offset: 0,
337 bytes_per_row: Some(w * 4),
338 rows_per_image: Some(h),
339 },
340 wgpu::Extent3d {
341 width: w,
342 height: h,
343 depth_or_array_layers: 1,
344 },
345 );
346
347 let bytes = (w as usize) * (h as usize) * 4;
348 let tex_arc = Arc::new(tex);
349 let entry = CacheEntry {
350 tex: tex_arc.clone(),
351 width: w,
352 height: h,
353 last_tick: self.current_tick,
354 bytes,
355 };
356 self.insert(key, entry);
357 Some((tex_arc, w, h))
358 }
359}
360
361fn apply_style_overrides_to_xml(data: &[u8], style: SvgStyle) -> Option<Vec<u8>> {
364 let mut svg_str = String::from_utf8(data.to_vec()).ok()?;
365
366 if let Some(stroke_color) = style.stroke {
368 let rgba = stroke_color.to_srgba_u8();
369 let hex_color = format!("#{:02x}{:02x}{:02x}", rgba[0], rgba[1], rgba[2]);
370
371 svg_str = svg_str.replace(
373 "stroke=\"currentColor\"",
374 &format!("stroke=\"{}\"", hex_color),
375 );
376 svg_str = svg_str.replace("stroke='currentColor'", &format!("stroke='{}'", hex_color));
377 }
378
379 if let Some(fill_color) = style.fill {
384 let rgba = fill_color.to_srgba_u8();
385 let hex_color = format!("#{:02x}{:02x}{:02x}", rgba[0], rgba[1], rgba[2]);
386
387 if let Some(svg_start) = svg_str.find("<svg ") {
392 let tag_end = svg_str[svg_start..].find('>').unwrap_or(svg_str.len());
393 let svg_tag = &svg_str[svg_start..svg_start + tag_end];
394 if !svg_tag.contains("fill=") {
395 svg_str.insert_str(svg_start + 5, &format!("fill=\"{}\" ", hex_color));
396 }
397 }
398
399 let mut result = String::new();
401 let mut remaining = svg_str.as_str();
402
403 while let Some(start) = remaining.find("fill=\"") {
404 result.push_str(&remaining[..start]);
405 let after_attr = &remaining[start + 6..]; if let Some(end_pos) = after_attr.find('"') {
407 let old_val = &after_attr[..end_pos];
408 if old_val == "none" {
409 result.push_str("fill=\"none\"");
410 } else {
411 result.push_str(&format!("fill=\"{}\"", hex_color));
412 }
413 remaining = &after_attr[end_pos + 1..]; } else {
415 result.push_str("fill=\"");
416 result.push_str(after_attr);
417 break;
418 }
419 }
420 result.push_str(remaining);
421 svg_str = result;
422 }
423
424 if let Some(width) = style.stroke_width {
426 let mut result = String::new();
428 let mut remaining = svg_str.as_str();
429
430 while let Some(start) = remaining.find("stroke-width=\"") {
431 result.push_str(&remaining[..start]);
433 result.push_str("stroke-width=\"");
434
435 let after_attr = &remaining[start + 14..];
437 if let Some(end_pos) = after_attr.find('"') {
438 result.push_str(&width.to_string());
440 remaining = &after_attr[end_pos..];
442 } else {
443 result.push_str(after_attr);
445 break;
446 }
447 }
448 result.push_str(remaining);
450 svg_str = result;
451 }
452
453 Some(svg_str.into_bytes())
454}
455
456#[derive(Clone, Copy, Debug, Default)]
460pub struct SvgImportStats {
461 pub rects: u32,
462 pub rounded_rects: u32,
463 pub ellipses: u32,
464 pub paths: u32,
465 pub strokes: u32,
466 pub skipped: u32,
467}
468
469fn color_from_usvg(color: usvg::Color, opacity: f32) -> crate::scene::ColorLinPremul {
470 crate::scene::ColorLinPremul::from_srgba(color.red, color.green, color.blue, opacity)
471}
472
473fn transform2d_from_usvg(t: usvg::Transform) -> crate::scene::Transform2D {
474 crate::scene::Transform2D {
476 m: [
477 t.sx as f32,
478 t.ky as f32,
479 t.kx as f32,
480 t.sy as f32,
481 t.tx as f32,
482 t.ty as f32,
483 ],
484 }
485}
486
487fn fill_rule_from_usvg(rule: usvg::FillRule) -> crate::scene::FillRule {
488 match rule {
489 usvg::FillRule::NonZero => crate::scene::FillRule::NonZero,
490 usvg::FillRule::EvenOdd => crate::scene::FillRule::EvenOdd,
491 }
492}
493
494fn import_path_fill(
497 painter: &mut crate::painter::Painter,
498 node_transform: usvg::Transform,
499 p: &usvg::Path,
500 color: crate::scene::ColorLinPremul,
501 stats: &mut SvgImportStats,
502) {
503 use crate::scene::{Path, PathCmd};
504 let mut cmds: Vec<PathCmd> = Vec::new();
505 for seg in p.data().segments() {
507 use usvg::tiny_skia_path::PathSegment;
508 match seg {
509 PathSegment::MoveTo(pt) => cmds.push(PathCmd::MoveTo([pt.x as f32, pt.y as f32])),
510 PathSegment::LineTo(pt) => cmds.push(PathCmd::LineTo([pt.x as f32, pt.y as f32])),
511 PathSegment::QuadTo(c, p) => cmds.push(PathCmd::QuadTo(
512 [c.x as f32, c.y as f32],
513 [p.x as f32, p.y as f32],
514 )),
515 PathSegment::CubicTo(c1, c2, p) => cmds.push(PathCmd::CubicTo(
516 [c1.x as f32, c1.y as f32],
517 [c2.x as f32, c2.y as f32],
518 [p.x as f32, p.y as f32],
519 )),
520 PathSegment::Close => cmds.push(PathCmd::Close),
521 }
522 }
523 let fill_rule = p
524 .fill()
525 .map(|f| fill_rule_from_usvg(f.rule()))
526 .unwrap_or(crate::scene::FillRule::NonZero);
527 let path = Path { cmds, fill_rule };
528 let t = transform2d_from_usvg(node_transform);
529 painter.push_transform(t);
530 painter.fill_path(path, color, 0);
531 painter.pop_transform();
532 stats.paths += 1;
533}
534
535fn detect_axis_aligned_rect(p: &usvg::Path) -> Option<crate::scene::Rect> {
539 use usvg::tiny_skia_path::PathSegment;
540 let mut points: Vec<[f32; 2]> = Vec::new();
542 let mut started = false;
543 for seg in p.data().segments() {
544 match seg {
545 PathSegment::MoveTo(pt) => {
546 if started {
547 break;
548 } started = true;
550 points.clear();
551 points.push([pt.x as f32, pt.y as f32]);
552 }
553 PathSegment::LineTo(pt) => {
554 if !started {
555 return None;
556 }
557 let q = [pt.x as f32, pt.y as f32];
558 if points
560 .last()
561 .map_or(true, |last| last[0] != q[0] || last[1] != q[1])
562 {
563 points.push(q);
564 }
565 }
566 PathSegment::QuadTo(..) | PathSegment::CubicTo(..) => {
567 return None;
569 }
570 PathSegment::Close => {
571 break;
572 }
573 }
574 }
575 if points.len() != 4 {
576 return None;
577 }
578 for i in 0..4 {
580 let a = points[i];
581 let b = points[(i + 1) % 4];
582 let dx = (a[0] - b[0]).abs();
583 let dy = (a[1] - b[1]).abs();
584 if dx > 1e-4 && dy > 1e-4 {
585 return None;
586 }
587 }
588 let mut minx = f32::INFINITY;
590 let mut miny = f32::INFINITY;
591 let mut maxx = f32::NEG_INFINITY;
592 let mut maxy = f32::NEG_INFINITY;
593 for p in &points {
594 minx = minx.min(p[0]);
595 miny = miny.min(p[1]);
596 maxx = maxx.max(p[0]);
597 maxy = maxy.max(p[1]);
598 }
599 let w = (maxx - minx).abs();
600 let h = (maxy - miny).abs();
601 if w <= 0.0 || h <= 0.0 {
602 return None;
603 }
604 Some(crate::scene::Rect {
605 x: minx.min(maxx),
606 y: miny.min(maxy),
607 w,
608 h,
609 })
610}
611
612fn paint_from_fill(fill: &usvg::Fill) -> Option<crate::scene::Brush> {
613 match fill.paint() {
614 usvg::Paint::Color(c) => Some(crate::scene::Brush::Solid(color_from_usvg(
615 *c,
616 fill.opacity().get() as f32,
617 ))),
618 _ => None,
619 }
620}
621
622pub fn import_svg_geometry_to_painter(
628 painter: &mut crate::painter::Painter,
629 path: &Path,
630) -> Option<SvgImportStats> {
631 let data = std::fs::read(path).ok()?;
632 let mut opt = usvg::Options::default();
633 opt.resources_dir = path.parent().map(|p| p.to_path_buf());
634 opt.fontdb = SYSTEM_FONTDB.clone();
635 let tree = usvg::Tree::from_data(&data, &opt).ok()?;
636 let mut stats = SvgImportStats::default();
637
638 fn walk(
640 group: &usvg::Group,
641 painter: &mut crate::painter::Painter,
642 stats: &mut SvgImportStats,
643 ) {
644 for node in group.children() {
645 match node {
646 usvg::Node::Path(p) => {
647 if let Some(fill) = p.fill() {
648 if let Some(crate::scene::Brush::Solid(col)) = paint_from_fill(fill) {
649 if let Some(rect) = detect_axis_aligned_rect(p) {
651 let t = transform2d_from_usvg(p.abs_transform());
652 painter.push_transform(t);
653 painter.rect(rect, crate::scene::Brush::Solid(col), 0);
654 painter.pop_transform();
655 stats.rects += 1;
656 } else {
657 import_path_fill(painter, p.abs_transform(), p, col, stats);
658 }
659 } else {
660 stats.skipped += 1;
662 }
663 }
664 if let Some(st) = p.stroke() {
666 if let usvg::Paint::Color(c) = st.paint() {
667 let col = color_from_usvg(*c, st.opacity().get() as f32);
668 if let Some(rect) = detect_axis_aligned_rect(p) {
670 let t = transform2d_from_usvg(p.abs_transform());
671 painter.push_transform(t);
672 painter.stroke_rect(
673 rect,
674 crate::scene::Stroke {
675 width: st.width().get() as f32,
676 },
677 crate::scene::Brush::Solid(col),
678 0,
679 );
680 painter.pop_transform();
681 stats.strokes += 1;
682 } else {
683 use crate::scene::{Path as EPath, PathCmd};
685 let mut cmds: Vec<PathCmd> = Vec::new();
686 for seg in p.data().segments() {
687 use usvg::tiny_skia_path::PathSegment;
688 match seg {
689 PathSegment::MoveTo(pt) => {
690 cmds.push(PathCmd::MoveTo([pt.x as f32, pt.y as f32]))
691 }
692 PathSegment::LineTo(pt) => {
693 cmds.push(PathCmd::LineTo([pt.x as f32, pt.y as f32]))
694 }
695 PathSegment::QuadTo(c, q) => cmds.push(PathCmd::QuadTo(
696 [c.x as f32, c.y as f32],
697 [q.x as f32, q.y as f32],
698 )),
699 PathSegment::CubicTo(c1, c2, q) => {
700 cmds.push(PathCmd::CubicTo(
701 [c1.x as f32, c1.y as f32],
702 [c2.x as f32, c2.y as f32],
703 [q.x as f32, q.y as f32],
704 ))
705 }
706 PathSegment::Close => cmds.push(PathCmd::Close),
707 }
708 }
709 let epath = EPath {
710 cmds,
711 fill_rule: crate::scene::FillRule::NonZero,
712 };
713 let t = transform2d_from_usvg(p.abs_transform());
714 painter.push_transform(t);
715 painter.stroke_path(
716 epath,
717 crate::scene::Stroke {
718 width: st.width().get() as f32,
719 },
720 col,
721 0,
722 );
723 painter.pop_transform();
724 stats.strokes += 1;
725 }
726 } else {
727 stats.skipped += 1;
728 }
729 }
730 }
731 usvg::Node::Group(g) => {
732 walk(g, painter, stats);
734 }
735 usvg::Node::Image(_img) => {
736 node.subroots(|subroot| walk(subroot, painter, stats));
739 }
740 usvg::Node::Text(_) => {
741 }
743 }
744 }
745 }
746
747 let root = tree.root();
748 walk(root, painter, &mut stats);
749
750 Some(stats)
751}
752
753pub fn svg_intrinsic_size(path: &Path) -> Option<(u32, u32)> {
756 let data = std::fs::read(path).ok()?;
757 let mut opt = usvg::Options::default();
758 opt.resources_dir = path.parent().map(|p| p.to_path_buf());
759 opt.fontdb = SYSTEM_FONTDB.clone();
760 let tree = usvg::Tree::from_data(&data, &opt).ok()?;
761 let size = tree.size().to_int_size();
762 Some((size.width().max(1), size.height().max(1)))
763}
764
765pub fn svg_requires_rasterization(path: &Path) -> Option<bool> {
769 let data = std::fs::read(path).ok()?;
770 let mut opt = usvg::Options::default();
771 opt.resources_dir = path.parent().map(|p| p.to_path_buf());
772 opt.fontdb = SYSTEM_FONTDB.clone();
773 let tree = usvg::Tree::from_data(&data, &opt).ok()?;
774
775 fn check_node(node: &usvg::Node) -> bool {
776 match node {
777 usvg::Node::Path(p) => {
778 if let Some(fill) = p.fill() {
780 if !matches!(fill.paint(), usvg::Paint::Color(_)) {
781 return true; }
783 }
784
785 if let Some(stroke) = p.stroke() {
787 if !matches!(stroke.paint(), usvg::Paint::Color(_)) {
788 return true; }
790 }
791
792 let mut needs_raster = false;
794 node.subroots(|subroot| {
795 if check_group(subroot) {
796 needs_raster = true;
797 }
798 });
799 needs_raster
800 }
801 usvg::Node::Image(_) => {
802 true
804 }
805 usvg::Node::Text(_) => {
806 true
808 }
809 usvg::Node::Group(g) => check_group(g),
810 }
811 }
812
813 fn check_group(group: &usvg::Group) -> bool {
814 for child in group.children() {
817 if check_node(&child) {
818 return true;
819 }
820 }
821 false
822 }
823
824 let requires_raster = check_group(tree.root());
825 Some(requires_raster)
826}