use crate::writer::fmt_num;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Rgb(pub u8, pub u8, pub u8);
fn color_component(c: u8) -> String {
fmt_num(c as f32 / 255.0)
}
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 clip_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
self.op(&format!("{} {} {} {} re W n", fmt_num(x), fmt_num(y), fmt_num(w), fmt_num(h)));
}
pub fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Rgb) {
self.op(&format!(
"{} {} {} rg {} {} {} {} re f",
color_component(color.0),
color_component(color.1),
color_component(color.2),
fmt_num(x),
fmt_num(y),
fmt_num(w),
fmt_num(h)
));
}
pub fn stroke_rect(&mut self, x: f32, y: f32, w: f32, h: f32, line_width: f32, color: Rgb) {
self.op(&format!(
"{} {} {} RG {} w {} {} {} {} re S",
color_component(color.0),
color_component(color.1),
color_component(color.2),
fmt_num(line_width),
fmt_num(x),
fmt_num(y),
fmt_num(w),
fmt_num(h)
));
}
pub fn line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, line_width: f32, color: Rgb) {
self.op(&format!(
"{} {} {} RG {} w {} {} m {} {} l S",
color_component(color.0),
color_component(color.1),
color_component(color.2),
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.buf.extend_from_slice(
format!(
"BT /{} {} Tf {} {} {} rg {} {} Td <",
font_resource,
fmt_num(size),
color_component(color.0),
color_component(color.1),
color_component(color.2),
fmt_num(x),
fmt_num(y),
)
.as_bytes(),
);
for b in encoded_bytes {
self.buf.extend_from_slice(format!("{b:02X}").as_bytes());
}
self.buf.extend_from_slice(b"> Tj ET\n");
}
#[allow(clippy::too_many_arguments)]
pub fn text_rotated(
&mut self,
font_resource: &str,
size: f32,
cx: f32,
cy: f32,
angle_deg: f32,
half_width: f32,
color: Rgb,
encoded_bytes: &[u8],
) {
let rad = angle_deg.to_radians();
let cos = rad.cos();
let sin = rad.sin();
self.buf.extend_from_slice(
format!(
"q {} {} {} {} {} {} cm BT /{} {} Tf {} {} {} rg {} 0 Td <",
fmt_num(cos),
fmt_num(sin),
fmt_num(-sin),
fmt_num(cos),
fmt_num(cx),
fmt_num(cy),
font_resource,
fmt_num(size),
color_component(color.0),
color_component(color.1),
color_component(color.2),
fmt_num(-half_width),
)
.as_bytes(),
);
for b in encoded_bytes {
self.buf.extend_from_slice(format!("{b:02X}").as_bytes());
}
self.buf.extend_from_slice(b"> Tj ET 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, 300.0, 400.0, 45.0, 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"));
}
}