From 9a1837896eaa86c29c279bf5f7e3ff569c4397db Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Fri, 26 Jun 2020 09:22:45 -0700 Subject: [PATCH] Add ability to generate SVG image based on image. --- Cargo.lock | 237 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 + src/lib.rs | 75 +++++++++++++++++ src/main.rs | 47 ++++++++++- 4 files changed, 362 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index f394fee..1481c8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,32 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d" +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.0" @@ -36,6 +62,21 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "clap" +version = "2.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "color_quant" version = "1.0.1" @@ -125,6 +166,15 @@ dependencies = [ "lzw", ] +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.14" @@ -268,9 +318,13 @@ dependencies = [ name = "perler" version = "0.1.0" dependencies = [ + "anyhow", "image", "lazy_static", "log", + "structopt", + "svg", + "thiserror", ] [[package]] @@ -285,6 +339,50 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "proc-macro-error" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc175e9777c3116627248584e8f8b3e2987405cabe1c0adf7d1dd28f09dc7880" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cc9795ca17eb581285ec44936da7fc2335a3f34f2ddd13118b6f4d515435c50" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "syn-mid", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rayon" version = "1.3.1" @@ -322,6 +420,93 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2f5e239ee807089b62adce73e48c625e0ed80df02c7ab3f068f5db5281065c" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510413f9de616762a4fbeab62509bf15c729603b72d7cd71280fbca431b1c118" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "svg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b65a64d32a41db2a8081aa03c1ccca26f246ff681add693f8b01307b137da79" + +[[package]] +name = "syn" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tiff" version = "0.5.0" @@ -332,3 +517,55 @@ dependencies = [ "lzw", "miniz_oxide", ] + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] +name = "unicode-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index d98977d..3c9a072 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,10 @@ edition = "2018" [dependencies] image = "0.23.6" log = "0.4.8" +svg = "0.8.0" +structopt = "0.3.15" +thiserror = "1.0.20" +anyhow = "1.0.31" [dev-dependencies] lazy_static = "1.4.0" diff --git a/src/lib.rs b/src/lib.rs index efaee83..46f69bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,80 @@ use image::{DynamicImage, GenericImage, GenericImageView}; use log::debug; +use svg::node::element::Group; +use svg::node::element::{Circle, Rectangle}; +use svg::node::Node; +use svg::Document; +use thiserror::Error; + +const GRID_SIZE: (usize, usize) = (29, 29); + +#[derive(Error, Debug)] +pub enum PerlerError { + #[error("source image too big {image_size:?} > {max_size:?}")] + TooBig { + image_size: (usize, usize), + max_size: (usize, usize), + }, +} + +pub fn svgify(input: &DynamicImage) -> Result { + let dim = input.dimensions(); + let (w, h) = (dim.0 as usize, dim.1 as usize); + if w > GRID_SIZE.0 || h > GRID_SIZE.1 { + return Err(PerlerError::TooBig { + image_size: (w, h), + max_size: GRID_SIZE, + }); + } + let box_offset = (5, 5); + let mut grp = Group::new(); + for y in 0..GRID_SIZE.0 { + for x in 0..GRID_SIZE.1 { + let x_pos = box_offset.0 + 5 + x * 5; + let y_pos = box_offset.1 + 5 + y * 5; + let c = Circle::new() + .set("stroke", "black") + .set("stroke-width", 0.1) + .set("cx", x_pos) + .set("cy", y_pos); + + let c = if x < w && y < h { + let p = input.get_pixel(x as u32, y as u32); + let [r, g, b, a] = p.0; + if a == 255 { + let color = format!("#{:02x}{:02x}{:02x}", r, g, b); + c.set("fill", color).set("r", 2.4) + } else { + c.set("fill", "none").set("r", 0.5) + } + } else { + c.set("fill", "none").set("r", 0.5) + }; + + grp = grp.add(c); + } + } + let board = Rectangle::new() + .set("fill", "none") + .set("width", 150) + .set("height", 150) + .set("stroke", "black") + .set("stroke-width", 0.1) + .set("x", box_offset.0) + .set("y", box_offset.1) + .set("rx", 1) + .set("ry", 1); + Ok(Document::new() + .set("width", "100%") + .set("height", "100%") + .set( + "viewBox", + (0, 0, (3 + GRID_SIZE.0) * 5, (3 + GRID_SIZE.1) * 5), + ) + //.set("viewBox", (0, 0, 215.9, 279.4)) + .add(board) + .add(grp)) +} pub fn simplify(input: &DynamicImage) -> DynamicImage { let (w, h) = input.dimensions(); diff --git a/src/main.rs b/src/main.rs index f328e4d..8c10256 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1 +1,46 @@ -fn main() {} +use std::fs::File; +use std::io; +use std::io::Write; +use std::path::PathBuf; + +use anyhow::Result; +//use image::imageops::colorops::{index_colors, BiLevel}; +//use image::math::nq::NeuQuant; +//use image::DynamicImage; +use structopt::StructOpt; + +use perler::{simplify, svgify}; + +/// Convert image to SVG in the style of perler instructions. +#[derive(StructOpt, Debug)] +#[structopt(name = "perler")] +struct Opt { + /// Input image file. + input: PathBuf, + /// Optional output .svg file. Default prints to stdout. + output: Option, +} + +fn main() -> Result<()> { + let opt = Opt::from_args(); + let img = image::open(opt.input)?; + /* + let mut img2 = img.to_rgba(); + let raw = img.into_rgba().into_raw(); + + let cmap = NeuQuant::new(1, 256, &raw); + index_colors(&mut img2, &cmap); + + let img = DynamicImage::ImageRgba8(img2.into()); + */ + let img = simplify(&img); + let out_svg = svgify(&img)?; + let w: Box = if let Some(path) = opt.output { + Box::new(File::create(path)?) + } else { + Box::new(io::stdout()) + }; + svg::write(w, &out_svg)?; + + Ok(()) +}