we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement SVG basics: svg, rect, circle, ellipse, line, path, text, g

New `svg` crate with:
- SVG path data parser (M, L, H, V, C, S, Q, T, A, Z and relative variants)
- 2D affine transform parsing (translate, rotate, scale, matrix, skewX/Y)
- Scanline rasterizer with even-odd fill and stroke expansion
- Shape-to-path converters (rect, circle, ellipse, rounded rect via cubic arcs)
- SVG color parsing (named colors, #hex, rgb())
- viewBox coordinate mapping
- Presentation attributes: fill, stroke, stroke-width, opacity, transform
- SVG text rendering via the text crate's glyph rasterizer

Integration:
- HTML parser: SVG foreign content mode with namespace tracking
- Style resolver: skip children of SVG root elements (replaced element)
- Browser: rasterize SVG elements to RGBA images for layout/render pipeline
- UA stylesheet: svg { display: block }

35 new tests covering path parsing, transforms, shape rendering, viewBox
scaling, color parsing, group transforms, and HTML-SVG integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+2502 -10
+9
Cargo.lock
··· 17 17 "we-platform", 18 18 "we-render", 19 19 "we-style", 20 + "we-svg", 20 21 "we-text", 21 22 "we-url", 22 23 ] ··· 105 106 "we-css", 106 107 "we-dom", 107 108 "we-html", 109 + ] 110 + 111 + [[package]] 112 + name = "we-svg" 113 + version = "0.1.0" 114 + dependencies = [ 115 + "we-dom", 116 + "we-text", 108 117 ] 109 118 110 119 [[package]]
+1
Cargo.toml
··· 15 15 "crates/image", 16 16 "crates/render", 17 17 "crates/js", 18 + "crates/svg", 18 19 "crates/browser", 19 20 ] 20 21
+1
crates/browser/Cargo.toml
··· 25 25 we-url = { path = "../url" } 26 26 we-encoding = { path = "../encoding" } 27 27 we-image = { path = "../image" } 28 + we-svg = { path = "../svg" }
+79 -4
crates/browser/src/main.rs
··· 6 6 use we_browser::loader::{Resource, ResourceLoader, ABOUT_BLANK_HTML}; 7 7 use we_browser::script_loader::execute_page_scripts; 8 8 use we_css::parser::Stylesheet; 9 - use we_dom::{Document, NodeId}; 9 + use we_dom::{Document, NodeData, NodeId}; 10 10 use we_html::parse_html; 11 11 use we_image::pixel::Image; 12 12 use we_layout::layout; ··· 15 15 use we_platform::metal::ClearColor; 16 16 use we_render::{build_display_list_with_page_scroll, RenderBackend, ScrollState}; 17 17 use we_style::computed::resolve_styles; 18 + use we_svg::{render_svg, svg_intrinsic_size}; 18 19 use we_text::font::{self, Font}; 19 20 use we_url::Url; 20 21 ··· 73 74 } 74 75 75 76 /// Build a `HashMap<NodeId, &Image>` reference map for the renderer. 76 - fn image_refs(store: &ImageStore) -> HashMap<NodeId, &Image> { 77 + fn image_refs<'a>( 78 + store: &'a ImageStore, 79 + svg_images: &'a HashMap<NodeId, Image>, 80 + ) -> HashMap<NodeId, &'a Image> { 77 81 let mut refs = HashMap::new(); 78 82 for (node_id, res) in store { 79 83 if let Some(ref img) = res.image { 80 84 refs.insert(*node_id, img); 81 85 } 82 86 } 87 + for (node_id, img) in svg_images { 88 + refs.insert(*node_id, img); 89 + } 83 90 refs 84 91 } 85 92 93 + /// Collect SVG elements from the DOM and compute their sizes for layout. 94 + fn collect_svg_sizes(doc: &Document) -> HashMap<NodeId, (f32, f32)> { 95 + let mut sizes = HashMap::new(); 96 + collect_svg_nodes_sizes(doc, doc.root(), &mut sizes); 97 + sizes 98 + } 99 + 100 + fn collect_svg_nodes_sizes(doc: &Document, node: NodeId, sizes: &mut HashMap<NodeId, (f32, f32)>) { 101 + if let NodeData::Element { 102 + ref tag_name, 103 + ref namespace, 104 + .. 105 + } = *doc.node_data(node) 106 + { 107 + if tag_name == "svg" && namespace.as_deref() == Some("http://www.w3.org/2000/svg") { 108 + if let Some((w, h)) = svg_intrinsic_size(doc, node) { 109 + sizes.insert(node, (w, h)); 110 + } 111 + return; // Don't recurse into SVG children. 112 + } 113 + } 114 + for child in doc.children(node) { 115 + collect_svg_nodes_sizes(doc, child, sizes); 116 + } 117 + } 118 + 119 + /// Rasterize all SVG elements in the DOM to RGBA images. 120 + fn rasterize_svgs(doc: &Document, font: &Font) -> HashMap<NodeId, Image> { 121 + let mut images = HashMap::new(); 122 + collect_and_rasterize_svgs(doc, doc.root(), font, &mut images); 123 + images 124 + } 125 + 126 + fn collect_and_rasterize_svgs( 127 + doc: &Document, 128 + node: NodeId, 129 + font: &Font, 130 + images: &mut HashMap<NodeId, Image>, 131 + ) { 132 + if let NodeData::Element { 133 + ref tag_name, 134 + ref namespace, 135 + .. 136 + } = *doc.node_data(node) 137 + { 138 + if tag_name == "svg" && namespace.as_deref() == Some("http://www.w3.org/2000/svg") { 139 + if let Some((w, h, data)) = render_svg(doc, node, Some(font)) { 140 + images.insert( 141 + node, 142 + Image { 143 + width: w, 144 + height: h, 145 + data, 146 + }, 147 + ); 148 + } 149 + return; // Don't recurse into SVG children. 150 + } 151 + } 152 + for child in doc.children(node) { 153 + collect_and_rasterize_svgs(doc, child, font, images); 154 + } 155 + } 156 + 86 157 /// Unified render pipeline: resolve styles → layout → build display list → 87 158 /// dispatch to active backend. 88 159 /// ··· 116 187 }; 117 188 118 189 // Build image maps for layout (sizes) and render (pixel data). 119 - let sizes = image_sizes(&page.images); 120 - let refs = image_refs(&page.images); 190 + let mut sizes = image_sizes(&page.images); 191 + let svg_sizes = collect_svg_sizes(&page.doc); 192 + sizes.extend(svg_sizes); 193 + 194 + let svg_images = rasterize_svgs(&page.doc, font); 195 + let refs = image_refs(&page.images, &svg_images); 121 196 122 197 // Layout. 123 198 let tree = layout(
+164
crates/html/src/tree_builder.rs
··· 42 42 ) 43 43 } 44 44 45 + /// SVG namespace URI. 46 + const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; 47 + 45 48 /// HTML tree builder that processes tokens and constructs a DOM tree. 46 49 pub struct TreeBuilder { 47 50 document: Document, ··· 54 57 original_insertion_mode: Option<InsertionMode>, 55 58 /// Pending text for the Text insertion mode (e.g., inside `<title>`). 56 59 pending_text: String, 60 + /// Depth counter for SVG foreign content. >0 means we are inside an `<svg>` element. 61 + svg_depth: usize, 57 62 } 58 63 59 64 impl TreeBuilder { ··· 67 72 insertion_mode: InsertionMode::Initial, 68 73 original_insertion_mode: None, 69 74 pending_text: String::new(), 75 + svg_depth: 0, 70 76 } 71 77 } 72 78 73 79 /// Process a single token, updating the DOM tree. 74 80 pub fn process_token(&mut self, token: Token) { 81 + // Handle SVG foreign content. 82 + if self.svg_depth > 0 { 83 + self.handle_svg_content(token); 84 + return; 85 + } 86 + 75 87 match self.insertion_mode { 76 88 InsertionMode::Initial => self.handle_initial(token), 77 89 InsertionMode::BeforeHtml => self.handle_before_html(token), ··· 373 385 self.insert_node(elem); 374 386 self.open_elements.push(elem); 375 387 } 388 + Token::StartTag { ref name, .. } if name == "svg" => { 389 + let elem = self.create_svg_element_from_token(&token); 390 + self.insert_node(elem); 391 + self.open_elements.push(elem); 392 + self.svg_depth = 1; 393 + } 376 394 Token::StartTag { ref name, .. } if is_void_element(name) => { 377 395 let elem = self.create_element_from_token(&token); 378 396 self.insert_node(elem); ··· 496 514 } 497 515 } 498 516 517 + // --- SVG foreign content --- 518 + 519 + /// Handle tokens while inside an SVG subtree. 520 + fn handle_svg_content(&mut self, token: Token) { 521 + match token { 522 + Token::Character(s) => { 523 + self.insert_text(&s); 524 + } 525 + Token::Comment(data) => { 526 + self.insert_comment(&data); 527 + } 528 + Token::StartTag { ref name, .. } if name == "svg" => { 529 + // Nested <svg>. 530 + let elem = self.create_svg_element_from_token(&token); 531 + self.insert_node(elem); 532 + self.open_elements.push(elem); 533 + self.svg_depth += 1; 534 + } 535 + Token::StartTag { self_closing, .. } => { 536 + let elem = self.create_svg_element_from_token(&token); 537 + self.insert_node(elem); 538 + if !self_closing { 539 + self.open_elements.push(elem); 540 + } 541 + } 542 + Token::EndTag { ref name } if name == "svg" => { 543 + self.pop_until("svg"); 544 + self.svg_depth -= 1; 545 + } 546 + Token::EndTag { ref name } => { 547 + // Pop matching element from the stack. 548 + self.handle_any_other_end_tag(name); 549 + } 550 + Token::Eof => {} 551 + _ => {} 552 + } 553 + } 554 + 499 555 // --- Helper methods --- 556 + 557 + /// Create a DOM element from a StartTag token with SVG namespace, setting attributes. 558 + fn create_svg_element_from_token(&mut self, token: &Token) -> NodeId { 559 + if let Token::StartTag { 560 + name, attributes, .. 561 + } = token 562 + { 563 + let id = self.document.create_element_ns(name, Some(SVG_NAMESPACE)); 564 + for (attr_name, attr_value) in attributes { 565 + self.document.set_attribute(id, attr_name, attr_value); 566 + } 567 + id 568 + } else { 569 + self.document 570 + .create_element_ns("unknown", Some(SVG_NAMESPACE)) 571 + } 572 + } 500 573 501 574 /// Create a DOM element from a StartTag token, setting attributes. 502 575 fn create_element_from_token(&mut self, token: &Token) -> NodeId { ··· 1155 1228 let children: Vec<NodeId> = doc.children(p).collect(); 1156 1229 assert_eq!(children.len(), 1); 1157 1230 assert_eq!(doc.text_content(children[0]), Some("Hello world")); 1231 + } 1232 + 1233 + #[test] 1234 + fn parse_inline_svg() { 1235 + let doc = parse_html( 1236 + "<html><body><svg width=\"100\" height=\"100\"><rect width=\"50\" height=\"50\" fill=\"red\"/></svg></body></html>", 1237 + ); 1238 + let root = doc.root(); 1239 + let html = doc.children(root).next().unwrap(); 1240 + let body = doc.children(html).nth(1).unwrap(); 1241 + let svg = doc.children(body).next().unwrap(); 1242 + 1243 + // SVG element should have SVG namespace. 1244 + if let NodeData::Element { 1245 + ref namespace, 1246 + ref tag_name, 1247 + .. 1248 + } = *doc.node_data(svg) 1249 + { 1250 + assert_eq!(tag_name, "svg"); 1251 + assert_eq!(namespace.as_deref(), Some(SVG_NAMESPACE)); 1252 + } else { 1253 + panic!("Expected Element node"); 1254 + } 1255 + 1256 + // SVG should have width/height attributes. 1257 + assert_eq!(doc.get_attribute(svg, "width"), Some("100")); 1258 + assert_eq!(doc.get_attribute(svg, "height"), Some("100")); 1259 + 1260 + // Rect child should also have SVG namespace. 1261 + let rect = doc.children(svg).next().unwrap(); 1262 + if let NodeData::Element { 1263 + ref namespace, 1264 + ref tag_name, 1265 + .. 1266 + } = *doc.node_data(rect) 1267 + { 1268 + assert_eq!(tag_name, "rect"); 1269 + assert_eq!(namespace.as_deref(), Some(SVG_NAMESPACE)); 1270 + } else { 1271 + panic!("Expected Element node"); 1272 + } 1273 + assert_eq!(doc.get_attribute(rect, "fill"), Some("red")); 1274 + } 1275 + 1276 + #[test] 1277 + fn parse_svg_with_nested_elements() { 1278 + let doc = parse_html( 1279 + "<body><svg width=\"200\" height=\"200\"><g><circle cx=\"50\" cy=\"50\" r=\"40\"/><text x=\"10\" y=\"80\">Hello</text></g></svg></body>", 1280 + ); 1281 + let root = doc.root(); 1282 + let html = doc.children(root).next().unwrap(); 1283 + let body = doc.children(html).nth(1).unwrap(); 1284 + let svg = doc.children(body).next().unwrap(); 1285 + 1286 + assert_eq!(doc.tag_name(svg), Some("svg")); 1287 + let g = doc.children(svg).next().unwrap(); 1288 + assert_eq!(doc.tag_name(g), Some("g")); 1289 + 1290 + let children: Vec<String> = child_tags(&doc, g); 1291 + assert_eq!(children, vec!["circle", "text"]); 1292 + 1293 + // Text element should contain text content. 1294 + let text_el = doc.children(g).nth(1).unwrap(); 1295 + assert_eq!(doc.deep_text_content(text_el), "Hello"); 1296 + } 1297 + 1298 + #[test] 1299 + fn svg_content_followed_by_html() { 1300 + let doc = parse_html( 1301 + "<body><svg width=\"50\" height=\"50\"><rect fill=\"blue\"/></svg><p>After SVG</p></body>", 1302 + ); 1303 + let root = doc.root(); 1304 + let html = doc.children(root).next().unwrap(); 1305 + let body = doc.children(html).nth(1).unwrap(); 1306 + 1307 + let children: Vec<String> = child_tags(&doc, body); 1308 + assert_eq!(children, vec!["svg", "p"]); 1309 + 1310 + // SVG children should be in SVG namespace. 1311 + let svg = doc.children(body).next().unwrap(); 1312 + let rect = doc.children(svg).next().unwrap(); 1313 + if let NodeData::Element { ref namespace, .. } = *doc.node_data(rect) { 1314 + assert_eq!(namespace.as_deref(), Some(SVG_NAMESPACE)); 1315 + } 1316 + 1317 + // Paragraph after SVG should be in HTML namespace (no namespace). 1318 + let p = doc.children(body).nth(1).unwrap(); 1319 + if let NodeData::Element { ref namespace, .. } = *doc.node_data(p) { 1320 + assert_eq!(namespace.as_deref(), None); 1321 + } 1158 1322 } 1159 1323 }
+21 -6
crates/style/src/computed.rs
··· 523 523 display: inline; 524 524 } 525 525 526 + svg { 527 + display: block; 528 + } 529 + 526 530 head, title, script, style, link, meta { 527 531 display: none; 528 532 } ··· 2090 2094 }) 2091 2095 } 2092 2096 } 2093 - NodeData::Element { .. } => { 2097 + NodeData::Element { 2098 + ref namespace, 2099 + ref tag_name, 2100 + .. 2101 + } => { 2094 2102 let style = 2095 2103 compute_style_for_element(doc, node, stylesheet, parent_style, viewport, media_ctx); 2096 2104 ··· 2098 2106 return None; 2099 2107 } 2100 2108 2109 + // SVG elements are rendered by the SVG rasterizer as replaced elements. 2110 + // Don't recurse into their children for HTML layout purposes. 2111 + let is_svg_root = 2112 + tag_name == "svg" && namespace.as_deref() == Some("http://www.w3.org/2000/svg"); 2113 + 2101 2114 let mut children = Vec::new(); 2102 - for child in doc.children(node) { 2103 - if let Some(styled) = 2104 - resolve_node(doc, child, stylesheet, &style, viewport, media_ctx) 2105 - { 2106 - children.push(styled); 2115 + if !is_svg_root { 2116 + for child in doc.children(node) { 2117 + if let Some(styled) = 2118 + resolve_node(doc, child, stylesheet, &style, viewport, media_ctx) 2119 + { 2120 + children.push(styled); 2121 + } 2107 2122 } 2108 2123 } 2109 2124
+12
crates/svg/Cargo.toml
··· 1 + [package] 2 + name = "we-svg" 3 + version = "0.1.0" 4 + edition.workspace = true 5 + 6 + [lib] 7 + name = "we_svg" 8 + path = "src/lib.rs" 9 + 10 + [dependencies] 11 + we-dom = { path = "../dom" } 12 + we-text = { path = "../text" }
+10
crates/svg/src/lib.rs
··· 1 + //! SVG parsing and rasterization for the `we` browser engine. 2 + //! 3 + //! Handles inline SVG elements: `<svg>`, `<rect>`, `<circle>`, `<ellipse>`, 4 + //! `<line>`, `<path>`, `<text>`, and `<g>`. 5 + 6 + pub mod path; 7 + pub mod render; 8 + pub mod transform; 9 + 10 + pub use render::{render_svg, svg_intrinsic_size};
+509
crates/svg/src/path.rs
··· 1 + //! SVG path data parser. 2 + //! 3 + //! Parses the `d` attribute of `<path>` elements into a sequence of 4 + //! path commands per the SVG specification. 5 + 6 + /// A single SVG path command. 7 + #[derive(Debug, Clone, PartialEq)] 8 + pub enum PathCommand { 9 + /// Move to (x, y). Absolute. 10 + MoveTo(f32, f32), 11 + /// Line to (x, y). Absolute. 12 + LineTo(f32, f32), 13 + /// Horizontal line to x. Absolute. 14 + HorizontalLineTo(f32), 15 + /// Vertical line to y. Absolute. 16 + VerticalLineTo(f32), 17 + /// Cubic bezier to (x, y) with control points (x1, y1) and (x2, y2). Absolute. 18 + CubicTo(f32, f32, f32, f32, f32, f32), 19 + /// Smooth cubic bezier to (x, y) with control point (x2, y2). Absolute. 20 + SmoothCubicTo(f32, f32, f32, f32), 21 + /// Quadratic bezier to (x, y) with control point (x1, y1). Absolute. 22 + QuadraticTo(f32, f32, f32, f32), 23 + /// Smooth quadratic bezier to (x, y). Absolute. 24 + SmoothQuadraticTo(f32, f32), 25 + /// Arc to (x, y) with radii (rx, ry), x-axis rotation, large-arc flag, sweep flag. Absolute. 26 + ArcTo(f32, f32, f32, bool, bool, f32, f32), 27 + /// Close path. 28 + Close, 29 + } 30 + 31 + /// Parse an SVG path data string into a list of absolute path commands. 32 + pub fn parse_path_data(d: &str) -> Vec<PathCommand> { 33 + let mut commands = Vec::new(); 34 + let mut parser = PathParser::new(d); 35 + parser.parse(&mut commands); 36 + commands 37 + } 38 + 39 + struct PathParser<'a> { 40 + input: &'a [u8], 41 + pos: usize, 42 + // Current point (for relative → absolute conversion). 43 + cx: f32, 44 + cy: f32, 45 + // Start of current subpath (for close). 46 + sx: f32, 47 + sy: f32, 48 + // Last control point (for smooth curves). 49 + last_cubic_cp: Option<(f32, f32)>, 50 + last_quad_cp: Option<(f32, f32)>, 51 + } 52 + 53 + impl<'a> PathParser<'a> { 54 + fn new(input: &'a str) -> Self { 55 + PathParser { 56 + input: input.as_bytes(), 57 + pos: 0, 58 + cx: 0.0, 59 + cy: 0.0, 60 + sx: 0.0, 61 + sy: 0.0, 62 + last_cubic_cp: None, 63 + last_quad_cp: None, 64 + } 65 + } 66 + 67 + fn parse(&mut self, commands: &mut Vec<PathCommand>) { 68 + while self.pos < self.input.len() { 69 + self.skip_whitespace_and_commas(); 70 + if self.pos >= self.input.len() { 71 + break; 72 + } 73 + 74 + let cmd = self.input[self.pos]; 75 + if cmd.is_ascii_alphabetic() { 76 + self.pos += 1; 77 + self.parse_command(cmd, commands); 78 + } else { 79 + // Implicit repeat of previous command. 80 + break; 81 + } 82 + } 83 + } 84 + 85 + fn parse_command(&mut self, cmd: u8, commands: &mut Vec<PathCommand>) { 86 + let relative = cmd.is_ascii_lowercase(); 87 + 88 + match cmd.to_ascii_uppercase() { 89 + b'M' => self.parse_move_to(relative, commands), 90 + b'L' => self.parse_line_to(relative, commands), 91 + b'H' => self.parse_horizontal(relative, commands), 92 + b'V' => self.parse_vertical(relative, commands), 93 + b'C' => self.parse_cubic(relative, commands), 94 + b'S' => self.parse_smooth_cubic(relative, commands), 95 + b'Q' => self.parse_quadratic(relative, commands), 96 + b'T' => self.parse_smooth_quadratic(relative, commands), 97 + b'A' => self.parse_arc(relative, commands), 98 + b'Z' => { 99 + commands.push(PathCommand::Close); 100 + self.cx = self.sx; 101 + self.cy = self.sy; 102 + self.last_cubic_cp = None; 103 + self.last_quad_cp = None; 104 + } 105 + _ => {} 106 + } 107 + } 108 + 109 + fn parse_move_to(&mut self, relative: bool, commands: &mut Vec<PathCommand>) { 110 + let mut first = true; 111 + while let Some((x, y)) = self.try_parse_coordinate_pair() { 112 + let (ax, ay) = if relative { 113 + (self.cx + x, self.cy + y) 114 + } else { 115 + (x, y) 116 + }; 117 + if first { 118 + commands.push(PathCommand::MoveTo(ax, ay)); 119 + self.sx = ax; 120 + self.sy = ay; 121 + first = false; 122 + } else { 123 + // Subsequent coordinate pairs are treated as LineTo. 124 + commands.push(PathCommand::LineTo(ax, ay)); 125 + } 126 + self.cx = ax; 127 + self.cy = ay; 128 + self.last_cubic_cp = None; 129 + self.last_quad_cp = None; 130 + } 131 + } 132 + 133 + fn parse_line_to(&mut self, relative: bool, commands: &mut Vec<PathCommand>) { 134 + while let Some((x, y)) = self.try_parse_coordinate_pair() { 135 + let (ax, ay) = if relative { 136 + (self.cx + x, self.cy + y) 137 + } else { 138 + (x, y) 139 + }; 140 + commands.push(PathCommand::LineTo(ax, ay)); 141 + self.cx = ax; 142 + self.cy = ay; 143 + self.last_cubic_cp = None; 144 + self.last_quad_cp = None; 145 + } 146 + } 147 + 148 + fn parse_horizontal(&mut self, relative: bool, commands: &mut Vec<PathCommand>) { 149 + while let Some(x) = self.try_parse_number() { 150 + let ax = if relative { self.cx + x } else { x }; 151 + commands.push(PathCommand::HorizontalLineTo(ax)); 152 + self.cx = ax; 153 + self.last_cubic_cp = None; 154 + self.last_quad_cp = None; 155 + } 156 + } 157 + 158 + fn parse_vertical(&mut self, relative: bool, commands: &mut Vec<PathCommand>) { 159 + while let Some(y) = self.try_parse_number() { 160 + let ay = if relative { self.cy + y } else { y }; 161 + commands.push(PathCommand::VerticalLineTo(ay)); 162 + self.cy = ay; 163 + self.last_cubic_cp = None; 164 + self.last_quad_cp = None; 165 + } 166 + } 167 + 168 + fn parse_cubic(&mut self, relative: bool, commands: &mut Vec<PathCommand>) { 169 + while let Some(coords) = self.try_parse_n_numbers(6) { 170 + let (x1, y1, x2, y2, x, y) = if relative { 171 + ( 172 + self.cx + coords[0], 173 + self.cy + coords[1], 174 + self.cx + coords[2], 175 + self.cy + coords[3], 176 + self.cx + coords[4], 177 + self.cy + coords[5], 178 + ) 179 + } else { 180 + ( 181 + coords[0], coords[1], coords[2], coords[3], coords[4], coords[5], 182 + ) 183 + }; 184 + commands.push(PathCommand::CubicTo(x1, y1, x2, y2, x, y)); 185 + self.cx = x; 186 + self.cy = y; 187 + self.last_cubic_cp = Some((x2, y2)); 188 + self.last_quad_cp = None; 189 + } 190 + } 191 + 192 + fn parse_smooth_cubic(&mut self, relative: bool, commands: &mut Vec<PathCommand>) { 193 + while let Some(coords) = self.try_parse_n_numbers(4) { 194 + let (x2, y2, x, y) = if relative { 195 + ( 196 + self.cx + coords[0], 197 + self.cy + coords[1], 198 + self.cx + coords[2], 199 + self.cy + coords[3], 200 + ) 201 + } else { 202 + (coords[0], coords[1], coords[2], coords[3]) 203 + }; 204 + commands.push(PathCommand::SmoothCubicTo(x2, y2, x, y)); 205 + // Reflect the last cubic control point. 206 + self.last_cubic_cp = Some((x2, y2)); 207 + self.cx = x; 208 + self.cy = y; 209 + self.last_quad_cp = None; 210 + } 211 + } 212 + 213 + fn parse_quadratic(&mut self, relative: bool, commands: &mut Vec<PathCommand>) { 214 + while let Some(coords) = self.try_parse_n_numbers(4) { 215 + let (x1, y1, x, y) = if relative { 216 + ( 217 + self.cx + coords[0], 218 + self.cy + coords[1], 219 + self.cx + coords[2], 220 + self.cy + coords[3], 221 + ) 222 + } else { 223 + (coords[0], coords[1], coords[2], coords[3]) 224 + }; 225 + commands.push(PathCommand::QuadraticTo(x1, y1, x, y)); 226 + self.cx = x; 227 + self.cy = y; 228 + self.last_quad_cp = Some((x1, y1)); 229 + self.last_cubic_cp = None; 230 + } 231 + } 232 + 233 + fn parse_smooth_quadratic(&mut self, relative: bool, commands: &mut Vec<PathCommand>) { 234 + while let Some((x, y)) = self.try_parse_coordinate_pair() { 235 + let (ax, ay) = if relative { 236 + (self.cx + x, self.cy + y) 237 + } else { 238 + (x, y) 239 + }; 240 + commands.push(PathCommand::SmoothQuadraticTo(ax, ay)); 241 + // Reflect the last quadratic control point. 242 + let cp = self 243 + .last_quad_cp 244 + .map(|(cpx, cpy)| (2.0 * self.cx - cpx, 2.0 * self.cy - cpy)) 245 + .unwrap_or((self.cx, self.cy)); 246 + self.last_quad_cp = Some(cp); 247 + self.cx = ax; 248 + self.cy = ay; 249 + self.last_cubic_cp = None; 250 + } 251 + } 252 + 253 + fn parse_arc(&mut self, relative: bool, commands: &mut Vec<PathCommand>) { 254 + loop { 255 + self.skip_whitespace_and_commas(); 256 + let Some(rx) = self.try_parse_number() else { 257 + break; 258 + }; 259 + let Some(ry) = self.try_parse_number() else { 260 + break; 261 + }; 262 + let Some(x_rot) = self.try_parse_number() else { 263 + break; 264 + }; 265 + let Some(large_arc) = self.try_parse_flag() else { 266 + break; 267 + }; 268 + let Some(sweep) = self.try_parse_flag() else { 269 + break; 270 + }; 271 + let Some((x, y)) = self.try_parse_coordinate_pair() else { 272 + break; 273 + }; 274 + let (ax, ay) = if relative { 275 + (self.cx + x, self.cy + y) 276 + } else { 277 + (x, y) 278 + }; 279 + commands.push(PathCommand::ArcTo(rx, ry, x_rot, large_arc, sweep, ax, ay)); 280 + self.cx = ax; 281 + self.cy = ay; 282 + self.last_cubic_cp = None; 283 + self.last_quad_cp = None; 284 + } 285 + } 286 + 287 + // --- Low-level parsing helpers --- 288 + 289 + fn skip_whitespace_and_commas(&mut self) { 290 + while self.pos < self.input.len() { 291 + let b = self.input[self.pos]; 292 + if b == b' ' || b == b'\t' || b == b'\r' || b == b'\n' || b == b',' { 293 + self.pos += 1; 294 + } else { 295 + break; 296 + } 297 + } 298 + } 299 + 300 + fn try_parse_number(&mut self) -> Option<f32> { 301 + self.skip_whitespace_and_commas(); 302 + if self.pos >= self.input.len() { 303 + return None; 304 + } 305 + 306 + let start = self.pos; 307 + 308 + // Optional sign. 309 + if self.pos < self.input.len() 310 + && (self.input[self.pos] == b'+' || self.input[self.pos] == b'-') 311 + { 312 + self.pos += 1; 313 + } 314 + 315 + let mut has_digits = false; 316 + 317 + // Integer part. 318 + while self.pos < self.input.len() && self.input[self.pos].is_ascii_digit() { 319 + self.pos += 1; 320 + has_digits = true; 321 + } 322 + 323 + // Fractional part. 324 + if self.pos < self.input.len() && self.input[self.pos] == b'.' { 325 + self.pos += 1; 326 + while self.pos < self.input.len() && self.input[self.pos].is_ascii_digit() { 327 + self.pos += 1; 328 + has_digits = true; 329 + } 330 + } 331 + 332 + if !has_digits { 333 + self.pos = start; 334 + return None; 335 + } 336 + 337 + // Exponent. 338 + if self.pos < self.input.len() 339 + && (self.input[self.pos] == b'e' || self.input[self.pos] == b'E') 340 + { 341 + self.pos += 1; 342 + if self.pos < self.input.len() 343 + && (self.input[self.pos] == b'+' || self.input[self.pos] == b'-') 344 + { 345 + self.pos += 1; 346 + } 347 + while self.pos < self.input.len() && self.input[self.pos].is_ascii_digit() { 348 + self.pos += 1; 349 + } 350 + } 351 + 352 + let s = std::str::from_utf8(&self.input[start..self.pos]).ok()?; 353 + s.parse::<f32>().ok() 354 + } 355 + 356 + fn try_parse_coordinate_pair(&mut self) -> Option<(f32, f32)> { 357 + let saved = self.pos; 358 + let x = self.try_parse_number()?; 359 + let y = match self.try_parse_number() { 360 + Some(y) => y, 361 + None => { 362 + self.pos = saved; 363 + return None; 364 + } 365 + }; 366 + Some((x, y)) 367 + } 368 + 369 + fn try_parse_n_numbers(&mut self, n: usize) -> Option<Vec<f32>> { 370 + let saved = self.pos; 371 + let mut nums = Vec::with_capacity(n); 372 + for _ in 0..n { 373 + match self.try_parse_number() { 374 + Some(v) => nums.push(v), 375 + None => { 376 + self.pos = saved; 377 + return None; 378 + } 379 + } 380 + } 381 + Some(nums) 382 + } 383 + 384 + fn try_parse_flag(&mut self) -> Option<bool> { 385 + self.skip_whitespace_and_commas(); 386 + if self.pos >= self.input.len() { 387 + return None; 388 + } 389 + match self.input[self.pos] { 390 + b'0' => { 391 + self.pos += 1; 392 + Some(false) 393 + } 394 + b'1' => { 395 + self.pos += 1; 396 + Some(true) 397 + } 398 + _ => None, 399 + } 400 + } 401 + } 402 + 403 + #[cfg(test)] 404 + mod tests { 405 + use super::*; 406 + 407 + #[test] 408 + fn parse_move_and_line() { 409 + let cmds = parse_path_data("M 10 20 L 30 40"); 410 + assert_eq!(cmds.len(), 2); 411 + assert_eq!(cmds[0], PathCommand::MoveTo(10.0, 20.0)); 412 + assert_eq!(cmds[1], PathCommand::LineTo(30.0, 40.0)); 413 + } 414 + 415 + #[test] 416 + fn parse_relative_line() { 417 + let cmds = parse_path_data("M 10 10 l 5 5"); 418 + assert_eq!(cmds.len(), 2); 419 + assert_eq!(cmds[0], PathCommand::MoveTo(10.0, 10.0)); 420 + assert_eq!(cmds[1], PathCommand::LineTo(15.0, 15.0)); 421 + } 422 + 423 + #[test] 424 + fn parse_horizontal_vertical() { 425 + let cmds = parse_path_data("M 0 0 H 50 V 50"); 426 + assert_eq!(cmds.len(), 3); 427 + assert_eq!(cmds[1], PathCommand::HorizontalLineTo(50.0)); 428 + assert_eq!(cmds[2], PathCommand::VerticalLineTo(50.0)); 429 + } 430 + 431 + #[test] 432 + fn parse_cubic_bezier() { 433 + let cmds = parse_path_data("M 0 0 C 10 20 30 40 50 60"); 434 + assert_eq!(cmds.len(), 2); 435 + assert_eq!( 436 + cmds[1], 437 + PathCommand::CubicTo(10.0, 20.0, 30.0, 40.0, 50.0, 60.0) 438 + ); 439 + } 440 + 441 + #[test] 442 + fn parse_quadratic_bezier() { 443 + let cmds = parse_path_data("M 0 0 Q 10 20 30 40"); 444 + assert_eq!(cmds.len(), 2); 445 + assert_eq!(cmds[1], PathCommand::QuadraticTo(10.0, 20.0, 30.0, 40.0)); 446 + } 447 + 448 + #[test] 449 + fn parse_arc() { 450 + let cmds = parse_path_data("M 0 0 A 25 25 0 0 1 50 50"); 451 + assert_eq!(cmds.len(), 2); 452 + assert_eq!( 453 + cmds[1], 454 + PathCommand::ArcTo(25.0, 25.0, 0.0, false, true, 50.0, 50.0) 455 + ); 456 + } 457 + 458 + #[test] 459 + fn parse_close() { 460 + let cmds = parse_path_data("M 0 0 L 50 0 L 50 50 Z"); 461 + assert_eq!(cmds.len(), 4); 462 + assert_eq!(cmds[3], PathCommand::Close); 463 + } 464 + 465 + #[test] 466 + fn parse_implicit_lineto_after_moveto() { 467 + let cmds = parse_path_data("M 0 0 10 20 30 40"); 468 + assert_eq!(cmds.len(), 3); 469 + assert_eq!(cmds[0], PathCommand::MoveTo(0.0, 0.0)); 470 + assert_eq!(cmds[1], PathCommand::LineTo(10.0, 20.0)); 471 + assert_eq!(cmds[2], PathCommand::LineTo(30.0, 40.0)); 472 + } 473 + 474 + #[test] 475 + fn parse_compact_notation() { 476 + let cmds = parse_path_data("M0,0L10,20"); 477 + assert_eq!(cmds.len(), 2); 478 + assert_eq!(cmds[0], PathCommand::MoveTo(0.0, 0.0)); 479 + assert_eq!(cmds[1], PathCommand::LineTo(10.0, 20.0)); 480 + } 481 + 482 + #[test] 483 + fn parse_negative_numbers() { 484 + let cmds = parse_path_data("M-10-20L-30-40"); 485 + assert_eq!(cmds.len(), 2); 486 + assert_eq!(cmds[0], PathCommand::MoveTo(-10.0, -20.0)); 487 + assert_eq!(cmds[1], PathCommand::LineTo(-30.0, -40.0)); 488 + } 489 + 490 + #[test] 491 + fn parse_empty_string() { 492 + let cmds = parse_path_data(""); 493 + assert!(cmds.is_empty()); 494 + } 495 + 496 + #[test] 497 + fn parse_smooth_cubic() { 498 + let cmds = parse_path_data("M 0 0 S 10 20 30 40"); 499 + assert_eq!(cmds.len(), 2); 500 + assert_eq!(cmds[1], PathCommand::SmoothCubicTo(10.0, 20.0, 30.0, 40.0)); 501 + } 502 + 503 + #[test] 504 + fn parse_smooth_quadratic() { 505 + let cmds = parse_path_data("M 0 0 T 30 40"); 506 + assert_eq!(cmds.len(), 2); 507 + assert_eq!(cmds[1], PathCommand::SmoothQuadraticTo(30.0, 40.0)); 508 + } 509 + }
+1371
crates/svg/src/render.rs
··· 1 + //! SVG rasterizer: renders SVG elements to RGBA pixel buffers. 2 + //! 3 + //! Walks an SVG DOM subtree, interprets presentation attributes, and 4 + //! rasterizes shapes via scanline fill. 5 + 6 + use we_dom::{Document, NodeId}; 7 + use we_text::font::Font; 8 + 9 + use crate::path::{parse_path_data, PathCommand}; 10 + use crate::transform::{parse_transform, Transform}; 11 + 12 + /// RGBA color. 13 + #[derive(Debug, Clone, Copy)] 14 + pub struct SvgColor { 15 + pub r: u8, 16 + pub g: u8, 17 + pub b: u8, 18 + pub a: u8, 19 + } 20 + 21 + impl SvgColor { 22 + pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { 23 + SvgColor { r, g, b, a } 24 + } 25 + 26 + fn black() -> Self { 27 + SvgColor { 28 + r: 0, 29 + g: 0, 30 + b: 0, 31 + a: 255, 32 + } 33 + } 34 + 35 + fn transparent() -> Self { 36 + SvgColor { 37 + r: 0, 38 + g: 0, 39 + b: 0, 40 + a: 0, 41 + } 42 + } 43 + } 44 + 45 + /// SVG presentation attributes for a single element. 46 + struct PresentationAttrs { 47 + fill: Option<SvgColor>, 48 + stroke: Option<SvgColor>, 49 + stroke_width: f32, 50 + opacity: f32, 51 + fill_opacity: f32, 52 + stroke_opacity: f32, 53 + transform: Transform, 54 + } 55 + 56 + impl Default for PresentationAttrs { 57 + fn default() -> Self { 58 + PresentationAttrs { 59 + fill: Some(SvgColor::black()), 60 + stroke: None, 61 + stroke_width: 1.0, 62 + opacity: 1.0, 63 + fill_opacity: 1.0, 64 + stroke_opacity: 1.0, 65 + transform: Transform::identity(), 66 + } 67 + } 68 + } 69 + 70 + /// SVG viewport / viewBox configuration. 71 + struct Viewport { 72 + width: f32, 73 + height: f32, 74 + view_box: Option<(f32, f32, f32, f32)>, 75 + } 76 + 77 + impl Viewport { 78 + fn transform(&self) -> Transform { 79 + if let Some((vx, vy, vw, vh)) = self.view_box { 80 + if vw > 0.0 && vh > 0.0 { 81 + let sx = self.width / vw; 82 + let sy = self.height / vh; 83 + return Transform::translate(-vx * sx, -vy * sy) 84 + .multiply(&Transform::scale(sx, sy)); 85 + } 86 + } 87 + Transform::identity() 88 + } 89 + } 90 + 91 + /// Render an SVG DOM subtree to an RGBA pixel buffer. 92 + /// 93 + /// `svg_node` must be an `<svg>` element in `doc`. Returns `(width, height, rgba_data)`. 94 + pub fn render_svg( 95 + doc: &Document, 96 + svg_node: NodeId, 97 + font: Option<&Font>, 98 + ) -> Option<(u32, u32, Vec<u8>)> { 99 + let tag = doc.tag_name(svg_node)?; 100 + if tag != "svg" { 101 + return None; 102 + } 103 + 104 + let width_attr = doc.get_attribute(svg_node, "width"); 105 + let height_attr = doc.get_attribute(svg_node, "height"); 106 + 107 + let view_box = parse_view_box(doc.get_attribute(svg_node, "viewBox")); 108 + 109 + let (width, height) = match (width_attr, height_attr, &view_box) { 110 + (Some(w), Some(h), _) => (parse_length(w), parse_length(h)), 111 + (None, None, Some((_, _, vw, vh))) => (*vw, *vh), 112 + (Some(w), None, Some((_, _, _, vh))) => (parse_length(w), *vh), 113 + (None, Some(h), Some((_, _, vw, _))) => (*vw, parse_length(h)), 114 + _ => (300.0, 150.0), // SVG default 115 + }; 116 + 117 + if width <= 0.0 || height <= 0.0 { 118 + return None; 119 + } 120 + 121 + let pw = width.ceil() as u32; 122 + let ph = height.ceil() as u32; 123 + if pw == 0 || ph == 0 || pw > 4096 || ph > 4096 { 124 + return None; 125 + } 126 + 127 + let viewport = Viewport { 128 + width, 129 + height, 130 + view_box, 131 + }; 132 + 133 + let mut canvas = Canvas::new(pw, ph); 134 + let base_transform = viewport.transform(); 135 + 136 + render_children(doc, svg_node, &base_transform, &mut canvas, font); 137 + 138 + Some((pw, ph, canvas.data)) 139 + } 140 + 141 + /// Get the intrinsic size of an SVG element without rendering it. 142 + pub fn svg_intrinsic_size(doc: &Document, svg_node: NodeId) -> Option<(f32, f32)> { 143 + let tag = doc.tag_name(svg_node)?; 144 + if tag != "svg" { 145 + return None; 146 + } 147 + 148 + let width_attr = doc.get_attribute(svg_node, "width"); 149 + let height_attr = doc.get_attribute(svg_node, "height"); 150 + let view_box = parse_view_box(doc.get_attribute(svg_node, "viewBox")); 151 + 152 + let (width, height) = match (width_attr, height_attr, &view_box) { 153 + (Some(w), Some(h), _) => (parse_length(w), parse_length(h)), 154 + (None, None, Some((_, _, vw, vh))) => (*vw, *vh), 155 + (Some(w), None, Some((_, _, _, vh))) => (parse_length(w), *vh), 156 + (None, Some(h), Some((_, _, vw, _))) => (*vw, parse_length(h)), 157 + _ => (300.0, 150.0), 158 + }; 159 + 160 + if width > 0.0 && height > 0.0 { 161 + Some((width, height)) 162 + } else { 163 + None 164 + } 165 + } 166 + 167 + fn render_children( 168 + doc: &Document, 169 + parent: NodeId, 170 + transform: &Transform, 171 + canvas: &mut Canvas, 172 + font: Option<&Font>, 173 + ) { 174 + for child in doc.children(parent) { 175 + render_element(doc, child, transform, canvas, font); 176 + } 177 + } 178 + 179 + fn render_element( 180 + doc: &Document, 181 + node: NodeId, 182 + parent_transform: &Transform, 183 + canvas: &mut Canvas, 184 + font: Option<&Font>, 185 + ) { 186 + let tag = match doc.tag_name(node) { 187 + Some(t) => t.to_string(), 188 + None => return, 189 + }; 190 + 191 + let attrs = parse_presentation_attrs(doc, node); 192 + let transform = parent_transform.multiply(&attrs.transform); 193 + 194 + match tag.as_str() { 195 + "g" => { 196 + render_children(doc, node, &transform, canvas, font); 197 + } 198 + "rect" => render_rect(doc, node, &attrs, &transform, canvas), 199 + "circle" => render_circle(doc, node, &attrs, &transform, canvas), 200 + "ellipse" => render_ellipse(doc, node, &attrs, &transform, canvas), 201 + "line" => render_line(doc, node, &attrs, &transform, canvas), 202 + "path" => render_path(doc, node, &attrs, &transform, canvas), 203 + "text" => render_text(doc, node, &attrs, &transform, canvas, font), 204 + _ => { 205 + // Unknown SVG element — recurse into children anyway. 206 + render_children(doc, node, &transform, canvas, font); 207 + } 208 + } 209 + } 210 + 211 + fn render_rect( 212 + doc: &Document, 213 + node: NodeId, 214 + attrs: &PresentationAttrs, 215 + transform: &Transform, 216 + canvas: &mut Canvas, 217 + ) { 218 + let x = parse_attr_f32(doc, node, "x").unwrap_or(0.0); 219 + let y = parse_attr_f32(doc, node, "y").unwrap_or(0.0); 220 + let w = parse_attr_f32(doc, node, "width").unwrap_or(0.0); 221 + let h = parse_attr_f32(doc, node, "height").unwrap_or(0.0); 222 + let rx = parse_attr_f32(doc, node, "rx").unwrap_or(0.0); 223 + let ry = parse_attr_f32(doc, node, "ry").unwrap_or(if rx > 0.0 { rx } else { 0.0 }); 224 + 225 + if w <= 0.0 || h <= 0.0 { 226 + return; 227 + } 228 + 229 + let path = if rx > 0.0 || ry > 0.0 { 230 + rounded_rect_to_path(x, y, w, h, rx.min(w / 2.0), ry.min(h / 2.0)) 231 + } else { 232 + vec![ 233 + PathCommand::MoveTo(x, y), 234 + PathCommand::LineTo(x + w, y), 235 + PathCommand::LineTo(x + w, y + h), 236 + PathCommand::LineTo(x, y + h), 237 + PathCommand::Close, 238 + ] 239 + }; 240 + 241 + fill_and_stroke(&path, attrs, transform, canvas); 242 + } 243 + 244 + fn render_circle( 245 + doc: &Document, 246 + node: NodeId, 247 + attrs: &PresentationAttrs, 248 + transform: &Transform, 249 + canvas: &mut Canvas, 250 + ) { 251 + let cx = parse_attr_f32(doc, node, "cx").unwrap_or(0.0); 252 + let cy = parse_attr_f32(doc, node, "cy").unwrap_or(0.0); 253 + let r = parse_attr_f32(doc, node, "r").unwrap_or(0.0); 254 + 255 + if r <= 0.0 { 256 + return; 257 + } 258 + 259 + let path = ellipse_to_path(cx, cy, r, r); 260 + fill_and_stroke(&path, attrs, transform, canvas); 261 + } 262 + 263 + fn render_ellipse( 264 + doc: &Document, 265 + node: NodeId, 266 + attrs: &PresentationAttrs, 267 + transform: &Transform, 268 + canvas: &mut Canvas, 269 + ) { 270 + let cx = parse_attr_f32(doc, node, "cx").unwrap_or(0.0); 271 + let cy = parse_attr_f32(doc, node, "cy").unwrap_or(0.0); 272 + let rx = parse_attr_f32(doc, node, "rx").unwrap_or(0.0); 273 + let ry = parse_attr_f32(doc, node, "ry").unwrap_or(0.0); 274 + 275 + if rx <= 0.0 || ry <= 0.0 { 276 + return; 277 + } 278 + 279 + let path = ellipse_to_path(cx, cy, rx, ry); 280 + fill_and_stroke(&path, attrs, transform, canvas); 281 + } 282 + 283 + fn render_line( 284 + doc: &Document, 285 + node: NodeId, 286 + attrs: &PresentationAttrs, 287 + transform: &Transform, 288 + canvas: &mut Canvas, 289 + ) { 290 + let x1 = parse_attr_f32(doc, node, "x1").unwrap_or(0.0); 291 + let y1 = parse_attr_f32(doc, node, "y1").unwrap_or(0.0); 292 + let x2 = parse_attr_f32(doc, node, "x2").unwrap_or(0.0); 293 + let y2 = parse_attr_f32(doc, node, "y2").unwrap_or(0.0); 294 + 295 + let path = vec![PathCommand::MoveTo(x1, y1), PathCommand::LineTo(x2, y2)]; 296 + 297 + // Lines only have stroke, no fill. 298 + if let Some(stroke_color) = attrs.stroke { 299 + let alpha = (attrs.opacity * attrs.stroke_opacity * 255.0) as u8; 300 + let color = SvgColor::new(stroke_color.r, stroke_color.g, stroke_color.b, alpha); 301 + let segments = flatten_path(&path); 302 + stroke_segments(&segments, attrs.stroke_width, &color, transform, canvas); 303 + } 304 + } 305 + 306 + fn render_path( 307 + doc: &Document, 308 + node: NodeId, 309 + attrs: &PresentationAttrs, 310 + transform: &Transform, 311 + canvas: &mut Canvas, 312 + ) { 313 + let d = match doc.get_attribute(node, "d") { 314 + Some(d) => d.to_string(), 315 + None => return, 316 + }; 317 + let path = parse_path_data(&d); 318 + fill_and_stroke(&path, attrs, transform, canvas); 319 + } 320 + 321 + fn render_text( 322 + doc: &Document, 323 + node: NodeId, 324 + attrs: &PresentationAttrs, 325 + transform: &Transform, 326 + canvas: &mut Canvas, 327 + font: Option<&Font>, 328 + ) { 329 + let font = match font { 330 + Some(f) => f, 331 + None => return, 332 + }; 333 + 334 + let x = parse_attr_f32(doc, node, "x").unwrap_or(0.0); 335 + let y = parse_attr_f32(doc, node, "y").unwrap_or(0.0); 336 + let font_size = parse_attr_f32(doc, node, "font-size").unwrap_or(16.0); 337 + 338 + let text = doc.deep_text_content(node); 339 + 340 + let fill_color = attrs.fill.unwrap_or_else(SvgColor::black); 341 + let alpha = (attrs.opacity * attrs.fill_opacity * 255.0) as u8; 342 + let color = SvgColor::new(fill_color.r, fill_color.g, fill_color.b, alpha); 343 + 344 + let glyphs = font.shape_text(&text, font_size); 345 + let mut gx = x; 346 + for glyph in &glyphs { 347 + let bitmap = font.rasterize_glyph(glyph.glyph_id, font_size); 348 + if let Some(bmp) = bitmap { 349 + let bx = gx + glyph.x_offset + bmp.bearing_x as f32; 350 + let by = y + glyph.y_offset - bmp.bearing_y as f32; 351 + 352 + for row in 0..bmp.height { 353 + for col in 0..bmp.width { 354 + let coverage = bmp.data[(row * bmp.width + col) as usize]; 355 + if coverage == 0 { 356 + continue; 357 + } 358 + let px = bx + col as f32; 359 + let py = by + row as f32; 360 + let (tx, ty) = transform.apply(px, py); 361 + 362 + let a = (coverage as u32 * color.a as u32) / 255; 363 + if a > 0 { 364 + canvas.blend_pixel( 365 + tx as i32, 366 + ty as i32, 367 + SvgColor::new(color.r, color.g, color.b, a as u8), 368 + ); 369 + } 370 + } 371 + } 372 + } 373 + gx += glyph.x_advance; 374 + } 375 + } 376 + 377 + // --- Shape-to-path converters --- 378 + 379 + fn ellipse_to_path(cx: f32, cy: f32, rx: f32, ry: f32) -> Vec<PathCommand> { 380 + // Approximate ellipse with 4 cubic bezier arcs. 381 + // Magic number for circular arc approximation: 0.5522847498 382 + let k = 0.552_285_f32; 383 + let kx = k * rx; 384 + let ky = k * ry; 385 + vec![ 386 + PathCommand::MoveTo(cx + rx, cy), 387 + PathCommand::CubicTo(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry), 388 + PathCommand::CubicTo(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy), 389 + PathCommand::CubicTo(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry), 390 + PathCommand::CubicTo(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy), 391 + PathCommand::Close, 392 + ] 393 + } 394 + 395 + fn rounded_rect_to_path(x: f32, y: f32, w: f32, h: f32, rx: f32, ry: f32) -> Vec<PathCommand> { 396 + let k = 0.552_285_f32; 397 + let kx = k * rx; 398 + let ky = k * ry; 399 + vec![ 400 + PathCommand::MoveTo(x + rx, y), 401 + PathCommand::LineTo(x + w - rx, y), 402 + PathCommand::CubicTo(x + w - rx + kx, y, x + w, y + ry - ky, x + w, y + ry), 403 + PathCommand::LineTo(x + w, y + h - ry), 404 + PathCommand::CubicTo( 405 + x + w, 406 + y + h - ry + ky, 407 + x + w - rx + kx, 408 + y + h, 409 + x + w - rx, 410 + y + h, 411 + ), 412 + PathCommand::LineTo(x + rx, y + h), 413 + PathCommand::CubicTo(x + rx - kx, y + h, x, y + h - ry + ky, x, y + h - ry), 414 + PathCommand::LineTo(x, y + ry), 415 + PathCommand::CubicTo(x, y + ry - ky, x + rx - kx, y, x + rx, y), 416 + PathCommand::Close, 417 + ] 418 + } 419 + 420 + // --- Path flattening --- 421 + 422 + /// A line segment in 2D. 423 + #[derive(Debug, Clone)] 424 + struct Segment { 425 + x0: f32, 426 + y0: f32, 427 + x1: f32, 428 + y1: f32, 429 + } 430 + 431 + /// Flatten a path into line segments by subdividing curves. 432 + fn flatten_path(commands: &[PathCommand]) -> Vec<Segment> { 433 + let mut segments = Vec::new(); 434 + let mut cx = 0.0_f32; 435 + let mut cy = 0.0_f32; 436 + let mut sx = 0.0_f32; 437 + let mut sy = 0.0_f32; 438 + let mut last_cubic_cp: Option<(f32, f32)> = None; 439 + let mut last_quad_cp: Option<(f32, f32)> = None; 440 + 441 + for cmd in commands { 442 + match *cmd { 443 + PathCommand::MoveTo(x, y) => { 444 + cx = x; 445 + cy = y; 446 + sx = x; 447 + sy = y; 448 + last_cubic_cp = None; 449 + last_quad_cp = None; 450 + } 451 + PathCommand::LineTo(x, y) => { 452 + segments.push(Segment { 453 + x0: cx, 454 + y0: cy, 455 + x1: x, 456 + y1: y, 457 + }); 458 + cx = x; 459 + cy = y; 460 + last_cubic_cp = None; 461 + last_quad_cp = None; 462 + } 463 + PathCommand::HorizontalLineTo(x) => { 464 + segments.push(Segment { 465 + x0: cx, 466 + y0: cy, 467 + x1: x, 468 + y1: cy, 469 + }); 470 + cx = x; 471 + last_cubic_cp = None; 472 + last_quad_cp = None; 473 + } 474 + PathCommand::VerticalLineTo(y) => { 475 + segments.push(Segment { 476 + x0: cx, 477 + y0: cy, 478 + x1: cx, 479 + y1: y, 480 + }); 481 + cy = y; 482 + last_cubic_cp = None; 483 + last_quad_cp = None; 484 + } 485 + PathCommand::CubicTo(x1, y1, x2, y2, x, y) => { 486 + flatten_cubic(cx, cy, x1, y1, x2, y2, x, y, &mut segments); 487 + cx = x; 488 + cy = y; 489 + last_cubic_cp = Some((x2, y2)); 490 + last_quad_cp = None; 491 + } 492 + PathCommand::SmoothCubicTo(x2, y2, x, y) => { 493 + let (x1, y1) = last_cubic_cp 494 + .map(|(cpx, cpy)| (2.0 * cx - cpx, 2.0 * cy - cpy)) 495 + .unwrap_or((cx, cy)); 496 + flatten_cubic(cx, cy, x1, y1, x2, y2, x, y, &mut segments); 497 + cx = x; 498 + cy = y; 499 + last_cubic_cp = Some((x2, y2)); 500 + last_quad_cp = None; 501 + } 502 + PathCommand::QuadraticTo(x1, y1, x, y) => { 503 + flatten_quadratic(cx, cy, x1, y1, x, y, &mut segments); 504 + cx = x; 505 + cy = y; 506 + last_quad_cp = Some((x1, y1)); 507 + last_cubic_cp = None; 508 + } 509 + PathCommand::SmoothQuadraticTo(x, y) => { 510 + let (x1, y1) = last_quad_cp 511 + .map(|(cpx, cpy)| (2.0 * cx - cpx, 2.0 * cy - cpy)) 512 + .unwrap_or((cx, cy)); 513 + flatten_quadratic(cx, cy, x1, y1, x, y, &mut segments); 514 + cx = x; 515 + cy = y; 516 + last_quad_cp = Some((x1, y1)); 517 + last_cubic_cp = None; 518 + } 519 + PathCommand::ArcTo(rx, ry, x_rot, large_arc, sweep, x, y) => { 520 + flatten_arc(cx, cy, rx, ry, x_rot, large_arc, sweep, x, y, &mut segments); 521 + cx = x; 522 + cy = y; 523 + last_cubic_cp = None; 524 + last_quad_cp = None; 525 + } 526 + PathCommand::Close => { 527 + if (cx - sx).abs() > 0.001 || (cy - sy).abs() > 0.001 { 528 + segments.push(Segment { 529 + x0: cx, 530 + y0: cy, 531 + x1: sx, 532 + y1: sy, 533 + }); 534 + } 535 + cx = sx; 536 + cy = sy; 537 + last_cubic_cp = None; 538 + last_quad_cp = None; 539 + } 540 + } 541 + } 542 + 543 + segments 544 + } 545 + 546 + /// Adaptively flatten a cubic bezier into line segments. 547 + #[allow(clippy::too_many_arguments)] 548 + fn flatten_cubic( 549 + x0: f32, 550 + y0: f32, 551 + x1: f32, 552 + y1: f32, 553 + x2: f32, 554 + y2: f32, 555 + x3: f32, 556 + y3: f32, 557 + out: &mut Vec<Segment>, 558 + ) { 559 + flatten_cubic_recursive(x0, y0, x1, y1, x2, y2, x3, y3, out, 0); 560 + } 561 + 562 + #[allow(clippy::too_many_arguments)] 563 + fn flatten_cubic_recursive( 564 + x0: f32, 565 + y0: f32, 566 + x1: f32, 567 + y1: f32, 568 + x2: f32, 569 + y2: f32, 570 + x3: f32, 571 + y3: f32, 572 + out: &mut Vec<Segment>, 573 + depth: u32, 574 + ) { 575 + const MAX_DEPTH: u32 = 8; 576 + const TOLERANCE: f32 = 0.25; 577 + 578 + if depth >= MAX_DEPTH { 579 + out.push(Segment { 580 + x0, 581 + y0, 582 + x1: x3, 583 + y1: y3, 584 + }); 585 + return; 586 + } 587 + 588 + // Flatness test: check if control points are close to the line from p0 to p3. 589 + let dx = x3 - x0; 590 + let dy = y3 - y0; 591 + let d_sq = dx * dx + dy * dy; 592 + 593 + if d_sq < 0.001 { 594 + out.push(Segment { 595 + x0, 596 + y0, 597 + x1: x3, 598 + y1: y3, 599 + }); 600 + return; 601 + } 602 + 603 + let d1 = ((x1 - x3) * dy - (y1 - y3) * dx).abs(); 604 + let d2 = ((x2 - x3) * dy - (y2 - y3) * dx).abs(); 605 + 606 + if (d1 + d2) * (d1 + d2) < TOLERANCE * d_sq { 607 + out.push(Segment { 608 + x0, 609 + y0, 610 + x1: x3, 611 + y1: y3, 612 + }); 613 + return; 614 + } 615 + 616 + // De Casteljau subdivision at t=0.5. 617 + let x01 = (x0 + x1) * 0.5; 618 + let y01 = (y0 + y1) * 0.5; 619 + let x12 = (x1 + x2) * 0.5; 620 + let y12 = (y1 + y2) * 0.5; 621 + let x23 = (x2 + x3) * 0.5; 622 + let y23 = (y2 + y3) * 0.5; 623 + let x012 = (x01 + x12) * 0.5; 624 + let y012 = (y01 + y12) * 0.5; 625 + let x123 = (x12 + x23) * 0.5; 626 + let y123 = (y12 + y23) * 0.5; 627 + let x0123 = (x012 + x123) * 0.5; 628 + let y0123 = (y012 + y123) * 0.5; 629 + 630 + flatten_cubic_recursive(x0, y0, x01, y01, x012, y012, x0123, y0123, out, depth + 1); 631 + flatten_cubic_recursive(x0123, y0123, x123, y123, x23, y23, x3, y3, out, depth + 1); 632 + } 633 + 634 + /// Flatten a quadratic bezier into line segments. 635 + fn flatten_quadratic(x0: f32, y0: f32, x1: f32, y1: f32, x2: f32, y2: f32, out: &mut Vec<Segment>) { 636 + // Convert quadratic to cubic: the control points of the equivalent cubic are 637 + // CP1 = P0 + 2/3 * (P1 - P0), CP2 = P2 + 2/3 * (P1 - P2). 638 + let cx1 = x0 + (2.0 / 3.0) * (x1 - x0); 639 + let cy1 = y0 + (2.0 / 3.0) * (y1 - y0); 640 + let cx2 = x2 + (2.0 / 3.0) * (x1 - x2); 641 + let cy2 = y2 + (2.0 / 3.0) * (y1 - y2); 642 + flatten_cubic(x0, y0, cx1, cy1, cx2, cy2, x2, y2, out); 643 + } 644 + 645 + /// Flatten an SVG arc into line segments. 646 + #[allow(clippy::too_many_arguments)] 647 + fn flatten_arc( 648 + cx: f32, 649 + cy: f32, 650 + mut rx: f32, 651 + mut ry: f32, 652 + x_rot: f32, 653 + large_arc: bool, 654 + sweep: bool, 655 + x: f32, 656 + y: f32, 657 + out: &mut Vec<Segment>, 658 + ) { 659 + // Handle degenerate cases. 660 + if (cx - x).abs() < 0.001 && (cy - y).abs() < 0.001 { 661 + return; 662 + } 663 + if rx.abs() < 0.001 || ry.abs() < 0.001 { 664 + out.push(Segment { 665 + x0: cx, 666 + y0: cy, 667 + x1: x, 668 + y1: y, 669 + }); 670 + return; 671 + } 672 + 673 + rx = rx.abs(); 674 + ry = ry.abs(); 675 + 676 + let phi = x_rot * std::f32::consts::PI / 180.0; 677 + let cos_phi = phi.cos(); 678 + let sin_phi = phi.sin(); 679 + 680 + // Step 1: compute (x1', y1'). 681 + let dx = (cx - x) / 2.0; 682 + let dy = (cy - y) / 2.0; 683 + let x1p = cos_phi * dx + sin_phi * dy; 684 + let y1p = -sin_phi * dx + cos_phi * dy; 685 + 686 + // Step 2: ensure radii are large enough. 687 + let x1p2 = x1p * x1p; 688 + let y1p2 = y1p * y1p; 689 + let rx2 = rx * rx; 690 + let ry2 = ry * ry; 691 + let lambda = x1p2 / rx2 + y1p2 / ry2; 692 + if lambda > 1.0 { 693 + let sqrt_lambda = lambda.sqrt(); 694 + rx *= sqrt_lambda; 695 + ry *= sqrt_lambda; 696 + } 697 + let rx2 = rx * rx; 698 + let ry2 = ry * ry; 699 + 700 + // Step 3: compute center point. 701 + let num = (rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2).max(0.0); 702 + let den = rx2 * y1p2 + ry2 * x1p2; 703 + let sq = if den > 0.0 { (num / den).sqrt() } else { 0.0 }; 704 + let sign = if large_arc == sweep { -1.0 } else { 1.0 }; 705 + let cxp = sign * sq * rx * y1p / ry; 706 + let cyp = sign * sq * -ry * x1p / rx; 707 + 708 + let cx_center = cos_phi * cxp - sin_phi * cyp + (cx + x) / 2.0; 709 + let cy_center = sin_phi * cxp + cos_phi * cyp + (cy + y) / 2.0; 710 + 711 + // Step 4: compute angles. 712 + let theta1 = angle_between(1.0, 0.0, (x1p - cxp) / rx, (y1p - cyp) / ry); 713 + let mut dtheta = angle_between( 714 + (x1p - cxp) / rx, 715 + (y1p - cyp) / ry, 716 + (-x1p - cxp) / rx, 717 + (-y1p - cyp) / ry, 718 + ); 719 + 720 + if !sweep && dtheta > 0.0 { 721 + dtheta -= 2.0 * std::f32::consts::PI; 722 + } else if sweep && dtheta < 0.0 { 723 + dtheta += 2.0 * std::f32::consts::PI; 724 + } 725 + 726 + // Step 5: approximate arc with line segments. 727 + let n = ((dtheta.abs() / (std::f32::consts::PI / 4.0)).ceil() as usize).max(1); 728 + let step = dtheta / n as f32; 729 + 730 + let mut prev_x = cx; 731 + let mut prev_y = cy; 732 + 733 + for i in 1..=n { 734 + let theta = theta1 + step * i as f32; 735 + let px = rx * theta.cos(); 736 + let py = ry * theta.sin(); 737 + let nx = cos_phi * px - sin_phi * py + cx_center; 738 + let ny = sin_phi * px + cos_phi * py + cy_center; 739 + out.push(Segment { 740 + x0: prev_x, 741 + y0: prev_y, 742 + x1: nx, 743 + y1: ny, 744 + }); 745 + prev_x = nx; 746 + prev_y = ny; 747 + } 748 + } 749 + 750 + fn angle_between(ux: f32, uy: f32, vx: f32, vy: f32) -> f32 { 751 + let n = (ux * ux + uy * uy).sqrt() * (vx * vx + vy * vy).sqrt(); 752 + if n == 0.0 { 753 + return 0.0; 754 + } 755 + let cos_a = (ux * vx + uy * vy) / n; 756 + let angle = cos_a.clamp(-1.0, 1.0).acos(); 757 + if ux * vy - uy * vx < 0.0 { 758 + -angle 759 + } else { 760 + angle 761 + } 762 + } 763 + 764 + // --- Fill and stroke --- 765 + 766 + fn fill_and_stroke( 767 + path: &[PathCommand], 768 + attrs: &PresentationAttrs, 769 + transform: &Transform, 770 + canvas: &mut Canvas, 771 + ) { 772 + let segments = flatten_path(path); 773 + 774 + // Fill. 775 + if let Some(fill_color) = attrs.fill { 776 + let alpha = (attrs.opacity * attrs.fill_opacity * 255.0) as u8; 777 + let color = SvgColor::new(fill_color.r, fill_color.g, fill_color.b, alpha); 778 + fill_segments(&segments, &color, transform, canvas); 779 + } 780 + 781 + // Stroke. 782 + if let Some(stroke_color) = attrs.stroke { 783 + let alpha = (attrs.opacity * attrs.stroke_opacity * 255.0) as u8; 784 + let color = SvgColor::new(stroke_color.r, stroke_color.g, stroke_color.b, alpha); 785 + stroke_segments(&segments, attrs.stroke_width, &color, transform, canvas); 786 + } 787 + } 788 + 789 + /// Fill a set of segments using scanline rasterization with the even-odd rule. 790 + fn fill_segments( 791 + segments: &[Segment], 792 + color: &SvgColor, 793 + transform: &Transform, 794 + canvas: &mut Canvas, 795 + ) { 796 + if segments.is_empty() { 797 + return; 798 + } 799 + 800 + // Transform all segments. 801 + let transformed: Vec<Segment> = segments 802 + .iter() 803 + .map(|s| { 804 + let (x0, y0) = transform.apply(s.x0, s.y0); 805 + let (x1, y1) = transform.apply(s.x1, s.y1); 806 + Segment { x0, y0, x1, y1 } 807 + }) 808 + .collect(); 809 + 810 + // Find bounding box. 811 + let mut min_y = f32::MAX; 812 + let mut max_y = f32::MIN; 813 + for s in &transformed { 814 + min_y = min_y.min(s.y0).min(s.y1); 815 + max_y = max_y.max(s.y0).max(s.y1); 816 + } 817 + 818 + let y_start = (min_y.floor() as i32).max(0); 819 + let y_end = (max_y.ceil() as i32).min(canvas.height as i32); 820 + 821 + for y in y_start..y_end { 822 + let scan_y = y as f32 + 0.5; 823 + 824 + // Find all x-intersections at this scanline. 825 + let mut intersections = Vec::new(); 826 + for s in &transformed { 827 + let (sy0, sy1) = if s.y0 < s.y1 { 828 + (s.y0, s.y1) 829 + } else { 830 + (s.y1, s.y0) 831 + }; 832 + if scan_y < sy0 || scan_y >= sy1 { 833 + continue; 834 + } 835 + let dy = s.y1 - s.y0; 836 + if dy.abs() < 0.0001 { 837 + continue; 838 + } 839 + let t = (scan_y - s.y0) / dy; 840 + let ix = s.x0 + t * (s.x1 - s.x0); 841 + intersections.push(ix); 842 + } 843 + 844 + intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); 845 + 846 + // Even-odd fill: fill between pairs of intersections. 847 + let mut i = 0; 848 + while i + 1 < intersections.len() { 849 + let x_start = (intersections[i].ceil() as i32).max(0); 850 + let x_end = (intersections[i + 1].floor() as i32 + 1).min(canvas.width as i32); 851 + for x in x_start..x_end { 852 + canvas.blend_pixel(x, y, *color); 853 + } 854 + i += 2; 855 + } 856 + } 857 + } 858 + 859 + /// Stroke segments by expanding each segment into a thick line. 860 + fn stroke_segments( 861 + segments: &[Segment], 862 + width: f32, 863 + color: &SvgColor, 864 + transform: &Transform, 865 + canvas: &mut Canvas, 866 + ) { 867 + if width <= 0.0 || segments.is_empty() { 868 + return; 869 + } 870 + 871 + let half_w = width / 2.0; 872 + 873 + for seg in segments { 874 + let (x0, y0) = transform.apply(seg.x0, seg.y0); 875 + let (x1, y1) = transform.apply(seg.x1, seg.y1); 876 + 877 + let dx = x1 - x0; 878 + let dy = y1 - y0; 879 + let len = (dx * dx + dy * dy).sqrt(); 880 + if len < 0.001 { 881 + continue; 882 + } 883 + 884 + // Normal vector scaled by half width. 885 + let nx = -dy / len * half_w; 886 + let ny = dx / len * half_w; 887 + 888 + // Quad corners of the thick line. 889 + let corners = [ 890 + (x0 + nx, y0 + ny), 891 + (x1 + nx, y1 + ny), 892 + (x1 - nx, y1 - ny), 893 + (x0 - nx, y0 - ny), 894 + ]; 895 + 896 + let quad_segs = vec![ 897 + Segment { 898 + x0: corners[0].0, 899 + y0: corners[0].1, 900 + x1: corners[1].0, 901 + y1: corners[1].1, 902 + }, 903 + Segment { 904 + x0: corners[1].0, 905 + y0: corners[1].1, 906 + x1: corners[2].0, 907 + y1: corners[2].1, 908 + }, 909 + Segment { 910 + x0: corners[2].0, 911 + y0: corners[2].1, 912 + x1: corners[3].0, 913 + y1: corners[3].1, 914 + }, 915 + Segment { 916 + x0: corners[3].0, 917 + y0: corners[3].1, 918 + x1: corners[0].0, 919 + y1: corners[0].1, 920 + }, 921 + ]; 922 + 923 + // Use identity transform since we already transformed. 924 + fill_segments(&quad_segs, color, &Transform::identity(), canvas); 925 + } 926 + } 927 + 928 + // --- Pixel canvas --- 929 + 930 + struct Canvas { 931 + width: u32, 932 + height: u32, 933 + /// RGBA8 pixel data. 934 + data: Vec<u8>, 935 + } 936 + 937 + impl Canvas { 938 + fn new(width: u32, height: u32) -> Self { 939 + Canvas { 940 + width, 941 + height, 942 + data: vec![0; (width * height * 4) as usize], 943 + } 944 + } 945 + 946 + fn blend_pixel(&mut self, x: i32, y: i32, color: SvgColor) { 947 + if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 { 948 + return; 949 + } 950 + let offset = ((y as u32 * self.width + x as u32) * 4) as usize; 951 + let a = color.a as u32; 952 + if a == 0 { 953 + return; 954 + } 955 + if a == 255 { 956 + self.data[offset] = color.r; 957 + self.data[offset + 1] = color.g; 958 + self.data[offset + 2] = color.b; 959 + self.data[offset + 3] = 255; 960 + } else { 961 + let inv_a = 255 - a; 962 + let dst_r = self.data[offset] as u32; 963 + let dst_g = self.data[offset + 1] as u32; 964 + let dst_b = self.data[offset + 2] as u32; 965 + let dst_a = self.data[offset + 3] as u32; 966 + self.data[offset] = ((color.r as u32 * a + dst_r * inv_a) / 255) as u8; 967 + self.data[offset + 1] = ((color.g as u32 * a + dst_g * inv_a) / 255) as u8; 968 + self.data[offset + 2] = ((color.b as u32 * a + dst_b * inv_a) / 255) as u8; 969 + self.data[offset + 3] = ((a + dst_a * inv_a / 255).min(255)) as u8; 970 + } 971 + } 972 + } 973 + 974 + // --- Attribute parsing helpers --- 975 + 976 + fn parse_presentation_attrs(doc: &Document, node: NodeId) -> PresentationAttrs { 977 + let mut attrs = PresentationAttrs::default(); 978 + 979 + if let Some(fill) = doc.get_attribute(node, "fill") { 980 + if fill == "none" { 981 + attrs.fill = None; 982 + } else { 983 + attrs.fill = parse_svg_color(fill); 984 + } 985 + } 986 + 987 + if let Some(stroke) = doc.get_attribute(node, "stroke") { 988 + if stroke == "none" { 989 + attrs.stroke = None; 990 + } else { 991 + attrs.stroke = parse_svg_color(stroke); 992 + } 993 + } 994 + 995 + if let Some(sw) = doc.get_attribute(node, "stroke-width") { 996 + attrs.stroke_width = parse_length(sw); 997 + } 998 + 999 + if let Some(o) = doc.get_attribute(node, "opacity") { 1000 + attrs.opacity = parse_length(o).clamp(0.0, 1.0); 1001 + } 1002 + 1003 + if let Some(fo) = doc.get_attribute(node, "fill-opacity") { 1004 + attrs.fill_opacity = parse_length(fo).clamp(0.0, 1.0); 1005 + } 1006 + 1007 + if let Some(so) = doc.get_attribute(node, "stroke-opacity") { 1008 + attrs.stroke_opacity = parse_length(so).clamp(0.0, 1.0); 1009 + } 1010 + 1011 + if let Some(t) = doc.get_attribute(node, "transform") { 1012 + attrs.transform = parse_transform(t); 1013 + } 1014 + 1015 + attrs 1016 + } 1017 + 1018 + fn parse_attr_f32(doc: &Document, node: NodeId, name: &str) -> Option<f32> { 1019 + doc.get_attribute(node, name).and_then(|v| { 1020 + let trimmed = v.trim(); 1021 + // Strip "px" suffix if present. 1022 + let s = trimmed.strip_suffix("px").unwrap_or(trimmed); 1023 + s.parse::<f32>().ok() 1024 + }) 1025 + } 1026 + 1027 + fn parse_length(s: &str) -> f32 { 1028 + let trimmed = s.trim(); 1029 + let s = trimmed.strip_suffix("px").unwrap_or(trimmed); 1030 + s.parse::<f32>().unwrap_or(0.0) 1031 + } 1032 + 1033 + fn parse_view_box(s: Option<&str>) -> Option<(f32, f32, f32, f32)> { 1034 + let s = s?; 1035 + let parts: Vec<f32> = s 1036 + .split(|c: char| c == ',' || c.is_ascii_whitespace()) 1037 + .filter(|p| !p.is_empty()) 1038 + .filter_map(|p| p.parse::<f32>().ok()) 1039 + .collect(); 1040 + if parts.len() == 4 { 1041 + Some((parts[0], parts[1], parts[2], parts[3])) 1042 + } else { 1043 + None 1044 + } 1045 + } 1046 + 1047 + /// Parse an SVG color string. 1048 + pub fn parse_svg_color(s: &str) -> Option<SvgColor> { 1049 + let s = s.trim(); 1050 + 1051 + // Named colors (common subset). 1052 + match s.to_ascii_lowercase().as_str() { 1053 + "black" => return Some(SvgColor::new(0, 0, 0, 255)), 1054 + "white" => return Some(SvgColor::new(255, 255, 255, 255)), 1055 + "red" => return Some(SvgColor::new(255, 0, 0, 255)), 1056 + "green" => return Some(SvgColor::new(0, 128, 0, 255)), 1057 + "blue" => return Some(SvgColor::new(0, 0, 255, 255)), 1058 + "yellow" => return Some(SvgColor::new(255, 255, 0, 255)), 1059 + "cyan" | "aqua" => return Some(SvgColor::new(0, 255, 255, 255)), 1060 + "magenta" | "fuchsia" => return Some(SvgColor::new(255, 0, 255, 255)), 1061 + "gray" | "grey" => return Some(SvgColor::new(128, 128, 128, 255)), 1062 + "silver" => return Some(SvgColor::new(192, 192, 192, 255)), 1063 + "maroon" => return Some(SvgColor::new(128, 0, 0, 255)), 1064 + "olive" => return Some(SvgColor::new(128, 128, 0, 255)), 1065 + "purple" => return Some(SvgColor::new(128, 0, 128, 255)), 1066 + "teal" => return Some(SvgColor::new(0, 128, 128, 255)), 1067 + "navy" => return Some(SvgColor::new(0, 0, 128, 255)), 1068 + "orange" => return Some(SvgColor::new(255, 165, 0, 255)), 1069 + "lime" => return Some(SvgColor::new(0, 255, 0, 255)), 1070 + "transparent" => return Some(SvgColor::transparent()), 1071 + _ => {} 1072 + } 1073 + 1074 + // #RGB or #RRGGBB 1075 + if let Some(hex) = s.strip_prefix('#') { 1076 + return parse_hex_color(hex); 1077 + } 1078 + 1079 + // rgb(r, g, b) 1080 + if let Some(inner) = s 1081 + .strip_prefix("rgb(") 1082 + .or_else(|| s.strip_prefix("RGB(")) 1083 + .and_then(|rest| rest.strip_suffix(')')) 1084 + { 1085 + let parts: Vec<&str> = inner.split(',').collect(); 1086 + if parts.len() == 3 { 1087 + let r = parts[0].trim().parse::<u8>().ok()?; 1088 + let g = parts[1].trim().parse::<u8>().ok()?; 1089 + let b = parts[2].trim().parse::<u8>().ok()?; 1090 + return Some(SvgColor::new(r, g, b, 255)); 1091 + } 1092 + } 1093 + 1094 + None 1095 + } 1096 + 1097 + fn parse_hex_color(hex: &str) -> Option<SvgColor> { 1098 + match hex.len() { 1099 + 3 => { 1100 + let r = u8::from_str_radix(&hex[0..1], 16).ok()?; 1101 + let g = u8::from_str_radix(&hex[1..2], 16).ok()?; 1102 + let b = u8::from_str_radix(&hex[2..3], 16).ok()?; 1103 + Some(SvgColor::new(r * 17, g * 17, b * 17, 255)) 1104 + } 1105 + 6 => { 1106 + let r = u8::from_str_radix(&hex[0..2], 16).ok()?; 1107 + let g = u8::from_str_radix(&hex[2..4], 16).ok()?; 1108 + let b = u8::from_str_radix(&hex[4..6], 16).ok()?; 1109 + Some(SvgColor::new(r, g, b, 255)) 1110 + } 1111 + _ => None, 1112 + } 1113 + } 1114 + 1115 + #[cfg(test)] 1116 + mod tests { 1117 + use super::*; 1118 + 1119 + #[test] 1120 + fn render_red_rect() { 1121 + let mut doc = Document::new(); 1122 + let root = doc.root(); 1123 + let svg = doc.create_element_ns("svg", Some("http://www.w3.org/2000/svg")); 1124 + doc.set_attribute(svg, "width", "100"); 1125 + doc.set_attribute(svg, "height", "100"); 1126 + doc.append_child(root, svg); 1127 + 1128 + let rect = doc.create_element_ns("rect", Some("http://www.w3.org/2000/svg")); 1129 + doc.set_attribute(rect, "width", "100"); 1130 + doc.set_attribute(rect, "height", "100"); 1131 + doc.set_attribute(rect, "fill", "red"); 1132 + doc.append_child(svg, rect); 1133 + 1134 + let result = render_svg(&doc, svg, None); 1135 + assert!(result.is_some()); 1136 + let (w, h, data) = result.unwrap(); 1137 + assert_eq!(w, 100); 1138 + assert_eq!(h, 100); 1139 + // Check center pixel is red (RGBA). 1140 + let center = ((50 * 100 + 50) * 4) as usize; 1141 + assert_eq!(data[center], 255); // R 1142 + assert_eq!(data[center + 1], 0); // G 1143 + assert_eq!(data[center + 2], 0); // B 1144 + assert_eq!(data[center + 3], 255); // A 1145 + } 1146 + 1147 + #[test] 1148 + fn render_blue_circle() { 1149 + let mut doc = Document::new(); 1150 + let root = doc.root(); 1151 + let svg = doc.create_element_ns("svg", Some("http://www.w3.org/2000/svg")); 1152 + doc.set_attribute(svg, "width", "100"); 1153 + doc.set_attribute(svg, "height", "100"); 1154 + doc.append_child(root, svg); 1155 + 1156 + let circle = doc.create_element_ns("circle", Some("http://www.w3.org/2000/svg")); 1157 + doc.set_attribute(circle, "cx", "50"); 1158 + doc.set_attribute(circle, "cy", "50"); 1159 + doc.set_attribute(circle, "r", "40"); 1160 + doc.set_attribute(circle, "fill", "blue"); 1161 + doc.append_child(svg, circle); 1162 + 1163 + let result = render_svg(&doc, svg, None); 1164 + assert!(result.is_some()); 1165 + let (w, h, data) = result.unwrap(); 1166 + assert_eq!(w, 100); 1167 + assert_eq!(h, 100); 1168 + // Check center pixel is blue. 1169 + let center = ((50 * 100 + 50) * 4) as usize; 1170 + assert_eq!(data[center], 0); // R 1171 + assert_eq!(data[center + 1], 0); // G 1172 + assert_eq!(data[center + 2], 255); // B 1173 + assert_eq!(data[center + 3], 255); // A 1174 + 1175 + // Check corner pixel is transparent (outside circle). 1176 + let corner = 0; 1177 + assert_eq!(data[corner + 3], 0); // A = 0 (transparent) 1178 + } 1179 + 1180 + #[test] 1181 + fn render_stroked_rect() { 1182 + let mut doc = Document::new(); 1183 + let root = doc.root(); 1184 + let svg = doc.create_element_ns("svg", Some("http://www.w3.org/2000/svg")); 1185 + doc.set_attribute(svg, "width", "100"); 1186 + doc.set_attribute(svg, "height", "100"); 1187 + doc.append_child(root, svg); 1188 + 1189 + let rect = doc.create_element_ns("rect", Some("http://www.w3.org/2000/svg")); 1190 + doc.set_attribute(rect, "x", "10"); 1191 + doc.set_attribute(rect, "y", "10"); 1192 + doc.set_attribute(rect, "width", "80"); 1193 + doc.set_attribute(rect, "height", "80"); 1194 + doc.set_attribute(rect, "fill", "none"); 1195 + doc.set_attribute(rect, "stroke", "green"); 1196 + doc.set_attribute(rect, "stroke-width", "4"); 1197 + doc.append_child(svg, rect); 1198 + 1199 + let result = render_svg(&doc, svg, None); 1200 + assert!(result.is_some()); 1201 + let (w, h, _data) = result.unwrap(); 1202 + assert_eq!(w, 100); 1203 + assert_eq!(h, 100); 1204 + } 1205 + 1206 + #[test] 1207 + fn render_path_triangle() { 1208 + let mut doc = Document::new(); 1209 + let root = doc.root(); 1210 + let svg = doc.create_element_ns("svg", Some("http://www.w3.org/2000/svg")); 1211 + doc.set_attribute(svg, "width", "100"); 1212 + doc.set_attribute(svg, "height", "100"); 1213 + doc.append_child(root, svg); 1214 + 1215 + let path = doc.create_element_ns("path", Some("http://www.w3.org/2000/svg")); 1216 + doc.set_attribute(path, "d", "M 50 10 L 90 90 L 10 90 Z"); 1217 + doc.set_attribute(path, "fill", "yellow"); 1218 + doc.append_child(svg, path); 1219 + 1220 + let result = render_svg(&doc, svg, None); 1221 + assert!(result.is_some()); 1222 + let (w, h, data) = result.unwrap(); 1223 + assert_eq!(w, 100); 1224 + assert_eq!(h, 100); 1225 + // Check a pixel inside the triangle (roughly center). 1226 + let px = ((60 * 100 + 50) * 4) as usize; 1227 + assert_eq!(data[px], 255); // R 1228 + assert_eq!(data[px + 1], 255); // G 1229 + assert_eq!(data[px + 2], 0); // B 1230 + assert_eq!(data[px + 3], 255); // A 1231 + } 1232 + 1233 + #[test] 1234 + fn viewbox_scaling() { 1235 + let mut doc = Document::new(); 1236 + let root = doc.root(); 1237 + let svg = doc.create_element_ns("svg", Some("http://www.w3.org/2000/svg")); 1238 + doc.set_attribute(svg, "width", "200"); 1239 + doc.set_attribute(svg, "height", "200"); 1240 + doc.set_attribute(svg, "viewBox", "0 0 100 100"); 1241 + doc.append_child(root, svg); 1242 + 1243 + let rect = doc.create_element_ns("rect", Some("http://www.w3.org/2000/svg")); 1244 + doc.set_attribute(rect, "width", "50"); 1245 + doc.set_attribute(rect, "height", "50"); 1246 + doc.set_attribute(rect, "fill", "red"); 1247 + doc.append_child(svg, rect); 1248 + 1249 + let result = render_svg(&doc, svg, None); 1250 + assert!(result.is_some()); 1251 + let (w, h, data) = result.unwrap(); 1252 + assert_eq!(w, 200); 1253 + assert_eq!(h, 200); 1254 + // With viewBox 0 0 100 100 mapped to 200x200, the 50x50 rect 1255 + // should fill 100x100 pixels. Check center of that area. 1256 + let px = ((50 * 200 + 50) * 4) as usize; 1257 + assert_eq!(data[px], 255); // R 1258 + assert_eq!(data[px + 3], 255); // A 1259 + // Check pixel at (150, 150) should be transparent (outside the rect). 1260 + let px2 = ((150 * 200 + 150) * 4) as usize; 1261 + assert_eq!(data[px2 + 3], 0); // A = 0 1262 + } 1263 + 1264 + #[test] 1265 + fn intrinsic_size() { 1266 + let mut doc = Document::new(); 1267 + let root = doc.root(); 1268 + let svg = doc.create_element_ns("svg", Some("http://www.w3.org/2000/svg")); 1269 + doc.set_attribute(svg, "width", "300"); 1270 + doc.set_attribute(svg, "height", "200"); 1271 + doc.append_child(root, svg); 1272 + 1273 + let size = svg_intrinsic_size(&doc, svg); 1274 + assert_eq!(size, Some((300.0, 200.0))); 1275 + } 1276 + 1277 + #[test] 1278 + fn intrinsic_size_from_viewbox() { 1279 + let mut doc = Document::new(); 1280 + let root = doc.root(); 1281 + let svg = doc.create_element_ns("svg", Some("http://www.w3.org/2000/svg")); 1282 + doc.set_attribute(svg, "viewBox", "0 0 400 300"); 1283 + doc.append_child(root, svg); 1284 + 1285 + let size = svg_intrinsic_size(&doc, svg); 1286 + assert_eq!(size, Some((400.0, 300.0))); 1287 + } 1288 + 1289 + #[test] 1290 + fn parse_colors() { 1291 + let red = parse_svg_color("red").unwrap(); 1292 + assert_eq!(red.r, 255); 1293 + assert_eq!(red.g, 0); 1294 + assert_eq!(red.b, 0); 1295 + 1296 + let hex = parse_svg_color("#ff8800").unwrap(); 1297 + assert_eq!(hex.r, 255); 1298 + assert_eq!(hex.g, 136); 1299 + assert_eq!(hex.b, 0); 1300 + 1301 + let short_hex = parse_svg_color("#f80").unwrap(); 1302 + assert_eq!(short_hex.r, 255); 1303 + assert_eq!(short_hex.g, 136); 1304 + assert_eq!(short_hex.b, 0); 1305 + 1306 + let rgb = parse_svg_color("rgb(10, 20, 30)").unwrap(); 1307 + assert_eq!(rgb.r, 10); 1308 + assert_eq!(rgb.g, 20); 1309 + assert_eq!(rgb.b, 30); 1310 + 1311 + assert!(parse_svg_color("none").is_none()); 1312 + } 1313 + 1314 + #[test] 1315 + fn transform_applied() { 1316 + let mut doc = Document::new(); 1317 + let root = doc.root(); 1318 + let svg = doc.create_element_ns("svg", Some("http://www.w3.org/2000/svg")); 1319 + doc.set_attribute(svg, "width", "100"); 1320 + doc.set_attribute(svg, "height", "100"); 1321 + doc.append_child(root, svg); 1322 + 1323 + let rect = doc.create_element_ns("rect", Some("http://www.w3.org/2000/svg")); 1324 + doc.set_attribute(rect, "width", "20"); 1325 + doc.set_attribute(rect, "height", "20"); 1326 + doc.set_attribute(rect, "fill", "red"); 1327 + doc.set_attribute(rect, "transform", "translate(40, 40)"); 1328 + doc.append_child(svg, rect); 1329 + 1330 + let result = render_svg(&doc, svg, None); 1331 + assert!(result.is_some()); 1332 + let (_w, _h, data) = result.unwrap(); 1333 + // The rect is at (40,40)-(60,60). Check center (50,50). 1334 + let px = ((50 * 100 + 50) * 4) as usize; 1335 + assert_eq!(data[px], 255); // R 1336 + assert_eq!(data[px + 3], 255); // A 1337 + // Check (10,10) should be transparent. 1338 + let px2 = ((10 * 100 + 10) * 4) as usize; 1339 + assert_eq!(data[px2 + 3], 0); 1340 + } 1341 + 1342 + #[test] 1343 + fn group_transform() { 1344 + let mut doc = Document::new(); 1345 + let root = doc.root(); 1346 + let svg = doc.create_element_ns("svg", Some("http://www.w3.org/2000/svg")); 1347 + doc.set_attribute(svg, "width", "100"); 1348 + doc.set_attribute(svg, "height", "100"); 1349 + doc.append_child(root, svg); 1350 + 1351 + let g = doc.create_element_ns("g", Some("http://www.w3.org/2000/svg")); 1352 + doc.set_attribute(g, "transform", "translate(30, 30)"); 1353 + doc.append_child(svg, g); 1354 + 1355 + let rect = doc.create_element_ns("rect", Some("http://www.w3.org/2000/svg")); 1356 + doc.set_attribute(rect, "width", "20"); 1357 + doc.set_attribute(rect, "height", "20"); 1358 + doc.set_attribute(rect, "fill", "blue"); 1359 + doc.append_child(g, rect); 1360 + 1361 + let result = render_svg(&doc, svg, None); 1362 + assert!(result.is_some()); 1363 + let (_w, _h, data) = result.unwrap(); 1364 + // Rect should be at (30,30)-(50,50). Check (40,40). 1365 + let px = ((40 * 100 + 40) * 4) as usize; 1366 + assert_eq!(data[px], 0); // R 1367 + assert_eq!(data[px + 1], 0); // G 1368 + assert_eq!(data[px + 2], 255); // B 1369 + assert_eq!(data[px + 3], 255); // A 1370 + } 1371 + }
+325
crates/svg/src/transform.rs
··· 1 + //! SVG transform parsing and 2D affine matrix operations. 2 + 3 + /// A 2D affine transform represented as a 3x2 matrix: 4 + /// ```text 5 + /// | a c e | 6 + /// | b d f | 7 + /// | 0 0 1 | 8 + /// ``` 9 + #[derive(Debug, Clone, Copy, PartialEq)] 10 + pub struct Transform { 11 + pub a: f32, 12 + pub b: f32, 13 + pub c: f32, 14 + pub d: f32, 15 + pub e: f32, 16 + pub f: f32, 17 + } 18 + 19 + impl Transform { 20 + /// Identity transform. 21 + pub fn identity() -> Self { 22 + Transform { 23 + a: 1.0, 24 + b: 0.0, 25 + c: 0.0, 26 + d: 1.0, 27 + e: 0.0, 28 + f: 0.0, 29 + } 30 + } 31 + 32 + /// Translation transform. 33 + pub fn translate(tx: f32, ty: f32) -> Self { 34 + Transform { 35 + a: 1.0, 36 + b: 0.0, 37 + c: 0.0, 38 + d: 1.0, 39 + e: tx, 40 + f: ty, 41 + } 42 + } 43 + 44 + /// Scale transform. 45 + pub fn scale(sx: f32, sy: f32) -> Self { 46 + Transform { 47 + a: sx, 48 + b: 0.0, 49 + c: 0.0, 50 + d: sy, 51 + e: 0.0, 52 + f: 0.0, 53 + } 54 + } 55 + 56 + /// Rotation transform (angle in degrees). 57 + pub fn rotate(angle_deg: f32) -> Self { 58 + let rad = angle_deg * std::f32::consts::PI / 180.0; 59 + let cos = rad.cos(); 60 + let sin = rad.sin(); 61 + Transform { 62 + a: cos, 63 + b: sin, 64 + c: -sin, 65 + d: cos, 66 + e: 0.0, 67 + f: 0.0, 68 + } 69 + } 70 + 71 + /// General matrix transform. 72 + pub fn matrix(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Self { 73 + Transform { a, b, c, d, e, f } 74 + } 75 + 76 + /// Multiply this transform by another: self * other. 77 + pub fn multiply(&self, other: &Transform) -> Transform { 78 + Transform { 79 + a: self.a * other.a + self.c * other.b, 80 + b: self.b * other.a + self.d * other.b, 81 + c: self.a * other.c + self.c * other.d, 82 + d: self.b * other.c + self.d * other.d, 83 + e: self.a * other.e + self.c * other.f + self.e, 84 + f: self.b * other.e + self.d * other.f + self.f, 85 + } 86 + } 87 + 88 + /// Apply this transform to a point. 89 + pub fn apply(&self, x: f32, y: f32) -> (f32, f32) { 90 + ( 91 + self.a * x + self.c * y + self.e, 92 + self.b * x + self.d * y + self.f, 93 + ) 94 + } 95 + } 96 + 97 + /// Parse an SVG `transform` attribute value into a combined transform. 98 + pub fn parse_transform(input: &str) -> Transform { 99 + let mut result = Transform::identity(); 100 + let mut pos = 0; 101 + let bytes = input.as_bytes(); 102 + 103 + while pos < bytes.len() { 104 + skip_ws(bytes, &mut pos); 105 + if pos >= bytes.len() { 106 + break; 107 + } 108 + 109 + let start = pos; 110 + while pos < bytes.len() && bytes[pos].is_ascii_alphabetic() { 111 + pos += 1; 112 + } 113 + let name = &input[start..pos]; 114 + 115 + skip_ws(bytes, &mut pos); 116 + if pos >= bytes.len() || bytes[pos] != b'(' { 117 + break; 118 + } 119 + pos += 1; // skip '(' 120 + 121 + let mut args = Vec::new(); 122 + loop { 123 + skip_ws_and_commas(bytes, &mut pos); 124 + if pos >= bytes.len() || bytes[pos] == b')' { 125 + break; 126 + } 127 + if let Some(n) = parse_number(bytes, &mut pos) { 128 + args.push(n); 129 + } else { 130 + break; 131 + } 132 + } 133 + 134 + if pos < bytes.len() && bytes[pos] == b')' { 135 + pos += 1; 136 + } 137 + 138 + let t = match name { 139 + "translate" => { 140 + let tx = args.first().copied().unwrap_or(0.0); 141 + let ty = args.get(1).copied().unwrap_or(0.0); 142 + Transform::translate(tx, ty) 143 + } 144 + "scale" => { 145 + let sx = args.first().copied().unwrap_or(1.0); 146 + let sy = args.get(1).copied().unwrap_or(sx); 147 + Transform::scale(sx, sy) 148 + } 149 + "rotate" => { 150 + let angle = args.first().copied().unwrap_or(0.0); 151 + if args.len() >= 3 { 152 + let cx = args[1]; 153 + let cy = args[2]; 154 + Transform::translate(cx, cy) 155 + .multiply(&Transform::rotate(angle)) 156 + .multiply(&Transform::translate(-cx, -cy)) 157 + } else { 158 + Transform::rotate(angle) 159 + } 160 + } 161 + "matrix" if args.len() >= 6 => { 162 + Transform::matrix(args[0], args[1], args[2], args[3], args[4], args[5]) 163 + } 164 + "skewX" => { 165 + let angle = args.first().copied().unwrap_or(0.0); 166 + let rad = angle * std::f32::consts::PI / 180.0; 167 + Transform { 168 + a: 1.0, 169 + b: 0.0, 170 + c: rad.tan(), 171 + d: 1.0, 172 + e: 0.0, 173 + f: 0.0, 174 + } 175 + } 176 + "skewY" => { 177 + let angle = args.first().copied().unwrap_or(0.0); 178 + let rad = angle * std::f32::consts::PI / 180.0; 179 + Transform { 180 + a: 1.0, 181 + b: rad.tan(), 182 + c: 0.0, 183 + d: 1.0, 184 + e: 0.0, 185 + f: 0.0, 186 + } 187 + } 188 + _ => Transform::identity(), 189 + }; 190 + 191 + result = result.multiply(&t); 192 + 193 + // Skip optional whitespace and commas between transforms. 194 + skip_ws_and_commas(bytes, &mut pos); 195 + } 196 + 197 + result 198 + } 199 + 200 + fn skip_ws(bytes: &[u8], pos: &mut usize) { 201 + while *pos < bytes.len() && bytes[*pos].is_ascii_whitespace() { 202 + *pos += 1; 203 + } 204 + } 205 + 206 + fn skip_ws_and_commas(bytes: &[u8], pos: &mut usize) { 207 + while *pos < bytes.len() && (bytes[*pos].is_ascii_whitespace() || bytes[*pos] == b',') { 208 + *pos += 1; 209 + } 210 + } 211 + 212 + fn parse_number(bytes: &[u8], pos: &mut usize) -> Option<f32> { 213 + skip_ws_and_commas(bytes, pos); 214 + let start = *pos; 215 + 216 + if *pos < bytes.len() && (bytes[*pos] == b'+' || bytes[*pos] == b'-') { 217 + *pos += 1; 218 + } 219 + 220 + let mut has_digits = false; 221 + while *pos < bytes.len() && bytes[*pos].is_ascii_digit() { 222 + *pos += 1; 223 + has_digits = true; 224 + } 225 + if *pos < bytes.len() && bytes[*pos] == b'.' { 226 + *pos += 1; 227 + while *pos < bytes.len() && bytes[*pos].is_ascii_digit() { 228 + *pos += 1; 229 + has_digits = true; 230 + } 231 + } 232 + if !has_digits { 233 + *pos = start; 234 + return None; 235 + } 236 + if *pos < bytes.len() && (bytes[*pos] == b'e' || bytes[*pos] == b'E') { 237 + *pos += 1; 238 + if *pos < bytes.len() && (bytes[*pos] == b'+' || bytes[*pos] == b'-') { 239 + *pos += 1; 240 + } 241 + while *pos < bytes.len() && bytes[*pos].is_ascii_digit() { 242 + *pos += 1; 243 + } 244 + } 245 + 246 + let s = std::str::from_utf8(&bytes[start..*pos]).ok()?; 247 + s.parse::<f32>().ok() 248 + } 249 + 250 + #[cfg(test)] 251 + mod tests { 252 + use super::*; 253 + 254 + #[test] 255 + fn identity() { 256 + let t = Transform::identity(); 257 + let (x, y) = t.apply(10.0, 20.0); 258 + assert!((x - 10.0).abs() < 0.001); 259 + assert!((y - 20.0).abs() < 0.001); 260 + } 261 + 262 + #[test] 263 + fn translate() { 264 + let t = Transform::translate(5.0, 10.0); 265 + let (x, y) = t.apply(1.0, 2.0); 266 + assert!((x - 6.0).abs() < 0.001); 267 + assert!((y - 12.0).abs() < 0.001); 268 + } 269 + 270 + #[test] 271 + fn scale() { 272 + let t = Transform::scale(2.0, 3.0); 273 + let (x, y) = t.apply(5.0, 10.0); 274 + assert!((x - 10.0).abs() < 0.001); 275 + assert!((y - 30.0).abs() < 0.001); 276 + } 277 + 278 + #[test] 279 + fn rotate_90() { 280 + let t = Transform::rotate(90.0); 281 + let (x, y) = t.apply(1.0, 0.0); 282 + assert!(x.abs() < 0.001); 283 + assert!((y - 1.0).abs() < 0.001); 284 + } 285 + 286 + #[test] 287 + fn parse_translate() { 288 + let t = parse_transform("translate(10, 20)"); 289 + let (x, y) = t.apply(0.0, 0.0); 290 + assert!((x - 10.0).abs() < 0.001); 291 + assert!((y - 20.0).abs() < 0.001); 292 + } 293 + 294 + #[test] 295 + fn parse_scale() { 296 + let t = parse_transform("scale(2)"); 297 + let (x, y) = t.apply(5.0, 5.0); 298 + assert!((x - 10.0).abs() < 0.001); 299 + assert!((y - 10.0).abs() < 0.001); 300 + } 301 + 302 + #[test] 303 + fn parse_combined() { 304 + let t = parse_transform("translate(10, 0) scale(2)"); 305 + let (x, y) = t.apply(5.0, 5.0); 306 + assert!((x - 20.0).abs() < 0.001); 307 + assert!((y - 10.0).abs() < 0.001); 308 + } 309 + 310 + #[test] 311 + fn parse_matrix() { 312 + let t = parse_transform("matrix(1 0 0 1 50 50)"); 313 + let (x, y) = t.apply(0.0, 0.0); 314 + assert!((x - 50.0).abs() < 0.001); 315 + assert!((y - 50.0).abs() < 0.001); 316 + } 317 + 318 + #[test] 319 + fn parse_rotate_around_point() { 320 + let t = parse_transform("rotate(90, 50, 50)"); 321 + let (x, y) = t.apply(50.0, 0.0); 322 + assert!((x - 100.0).abs() < 0.01); 323 + assert!((y - 50.0).abs() < 0.01); 324 + } 325 + }