1use image::DynamicImage;
2use log::info;
3
4#[cfg(feature = "parallel")]
5use rayon::prelude::*;
6use resvg::render;
7use resvg::tiny_skia;
8use resvg::usvg;
9use serde::{Deserialize, Serialize};
10use std::error::Error;
11use std::fs::File;
12use std::io::BufReader;
13use std::path::Path;
14use std::time::Duration;
15use svg::node::element::path::Data;
16use svg::node::element::{Path as PathSVG, Rectangle};
17use svg::{Document, Node};
18
19use crate::peg::{Peg, Yarn};
20use crate::utils;
21
22#[derive(Debug, Serialize, Deserialize)]
23pub struct Blueprint {
25 pub peg_order: Vec<Peg>,
27 pub width: u32,
29 pub height: u32,
31 pub background: Option<(u8, u8, u8)>,
34 pub render_scale: f64,
36 #[serde(skip)]
38 pub progress_bar: bool,
39}
40
41impl Blueprint {
42 pub fn new(
44 peg_order: Vec<Peg>,
45 width: u32,
46 height: u32,
47 background: Option<(u8, u8, u8)>,
48 render_scale: f64,
49 progress_bar: bool,
50 ) -> Self {
51 Self {
52 peg_order,
53 width,
54 height,
55 background,
56 render_scale,
57 progress_bar,
58 }
59 }
60
61 pub fn from_refs(
63 peg_order: Vec<&Peg>,
64 width: u32,
65 height: u32,
66 background: Option<(u8, u8, u8)>,
67 render_scale: f64,
68 progress_bar: bool,
69 ) -> Self {
70 Self {
71 peg_order: peg_order.into_iter().copied().collect(),
72 width,
73 height,
74 background,
75 render_scale,
76 progress_bar,
77 }
78 }
79
80 pub fn from_file<P: AsRef<Path>>(file_path: P) -> Result<Self, Box<dyn Error>> {
82 let reader = BufReader::new(File::open(file_path)?);
83 let out: Self = serde_json::from_reader(reader)?;
84
85 Ok(out)
86 }
87
88 pub fn to_file<P: AsRef<Path>>(&self, file_path: P) -> Result<(), Box<dyn Error>> {
90 let file = File::create(file_path)?;
91 serde_json::to_writer(&file, &self)?;
92 Ok(())
93 }
94
95 pub fn zip(
110 &self,
111 ) -> std::iter::Zip<std::slice::Iter<Peg>, std::iter::Skip<std::slice::Iter<Peg>>> {
112 self.peg_order.iter().zip(self.peg_order.iter().skip(1))
113 }
114
115 pub fn render_img(&self, yarn: &Yarn) -> Result<image::RgbaImage, Box<dyn Error>> {
121 let document = self.render_svg(yarn)?;
122 let svg_data = document.to_string();
123 let svg_tree = usvg::Tree::from_str(&svg_data, &usvg::Options::default())?;
124
125 let render_width = (self.width as f64 * self.render_scale).round() as u32;
126 let render_height = (self.height as f64 * self.render_scale).round() as u32;
127
128 #[cfg(feature = "parallel")]
130 let num_chunks = rayon::current_num_threads();
131 #[cfg(not(feature = "parallel"))]
132 let num_chunks = 1;
133
134 let chunk_height = (render_height + num_chunks as u32 - 1) / num_chunks as u32;
135
136 let pbar = utils::spinner(!self.progress_bar).with_message("Rendering image");
137 pbar.enable_steady_tick(Duration::from_millis(100));
138
139 let chunks: Vec<tiny_skia::Pixmap> = utils::iter_or_par_iter!(0..num_chunks, into)
141 .map(|i| {
142 let start_y = i as u32 * chunk_height;
143 let end_y = ((i + 1) as u32 * chunk_height).min(render_height);
144
145 let mut pixmap = tiny_skia::Pixmap::new(render_width, end_y - start_y).unwrap();
146
147 let transform = tiny_skia::Transform::from_translate(0.0, -(start_y as f32));
148 render(&svg_tree, transform, &mut pixmap.as_mut());
149 pixmap
150 })
151 .collect();
152
153 pbar.finish_and_clear();
154
155 let mut final_pixmap = tiny_skia::Pixmap::new(render_width, render_height).unwrap();
157
158 for (i, pixmap) in chunks.into_iter().enumerate() {
160 let start_y = i as u32 * chunk_height;
161 final_pixmap.draw_pixmap(
162 0,
163 start_y as i32,
164 pixmap.as_ref(),
165 &tiny_skia::PixmapPaint::default(),
166 tiny_skia::Transform::identity(),
167 None,
168 );
169 }
170
171 let img =
173 image::ImageBuffer::from_vec(render_width, render_height, final_pixmap.data().to_vec())
174 .unwrap();
175
176 Ok(img)
177 }
178
179 pub fn render_svg(&self, yarn: &Yarn) -> Result<Document, Box<dyn Error>> {
185 let (r, g, b) = yarn.color;
186 let render_width = (self.width as f64 * self.render_scale).round() as u32;
187 let render_height = (self.height as f64 * self.render_scale).round() as u32;
188 info!("Render resolution: {render_width}x{render_height}");
189
190 let mut document = Document::new()
191 .set("viewbox", (0, 0, render_width, render_height))
192 .set("width", render_width)
193 .set("height", render_height);
194
195 if let Some((bg_r, bg_g, bg_b)) = self.background {
196 let background = Rectangle::new()
197 .set("x", 0)
198 .set("y", 0)
199 .set("width", "100%")
200 .set("height", "100%")
201 .set("fill", format!("rgb({bg_r}, {bg_g}, {bg_b})"));
202 document.append(background);
203 }
204
205 let pbar = utils::pbar(self.peg_order.len() as u64 - 1, !self.progress_bar)?
206 .with_message("Rendering svg");
207
208 for (peg_a, peg_b) in pbar.wrap_iter(self.zip()) {
209 let data = Data::new()
210 .move_to((
211 (peg_a.x as f64 * self.render_scale) as u32,
212 (peg_a.y as f64 * self.render_scale) as u32,
213 ))
214 .line_to((
215 (peg_b.x as f64 * self.render_scale) as u32,
216 (peg_b.y as f64 * self.render_scale) as u32,
217 ));
218 let path = PathSVG::new()
219 .set("fill", "none")
220 .set("stroke", format!("rgb({r}, {g}, {b})"))
221 .set("stroke-width", yarn.width)
222 .set("opacity", yarn.opacity)
223 .set("stroke-linecap", "round")
224 .set("d", data);
225 document.append(path);
226 }
227 Ok(document)
228 }
229
230 pub fn render<P: AsRef<Path>>(&self, path: P, yarn: &Yarn) -> Result<(), Box<dyn Error>> {
237 let path = path.as_ref();
238 let extension = path.extension().ok_or("Could not detemine extension.")?;
239 if extension == "svg" {
240 let svg_img = self.render_svg(yarn)?;
241 svg::save(path, &svg_img)?;
242 } else {
243 let img = self.render_img(yarn)?;
244 if path.extension().unwrap() != "png" {
245 let out = DynamicImage::from(img).to_rgb8();
247 out.save(path)?;
248 } else {
249 img.save(path)?;
250 }
251 }
252
253 Ok(())
254 }
255}
256
257#[cfg(test)]
258mod test {
259 use super::*;
260 use std::fs;
261 use std::path::PathBuf;
262
263 static TEST_DIR: &str = "./test_blueprint/";
264
265 #[cfg(test)]
266 #[ctor::ctor]
267 fn setup() {
268 let test_dir = PathBuf::from(TEST_DIR);
269 if !test_dir.is_dir() {
270 fs::create_dir(test_dir).unwrap();
271 }
272 }
273
274 #[cfg(test)]
275 #[ctor::dtor]
276 fn teardown() {
277 let test_dir = PathBuf::from(TEST_DIR);
278 if test_dir.is_dir() {
279 fs::remove_dir_all(&test_dir).unwrap();
280 }
281 }
282
283 #[test]
284 fn blueprint_to_from_file() {
285 let bp = Blueprint::new(
286 vec![Peg::new(0, 0), Peg::new(63, 63)],
287 64,
288 64,
289 Some((0, 0, 0)),
290 1.,
291 true,
292 );
293 let bp_file = PathBuf::from(TEST_DIR).join("bp.json");
294 assert!(bp.to_file(&bp_file).is_ok());
295
296 let bp_read = Blueprint::from_file(&bp_file).unwrap();
297 assert_eq!(bp.height, bp_read.height);
298 assert_eq!(bp.width, bp_read.width);
299 for (peg_a, peg_b) in bp.peg_order.iter().zip(&bp_read.peg_order) {
300 assert_eq!(peg_a.id, peg_b.id);
301 assert_eq!(peg_a.x, peg_b.x);
302 assert_eq!(peg_a.y, peg_b.y);
303 }
304 }
305
306 #[test]
307 fn zip() {
308 let bp = Blueprint::new(
309 vec![Peg::new(0, 0), Peg::new(63, 63)],
310 64,
311 64,
312 Some((255, 255, 255)),
313 1.,
314 true,
315 );
316 assert_eq!(bp.zip().len(), 1);
317 }
318}