1use image::{Rgba, RgbaImage};
2
3type Point = (i32, i32);
4type Edge = (Point, Point);
5
6pub 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
21pub 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 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
204fn 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}