use kurbo::{Affine, PathEl, Point};
use miniz_oxide::deflate::{compress_to_vec_zlib, CompressionLevel};
use pdf_writer::{Content, Filter, Finish, PdfWriter, Rect, Ref, TextStr};
use crate::drawing::Drawing;
use super::{KeyDrawing, Path};
const PDF_DPI: f64 = 72.0; const COMPRESSION_LEVEL: u8 = CompressionLevel::DefaultLevel as u8;
macro_rules! transform {
($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);
impl RefGen {
fn new() -> Self {
Self(0)
}
fn next(&mut self) -> Ref {
self.0 += 1;
Ref::new(self.0)
}
}
pub(crate) fn draw(drawing: &Drawing) -> Vec<u8> {
let scale = drawing.scale * PDF_DPI * 0.75; let size = drawing.bounds.size() * scale;
let mut ref_gen = RefGen::new();
let mut writer = PdfWriter::new();
writer.set_version(1, 3);
let catalog_id = ref_gen.next();
let tree_id = ref_gen.next();
let page_id = ref_gen.next();
let content_id = ref_gen.next();
let doc_info_id = ref_gen.next();
writer.catalog(catalog_id).pages(tree_id);
writer.pages(tree_id).kids([page_id]).count(1);
#[allow(clippy::cast_possible_truncation)]
writer
.page(page_id)
.media_box(Rect::new(0.0, 0.0, size.width as f32, size.height as f32))
.parent(tree_id)
.contents(content_id)
.finish();
let mut content = Content::new();
let affine = Affine::scale_non_uniform(scale, -scale).then_translate((0., size.height).into());
for key in &drawing.keys {
draw_key(&mut content, key, &affine);
}
let data = compress_to_vec_zlib(&content.finish(), COMPRESSION_LEVEL);
writer
.stream(content_id, &data)
.filter(Filter::FlateDecode)
.finish();
writer
.document_info(doc_info_id)
.creator(TextStr("keyset-rs"))
.producer(TextStr("keyset-rs"))
.title(TextStr("Keyset Layout"))
.finish();
writer.finish()
}
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 {
draw_path(content, path, &affine);
}
}
fn draw_path(content: &mut Content, path: &Path, affine: &Affine) {
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, *affine);
content.move_to(x, y);
}
PathEl::LineTo(p) => {
p0 = p;
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, p2, p, *affine);
content.cubic_to(x1, y1, x2, y2, x, y);
}
PathEl::QuadTo(p1, p) => {
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, p2, p, *affine);
content.cubic_to(x1, y1, x2, y2, x, y);
}
PathEl::ClosePath => {
p0 = origin;
content.close_path();
}
}
}
if let Some(color) = path.fill {
let (r, g, b) = color.into();
content.set_fill_rgb(r, g, b);
}
if let Some(outline) = path.outline {
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 * affine.as_coeffs()[0]) as f32);
}
match (path.fill, path.outline) {
(Some(_), Some(_)) => {
content.fill_even_odd_and_stroke();
}
(Some(_), None) => {
content.fill_even_odd();
}
(None, Some(_)) => {
content.stroke();
}
(None, None) => {} };
}
#[cfg(test)]
mod tests {
use crate::{DrawingOptions, Key};
#[test]
fn test_to_svg() {
let options = DrawingOptions {
show_margin: true, ..Default::default()
};
let keys = [Key::example()];
let drawing = options.draw(&keys);
let pdf = drawing.to_pdf();
let ai = drawing.to_ai();
assert_eq!(pdf, ai);
}
}