Skip to content

Commit

Permalink
Add PNG output format
Browse files Browse the repository at this point in the history
  • Loading branch information
staticintlucas committed Aug 10, 2023
1 parent e4f647d commit 6acaf22
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 10 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand Down
8 changes: 7 additions & 1 deletion src/drawing/mod.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -39,6 +41,10 @@ impl Drawing {
pub fn to_svg(&self) -> String {
svg::draw(self)
}

pub fn to_png(&self) -> Result<Vec<u8>> {
png::draw(self)
}
}

#[derive(Debug, Clone)]
Expand Down
111 changes: 111 additions & 0 deletions src/drawing/png.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>> {
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);
}
}
15 changes: 8 additions & 7 deletions src/drawing/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down Expand Up @@ -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")
}
}

Expand Down
13 changes: 12 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::fmt;

use crate::drawing::PngEncodingError;
use crate::kle::InvalidKleLayout;

#[derive(Debug)]
Expand All @@ -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 {
Expand All @@ -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}"),
}
}
}
Expand All @@ -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,
}
}
}
Expand Down Expand Up @@ -71,6 +74,14 @@ impl From<InvalidKleLayout> for Error {
}
}

impl From<PngEncodingError> for Error {
fn from(error: PngEncodingError) -> Self {
Self {
inner: Box::new(ErrorImpl::PngEncodingError(error)),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
34 changes: 33 additions & 1 deletion src/utils/color.rs
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -29,6 +29,19 @@ impl From<Color> for RGB8 {
}
}

impl From<RGB<f32>> for Color {
fn from(value: RGB<f32>) -> Self {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
value.map(|c| (65536.0 * c) as u16).into()
}
}

impl From<Color> for RGB<f32> {
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()
Expand All @@ -53,6 +66,25 @@ impl From<Color> for (u8, u8, u8) {
}
}

impl From<(f32, f32, f32)> for Color {
fn from(value: (f32, f32, f32)) -> Self {
RGB::<f32>::from(value).into()
}
}

impl From<Color> for (f32, f32, f32) {
fn from(value: Color) -> Self {
RGB::<f32>::from(value).into()
}
}

impl From<Color> 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();
Expand Down

0 comments on commit 6acaf22

Please sign in to comment.