diff --git a/src/drawing/mod.rs b/src/drawing/mod.rs index 4de27ab..8b5b8d9 100644 --- a/src/drawing/mod.rs +++ b/src/drawing/mod.rs @@ -35,7 +35,7 @@ impl Drawing { Self { bounds, keys, - scale: options.dpi * 0.75, // 1u = 0.75in => dpu = 0.75*dpi + scale: options.scale, } } @@ -43,8 +43,8 @@ impl Drawing { svg::draw(self) } - pub fn to_png(&self) -> Result> { - png::draw(self) + pub fn to_png(&self, dpi: f64) -> Result> { + png::draw(self, dpi) } pub fn to_pdf(&self) -> Vec { @@ -66,7 +66,7 @@ impl Drawing { pub struct DrawingOptions { profile: Profile, font: Font, - dpi: f64, + scale: f64, outline_width: f64, show_keys: bool, show_margin: bool, @@ -77,7 +77,7 @@ impl Default for DrawingOptions { Self { profile: Profile::default(), font: Font::default(), - dpi: 96., + scale: 1., outline_width: 10., show_keys: true, show_margin: false, @@ -101,8 +101,8 @@ impl DrawingOptions { self } - pub fn dpi(&mut self, dpi: f64) -> &mut Self { - self.dpi = dpi; + pub fn scale(&mut self, scale: f64) -> &mut Self { + self.scale = scale; self } @@ -140,7 +140,7 @@ mod tests { fn test_drawing_options() { let options = DrawingOptions::default(); - assert_approx_eq!(options.dpi, 96.); + assert_approx_eq!(options.scale, 1.); assert_eq!(options.font.glyphs.len(), 0); let profile = Profile::default(); @@ -149,7 +149,7 @@ mod tests { options .profile(profile) .font(font) - .dpi(192.) + .scale(2.) .show_keys(false) .show_margin(true); @@ -158,6 +158,6 @@ mod tests { ProfileType::Cylindrical { .. } ); assert_eq!(options.font.glyphs.len(), 2); - assert_eq!(options.dpi, 192.); + assert_eq!(options.scale, 2.); } } diff --git a/src/drawing/pdf.rs b/src/drawing/pdf.rs index 21f06f6..2aba22f 100644 --- a/src/drawing/pdf.rs +++ b/src/drawing/pdf.rs @@ -1,4 +1,4 @@ -use kurbo::{PathEl, Point}; +use kurbo::{Affine, PathEl, Point}; use miniz_oxide::deflate::{compress_to_vec_zlib, CompressionLevel}; use pdf_writer::{Content, Filter, Finish, PdfWriter, Rect, Ref, TextStr}; @@ -6,13 +6,29 @@ use crate::drawing::Drawing; use super::{KeyDrawing, Path}; +const PDF_DPI: f64 = 72.0; // PDF uses 72 dpi const COMPRESSION_LEVEL: u8 = CompressionLevel::DefaultLevel as u8; macro_rules! transform { - (($($x:expr, $y:expr),+), $origin:expr, $scale:expr) => { - // Negate Y since PDF has rising Y axis - ($((($origin.x + $x / 1e3) * $scale) as f32, (($origin.y - $y / 1e3) * $scale) as f32),+) - }; + ($p:expr, $affine:expr) => {{ + let p = $affine * $p; + (p.x as f32, p.y as f32) + }}; + ($p1:expr, $p:expr, $affine:expr) => {{ + let (p1, p) = ($affine * $p1, $affine * $p); + (p1.x as f32, p1.y as f32, p.x as f32, p.y as f32) + }}; + ($p1:expr, $p2:expr, $p:expr, $affine:expr) => {{ + let (p1, p2, p) = ($affine * $p1, $affine * $p2, $affine * $p); + ( + p1.x as f32, + p1.y as f32, + p2.x as f32, + p2.y as f32, + p.x as f32, + p.y as f32, + ) + }}; } struct RefGen(i32); @@ -29,7 +45,7 @@ impl RefGen { } pub(crate) fn draw(drawing: &Drawing) -> Vec { - let scale = drawing.scale * (72.0 / 96.0); + let scale = drawing.scale * PDF_DPI * 0.75; // 0.75 in/key let size = drawing.bounds.size() * scale; let mut ref_gen = RefGen::new(); @@ -56,8 +72,10 @@ pub(crate) fn draw(drawing: &Drawing) -> Vec { let mut content = Content::new(); + // Flip origin since PDF has rising Y axis + let affine = Affine::scale_non_uniform(scale, -scale).then_translate((0., size.height).into()); for key in &drawing.keys { - draw_key(&mut content, key, drawing.bounds.height(), scale); + draw_key(&mut content, key, &affine); } let data = compress_to_vec_zlib(&content.finish(), COMPRESSION_LEVEL); @@ -76,45 +94,45 @@ pub(crate) fn draw(drawing: &Drawing) -> Vec { writer.finish() } -fn draw_key(content: &mut Content, key: &KeyDrawing, height: f64, scale: f64) { +fn draw_key(content: &mut Content, key: &KeyDrawing, affine: &Affine) { + let affine = *affine * Affine::scale(1e-3).then_translate(key.origin.to_vec2()); for path in &key.paths { - // Flip origin since PDF has rising Y axis - let origin = Point::new(key.origin.x, height - key.origin.y); - draw_path(content, path, origin, scale); + draw_path(content, path, &affine); } } -fn draw_path(content: &mut Content, path: &Path, origin: Point, scale: f64) { +fn draw_path(content: &mut Content, path: &Path, affine: &Affine) { // previous point, needed for quad => cubic Bézier conversion + let mut origin = Point::ORIGIN; let mut p0 = Point::ORIGIN; for el in &path.path { match el { PathEl::MoveTo(p) => { + origin = p; p0 = p; - let (x, y) = transform!((p.x, p.y), origin, scale); + let (x, y) = transform!(p, *affine); content.move_to(x, y); } PathEl::LineTo(p) => { p0 = p; - let (x, y) = transform!((p.x, p.y), origin, scale); + let (x, y) = transform!(p, *affine); content.line_to(x, y); } PathEl::CurveTo(p1, p2, p) => { p0 = p; - let (x1, y1, x2, y2, x, y) = - transform!((p1.x, p1.y, p2.x, p2.y, p.x, p.y), origin, scale); + let (x1, y1, x2, y2, x, y) = transform!(p1, p2, p, *affine); content.cubic_to(x1, y1, x2, y2, x, y); } PathEl::QuadTo(p1, p) => { // Convert quad to cubic since PostScript doesn't have quadratic Béziers let (p1, p2) = (p0 + (2.0 / 3.0) * (p1 - p0), p + (2.0 / 3.0) * (p1 - p)); p0 = p; - let (x1, y1, x2, y2, x, y) = - transform!((p1.x, p1.y, p2.x, p2.y, p.x, p.y), origin, scale); + let (x1, y1, x2, y2, x, y) = transform!(p1, p2, p, *affine); content.cubic_to(x1, y1, x2, y2, x, y); } PathEl::ClosePath => { + p0 = origin; content.close_path(); } } @@ -129,7 +147,7 @@ fn draw_path(content: &mut Content, path: &Path, origin: Point, scale: f64) { let (r, g, b) = outline.color.into(); content.set_stroke_rgb(r, g, b); #[allow(clippy::cast_possible_truncation)] - content.set_line_width((outline.width * scale / 1e3) as f32); + content.set_line_width((outline.width * affine.as_coeffs()[0]) as f32); } match (path.fill, path.outline) { diff --git a/src/drawing/png.rs b/src/drawing/png.rs index 2566cf1..5d9b94c 100644 --- a/src/drawing/png.rs +++ b/src/drawing/png.rs @@ -1,6 +1,6 @@ use std::fmt; -use kurbo::{PathEl, Point}; +use kurbo::{Affine, PathEl}; use tiny_skia::{FillRule, Paint, PathBuilder, Pixmap, Shader, Stroke, Transform}; use crate::drawing::Drawing; @@ -20,15 +20,32 @@ impl fmt::Display for PngEncodingError { } 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),+) - }; + ($p:expr, $affine:expr) => {{ + let p = $affine * $p; + (p.x as f32, p.y as f32) + }}; + ($p1:expr, $p:expr, $affine:expr) => {{ + let (p1, p) = ($affine * $p1, $affine * $p); + (p1.x as f32, p1.y as f32, p.x as f32, p.y as f32) + }}; + ($p1:expr, $p2:expr, $p:expr, $affine:expr) => {{ + let (p1, p2, p) = ($affine * $p1, $affine * $p2, $affine * $p); + ( + p1.x as f32, + p1.y as f32, + p2.x as f32, + p2.y as f32, + p.x as f32, + p.y as f32, + ) + }}; } impl std::error::Error for PngEncodingError {} -pub(crate) fn draw(drawing: &Drawing) -> Result> { - let size = drawing.bounds.size() * drawing.scale; +pub(crate) fn draw(drawing: &Drawing, dpi: f64) -> Result> { + let scale = drawing.scale * dpi * 0.75; // 0.75 in/key + let size = drawing.bounds.size() * scale; #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let (width, height) = (size.width.ceil() as u32, size.height.ceil() as u32); @@ -39,8 +56,9 @@ pub(crate) fn draw(drawing: &Drawing) -> Result> { pixmap.fill(tiny_skia::Color::TRANSPARENT); + let affine = Affine::scale(scale); for key in &drawing.keys { - draw_key(&mut pixmap, key, drawing.scale); + draw_key(&mut pixmap, key, &affine); } Ok(pixmap.encode_png().map_err(|e| PngEncodingError { @@ -48,31 +66,31 @@ pub(crate) fn draw(drawing: &Drawing) -> Result> { })?) } -fn draw_key(pixmap: &mut Pixmap, key: &KeyDrawing, scale: f64) { +fn draw_key(pixmap: &mut Pixmap, key: &KeyDrawing, affine: &Affine) { + let affine = *affine * Affine::scale(1e-3).then_translate(key.origin.to_vec2()); for path in &key.paths { - draw_path(pixmap, path, key.origin, scale); + draw_path(pixmap, path, &affine); } } -fn draw_path(pixmap: &mut Pixmap, path: &Path, origin: Point, scale: f64) { +fn draw_path(pixmap: &mut Pixmap, path: &Path, affine: &Affine) { 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); + let (x, y) = transform!(p, *affine); path_builder.move_to(x, y); } PathEl::LineTo(p) => { - let (x, y) = transform!((p.x, p.y), origin, scale); + let (x, y) = transform!(p, *affine); 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); + let (x1, y1, x2, y2, x, y) = transform!(p1, p2, p, *affine); 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); + let (x1, y1, x, y) = transform!(p1, p, *affine); path_builder.quad_to(x1, y1, x, y); } PathEl::ClosePath => path_builder.close(), @@ -103,7 +121,7 @@ fn draw_path(pixmap: &mut Pixmap, path: &Path, origin: Point, scale: f64) { }; #[allow(clippy::cast_possible_truncation)] let stroke = Stroke { - width: (outline.width * scale / 1e3) as f32, + width: (outline.width * affine.as_coeffs()[0]) 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 08ad577..30df285 100644 --- a/src/drawing/svg.rs +++ b/src/drawing/svg.rs @@ -15,11 +15,11 @@ macro_rules! fmt_num { pub(crate) fn draw(drawing: &Drawing) -> String { let size = drawing.bounds.size() * drawing.scale; - let view_box = drawing.bounds.scale_from_origin(1e3); + let view_box = drawing.bounds.scale_from_origin(1e3); // Use 1000 user units per key let document = Document::new() - .set("width", fmt_num!("{}", size.width)) - .set("height", fmt_num!("{}", size.height)) + .set("width", fmt_num!("{}mm", size.width * 19.05)) + .set("height", fmt_num!("{}mm", size.height * 19.05)) .set( "viewBox", fmt_num!(