Skip to main content

pngtosvg/
lib.rs

1use image::{Rgba, RgbaImage};
2
3type Point = (i32, i32);
4type Edge = (Point, Point);
5
6/// Reads a file from `path`, converts it to an RGBA image, and then
7/// converts it to an SVG string using the `rgba_image_to_svg_contiguous` function.
8///
9/// # Arguments
10///
11/// * `path` - A path to the image file to convert.
12///
13/// # Returns
14///
15/// * `Result<String, Box<dyn std::error::Error>>` - The SVG string on success, or an error on failure.
16pub fn convert_file_to_svg(path: &std::path::Path) -> Result<String, Box<dyn std::error::Error>> {
17    let img = image::open(path)?.to_rgba8();
18    Ok(rgba_image_to_svg_contiguous(&img))
19}
20
21/// This function processes the image to find contiguous regions of the same color
22/// and generates SVG paths for them.
23///
24/// # Arguments
25///
26/// * `img` - A reference to an `RgbaImage` to convert.
27///
28/// # Returns
29///
30/// * `String` - A string containing the SVG representation of the image.
31pub fn rgba_image_to_svg_contiguous(img: &RgbaImage) -> String {
32    let width = img.width();
33    let height = img.height();
34    
35    let raw = img.as_raw();
36    let get_rgba = |x: u32, y: u32| -> [u8; 4] {
37        let i = ((y * width + x) * 4) as usize;
38        [raw[i], raw[i+1], raw[i+2], raw[i+3]]
39    };
40
41    let mut visited = vec![false; (width * height) as usize];
42    
43    let mut svg = String::with_capacity((width * height * 5) as usize);
44    svg.push_str(&svg_header(width, height));
45
46    let edges_offsets = [
47        ((-1, 0), ((0, 0), (0, 1))),
48        ((0, 1), ((0, 1), (1, 1))),
49        ((1, 0), ((1, 1), (1, 0))),
50        ((0, -1), ((1, 0), (0, 0))),
51    ];
52
53    use std::fmt::Write;
54
55    let mut queue = Vec::new();
56    let mut current_edges = Vec::new();
57    let mut used = Vec::new();
58    let mut piece = Vec::new();
59
60    for y in 0..height {
61        for x in 0..width {
62            let idx = (y * width + x) as usize;
63            if visited[idx] {
64                continue;
65            }
66            
67            let rgba = get_rgba(x, y);
68            if rgba[3] == 0 {
69                visited[idx] = true;
70                continue;
71            }
72
73            queue.clear();
74            queue.push((x as i32, y as i32));
75            visited[idx] = true;
76
77            current_edges.clear();
78
79            while let Some(here) = queue.pop() {
80                for &(offset, (start_offset, end_offset)) in &edges_offsets {
81                    let nx = here.0 + offset.0;
82                    let ny = here.1 + offset.1;
83                    
84                    let is_boundary;
85                    
86                    if nx < 0 || nx >= width as i32 || ny < 0 || ny >= height as i32 {
87                        is_boundary = true;
88                    } else {
89                        let nx_u = nx as u32;
90                        let ny_u = ny as u32;
91                        
92                        if get_rgba(nx_u, ny_u) != rgba {
93                            is_boundary = true;
94                        } else {
95                            is_boundary = false;
96                            let n_idx = (ny_u * width + nx_u) as usize;
97                            if !visited[n_idx] {
98                                visited[n_idx] = true;
99                                queue.push((nx, ny));
100                            }
101                        }
102                    }
103
104                    if is_boundary {
105                        let start = (here.0 + start_offset.0, here.1 + start_offset.1);
106                        let end = (here.0 + end_offset.0, here.1 + end_offset.1);
107                        current_edges.push((start, end));
108                    }
109                }
110            }
111
112            // Immediately convert current_edges to shapes and push to SVG
113            if current_edges.is_empty() {
114                continue;
115            }
116
117            current_edges.sort_unstable();
118            
119            used.clear();
120            used.resize(current_edges.len(), false);
121            
122            let opacity = rgba[3] as f32 / 255.0;
123            let directions = [(0, 1), (1, 0), (0, -1), (-1, 0)];
124
125            let mut has_started_path = false;
126
127            for i in 0..current_edges.len() {
128                if used[i] {
129                    continue;
130                }
131                used[i] = true;
132                let first_edge = current_edges[i];
133                
134                piece.clear();
135                piece.push(first_edge.0);
136                piece.push(first_edge.1);
137
138                loop {
139                    let last_point = *piece.last().unwrap();
140                    let mut found = false;
141
142                    for &direction in &directions {
143                        let next_point = (last_point.0 + direction.0, last_point.1 + direction.1);
144                        let next_edge = (last_point, next_point);
145
146                        if let Ok(idx) = current_edges.binary_search(&next_edge) {
147                            if !used[idx] {
148                                used[idx] = true;
149                                
150                                if piece.len() >= 2 {
151                                    let prev_direction = (
152                                        piece[piece.len() - 1].0 - piece[piece.len() - 2].0,
153                                        piece[piece.len() - 1].1 - piece[piece.len() - 2].1,
154                                    );
155                                    if prev_direction == direction {
156                                        piece.pop();
157                                    }
158                                }
159                                piece.push(next_point);
160                                found = true;
161                                break;
162                            }
163                        }
164                    }
165
166                    if !found || piece.first() == piece.last() {
167                        break;
168                    }
169                }
170
171                if piece.first() == piece.last() {
172                    piece.pop();
173                }
174
175                if !piece.is_empty() {
176                    if !has_started_path {
177                        svg.push_str(r#" <path d=""#);
178                        has_started_path = true;
179                    }
180                    if let Some(&start) = piece.first() {
181                        let _ = write!(svg, " M {},{}", start.0, start.1);
182                        for point in piece.iter().skip(1) {
183                            let _ = write!(svg, " L {},{}", point.0, point.1);
184                        }
185                        svg.push_str(" Z");
186                    }
187                }
188            }
189
190            if has_started_path {
191                let _ = write!(
192                    svg,
193                    r#"" style="fill:rgb({},{},{}); fill-opacity:{}; stroke:none;" />"#,
194                    rgba[0], rgba[1], rgba[2], opacity
195                );
196            }
197        }
198    }
199
200    svg.push_str("</svg>\n");
201    svg
202}
203
204/// Generates the standard SVG header.
205fn svg_header(width: u32, height: u32) -> String {
206    format!(
207        r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?>
208<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" 
209  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
210<svg width="{}" height="{}"
211     xmlns="http://www.w3.org/2000/svg" version="1.1">
212"#,
213        width, height
214    )
215}