strandify/
blueprint.rs

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)]
23/// A string art [`Blueprint`]. Holds the result of the [`crate::pather::Pather`]'s pathing algorithm and renders it to file.
24pub struct Blueprint {
25    /// The order with which to connect the [`Pegs`](Peg).
26    pub peg_order: Vec<Peg>,
27    /// Width of the [`Blueprint`], should be the same dimensions as the image used.
28    pub width: u32,
29    /// Height of the [`Blueprint`], should be the same dimensions as the image used.
30    pub height: u32,
31    /// Background color, if None no background is added. It will be transparent for svg and alpha
32    /// compatible image formats.
33    pub background: Option<(u8, u8, u8)>,
34    /// Render scale, how much to up/down scale the render.
35    pub render_scale: f64,
36    /// Display progress bar.
37    #[serde(skip)]
38    pub progress_bar: bool,
39}
40
41impl Blueprint {
42    /// Creates a new [`Blueprint`].
43    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    /// Create a [`Blueprint`] from [`Peg`] references.
62    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    /// Read a [`Blueprint`] from a json file.
81    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    /// Write a [`Blueprint`] to a json file.
89    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    /// Iterate over successive pairs of [`Pegs`](Peg).
96    ///
97    /// # Examples
98    ///
99    ///```
100    /// use strandify::blueprint::Blueprint;
101    /// use strandify::peg::Peg;
102    /// let bp = Blueprint::new(vec![Peg::new(0, 0), Peg::new(3, 3)], 4, 4, Some((255, 255, 255)), 1., false);
103    /// for (peg_a, peg_b) in bp.zip() {
104    ///     assert_eq!(peg_a.id, 0);
105    ///     assert_eq!(peg_b.id, 1);
106    /// }
107    /// assert_eq!(bp.zip().len(), 1);
108    ///```
109    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    /// Render the [`Blueprint`] as a raster image.
116    ///
117    /// # Arguments
118    ///
119    /// * `yarn`: The [`Yarn`] to use to render the [`Blueprint`].
120    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        // divide the height into chunks to be processed in parallel
129        #[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        // render each chunk in parallel
140        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        // create the final image buffer
156        let mut final_pixmap = tiny_skia::Pixmap::new(render_width, render_height).unwrap();
157
158        // combine the chunks back into the final image
159        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        // convert the final pixmap to an image::RgbaImage
172        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    /// Render the [`Blueprint`] as a svg.
180    ///
181    /// # Arguments
182    ///
183    /// * `yarn`: The [`Yarn`] to use to render the [`Blueprint`].
184    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    /// Render the [`Blueprint`].
231    ///
232    /// # Arguments:
233    ///
234    /// * `path`: Output file path, image format or svg.
235    /// * `yarn`: The [`Yarn`] to use to render the [`Blueprint`].
236    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                // drop alpha channel
246                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}