use rustyfi_backend::{Closing, Color, GraphicsElem, Path, PathSeg, PureHorzBox};
use std::fmt::Write as _;
use std::sync::atomic::{AtomicUsize, Ordering};
pub(super) type NestedEmitter<'a> = &'a mut dyn FnMut(&mut String, &PureHorzBox, f64, f64);
#[allow(clippy::too_many_arguments)]
pub(super) fn emit_graphics(
out: &mut String,
elems: &[GraphicsElem],
width: f64,
height: f64,
depth: f64,
tx: f64,
ty: f64,
nested: NestedEmitter<'_>,
) {
if elems.is_empty() {
return;
}
let total_h = height + depth;
let top = ty - height;
let _ = write!(
out,
"<svg class=\"gfx\" style=\"position:absolute; left:{tx}pt; top:{top}pt; overflow:visible;\" \
width=\"{width}pt\" height=\"{total_h}pt\" viewBox=\"0 0 {width} {total_h}\">\n\
<g transform=\"translate(0,{height}) scale(1,-1)\">\n",
);
emit_elems(out, elems, tx, ty, nested);
out.push_str("</g>\n</svg>\n");
}
fn emit_elems(
out: &mut String,
elems: &[GraphicsElem],
tx: f64,
ty: f64,
nested: NestedEmitter<'_>,
) {
for elem in elems {
match elem {
GraphicsElem::Fill(color, path) => {
let _ = write!(
out,
"<path d=\"{}\" fill=\"{}\" fill-rule=\"evenodd\" stroke=\"none\"/>\n",
path_d(path),
css_color(*color),
);
}
GraphicsElem::Stroke(width, color, path) => {
let _ = write!(
out,
"<path d=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{}\"/>\n",
path_d(path),
css_color(*color),
width.0,
);
}
GraphicsElem::DashedStroke(width, dash, color, path) => {
let _ = write!(
out,
"<path d=\"{}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{}\" \
stroke-dasharray=\"{} {}\" stroke-dashoffset=\"{}\"/>\n",
path_d(path),
css_color(*color),
width.0,
dash.0 .0,
dash.1 .0,
dash.2 .0,
);
}
GraphicsElem::Text { pt, contents, .. } => {
for (dx, bx) in contents {
let page_x = tx + (pt.0 + *dx).0;
let page_y = ty - pt.1 .0;
nested(out, bx, page_x, page_y);
}
}
GraphicsElem::Group(inner) => {
out.push_str("<g>\n");
emit_elems(out, inner, tx, ty, nested);
out.push_str("</g>\n");
}
GraphicsElem::Clip(path, inner) => {
let id = next_clip_id();
let _ = write!(
out,
"<clipPath id=\"html-clip-{id}\"><path d=\"{}\" fill-rule=\"evenodd\"/></clipPath>\n\
<g clip-path=\"url(#html-clip-{id})\">\n",
path_d(path),
);
emit_elems(out, inner, tx, ty, nested);
out.push_str("</g>\n");
}
GraphicsElem::Destination { .. } => {}
}
}
}
static NEXT_CLIP_ID: AtomicUsize = AtomicUsize::new(0);
fn next_clip_id() -> usize {
NEXT_CLIP_ID.fetch_add(1, Ordering::Relaxed)
}
fn path_d(path: &Path) -> String {
let mut d = String::new();
for sub in &path.subpaths {
let _ = write!(d, "M{} {} ", sub.start.0 .0, sub.start.1 .0);
for seg in &sub.segs {
match seg {
PathSeg::Line(pt) => {
let _ = write!(d, "L{} {} ", pt.0 .0, pt.1 .0);
}
PathSeg::Bezier(c1, c2, dest) => {
let _ = write!(
d,
"C{} {} {} {} {} {} ",
c1.0 .0, c1.1 .0, c2.0 .0, c2.1 .0, dest.0 .0, dest.1 .0,
);
}
}
}
match sub.closing {
Closing::Open => {}
Closing::Line => d.push_str("Z "),
Closing::Bezier(c1, c2) => {
let _ = write!(
d,
"C{} {} {} {} {} {} ",
c1.0 .0, c1.1 .0, c2.0 .0, c2.1 .0, sub.start.0 .0, sub.start.1 .0,
);
d.push_str("Z ");
}
}
}
d.trim_end().to_string()
}
pub(super) fn css_color(color: Color) -> String {
let (r, g, b) = match color {
Color::Gray(v) => (v, v, v),
Color::Rgb(r, g, b) => (r, g, b),
Color::Cmyk(c, m, y, k) => (
(1.0 - c) * (1.0 - k),
(1.0 - m) * (1.0 - k),
(1.0 - y) * (1.0 - k),
),
};
let to_u8 = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
format!("rgb({},{},{})", to_u8(r), to_u8(g), to_u8(b))
}
#[cfg(test)]
mod tests {
use super::*;
use rustyfi_backend::Length;
#[test]
fn cmyk_black_and_white_round_trip() {
assert_eq!(css_color(Color::Cmyk(0.0, 0.0, 0.0, 1.0)), "rgb(0,0,0)");
assert_eq!(
css_color(Color::Cmyk(0.0, 0.0, 0.0, 0.0)),
"rgb(255,255,255)"
);
assert_eq!(css_color(Color::Cmyk(1.0, 0.0, 0.0, 0.0)), "rgb(0,255,255)");
}
#[test]
fn path_d_maps_line_and_bezier_segments() {
let path = Path {
subpaths: vec![rustyfi_backend::Subpath {
start: (Length::pt(0.0), Length::pt(0.0)),
segs: vec![
PathSeg::Line((Length::pt(10.0), Length::pt(0.0))),
PathSeg::Bezier(
(Length::pt(10.0), Length::pt(5.0)),
(Length::pt(5.0), Length::pt(10.0)),
(Length::pt(0.0), Length::pt(10.0)),
),
],
closing: Closing::Line,
}],
};
let d = path_d(&path);
assert_eq!(d, "M0 0 L10 0 C10 5 5 10 0 10 Z");
}
}