use fop_layout::area::BorderStyle;
use fop_types::{Color, Length, Result};
use std::fmt::Write as FmtWrite;
pub struct PdfGraphics {
operations: String,
}
impl PdfGraphics {
pub fn new() -> Self {
Self {
operations: String::new(),
}
}
pub fn content(&self) -> &str {
&self.operations
}
pub fn set_stroke_color(&mut self, color: Color) -> Result<()> {
writeln!(
&mut self.operations,
"{:.3} {:.3} {:.3} RG",
color.r_f32(),
color.g_f32(),
color.b_f32()
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
pub fn set_fill_color(&mut self, color: Color) -> Result<()> {
writeln!(
&mut self.operations,
"{:.3} {:.3} {:.3} rg",
color.r_f32(),
color.g_f32(),
color.b_f32()
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
pub fn set_opacity(&mut self, gs_name: &str) -> Result<()> {
writeln!(&mut self.operations, "/{} gs", gs_name)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
pub fn set_stroke_opacity(&mut self, gs_name: &str) -> Result<()> {
writeln!(&mut self.operations, "/{} gs", gs_name)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
pub fn set_line_width(&mut self, width: Length) -> Result<()> {
writeln!(&mut self.operations, "{:.3} w", width.to_pt())
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
pub fn set_dash_pattern(&mut self, dash_array: &[f64], phase: f64) -> Result<()> {
write!(&mut self.operations, "[")
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
for (i, &dash) in dash_array.iter().enumerate() {
if i > 0 {
write!(&mut self.operations, " ")
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
}
write!(&mut self.operations, "{:.3}", dash)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
}
writeln!(&mut self.operations, "] {:.3} d", phase)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
pub fn draw_rectangle(
&mut self,
x: Length,
y: Length,
width: Length,
height: Length,
) -> Result<()> {
writeln!(
&mut self.operations,
"{:.3} {:.3} {:.3} {:.3} re S",
x.to_pt(),
y.to_pt(),
width.to_pt(),
height.to_pt()
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
pub fn fill_rectangle(
&mut self,
x: Length,
y: Length,
width: Length,
height: Length,
) -> Result<()> {
self.fill_rectangle_with_radius(x, y, width, height, None)
}
pub fn fill_rectangle_with_radius(
&mut self,
x: Length,
y: Length,
width: Length,
height: Length,
border_radius: Option<[Length; 4]>,
) -> Result<()> {
if let Some(radii) = border_radius {
self.draw_rounded_rectangle(x, y, width, height, radii, true)
} else {
writeln!(
&mut self.operations,
"{:.3} {:.3} {:.3} {:.3} re f",
x.to_pt(),
y.to_pt(),
width.to_pt(),
height.to_pt()
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
}
#[allow(clippy::too_many_arguments)]
pub fn draw_rounded_rectangle(
&mut self,
x: Length,
y: Length,
width: Length,
height: Length,
radii: [Length; 4],
fill: bool,
) -> Result<()> {
let x_pt = x.to_pt();
let y_pt = y.to_pt();
let w_pt = width.to_pt();
let h_pt = height.to_pt();
let [tl, tr, br, bl] = radii;
let tl_pt = tl.to_pt().min(w_pt / 2.0).min(h_pt / 2.0);
let tr_pt = tr.to_pt().min(w_pt / 2.0).min(h_pt / 2.0);
let br_pt = br.to_pt().min(w_pt / 2.0).min(h_pt / 2.0);
let bl_pt = bl.to_pt().min(w_pt / 2.0).min(h_pt / 2.0);
const KAPPA: f64 = 0.552284749831;
write!(&mut self.operations, "{:.3} {:.3} m ", x_pt + bl_pt, y_pt)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
write!(
&mut self.operations,
"{:.3} {:.3} l ",
x_pt + w_pt - br_pt,
y_pt
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
if br_pt > 0.0 {
write!(
&mut self.operations,
"{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c ",
x_pt + w_pt - br_pt + br_pt * KAPPA,
y_pt,
x_pt + w_pt,
y_pt + br_pt - br_pt * KAPPA,
x_pt + w_pt,
y_pt + br_pt
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
}
write!(
&mut self.operations,
"{:.3} {:.3} l ",
x_pt + w_pt,
y_pt + h_pt - tr_pt
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
if tr_pt > 0.0 {
write!(
&mut self.operations,
"{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c ",
x_pt + w_pt,
y_pt + h_pt - tr_pt + tr_pt * KAPPA,
x_pt + w_pt - tr_pt + tr_pt * KAPPA,
y_pt + h_pt,
x_pt + w_pt - tr_pt,
y_pt + h_pt
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
}
write!(
&mut self.operations,
"{:.3} {:.3} l ",
x_pt + tl_pt,
y_pt + h_pt
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
if tl_pt > 0.0 {
write!(
&mut self.operations,
"{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c ",
x_pt + tl_pt - tl_pt * KAPPA,
y_pt + h_pt,
x_pt,
y_pt + h_pt - tl_pt + tl_pt * KAPPA,
x_pt,
y_pt + h_pt - tl_pt
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
}
write!(&mut self.operations, "{:.3} {:.3} l ", x_pt, y_pt + bl_pt)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
if bl_pt > 0.0 {
write!(
&mut self.operations,
"{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c ",
x_pt,
y_pt + bl_pt - bl_pt * KAPPA,
x_pt + bl_pt - bl_pt * KAPPA,
y_pt,
x_pt + bl_pt,
y_pt
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
}
if fill {
writeln!(&mut self.operations, "f")
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
} else {
writeln!(&mut self.operations, "S")
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
}
Ok(())
}
pub fn draw_line(&mut self, x1: Length, y1: Length, x2: Length, y2: Length) -> Result<()> {
writeln!(
&mut self.operations,
"{:.3} {:.3} m {:.3} {:.3} l S",
x1.to_pt(),
y1.to_pt(),
x2.to_pt(),
y2.to_pt()
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn draw_borders(
&mut self,
x: Length,
y: Length,
width: Length,
height: Length,
border_widths: [Length; 4], border_colors: [Color; 4],
border_styles: [BorderStyle; 4],
) -> Result<()> {
self.draw_borders_with_radius(
x,
y,
width,
height,
border_widths,
border_colors,
border_styles,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub fn draw_borders_with_radius(
&mut self,
x: Length,
y: Length,
width: Length,
height: Length,
border_widths: [Length; 4], border_colors: [Color; 4],
border_styles: [BorderStyle; 4],
border_radius: Option<[Length; 4]>, ) -> Result<()> {
let [top_width, right_width, bottom_width, left_width] = border_widths;
let [top_color, right_color, bottom_color, left_color] = border_colors;
let [top_style, right_style, bottom_style, left_style] = border_styles;
if let Some(radii) = border_radius {
let uniform_color =
top_color == right_color && top_color == bottom_color && top_color == left_color;
let uniform_width =
top_width == right_width && top_width == bottom_width && top_width == left_width;
let uniform_style =
top_style == right_style && top_style == bottom_style && top_style == left_style;
if uniform_color
&& uniform_width
&& uniform_style
&& top_width > Length::ZERO
&& !matches!(top_style, BorderStyle::None | BorderStyle::Hidden)
{
self.set_stroke_color(top_color)?;
self.set_line_width(top_width)?;
self.apply_border_style(top_style)?;
self.draw_rounded_rectangle(x, y, width, height, radii, false)?;
self.set_dash_pattern(&[], 0.0)?;
return Ok(());
}
}
if top_width > Length::ZERO && !matches!(top_style, BorderStyle::None | BorderStyle::Hidden)
{
self.set_stroke_color(top_color)?;
self.set_line_width(top_width)?;
self.apply_border_style(top_style)?;
let y_top = y + height;
self.draw_line(x, y_top, x + width, y_top)?;
self.set_dash_pattern(&[], 0.0)?;
}
if right_width > Length::ZERO
&& !matches!(right_style, BorderStyle::None | BorderStyle::Hidden)
{
self.set_stroke_color(right_color)?;
self.set_line_width(right_width)?;
self.apply_border_style(right_style)?;
let x_right = x + width;
self.draw_line(x_right, y, x_right, y + height)?;
self.set_dash_pattern(&[], 0.0)?;
}
if bottom_width > Length::ZERO
&& !matches!(bottom_style, BorderStyle::None | BorderStyle::Hidden)
{
self.set_stroke_color(bottom_color)?;
self.set_line_width(bottom_width)?;
self.apply_border_style(bottom_style)?;
self.draw_line(x, y, x + width, y)?;
self.set_dash_pattern(&[], 0.0)?;
}
if left_width > Length::ZERO
&& !matches!(left_style, BorderStyle::None | BorderStyle::Hidden)
{
self.set_stroke_color(left_color)?;
self.set_line_width(left_width)?;
self.apply_border_style(left_style)?;
self.draw_line(x, y, x, y + height)?;
self.set_dash_pattern(&[], 0.0)?;
}
Ok(())
}
fn apply_border_style(&mut self, style: BorderStyle) -> Result<()> {
match style {
BorderStyle::Solid => self.set_dash_pattern(&[], 0.0),
BorderStyle::Dashed => self.set_dash_pattern(&[6.0, 3.0], 0.0),
BorderStyle::Dotted => self.set_dash_pattern(&[1.0, 2.0], 0.0),
_ => self.set_dash_pattern(&[], 0.0),
}
}
pub fn save_state(&mut self) -> Result<()> {
writeln!(&mut self.operations, "q")
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
pub fn restore_state(&mut self) -> Result<()> {
writeln!(&mut self.operations, "Q")
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
pub fn save_clip_state(
&mut self,
x: Length,
y: Length,
width: Length,
height: Length,
) -> Result<()> {
writeln!(&mut self.operations, "q")
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
writeln!(
&mut self.operations,
"{:.3} {:.3} {:.3} {:.3} re W n",
x.to_pt(),
y.to_pt(),
width.to_pt(),
height.to_pt()
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
pub fn restore_clip_state(&mut self) -> Result<()> {
self.restore_state()
}
pub fn fill_gradient(
&mut self,
x: Length,
y: Length,
width: Length,
height: Length,
gradient_index: usize,
) -> Result<()> {
writeln!(&mut self.operations, "q")
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
writeln!(
&mut self.operations,
"{:.3} 0 0 {:.3} {:.3} {:.3} cm",
width.to_pt() / 100.0, height.to_pt() / 100.0, x.to_pt(),
y.to_pt()
)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
writeln!(&mut self.operations, "/Sh{} sh", gradient_index)
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
writeln!(&mut self.operations, "Q")
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
Ok(())
}
pub fn fill_gradient_with_radius(
&mut self,
x: Length,
y: Length,
width: Length,
height: Length,
gradient_index: usize,
border_radius: Option<[Length; 4]>,
) -> Result<()> {
if border_radius.is_some() {
self.save_state()?;
if let Some(radii) = border_radius {
self.draw_rounded_rectangle(x, y, width, height, radii, false)?;
writeln!(&mut self.operations, "W n")
.map_err(|e| fop_types::FopError::Generic(e.to_string()))?;
}
self.fill_gradient(x, y, width, height, gradient_index)?;
self.restore_state()?;
} else {
self.fill_gradient(x, y, width, height, gradient_index)?;
}
Ok(())
}
}
impl Default for PdfGraphics {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graphics_creation() {
let graphics = PdfGraphics::new();
assert_eq!(graphics.content(), "");
}
#[test]
fn test_set_stroke_color() {
let mut graphics = PdfGraphics::new();
graphics
.set_stroke_color(Color::RED)
.expect("test: should succeed");
assert!(graphics.content().contains("1.000 0.000 0.000 RG"));
}
#[test]
fn test_set_fill_color() {
let mut graphics = PdfGraphics::new();
graphics
.set_fill_color(Color::BLUE)
.expect("test: should succeed");
assert!(graphics.content().contains("0.000 0.000 1.000 rg"));
}
#[test]
fn test_set_line_width() {
let mut graphics = PdfGraphics::new();
graphics
.set_line_width(Length::from_pt(2.0))
.expect("test: should succeed");
assert!(graphics.content().contains("2.000 w"));
}
#[test]
fn test_draw_rectangle() {
let mut graphics = PdfGraphics::new();
graphics
.draw_rectangle(
Length::from_pt(10.0),
Length::from_pt(20.0),
Length::from_pt(100.0),
Length::from_pt(50.0),
)
.expect("test: should succeed");
assert!(graphics
.content()
.contains("10.000 20.000 100.000 50.000 re S"));
}
#[test]
fn test_fill_rectangle() {
let mut graphics = PdfGraphics::new();
graphics
.fill_rectangle(
Length::from_pt(0.0),
Length::from_pt(0.0),
Length::from_pt(50.0),
Length::from_pt(50.0),
)
.expect("test: should succeed");
assert!(graphics.content().contains("re f"));
}
#[test]
fn test_draw_line() {
let mut graphics = PdfGraphics::new();
graphics
.draw_line(
Length::from_pt(0.0),
Length::from_pt(0.0),
Length::from_pt(100.0),
Length::from_pt(100.0),
)
.expect("test: should succeed");
assert!(graphics.content().contains("m"));
assert!(graphics.content().contains("l S"));
}
#[test]
fn test_draw_borders() {
let mut graphics = PdfGraphics::new();
graphics
.draw_borders(
Length::from_pt(10.0),
Length::from_pt(10.0),
Length::from_pt(100.0),
Length::from_pt(50.0),
[Length::from_pt(1.0); 4],
[Color::BLACK; 4],
[BorderStyle::Solid; 4],
)
.expect("test: should succeed");
assert!(graphics.content().contains("m"));
assert!(graphics.content().contains("l S"));
}
#[test]
fn test_save_restore_state() {
let mut graphics = PdfGraphics::new();
graphics.save_state().expect("test: should succeed");
graphics.restore_state().expect("test: should succeed");
assert!(graphics.content().contains("q"));
assert!(graphics.content().contains("Q"));
}
#[test]
fn test_set_dash_pattern() {
let mut graphics = PdfGraphics::new();
graphics
.set_dash_pattern(&[], 0.0)
.expect("test: should succeed");
assert!(graphics.content().contains("[] 0.000 d"));
let mut graphics2 = PdfGraphics::new();
graphics2
.set_dash_pattern(&[6.0, 3.0], 0.0)
.expect("test: should succeed");
assert!(graphics2.content().contains("[6.000 3.000] 0.000 d"));
let mut graphics3 = PdfGraphics::new();
graphics3
.set_dash_pattern(&[1.0, 2.0], 0.0)
.expect("test: should succeed");
assert!(graphics3.content().contains("[1.000 2.000] 0.000 d"));
}
#[test]
fn test_border_styles() {
let mut graphics = PdfGraphics::new();
graphics
.draw_borders(
Length::from_pt(10.0),
Length::from_pt(10.0),
Length::from_pt(100.0),
Length::from_pt(50.0),
[Length::from_pt(2.0); 4],
[Color::RED; 4],
[BorderStyle::Dashed; 4],
)
.expect("test: should succeed");
assert!(graphics.content().contains("[6.000 3.000] 0.000 d"));
assert!(graphics.content().contains("1.000 0.000 0.000 RG")); }
}
#[cfg(test)]
mod tests_extended {
use super::*;
#[test]
fn test_set_stroke_color_green() {
let mut g = PdfGraphics::new();
g.set_stroke_color(Color::GREEN)
.expect("test: should succeed");
assert!(g.content().contains("0.000 1.000 0.000 RG"));
}
#[test]
fn test_set_stroke_color_black() {
let mut g = PdfGraphics::new();
g.set_stroke_color(Color::BLACK)
.expect("test: should succeed");
assert!(g.content().contains("0.000 0.000 0.000 RG"));
}
#[test]
fn test_set_stroke_color_white() {
let mut g = PdfGraphics::new();
g.set_stroke_color(Color::WHITE)
.expect("test: should succeed");
assert!(g.content().contains("1.000 1.000 1.000 RG"));
}
#[test]
fn test_set_fill_color_red() {
let mut g = PdfGraphics::new();
g.set_fill_color(Color::RED).expect("test: should succeed");
assert!(g.content().contains("1.000 0.000 0.000 rg"));
}
#[test]
fn test_set_fill_color_green() {
let mut g = PdfGraphics::new();
g.set_fill_color(Color::GREEN)
.expect("test: should succeed");
assert!(g.content().contains("0.000 1.000 0.000 rg"));
}
#[test]
fn test_set_fill_color_custom_rgb() {
let mut g = PdfGraphics::new();
g.set_fill_color(Color::rgb(128, 64, 32))
.expect("test: should succeed");
let content = g.content().to_string();
assert!(content.contains("rg"), "fill operator missing: {}", content);
assert!(!content.contains("RG"), "should use lowercase rg not RG");
}
#[test]
fn test_stroke_uses_rg_uppercase_operator() {
let mut g = PdfGraphics::new();
g.set_stroke_color(Color::BLUE)
.expect("test: should succeed");
assert!(g.content().contains("RG"));
assert!(!g.content().contains(" rg"));
}
#[test]
fn test_fill_uses_rg_lowercase_operator() {
let mut g = PdfGraphics::new();
g.set_fill_color(Color::BLUE).expect("test: should succeed");
assert!(g.content().contains(" rg") || g.content().ends_with("rg\n"));
assert!(!g.content().contains("RG"));
}
#[test]
fn test_line_width_zero() {
let mut g = PdfGraphics::new();
g.set_line_width(Length::ZERO)
.expect("test: should succeed");
assert!(g.content().contains("0.000 w"));
}
#[test]
fn test_line_width_fractional() {
let mut g = PdfGraphics::new();
g.set_line_width(Length::from_pt(0.5))
.expect("test: should succeed");
assert!(g.content().contains("0.500 w"));
}
#[test]
fn test_line_width_large() {
let mut g = PdfGraphics::new();
g.set_line_width(Length::from_pt(10.0))
.expect("test: should succeed");
assert!(g.content().contains("10.000 w"));
}
#[test]
fn test_draw_line_uses_m_operator() {
let mut g = PdfGraphics::new();
g.draw_line(
Length::from_pt(5.0),
Length::from_pt(10.0),
Length::from_pt(50.0),
Length::from_pt(100.0),
)
.expect("test: should succeed");
let c = g.content();
assert!(c.contains("5.000 10.000 m"), "expected 'm' operator: {}", c);
assert!(
c.contains("50.000 100.000 l"),
"expected 'l' operator: {}",
c
);
assert!(c.contains("S"), "expected 'S' operator: {}", c);
}
#[test]
fn test_draw_rectangle_uses_re_operator() {
let mut g = PdfGraphics::new();
g.draw_rectangle(
Length::from_pt(1.0),
Length::from_pt(2.0),
Length::from_pt(30.0),
Length::from_pt(40.0),
)
.expect("test: should succeed");
let c = g.content();
assert!(c.contains("re S"), "expected 're S': {}", c);
assert!(c.contains("1.000 2.000 30.000 40.000"));
}
#[test]
fn test_fill_rectangle_uses_re_f_operator() {
let mut g = PdfGraphics::new();
g.fill_rectangle(
Length::from_pt(3.0),
Length::from_pt(4.0),
Length::from_pt(60.0),
Length::from_pt(80.0),
)
.expect("test: should succeed");
let c = g.content();
assert!(c.contains("re f"), "expected 're f': {}", c);
assert!(c.contains("3.000 4.000 60.000 80.000"));
}
#[test]
fn test_draw_line_stroke_operator_s() {
let mut g = PdfGraphics::new();
g.draw_line(
Length::from_pt(0.0),
Length::from_pt(0.0),
Length::from_pt(100.0),
Length::from_pt(0.0),
)
.expect("test: should succeed");
assert!(g.content().contains("l S"), "stroke 'S' missing");
}
#[test]
fn test_fill_rectangle_f_operator() {
let mut g = PdfGraphics::new();
g.fill_rectangle(
Length::ZERO,
Length::ZERO,
Length::from_pt(50.0),
Length::from_pt(50.0),
)
.expect("test: should succeed");
assert!(g.content().contains("re f"));
}
#[test]
fn test_save_state_q_operator() {
let mut g = PdfGraphics::new();
g.save_state().expect("test: should succeed");
assert!(g.content().contains("q\n"), "q operator missing");
}
#[test]
fn test_restore_state_q_operator() {
let mut g = PdfGraphics::new();
g.restore_state().expect("test: should succeed");
assert!(g.content().contains("Q\n"), "Q operator missing");
}
#[test]
fn test_save_restore_nesting() {
let mut g = PdfGraphics::new();
g.save_state().expect("test: should succeed");
g.save_state().expect("test: should succeed");
g.restore_state().expect("test: should succeed");
g.restore_state().expect("test: should succeed");
let c = g.content();
assert_eq!(c.matches("q\n").count(), 2);
assert_eq!(c.matches("Q\n").count(), 2);
}
#[test]
fn test_dash_pattern_single_value() {
let mut g = PdfGraphics::new();
g.set_dash_pattern(&[3.0], 0.0)
.expect("test: should succeed");
assert!(g.content().contains("[3.000] 0.000 d"));
}
#[test]
fn test_dash_pattern_with_phase() {
let mut g = PdfGraphics::new();
g.set_dash_pattern(&[4.0, 2.0], 1.0)
.expect("test: should succeed");
assert!(g.content().contains("[4.000 2.000] 1.000 d"));
}
#[test]
fn test_dash_pattern_reset_to_solid() {
let mut g = PdfGraphics::new();
g.set_dash_pattern(&[6.0, 3.0], 0.0)
.expect("test: should succeed");
g.set_dash_pattern(&[], 0.0).expect("test: should succeed");
let c = g.content();
assert!(c.contains("[] 0.000 d"), "solid reset missing: {}", c);
}
#[test]
fn test_save_clip_state_uses_w_n() {
let mut g = PdfGraphics::new();
g.save_clip_state(
Length::from_pt(10.0),
Length::from_pt(20.0),
Length::from_pt(100.0),
Length::from_pt(50.0),
)
.expect("test: should succeed");
let c = g.content();
assert!(c.contains("q\n"), "q missing: {}", c);
assert!(c.contains("re W n"), "clipping 'W n' missing: {}", c);
assert!(c.contains("10.000 20.000 100.000 50.000"), "coords missing");
}
#[test]
fn test_restore_clip_state_uses_q_operator() {
let mut g = PdfGraphics::new();
g.save_clip_state(
Length::ZERO,
Length::ZERO,
Length::from_pt(200.0),
Length::from_pt(100.0),
)
.expect("test: should succeed");
g.restore_clip_state().expect("test: should succeed");
let c = g.content();
assert!(
c.contains("Q\n"),
"Q operator missing after restore_clip_state"
);
}
#[test]
fn test_fill_gradient_cm_operator() {
let mut g = PdfGraphics::new();
g.fill_gradient(
Length::from_pt(10.0),
Length::from_pt(20.0),
Length::from_pt(100.0),
Length::from_pt(50.0),
0,
)
.expect("test: should succeed");
let c = g.content();
assert!(c.contains("cm"), "cm operator missing: {}", c);
assert!(c.contains("/Sh0 sh"), "shading ref missing: {}", c);
assert!(c.contains("q\n"), "q missing");
assert!(c.contains("Q\n"), "Q missing");
}
#[test]
fn test_fill_gradient_index_increments() {
let mut g = PdfGraphics::new();
g.fill_gradient(
Length::ZERO,
Length::ZERO,
Length::from_pt(50.0),
Length::from_pt(50.0),
1,
)
.expect("test: should succeed");
assert!(g.content().contains("/Sh1 sh"));
}
#[test]
fn test_set_opacity_gs_operator() {
let mut g = PdfGraphics::new();
g.set_opacity("gs1").expect("test: should succeed");
assert!(g.content().contains("/gs1 gs"));
}
#[test]
fn test_set_stroke_opacity_gs_operator() {
let mut g = PdfGraphics::new();
g.set_stroke_opacity("gs2").expect("test: should succeed");
assert!(g.content().contains("/gs2 gs"));
}
#[test]
fn test_draw_borders_dotted() {
let mut g = PdfGraphics::new();
g.draw_borders(
Length::from_pt(5.0),
Length::from_pt(5.0),
Length::from_pt(80.0),
Length::from_pt(40.0),
[Length::from_pt(1.0); 4],
[Color::BLACK; 4],
[BorderStyle::Dotted; 4],
)
.expect("test: should succeed");
assert!(
g.content().contains("[1.000 2.000]"),
"dotted dash pattern missing"
);
}
#[test]
fn test_draw_borders_none_produces_no_lines() {
let mut g = PdfGraphics::new();
g.draw_borders(
Length::from_pt(5.0),
Length::from_pt(5.0),
Length::from_pt(80.0),
Length::from_pt(40.0),
[Length::from_pt(1.0); 4],
[Color::BLACK; 4],
[BorderStyle::None; 4],
)
.expect("test: should succeed");
let c = g.content();
assert!(
!c.contains("l S"),
"none border style should not produce 'l S': {}",
c
);
}
#[test]
fn test_draw_borders_zero_width_produces_no_lines() {
let mut g = PdfGraphics::new();
g.draw_borders(
Length::from_pt(0.0),
Length::from_pt(0.0),
Length::from_pt(100.0),
Length::from_pt(50.0),
[Length::ZERO; 4],
[Color::BLACK; 4],
[BorderStyle::Solid; 4],
)
.expect("test: should succeed");
assert!(!g.content().contains("l S"), "zero-width should not stroke");
}
#[test]
fn test_fill_rectangle_with_radius_uses_bezier() {
let mut g = PdfGraphics::new();
let radii = [Length::from_pt(5.0); 4];
g.fill_rectangle_with_radius(
Length::from_pt(10.0),
Length::from_pt(10.0),
Length::from_pt(100.0),
Length::from_pt(50.0),
Some(radii),
)
.expect("test: should succeed");
let c = g.content();
assert!(c.contains(" c "), "Bezier 'c' operator missing: {}", c);
assert!(c.contains("f\n"), "fill 'f' missing");
}
#[test]
fn test_fill_rectangle_no_radius_uses_simple_re() {
let mut g = PdfGraphics::new();
g.fill_rectangle_with_radius(
Length::from_pt(10.0),
Length::from_pt(10.0),
Length::from_pt(100.0),
Length::from_pt(50.0),
None,
)
.expect("test: should succeed");
let c = g.content();
assert!(c.contains("re f"), "expected 're f': {}", c);
}
#[test]
fn test_default_creates_empty_graphics() {
let g = PdfGraphics::default();
assert_eq!(g.content(), "");
}
}