use crate::color::Color;
use crate::content::ContentStream;
use crate::font::BuiltinFont;
use crate::object::{PdfDict, PdfObject};
#[derive(Debug, Clone, Copy)]
pub enum WatermarkLayer {
Behind,
Over,
}
#[derive(Debug, Clone)]
pub struct Watermark {
pub text: String,
pub font: BuiltinFont,
pub font_size: f64,
pub color: Color,
pub opacity: f64,
pub rotation_deg: f64,
pub layer: WatermarkLayer,
}
impl Watermark {
pub fn diagonal(text: impl Into<String>) -> Self {
Self {
text: text.into(),
font: BuiltinFont::HelveticaBold,
font_size: 60.0,
color: Color::rgb_u8(180, 180, 180),
opacity: 0.35,
rotation_deg: 45.0,
layer: WatermarkLayer::Over,
}
}
pub fn font(mut self, f: BuiltinFont) -> Self { self.font = f; self }
pub fn font_size(mut self, s: f64) -> Self { self.font_size = s; self }
pub fn color(mut self, c: Color) -> Self { self.color = c; self }
pub fn opacity(mut self, o: f64) -> Self { self.opacity = o.clamp(0.0, 1.0); self }
pub fn rotation(mut self, deg: f64) -> Self { self.rotation_deg = deg; self }
pub fn layer(mut self, l: WatermarkLayer) -> Self { self.layer = l; self }
pub fn render_to_stream(&self, page_w: f64, page_h: f64) -> Vec<u8> {
let mut cs = ContentStream::new();
let font_key = "WmF1Reg"; let text_w = self.font.string_width(&self.text, self.font_size);
cs.save();
cs.push_raw("q");
cs.push_raw(format!("/WmGS gs"));
let cx = page_w / 2.0;
let cy = page_h / 2.0;
let angle = self.rotation_deg.to_radians();
let cos = angle.cos();
let sin = angle.sin();
let tx = cx - cos * text_w / 2.0 + sin * self.font_size / 4.0;
let ty = cy - sin * text_w / 2.0 - cos * self.font_size / 4.0;
cs.push_raw(format!(
"{:.4} {:.4} {:.4} {:.4} {:.4} {:.4} cm",
cos, sin, -sin, cos, tx, ty
));
cs.push_raw(self.color.fill_op());
cs.push_raw("BT");
cs.push_raw(format!("/{} {:.2} Tf", font_key, self.font_size));
cs.push_raw("0 0 Td");
let escaped = crate::content::escape_for_stream(&self.text);
cs.push_raw(format!("({}) Tj", escaped));
cs.push_raw("ET");
cs.push_raw("Q");
cs.restore();
cs.to_bytes()
}
pub fn ext_gstate(&self) -> PdfDict {
let mut gs = PdfDict::new();
let op = PdfObject::Real(self.opacity);
gs.set("ca", op.clone()); gs.set("CA", op); gs.set("Type", PdfObject::name("ExtGState"));
gs
}
pub fn font_resource(&self) -> PdfDict {
let mut f = PdfDict::new();
f.set("Type", PdfObject::name("Font"));
f.set("Subtype", PdfObject::name("Type1"));
f.set("BaseFont", PdfObject::name(self.font.pdf_name()));
f.set("Encoding", PdfObject::name("WinAnsiEncoding"));
f
}
}
#[derive(Debug, Clone, Copy)]
pub enum HFAlign {
Left,
Center,
Right,
}
#[derive(Debug, Clone)]
pub struct HFSlot {
pub text: String, pub align: HFAlign,
pub font: BuiltinFont,
pub font_size: f64,
pub color: Color,
}
impl HFSlot {
pub fn new(text: impl Into<String>, align: HFAlign) -> Self {
Self {
text: text.into(),
align,
font: BuiltinFont::Helvetica,
font_size: 9.0,
color: Color::rgb_u8(100, 100, 100),
}
}
pub fn font(mut self, f: BuiltinFont) -> Self { self.font = f; self }
pub fn font_size(mut self, s: f64) -> Self { self.font_size = s; self }
pub fn color(mut self, c: Color) -> Self { self.color = c; self }
}
#[derive(Debug, Clone)]
pub struct HeaderFooter {
pub left: Option<HFSlot>,
pub center: Option<HFSlot>,
pub right: Option<HFSlot>,
pub height: f64,
pub separator_line: bool,
pub separator_color: Color,
}
impl Default for HeaderFooter {
fn default() -> Self {
Self {
left: None,
center: None,
right: None,
height: 20.0,
separator_line: true,
separator_color: Color::rgb_u8(200, 200, 200),
}
}
}
impl HeaderFooter {
pub fn new() -> Self {
Self::default()
}
pub fn left(mut self, slot: HFSlot) -> Self { self.left = Some(slot); self }
pub fn center(mut self, slot: HFSlot) -> Self { self.center = Some(slot); self }
pub fn right(mut self, slot: HFSlot) -> Self { self.right = Some(slot); self }
pub fn height(mut self, h: f64) -> Self { self.height = h; self }
pub fn no_line(mut self) -> Self { self.separator_line = false; self }
pub fn render(
&self,
page_w: f64,
page_h: f64,
page_num: usize,
page_total: usize,
is_header: bool,
) -> Vec<u8> {
let mut cs = ContentStream::new();
let margin = 40.0;
let usable = page_w - 2.0 * margin;
let band_y = if is_header { page_h - self.height } else { 0.0 };
let text_y = band_y + 6.0;
let bg = Color::rgb_u8(245, 246, 248);
cs.push_raw(bg.fill_op());
cs.push_raw(format!("{:.4} {:.4} {:.4} {:.4} re f", 0.0, band_y, page_w, self.height));
if self.separator_line {
let line_y = if is_header { band_y } else { self.height };
cs.push_raw(self.separator_color.stroke_op());
cs.push_raw("0.5 w");
cs.push_raw(format!("{:.4} {:.4} m {:.4} {:.4} l S", 0.0, line_y, page_w, line_y));
}
for slot in [&self.left, &self.center, &self.right].iter().filter_map(|s| s.as_ref()) {
let resolved = slot.text
.replace("{page}", &page_num.to_string())
.replace("{total}", &page_total.to_string());
let text_w = slot.font.string_width(&resolved, slot.font_size);
let x = match slot.align {
HFAlign::Left => margin,
HFAlign::Center => margin + usable / 2.0 - text_w / 2.0,
HFAlign::Right => margin + usable - text_w,
};
let fk = match slot.align {
HFAlign::Left => "HFL",
HFAlign::Center => "HFC",
HFAlign::Right => "HFR",
};
cs.push_raw(slot.color.fill_op());
cs.push_raw("BT");
cs.push_raw(format!("/{} {:.2} Tf", fk, slot.font_size));
cs.push_raw(format!("{:.4} {:.4} Td", x, text_y));
let esc = crate::content::escape_for_stream(&resolved);
cs.push_raw(format!("({}) Tj", esc));
cs.push_raw("ET");
}
cs.to_bytes()
}
pub fn font_resources(&self) -> Vec<(&'static str, PdfDict)> {
let slots = [
("HFL", &self.left),
("HFC", &self.center),
("HFR", &self.right),
];
let mut out = Vec::new();
for (key, slot_opt) in &slots {
if let Some(slot) = slot_opt {
let mut f = PdfDict::new();
f.set("Type", PdfObject::name("Font"));
f.set("Subtype", PdfObject::name("Type1"));
f.set("BaseFont", PdfObject::name(slot.font.pdf_name()));
f.set("Encoding", PdfObject::name("WinAnsiEncoding"));
out.push((*key, f));
}
}
out
}
}