use crate::Color;
fn color_fill(color: Color) -> String {
match color {
Color::Rgb([r, g, b]) => format!("{r:.4} {g:.4} {b:.4} rg"),
Color::Cmyk([c, m, y, k]) => format!("{c:.4} {m:.4} {y:.4} {k:.4} k"),
}
}
fn color_stroke(color: Color) -> String {
match color {
Color::Rgb([r, g, b]) => format!("{r:.4} {g:.4} {b:.4} RG"),
Color::Cmyk([c, m, y, k]) => format!("{c:.4} {m:.4} {y:.4} {k:.4} K"),
}
}
pub(crate) fn rect_stream(rect: &[f32; 4], color: Color, gs_name: &str) -> Vec<u8> {
let fill = color_fill(color);
format!(
"q\n/{gs} gs\n{fill}\n{x:.4} {y:.4} {w:.4} {h:.4} re\nf\nQ\n",
gs = gs_name,
x = rect[0],
y = rect[1],
w = rect[2],
h = rect[3],
)
.into_bytes()
}
pub(crate) fn line_stream(
from: &[f32; 2],
to: &[f32; 2],
color: Color,
width: f32,
gs_name: &str,
) -> Vec<u8> {
let stroke = color_stroke(color);
format!(
"q\n/{gs} gs\n{stroke}\n{lw:.4} w\n{x1:.4} {y1:.4} m\n{x2:.4} {y2:.4} l\nS\nQ\n",
gs = gs_name,
lw = width,
x1 = from[0],
y1 = from[1],
x2 = to[0],
y2 = to[1],
)
.into_bytes()
}
pub(crate) fn rect_stroke_stream(
rect: &[f32; 4],
color: Color,
line_width: f32,
gs_name: &str,
) -> Vec<u8> {
let stroke = color_stroke(color);
format!(
"q\n/{gs} gs\n{stroke}\n{lw:.4} w\n{x:.4} {y:.4} {w:.4} {h:.4} re\nS\nQ\n",
gs = gs_name,
lw = line_width,
x = rect[0],
y = rect[1],
w = rect[2],
h = rect[3],
)
.into_bytes()
}
pub(crate) fn polygon_stream(
points: &[[f32; 2]],
color: Color,
gs_name: &str,
filled: bool,
stroke_width: f32,
) -> Vec<u8> {
if points.len() < 2 {
return Vec::new();
}
let stroke = stroke_width > 0.0;
let mut s = format!("q\n/{gs} gs\n", gs = gs_name);
if filled {
s.push_str(&format!("{}\n", color_fill(color)));
}
if stroke {
s.push_str(&format!("{}\n{:.4} w\n", color_stroke(color), stroke_width));
}
s.push_str(&format!("{:.4} {:.4} m\n", points[0][0], points[0][1]));
for pt in &points[1..] {
s.push_str(&format!("{:.4} {:.4} l\n", pt[0], pt[1]));
}
let paint_op = match (filled, stroke) {
(true, true) => "B",
(true, false) => "f",
(false, true) => "S",
(false, false) => return Vec::new(),
};
s.push_str(&format!("h\n{}\nQ\n", paint_op));
s.into_bytes()
}
pub(crate) fn polyline_stream(
points: &[[f32; 2]],
color: Color,
width: f32,
gs_name: &str,
) -> Vec<u8> {
if points.len() < 2 {
return Vec::new();
}
let stroke = color_stroke(color);
let mut s = format!(
"q\n/{gs} gs\n{stroke}\n{lw:.4} w\n{x0:.4} {y0:.4} m\n",
gs = gs_name,
lw = width,
x0 = points[0][0],
y0 = points[0][1],
);
for pt in &points[1..] {
s.push_str(&format!("{:.4} {:.4} l\n", pt[0], pt[1]));
}
s.push_str("S\nQ\n");
s.into_bytes()
}
pub(crate) fn ellipse_stream(
rect: &[f32; 4],
color: Color,
gs_name: &str,
filled: bool,
stroke_width: f32,
) -> Vec<u8> {
let stroke = stroke_width > 0.0;
const K: f32 = 0.552_285; let (x, y, w, h) = (rect[0], rect[1], rect[2], rect[3]);
let cx = x + w / 2.0;
let cy = y + h / 2.0;
let rx = w / 2.0;
let ry = h / 2.0;
let kx = K * rx;
let ky = K * ry;
let paint_op = match (filled, stroke) {
(true, true) => "B",
(true, false) => "f",
(false, true) => "S",
(false, false) => return Vec::new(),
};
let mut s = format!("q\n/{gs} gs\n", gs = gs_name);
if filled {
s.push_str(&format!("{}\n", color_fill(color)));
}
if stroke {
s.push_str(&format!("{}\n{:.4} w\n", color_stroke(color), stroke_width));
}
s.push_str(&format!("{:.4} {:.4} m\n", cx, cy + ry));
s.push_str(&format!(
"{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c\n",
cx + kx,
cy + ry,
cx + rx,
cy + ky,
cx + rx,
cy
));
s.push_str(&format!(
"{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c\n",
cx + rx,
cy - ky,
cx + kx,
cy - ry,
cx,
cy - ry
));
s.push_str(&format!(
"{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c\n",
cx - kx,
cy - ry,
cx - rx,
cy - ky,
cx - rx,
cy
));
s.push_str(&format!(
"{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c\n",
cx - rx,
cy + ky,
cx - kx,
cy + ry,
cx,
cy + ry
));
s.push_str(&format!("h\n{}\nQ\n", paint_op));
s.into_bytes()
}
pub(crate) fn path_stream(
points: &[[f32; 2]],
closed: bool,
color: Color,
gs_name: &str,
filled: bool,
stroke_width: f32,
) -> Vec<u8> {
if points.len() < 2 {
return Vec::new();
}
let stroke = stroke_width > 0.0;
let paint_op = match (filled, stroke) {
(true, true) => "B",
(true, false) => "f",
(false, true) => "S",
(false, false) => return Vec::new(),
};
let mut s = format!("q\n/{gs} gs\n", gs = gs_name);
if filled {
s.push_str(&format!("{}\n", color_fill(color)));
}
if stroke {
s.push_str(&format!("{}\n{:.4} w\n", color_stroke(color), stroke_width));
}
s.push_str(&format!("{:.4} {:.4} m\n", points[0][0], points[0][1]));
for pt in &points[1..] {
s.push_str(&format!("{:.4} {:.4} l\n", pt[0], pt[1]));
}
if closed {
s.push_str("h\n");
}
s.push_str(&format!("{}\nQ\n", paint_op));
s.into_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rect_stream_contains_operators() {
let bytes = rect_stream(
&[10.0, 20.0, 100.0, 50.0],
Color::Rgb([1.0, 0.0, 0.0]),
"GS0",
);
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("/GS0 gs"), "should set ExtGState");
assert!(s.contains("re\nf"), "should fill rectangle");
assert!(
s.contains("1.0000 0.0000 0.0000 rg"),
"should set fill color"
);
}
#[test]
fn rect_stroke_stream_uses_capital_rg() {
let bytes = rect_stroke_stream(
&[10.0, 20.0, 100.0, 50.0],
Color::Rgb([1.0, 0.0, 0.0]),
2.0,
"GS0",
);
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("/GS0 gs"));
assert!(s.contains("re\nS"), "should stroke rectangle");
assert!(
s.contains("1.0000 0.0000 0.0000 RG"),
"should use stroke color (RG)"
);
assert!(s.contains("2.0000 w"), "should set line width");
assert!(!s.contains(" rg"), "should NOT set fill color");
}
#[test]
fn polygon_stream_filled_contains_rg_and_f() {
let pts = [[0.0_f32, 0.0], [10.0, 0.0], [5.0, 10.0]];
let bytes = polygon_stream(&pts, Color::Rgb([0.0, 1.0, 0.0]), "GS1", true, 0.0);
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("0.0000 1.0000 0.0000 rg"), "fill color rg");
assert!(s.contains(" m\n"), "moveto");
assert!(s.contains(" l\n"), "lineto");
assert!(s.contains("h\n"), "close path");
assert!(s.contains("\nf\n"), "fill operator");
assert!(!s.contains("\nS\n"), "no stroke");
}
#[test]
fn polygon_stream_stroked_contains_rg_and_s() {
let pts = [[0.0_f32, 0.0], [10.0, 0.0], [5.0, 10.0]];
let bytes = polygon_stream(&pts, Color::Rgb([1.0, 0.0, 0.0]), "GS2", false, 1.5);
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("1.0000 0.0000 0.0000 RG"), "stroke color RG");
assert!(s.contains("\nS\n"), "stroke operator");
assert!(!s.contains("\nf\n"), "no fill");
}
#[test]
fn polygon_stream_fill_and_stroke() {
let pts = [[0.0_f32, 0.0], [10.0, 0.0], [5.0, 10.0]];
let bytes = polygon_stream(&pts, Color::Rgb([0.0, 0.5, 1.0]), "GS3", true, 2.0);
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("rg"), "fill color");
assert!(s.contains("RG"), "stroke color");
assert!(s.contains("2.0000 w"), "stroke width");
assert!(s.contains("\nB\n"), "fill-then-stroke operator");
}
#[test]
fn polygon_stream_empty_returns_empty() {
assert!(polygon_stream(&[], Color::Rgb([0.0; 3]), "GS0", true, 0.0).is_empty());
assert!(polygon_stream(&[[0.0, 0.0]], Color::Rgb([0.0; 3]), "GS0", true, 0.0).is_empty());
assert!(
polygon_stream(
&[[0.0, 0.0], [10.0, 0.0]],
Color::Rgb([0.0; 3]),
"GS0",
false,
0.0
)
.is_empty()
);
}
#[test]
fn line_stream_contains_operators() {
let bytes = line_stream(
&[0.0, 0.0],
&[100.0, 0.0],
Color::Rgb([0.0, 0.0, 1.0]),
2.0,
"GS1",
);
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("/GS1 gs"), "should set ExtGState");
assert!(s.contains("m\n"), "should have moveto");
assert!(s.contains("l\nS"), "should stroke line");
assert!(s.contains("2.0000 w"), "should set line width");
}
#[test]
fn polyline_no_closepath() {
let pts = [[0.0_f32, 0.0], [50.0, 0.0], [50.0, 50.0]];
let bytes = polyline_stream(&pts, Color::Rgb([1.0, 0.0, 0.0]), 1.5, "GS0");
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("/GS0 gs"));
assert!(s.contains("m\n"), "moveto");
assert_eq!(s.matches(" l\n").count(), 2);
assert!(s.contains("\nS\n"), "stroke without close");
assert!(!s.contains("\nh\n"), "must NOT close path");
}
#[test]
fn polyline_fewer_than_2_points_is_empty() {
assert!(polyline_stream(&[], Color::Rgb([0.0; 3]), 1.0, "GS0").is_empty());
assert!(polyline_stream(&[[0.0, 0.0]], Color::Rgb([0.0; 3]), 1.0, "GS0").is_empty());
}
}