use crate::color::Color;
use crate::font::BuiltinFont;
#[derive(Debug, Clone, Copy)]
pub enum LineCap {
Butt = 0,
Round = 1,
Square = 2,
}
#[derive(Debug, Clone, Copy)]
pub enum LineJoin {
Miter = 0,
Round = 1,
Bevel = 2,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextAlign {
Left,
Center,
Right,
}
pub struct ContentStream {
ops: Vec<String>,
}
impl ContentStream {
pub fn new() -> Self {
Self { ops: Vec::new() }
}
fn push(&mut self, op: impl Into<String>) -> &mut Self {
self.ops.push(op.into());
self
}
pub fn save(&mut self) -> &mut Self {
self.push("q")
}
pub fn restore(&mut self) -> &mut Self {
self.push("Q")
}
pub fn line_width(&mut self, w: f64) -> &mut Self {
self.push(format!("{:.4} w", w))
}
pub fn line_cap(&mut self, cap: LineCap) -> &mut Self {
self.push(format!("{} J", cap as u8))
}
pub fn line_join(&mut self, join: LineJoin) -> &mut Self {
self.push(format!("{} j", join as u8))
}
pub fn dash_pattern(&mut self, dash: &[f64], phase: f64) -> &mut Self {
let parts: Vec<String> = dash.iter().map(|d| format!("{:.2}", d)).collect();
self.push(format!("[{}] {:.2} d", parts.join(" "), phase))
}
pub fn fill_color(&mut self, color: Color) -> &mut Self {
self.push(color.fill_op())
}
pub fn stroke_color(&mut self, color: Color) -> &mut Self {
self.push(color.stroke_op())
}
pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
self.push(format!("{:.4} {:.4} m", x, y))
}
pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
self.push(format!("{:.4} {:.4} l", x, y))
}
pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
self.push(format!("{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} c", x1, y1, x2, y2, x3, y3))
}
pub fn close_path(&mut self) -> &mut Self {
self.push("h")
}
pub fn stroke(&mut self) -> &mut Self {
self.push("S")
}
pub fn fill(&mut self) -> &mut Self {
self.push("f")
}
pub fn fill_stroke(&mut self) -> &mut Self {
self.push("B")
}
pub fn end_path(&mut self) -> &mut Self {
self.push("n")
}
pub fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) -> &mut Self {
self.push(format!("{:.4} {:.4} {:.4} {:.4} re", x, y, w, h))
}
pub fn filled_rect(&mut self, x: f64, y: f64, w: f64, h: f64, color: Color) -> &mut Self {
self.save();
self.fill_color(color);
self.rect(x, y, w, h);
self.fill();
self.restore();
self
}
pub fn stroked_rect(
&mut self,
x: f64, y: f64, w: f64, h: f64,
color: Color,
line_w: f64,
) -> &mut Self {
self.save();
self.stroke_color(color);
self.line_width(line_w);
self.rect(x, y, w, h);
self.stroke();
self.restore();
self
}
pub fn line(
&mut self,
x1: f64, y1: f64, x2: f64, y2: f64,
color: Color,
width: f64,
) -> &mut Self {
self.save();
self.stroke_color(color);
self.line_width(width);
self.move_to(x1, y1);
self.line_to(x2, y2);
self.stroke();
self.restore();
self
}
pub fn circle(&mut self, cx: f64, cy: f64, r: f64) -> &mut Self {
let k = 0.5523 * r;
self.move_to(cx + r, cy);
self.curve_to(cx + r, cy + k, cx + k, cy + r, cx, cy + r);
self.curve_to(cx - k, cy + r, cx - r, cy + k, cx - r, cy);
self.curve_to(cx - r, cy - k, cx - k, cy - r, cx, cy - r);
self.curve_to(cx + k, cy - r, cx + r, cy - k, cx + r, cy);
self.close_path();
self
}
pub fn begin_text(&mut self) -> &mut Self {
self.push("BT")
}
pub fn end_text(&mut self) -> &mut Self {
self.push("ET")
}
pub fn set_font(&mut self, font_key: &str, size: f64) -> &mut Self {
self.push(format!("/{} {:.4} Tf", font_key, size))
}
pub fn text_position(&mut self, x: f64, y: f64) -> &mut Self {
self.push(format!("{:.4} {:.4} Td", x, y))
}
pub fn text_matrix(&mut self, x: f64, y: f64) -> &mut Self {
self.push(format!("1 0 0 1 {:.4} {:.4} Tm", x, y))
}
pub fn show_text(&mut self, text: &str) -> &mut Self {
let escaped = pdf_string_escape(text);
self.push(format!("({}) Tj", escaped))
}
pub fn text_leading(&mut self, leading: f64) -> &mut Self {
self.push(format!("{:.4} TL", leading))
}
pub fn next_line_text(&mut self, text: &str) -> &mut Self {
let escaped = pdf_string_escape(text);
self.push(format!("({}) '", escaped))
}
pub fn char_spacing(&mut self, spacing: f64) -> &mut Self {
self.push(format!("{:.4} Tc", spacing))
}
pub fn word_spacing(&mut self, spacing: f64) -> &mut Self {
self.push(format!("{:.4} Tw", spacing))
}
pub fn text_rise(&mut self, rise: f64) -> &mut Self {
self.push(format!("{:.4} Ts", rise))
}
pub fn draw_text(
&mut self,
text: &str,
x: f64,
y: f64,
font_key: &str,
font: BuiltinFont,
size: f64,
color: Color,
align: TextAlign,
) -> &mut Self {
let actual_x = match align {
TextAlign::Left => x,
TextAlign::Center => x - font.string_width(text, size) / 2.0,
TextAlign::Right => x - font.string_width(text, size),
};
self.save();
self.fill_color(color);
self.begin_text();
self.set_font(font_key, size);
self.text_matrix(actual_x, y);
self.show_text(text);
self.end_text();
self.restore();
self
}
pub fn draw_text_box(
&mut self,
text: &str,
x: f64,
y: f64,
width: f64,
font_key: &str,
font: BuiltinFont,
size: f64,
color: Color,
line_height: f64,
) -> f64 {
let words: Vec<&str> = text.split_whitespace().collect();
let space_w = font.char_width(' ') * size / 1000.0;
let mut lines: Vec<String> = Vec::new();
let mut current_line = String::new();
let mut current_width = 0.0;
for word in &words {
let word_w = font.string_width(word, size);
if current_line.is_empty() {
current_line.push_str(word);
current_width = word_w;
} else if current_width + space_w + word_w <= width {
current_line.push(' ');
current_line.push_str(word);
current_width += space_w + word_w;
} else {
lines.push(current_line.clone());
current_line = word.to_string();
current_width = word_w;
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
self.save();
self.fill_color(color);
self.begin_text();
self.set_font(font_key, size);
let mut cur_y = y;
for line in &lines {
self.text_matrix(x, cur_y);
self.show_text(line);
cur_y -= line_height;
}
self.end_text();
self.restore();
cur_y
}
pub fn draw_image(&mut self, image_key: &str, x: f64, y: f64, w: f64, h: f64) -> &mut Self {
self.save();
self.push(format!("{:.4} 0 0 {:.4} {:.4} {:.4} cm", w, h, x, y));
self.push(format!("/{} Do", image_key));
self.restore();
self
}
pub fn clip(&mut self) -> &mut Self {
self.push("W")
}
pub fn transform(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> &mut Self {
self.push(format!("{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} cm", a, b, c, d, e, f))
}
pub fn translate(&mut self, tx: f64, ty: f64) -> &mut Self {
self.transform(1.0, 0.0, 0.0, 1.0, tx, ty)
}
pub fn scale(&mut self, sx: f64, sy: f64) -> &mut Self {
self.transform(sx, 0.0, 0.0, sy, 0.0, 0.0)
}
pub fn rotate(&mut self, angle: f64) -> &mut Self {
let cos = angle.cos();
let sin = angle.sin();
self.transform(cos, sin, -sin, cos, 0.0, 0.0)
}
pub fn to_bytes(&self) -> Vec<u8> {
self.ops.join("\n").into_bytes()
}
}
impl ContentStream {
pub fn push_raw(&mut self, op: impl Into<String>) -> &mut Self {
self.ops.push(op.into());
self
}
}
pub fn escape_for_stream(text: &str) -> String {
pdf_string_escape(text)
}
fn pdf_string_escape(text: &str) -> String {
let mut out = String::new();
for c in text.chars() {
match c {
'(' => out.push_str("\\("),
')' => out.push_str("\\)"),
'\\' => out.push_str("\\\\"),
'\r' => out.push_str("\\r"),
'\n' => out.push_str("\\n"),
c if c as u32 > 127 => {
let code = c as u32;
if (0x80..=0xFF).contains(&code) {
out.push_str(&format!("\\{:03o}", code));
} else {
let replacement = match c {
'\u{2014}' => "--", '\u{2013}' => "-", '\u{2018}' | '\u{2019}' => "'", '\u{201C}' | '\u{201D}' => "\"", '\u{2026}' => "...", '\u{00A0}' => " ", _ => "?",
};
out.push_str(replacement);
}
}
_ => out.push(c),
}
}
out
}