use crate::writer::fmt_num;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Rgb(pub u8, pub u8, pub u8);
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct TextRotation {
pub cx: f32,
pub cy: f32,
pub angle_deg: f32,
pub half_width: f32,
}
fn color_component(c: u8) -> String {
fmt_num(c as f32 / 255.0)
}
fn color_op(color: Rgb, op: &str) -> String {
format!(
"{} {} {} {}",
color_component(color.0),
color_component(color.1),
color_component(color.2),
op
)
}
pub struct ContentBuilder {
buf: Vec<u8>,
}
impl Default for ContentBuilder {
fn default() -> Self {
Self::new()
}
}
impl ContentBuilder {
pub fn new() -> Self {
ContentBuilder { buf: Vec::new() }
}
fn op(&mut self, s: &str) {
self.buf.extend_from_slice(s.as_bytes());
self.buf.push(b'\n');
}
pub fn save(&mut self) {
self.op("q");
}
pub fn restore(&mut self) {
self.op("Q");
}
pub fn begin_marked_content(&mut self, tag: &str, mcid: u32) {
self.op(&format!("/{tag} << /MCID {mcid} >> BDC"));
}
pub fn begin_artifact(&mut self) {
self.op("/Artifact BMC");
}
pub fn end_marked_content(&mut self) {
self.op("EMC");
}
fn rect_operands(x: f32, y: f32, w: f32, h: f32) -> String {
format!("{} {} {} {}", fmt_num(x), fmt_num(y), fmt_num(w), fmt_num(h))
}
pub fn clip_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
self.op(&format!("{} re W n", Self::rect_operands(x, y, w, h)));
}
fn rect_op(&mut self, rect_prefix: &str, x: f32, y: f32, w: f32, h: f32, op: &str) {
self.op(&format!("{} {} re {}", rect_prefix, Self::rect_operands(x, y, w, h), op));
}
pub fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Rgb) {
self.rect_op(&color_op(color, "rg"), x, y, w, h, "f");
}
pub fn stroke_rect(&mut self, x: f32, y: f32, w: f32, h: f32, line_width: f32, color: Rgb) {
let prefix = format!("{} {} w", color_op(color, "RG"), fmt_num(line_width));
self.rect_op(&prefix, x, y, w, h, "S");
}
pub fn set_dash(&mut self, dash: f32, gap: f32) {
self.op(&format!("[{} {}] 0 d", fmt_num(dash), fmt_num(gap)));
}
pub fn reset_dash(&mut self) {
self.op("[] 0 d");
}
#[allow(clippy::too_many_arguments)]
pub fn draw_rounded_rect(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
fill: Option<Rgb>,
stroke: Option<(f32, Rgb)>,
dash: Option<(f32, f32)>,
) {
let r = radius.min(w / 2.0).min(h / 2.0);
let k = r * 0.552_284_8;
let mut path = String::new();
path.push_str(&format!("{} {} m\n", fmt_num(x + r), fmt_num(y)));
path.push_str(&format!("{} {} l\n", fmt_num(x + w - r), fmt_num(y)));
path.push_str(&format!(
"{} {} {} {} {} {} c\n",
fmt_num(x + w - r + k),
fmt_num(y),
fmt_num(x + w),
fmt_num(y + r - k),
fmt_num(x + w),
fmt_num(y + r)
));
path.push_str(&format!("{} {} l\n", fmt_num(x + w), fmt_num(y + h - r)));
path.push_str(&format!(
"{} {} {} {} {} {} c\n",
fmt_num(x + w),
fmt_num(y + h - r + k),
fmt_num(x + w - r + k),
fmt_num(y + h),
fmt_num(x + w - r),
fmt_num(y + h)
));
path.push_str(&format!("{} {} l\n", fmt_num(x + r), fmt_num(y + h)));
path.push_str(&format!(
"{} {} {} {} {} {} c\n",
fmt_num(x + r - k),
fmt_num(y + h),
fmt_num(x),
fmt_num(y + h - r + k),
fmt_num(x),
fmt_num(y + h - r)
));
path.push_str(&format!("{} {} l\n", fmt_num(x), fmt_num(y + r)));
path.push_str(&format!(
"{} {} {} {} {} {} c\nh",
fmt_num(x),
fmt_num(y + r - k),
fmt_num(x + r - k),
fmt_num(y),
fmt_num(x + r),
fmt_num(y)
));
if let Some((dash_len, gap_len)) = dash {
self.set_dash(dash_len, gap_len);
}
match (fill, stroke) {
(Some(f), Some((sw, s))) => {
let fill_str = color_op(f, "rg");
let stroke_str = color_op(s, "RG");
self.op(&format!("{} {} {} w\n{} b", fill_str, stroke_str, fmt_num(sw), path));
}
(Some(f), None) => {
let fill_str = color_op(f, "rg");
self.op(&format!("{}\n{} f", fill_str, path));
}
(None, Some((sw, s))) => {
let stroke_str = color_op(s, "RG");
self.op(&format!("{} {} w\n{} S", stroke_str, fmt_num(sw), path));
}
(None, None) => {}
}
if dash.is_some() {
self.reset_dash();
}
}
pub fn line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, line_width: f32, color: Rgb) {
self.op(&format!(
"{} {} w {} {} m {} {} l S",
color_op(color, "RG"),
fmt_num(line_width),
fmt_num(x1),
fmt_num(y1),
fmt_num(x2),
fmt_num(y2)
));
}
pub fn text(&mut self, font_resource: &str, size: f32, x: f32, y: f32, color: Rgb, encoded_bytes: &[u8]) {
self.text_block(font_resource, size, color, x, y, encoded_bytes);
self.buf.push(b'\n');
}
fn write_hex_string(&mut self, encoded_bytes: &[u8]) {
for b in encoded_bytes {
self.buf.extend_from_slice(format!("{b:02X}").as_bytes());
}
}
fn text_block(&mut self, font_resource: &str, size: f32, color: Rgb, tx: f32, ty: f32, encoded_bytes: &[u8]) {
self.buf.extend_from_slice(
format!(
"BT /{} {} Tf {} {} {} Td <",
font_resource,
fmt_num(size),
color_op(color, "rg"),
fmt_num(tx),
fmt_num(ty),
)
.as_bytes(),
);
self.write_hex_string(encoded_bytes);
self.buf.extend_from_slice(b"> Tj ET");
}
pub fn text_rotated(&mut self, font_resource: &str, size: f32, rotation: TextRotation, color: Rgb, encoded_bytes: &[u8]) {
let rad = rotation.angle_deg.to_radians();
let cos = rad.cos();
let sin = rad.sin();
self.buf.extend_from_slice(
format!(
"q {} {} {} {} {} {} cm ",
fmt_num(cos),
fmt_num(sin),
fmt_num(-sin),
fmt_num(cos),
fmt_num(rotation.cx),
fmt_num(rotation.cy),
)
.as_bytes(),
);
self.text_block(font_resource, size, color, -rotation.half_width, 0.0, encoded_bytes);
self.buf.extend_from_slice(b" Q\n");
}
pub fn draw_image(&mut self, image_resource: &str, x: f32, y: f32, w: f32, h: f32) {
self.op(&format!(
"q {} 0 0 {} {} {} cm /{} Do Q",
fmt_num(w),
fmt_num(h),
fmt_num(x),
fmt_num(y),
image_resource
));
}
pub fn into_bytes(self) -> Vec<u8> {
self.buf
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_expected_operators() {
let mut c = ContentBuilder::new();
c.save();
c.clip_rect(0.0, 0.0, 100.0, 50.0);
c.text("F1", 12.0, 10.0, 20.0, Rgb(0, 0, 0), &[0x00, 0x01, 0x00, 0x02, 0x00, 0x03]);
c.restore();
let bytes = c.into_bytes();
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("q\n"));
assert!(s.contains("0 0 100 50 re W n"));
assert!(s.contains("<000100020003> Tj"));
assert!(s.trim_end().ends_with('Q'));
}
#[test]
fn draw_image_emits_the_cm_do_idiom() {
let mut c = ContentBuilder::new();
c.draw_image("Im1", 10.0, 20.0, 100.0, 50.0);
let s = String::from_utf8(c.into_bytes()).unwrap();
assert!(s.contains("100 0 0 50 10 20 cm /Im1 Do"));
}
#[test]
fn text_rotated_emits_a_rotation_matrix_and_centers_horizontally() {
let mut c = ContentBuilder::new();
c.text_rotated(
"F1",
72.0,
TextRotation {
cx: 300.0,
cy: 400.0,
angle_deg: 45.0,
half_width: 50.0,
},
Rgb(210, 210, 210),
&[0x00, 0x01],
);
let s = String::from_utf8(c.into_bytes()).unwrap();
assert!(s.contains("0.707 0.707 -0.707 0.707 300 400 cm"));
assert!(
s.contains("-50 0 Td"),
"text must be horizontally centered via a -half_width offset"
);
assert!(s.trim_end().ends_with("Q"));
}
}