From 6acaf220cdafc9e0a1929a765057ff25fdc4acc8 Mon Sep 17 00:00:00 2001 From: Lucas Jansen <7199136+staticintlucas@users.noreply.github.com> Date: Fri, 11 Aug 2023 00:06:47 +0100 Subject: [PATCH] Add PNG output format --- Cargo.toml | 1 + src/drawing/mod.rs | 8 +++- src/drawing/png.rs | 111 +++++++++++++++++++++++++++++++++++++++++++++ src/drawing/svg.rs | 15 +++--- src/error.rs | 13 +++++- src/utils/color.rs | 34 +++++++++++++- 6 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 src/drawing/png.rs diff --git a/Cargo.toml b/Cargo.toml index 3f5d238..3916bc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ rgb = { version = "0.8", default-features = false } serde = { version = "1.0", default-features = false, features = ["derive"] } serde_json = "1.0" svg = "0.13" +tiny-skia = "0.11" toml = { version = "0.7", default-features = false, features = ["parse"] } ttf-parser = { version = "0.18", default-features = false, features = ["std"] } diff --git a/src/drawing/mod.rs b/src/drawing/mod.rs index ae56f7f..3255415 100644 --- a/src/drawing/mod.rs +++ b/src/drawing/mod.rs @@ -1,11 +1,13 @@ mod imp; +mod png; mod svg; use kurbo::{Point, Rect, Size}; -use crate::{Font, Key, Profile}; +use crate::{error::Result, Font, Key, Profile}; pub(crate) use imp::{KeyDrawing, Path}; +pub(crate) use png::PngEncodingError; #[derive(Debug, Clone)] pub struct Drawing { @@ -39,6 +41,10 @@ impl Drawing { pub fn to_svg(&self) -> String { svg::draw(self) } + + pub fn to_png(&self) -> Result> { + png::draw(self) + } } #[derive(Debug, Clone)] diff --git a/src/drawing/png.rs b/src/drawing/png.rs new file mode 100644 index 0000000..2566cf1 --- /dev/null +++ b/src/drawing/png.rs @@ -0,0 +1,111 @@ +use std::fmt; + +use kurbo::{PathEl, Point}; +use tiny_skia::{FillRule, Paint, PathBuilder, Pixmap, Shader, Stroke, Transform}; + +use crate::drawing::Drawing; +use crate::error::Result; + +use super::{KeyDrawing, Path}; + +#[derive(Debug)] +pub(crate) struct PngEncodingError { + message: String, +} + +impl fmt::Display for PngEncodingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +macro_rules! transform { + (($($x:expr, $y:expr),+), $origin:expr, $scale:expr) => { + ($((($origin.x + $x / 1e3) * $scale) as f32, (($origin.y + $y / 1e3) * $scale) as f32),+) + }; +} + +impl std::error::Error for PngEncodingError {} + +pub(crate) fn draw(drawing: &Drawing) -> Result> { + let size = drawing.bounds.size() * drawing.scale; + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let (width, height) = (size.width.ceil() as u32, size.height.ceil() as u32); + + let mut pixmap = Pixmap::new(width, height).ok_or(PngEncodingError { + message: format!("cannot create pixmap with size (w: {width}, h: {height})"), + })?; + + pixmap.fill(tiny_skia::Color::TRANSPARENT); + + for key in &drawing.keys { + draw_key(&mut pixmap, key, drawing.scale); + } + + Ok(pixmap.encode_png().map_err(|e| PngEncodingError { + message: e.to_string(), + })?) +} + +fn draw_key(pixmap: &mut Pixmap, key: &KeyDrawing, scale: f64) { + for path in &key.paths { + draw_path(pixmap, path, key.origin, scale); + } +} + +fn draw_path(pixmap: &mut Pixmap, path: &Path, origin: Point, scale: f64) { + let mut path_builder = PathBuilder::new(); + for el in &path.path { + match el { + PathEl::MoveTo(p) => { + let (x, y) = transform!((p.x, p.y), origin, scale); + path_builder.move_to(x, y); + } + PathEl::LineTo(p) => { + let (x, y) = transform!((p.x, p.y), origin, scale); + path_builder.line_to(x, y); + } + PathEl::CurveTo(p1, p2, p) => { + let (x1, y1, x2, y2, x, y) = + transform!((p1.x, p1.y, p2.x, p2.y, p.x, p.y), origin, scale); + path_builder.cubic_to(x1, y1, x2, y2, x, y); + } + PathEl::QuadTo(p1, p) => { + let (x1, y1, x, y) = transform!((p1.x, p1.y, p.x, p.y), origin, scale); + path_builder.quad_to(x1, y1, x, y); + } + PathEl::ClosePath => path_builder.close(), + } + } + let Some(skia_path) = path_builder.finish() else { return }; + + if let Some(color) = path.fill { + let paint = Paint { + shader: Shader::SolidColor(color.into()), + anti_alias: true, + ..Default::default() + }; + pixmap.fill_path( + &skia_path, + &paint, + FillRule::EvenOdd, + Transform::default(), + None, + ); + } + + if let Some(outline) = path.outline { + let paint = Paint { + shader: Shader::SolidColor(outline.color.into()), + anti_alias: true, + ..Default::default() + }; + #[allow(clippy::cast_possible_truncation)] + let stroke = Stroke { + width: (outline.width * scale / 1e3) as f32, + ..Default::default() + }; + pixmap.stroke_path(&skia_path, &paint, &stroke, Transform::default(), None); + } +} diff --git a/src/drawing/svg.rs b/src/drawing/svg.rs index 062ac06..08ad577 100644 --- a/src/drawing/svg.rs +++ b/src/drawing/svg.rs @@ -49,8 +49,8 @@ fn draw_key(key: &KeyDrawing) -> Group { key.paths.iter().map(draw_path).fold(group, Group::add) } -fn draw_path(key: &Path) -> SvgPath { - let data = key +fn draw_path(path: &Path) -> SvgPath { + let data = path .path .iter() .scan((Point::ORIGIN, Point::ORIGIN), |(origin, point), el| { @@ -88,18 +88,19 @@ fn draw_path(key: &Path) -> SvgPath { }) .join(""); - let fill = if let Some(color) = key.fill { + let fill = if let Some(color) = path.fill { color.to_string() } else { "none".into() }; - let path = SvgPath::new().set("d", data).set("fill", fill); + let svg_path = SvgPath::new().set("d", data).set("fill", fill); - if let Some(outline) = key.outline { - path.set("stroke", outline.color.to_string()) + if let Some(outline) = path.outline { + svg_path + .set("stroke", outline.color.to_string()) .set("stroke-width", fmt_num!("{}", outline.width)) } else { - path.set("stroke", "none") + svg_path.set("stroke", "none") } } diff --git a/src/error.rs b/src/error.rs index e627ae5..5519405 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,6 @@ use std::fmt; +use crate::drawing::PngEncodingError; use crate::kle::InvalidKleLayout; #[derive(Debug)] @@ -15,6 +16,7 @@ pub(crate) enum ErrorImpl { TomlParseError(toml::de::Error), FontParseError(ttf_parser::FaceParsingError), InvalidKleLayout(InvalidKleLayout), + PngEncodingError(PngEncodingError), } impl fmt::Display for Error { @@ -24,6 +26,7 @@ impl fmt::Display for Error { ErrorImpl::TomlParseError(error) => write!(f, "error parsing TOML: {error}"), ErrorImpl::FontParseError(error) => write!(f, "error parsing font: {error}"), ErrorImpl::InvalidKleLayout(error) => write!(f, "error parsing KLE layout: {error}"), + ErrorImpl::PngEncodingError(error) => write!(f, "error encoding PNG: {error}"), } } } @@ -34,7 +37,7 @@ impl std::error::Error for Error { ErrorImpl::JsonParseError(error) => Some(error), ErrorImpl::TomlParseError(error) => Some(error), ErrorImpl::FontParseError(error) => Some(error), - ErrorImpl::InvalidKleLayout(_) => None, + ErrorImpl::InvalidKleLayout(_) | ErrorImpl::PngEncodingError(_) => None, } } } @@ -71,6 +74,14 @@ impl From for Error { } } +impl From for Error { + fn from(error: PngEncodingError) -> Self { + Self { + inner: Box::new(ErrorImpl::PngEncodingError(error)), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/utils/color.rs b/src/utils/color.rs index 1abd86b..cc55702 100644 --- a/src/utils/color.rs +++ b/src/utils/color.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -use rgb::{ComponentMap, RGB16, RGB8}; +use rgb::{ComponentMap, RGB, RGB16, RGB8}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Color(RGB16); @@ -29,6 +29,19 @@ impl From for RGB8 { } } +impl From> for Color { + fn from(value: RGB) -> Self { + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + value.map(|c| (65536.0 * c) as u16).into() + } +} + +impl From for RGB { + fn from(value: Color) -> Self { + RGB16::from(value).map(|c| f32::from(c) / 65535.0) + } +} + impl From<(u16, u16, u16)> for Color { fn from(value: (u16, u16, u16)) -> Self { RGB16::from(value).into() @@ -53,6 +66,25 @@ impl From for (u8, u8, u8) { } } +impl From<(f32, f32, f32)> for Color { + fn from(value: (f32, f32, f32)) -> Self { + RGB::::from(value).into() + } +} + +impl From for (f32, f32, f32) { + fn from(value: Color) -> Self { + RGB::::from(value).into() + } +} + +impl From for tiny_skia::Color { + fn from(value: Color) -> Self { + let (r, g, b) = value.into(); + tiny_skia::Color::from_rgba(r, g, b, 1.0).unwrap() + } +} + impl Display for Color { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let (r, g, b) = RGB8::from(*self).into();