use crate::error::{PdfError, Result};
use crate::graphics::Color;
use crate::objects::{Dictionary, Object};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ShadingType {
FunctionBased = 1,
Axial = 2,
Radial = 3,
FreeFormGouraud = 4,
LatticeFormGouraud = 5,
CoonsPatch = 6,
TensorProductPatch = 7,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ColorStop {
pub position: f64,
pub color: Color,
}
impl ColorStop {
pub fn new(position: f64, color: Color) -> Self {
Self {
position: position.clamp(0.0, 1.0),
color,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone)]
pub struct AxialShading {
pub name: String,
pub start_point: Point,
pub end_point: Point,
pub color_stops: Vec<ColorStop>,
pub extend_start: bool,
pub extend_end: bool,
}
impl AxialShading {
pub fn new(
name: String,
start_point: Point,
end_point: Point,
color_stops: Vec<ColorStop>,
) -> Self {
Self {
name,
start_point,
end_point,
color_stops,
extend_start: false,
extend_end: false,
}
}
pub fn with_extend(mut self, extend_start: bool, extend_end: bool) -> Self {
self.extend_start = extend_start;
self.extend_end = extend_end;
self
}
pub fn linear_gradient(
name: String,
start_point: Point,
end_point: Point,
start_color: Color,
end_color: Color,
) -> Self {
let color_stops = vec![
ColorStop::new(0.0, start_color),
ColorStop::new(1.0, end_color),
];
Self::new(name, start_point, end_point, color_stops)
}
pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
let mut shading_dict = Dictionary::new();
shading_dict.set("ShadingType", Object::Integer(ShadingType::Axial as i64));
let coords = vec![
Object::Real(self.start_point.x),
Object::Real(self.start_point.y),
Object::Real(self.end_point.x),
Object::Real(self.end_point.y),
];
shading_dict.set("Coords", Object::Array(coords));
shading_dict.set("Function", Object::Integer(1));
let extend = vec![
Object::Boolean(self.extend_start),
Object::Boolean(self.extend_end),
];
shading_dict.set("Extend", Object::Array(extend));
Ok(shading_dict)
}
pub fn validate(&self) -> Result<()> {
if self.color_stops.is_empty() {
return Err(PdfError::InvalidStructure(
"Axial shading must have at least one color stop".to_string(),
));
}
for window in self.color_stops.windows(2) {
if window[0].position > window[1].position {
return Err(PdfError::InvalidStructure(
"Color stops must be in ascending order".to_string(),
));
}
}
if (self.start_point.x - self.end_point.x).abs() < f64::EPSILON
&& (self.start_point.y - self.end_point.y).abs() < f64::EPSILON
{
return Err(PdfError::InvalidStructure(
"Start and end points cannot be the same".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct RadialShading {
pub name: String,
pub start_center: Point,
pub start_radius: f64,
pub end_center: Point,
pub end_radius: f64,
pub color_stops: Vec<ColorStop>,
pub extend_start: bool,
pub extend_end: bool,
}
impl RadialShading {
pub fn new(
name: String,
start_center: Point,
start_radius: f64,
end_center: Point,
end_radius: f64,
color_stops: Vec<ColorStop>,
) -> Self {
Self {
name,
start_center,
start_radius: start_radius.max(0.0),
end_center,
end_radius: end_radius.max(0.0),
color_stops,
extend_start: false,
extend_end: false,
}
}
pub fn with_extend(mut self, extend_start: bool, extend_end: bool) -> Self {
self.extend_start = extend_start;
self.extend_end = extend_end;
self
}
pub fn radial_gradient(
name: String,
center: Point,
start_radius: f64,
end_radius: f64,
start_color: Color,
end_color: Color,
) -> Self {
let color_stops = vec![
ColorStop::new(0.0, start_color),
ColorStop::new(1.0, end_color),
];
Self::new(name, center, start_radius, center, end_radius, color_stops)
}
pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
let mut shading_dict = Dictionary::new();
shading_dict.set("ShadingType", Object::Integer(ShadingType::Radial as i64));
let coords = vec![
Object::Real(self.start_center.x),
Object::Real(self.start_center.y),
Object::Real(self.start_radius),
Object::Real(self.end_center.x),
Object::Real(self.end_center.y),
Object::Real(self.end_radius),
];
shading_dict.set("Coords", Object::Array(coords));
shading_dict.set("Function", Object::Integer(1));
let extend = vec![
Object::Boolean(self.extend_start),
Object::Boolean(self.extend_end),
];
shading_dict.set("Extend", Object::Array(extend));
Ok(shading_dict)
}
pub fn validate(&self) -> Result<()> {
if self.color_stops.is_empty() {
return Err(PdfError::InvalidStructure(
"Radial shading must have at least one color stop".to_string(),
));
}
for window in self.color_stops.windows(2) {
if window[0].position > window[1].position {
return Err(PdfError::InvalidStructure(
"Color stops must be in ascending order".to_string(),
));
}
}
if self.start_radius < 0.0 || self.end_radius < 0.0 {
return Err(PdfError::InvalidStructure(
"Radii cannot be negative".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct FunctionBasedShading {
pub name: String,
pub domain: [f64; 4],
pub matrix: Option<[f64; 6]>,
pub function_id: u32,
}
impl FunctionBasedShading {
pub fn new(name: String, domain: [f64; 4], function_id: u32) -> Self {
Self {
name,
domain,
matrix: None,
function_id,
}
}
pub fn with_matrix(mut self, matrix: [f64; 6]) -> Self {
self.matrix = Some(matrix);
self
}
pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
let mut shading_dict = Dictionary::new();
shading_dict.set(
"ShadingType",
Object::Integer(ShadingType::FunctionBased as i64),
);
let domain = vec![
Object::Real(self.domain[0]),
Object::Real(self.domain[1]),
Object::Real(self.domain[2]),
Object::Real(self.domain[3]),
];
shading_dict.set("Domain", Object::Array(domain));
if let Some(matrix) = self.matrix {
let matrix_objects: Vec<Object> = matrix.iter().map(|&x| Object::Real(x)).collect();
shading_dict.set("Matrix", Object::Array(matrix_objects));
}
shading_dict.set("Function", Object::Integer(self.function_id as i64));
Ok(shading_dict)
}
pub fn validate(&self) -> Result<()> {
if self.domain[0] >= self.domain[1] || self.domain[2] >= self.domain[3] {
return Err(PdfError::InvalidStructure(
"Invalid domain: min values must be less than max values".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ShadingPattern {
pub name: String,
pub shading: ShadingDefinition,
pub matrix: Option<[f64; 6]>,
}
#[derive(Debug, Clone)]
pub enum ShadingDefinition {
Axial(AxialShading),
Radial(RadialShading),
FunctionBased(FunctionBasedShading),
}
impl ShadingDefinition {
pub fn name(&self) -> &str {
match self {
ShadingDefinition::Axial(shading) => &shading.name,
ShadingDefinition::Radial(shading) => &shading.name,
ShadingDefinition::FunctionBased(shading) => &shading.name,
}
}
pub fn validate(&self) -> Result<()> {
match self {
ShadingDefinition::Axial(shading) => shading.validate(),
ShadingDefinition::Radial(shading) => shading.validate(),
ShadingDefinition::FunctionBased(shading) => shading.validate(),
}
}
pub fn to_pdf_dictionary(&self) -> Result<Dictionary> {
match self {
ShadingDefinition::Axial(shading) => shading.to_pdf_dictionary(),
ShadingDefinition::Radial(shading) => shading.to_pdf_dictionary(),
ShadingDefinition::FunctionBased(shading) => shading.to_pdf_dictionary(),
}
}
}
impl ShadingPattern {
pub fn new(name: String, shading: ShadingDefinition) -> Self {
Self {
name,
shading,
matrix: None,
}
}
pub fn with_matrix(mut self, matrix: [f64; 6]) -> Self {
self.matrix = Some(matrix);
self
}
pub fn to_pdf_pattern_dictionary(&self) -> Result<Dictionary> {
let mut pattern_dict = Dictionary::new();
pattern_dict.set("Type", Object::Name("Pattern".to_string()));
pattern_dict.set("PatternType", Object::Integer(2));
pattern_dict.set("Shading", Object::Integer(1));
if let Some(matrix) = self.matrix {
let matrix_objects: Vec<Object> = matrix.iter().map(|&x| Object::Real(x)).collect();
pattern_dict.set("Matrix", Object::Array(matrix_objects));
}
Ok(pattern_dict)
}
pub fn validate(&self) -> Result<()> {
self.shading.validate()
}
}
#[derive(Debug, Clone)]
pub struct ShadingManager {
shadings: HashMap<String, ShadingDefinition>,
patterns: HashMap<String, ShadingPattern>,
next_id: usize,
}
impl Default for ShadingManager {
fn default() -> Self {
Self::new()
}
}
impl ShadingManager {
pub fn new() -> Self {
Self {
shadings: HashMap::new(),
patterns: HashMap::new(),
next_id: 1,
}
}
pub fn add_shading(&mut self, mut shading: ShadingDefinition) -> Result<String> {
shading.validate()?;
let name = shading.name().to_string();
let final_name = if name.is_empty() || self.shadings.contains_key(&name) {
let auto_name = format!("Sh{}", self.next_id);
self.next_id += 1;
match &mut shading {
ShadingDefinition::Axial(s) => s.name = auto_name.clone(),
ShadingDefinition::Radial(s) => s.name = auto_name.clone(),
ShadingDefinition::FunctionBased(s) => s.name = auto_name.clone(),
}
auto_name
} else {
name
};
self.shadings.insert(final_name.clone(), shading);
Ok(final_name)
}
pub fn add_shading_pattern(&mut self, mut pattern: ShadingPattern) -> Result<String> {
pattern.validate()?;
if pattern.name.is_empty() || self.patterns.contains_key(&pattern.name) {
pattern.name = format!("SP{}", self.next_id);
self.next_id += 1;
}
let name = pattern.name.clone();
self.patterns.insert(name.clone(), pattern);
Ok(name)
}
pub fn get_shading(&self, name: &str) -> Option<&ShadingDefinition> {
self.shadings.get(name)
}
pub fn get_pattern(&self, name: &str) -> Option<&ShadingPattern> {
self.patterns.get(name)
}
pub fn shadings(&self) -> &HashMap<String, ShadingDefinition> {
&self.shadings
}
pub fn patterns(&self) -> &HashMap<String, ShadingPattern> {
&self.patterns
}
pub fn clear(&mut self) {
self.shadings.clear();
self.patterns.clear();
self.next_id = 1;
}
pub fn shading_count(&self) -> usize {
self.shadings.len()
}
pub fn pattern_count(&self) -> usize {
self.patterns.len()
}
pub fn total_count(&self) -> usize {
self.shading_count() + self.pattern_count()
}
pub fn create_linear_gradient(
&mut self,
start_point: Point,
end_point: Point,
start_color: Color,
end_color: Color,
) -> Result<String> {
let shading = ShadingDefinition::Axial(AxialShading::linear_gradient(
String::new(), start_point,
end_point,
start_color,
end_color,
));
self.add_shading(shading)
}
pub fn create_radial_gradient(
&mut self,
center: Point,
start_radius: f64,
end_radius: f64,
start_color: Color,
end_color: Color,
) -> Result<String> {
let shading = ShadingDefinition::Radial(RadialShading::radial_gradient(
String::new(), center,
start_radius,
end_radius,
start_color,
end_color,
));
self.add_shading(shading)
}
pub fn to_resource_dictionary(&self) -> Result<String> {
if self.shadings.is_empty() && self.patterns.is_empty() {
return Ok(String::new());
}
let mut dict = String::new();
if !self.shadings.is_empty() {
dict.push_str("/Shading <<");
for name in self.shadings.keys() {
dict.push_str(&format!(" /{} {} 0 R", name, self.next_id));
}
dict.push_str(" >>");
}
if !self.patterns.is_empty() {
if !dict.is_empty() {
dict.push('\n');
}
dict.push_str("/Pattern <<");
for name in self.patterns.keys() {
dict.push_str(&format!(" /{} {} 0 R", name, self.next_id));
}
dict.push_str(" >>");
}
Ok(dict)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_stop_creation() {
let stop = ColorStop::new(0.5, Color::red());
assert_eq!(stop.position, 0.5);
assert_eq!(stop.color, Color::red());
let stop_clamped = ColorStop::new(1.5, Color::blue());
assert_eq!(stop_clamped.position, 1.0);
}
#[test]
fn test_point_creation() {
let point = Point::new(10.0, 20.0);
assert_eq!(point.x, 10.0);
assert_eq!(point.y, 20.0);
}
#[test]
fn test_axial_shading_creation() {
let start = Point::new(0.0, 0.0);
let end = Point::new(100.0, 100.0);
let stops = vec![
ColorStop::new(0.0, Color::red()),
ColorStop::new(1.0, Color::blue()),
];
let shading = AxialShading::new("TestGradient".to_string(), start, end, stops);
assert_eq!(shading.name, "TestGradient");
assert_eq!(shading.start_point, start);
assert_eq!(shading.end_point, end);
assert_eq!(shading.color_stops.len(), 2);
assert!(!shading.extend_start);
assert!(!shading.extend_end);
}
#[test]
fn test_axial_shading_linear_gradient() {
let start = Point::new(0.0, 0.0);
let end = Point::new(100.0, 0.0);
let shading = AxialShading::linear_gradient(
"LinearGrad".to_string(),
start,
end,
Color::red(),
Color::blue(),
);
assert_eq!(shading.color_stops.len(), 2);
assert_eq!(shading.color_stops[0].position, 0.0);
assert_eq!(shading.color_stops[1].position, 1.0);
}
#[test]
fn test_axial_shading_with_extend() {
let start = Point::new(0.0, 0.0);
let end = Point::new(100.0, 0.0);
let shading = AxialShading::linear_gradient(
"ExtendedGrad".to_string(),
start,
end,
Color::red(),
Color::blue(),
)
.with_extend(true, true);
assert!(shading.extend_start);
assert!(shading.extend_end);
}
#[test]
fn test_axial_shading_validation_valid() {
let start = Point::new(0.0, 0.0);
let end = Point::new(100.0, 0.0);
let shading = AxialShading::linear_gradient(
"ValidGrad".to_string(),
start,
end,
Color::red(),
Color::blue(),
);
assert!(shading.validate().is_ok());
}
#[test]
fn test_axial_shading_validation_no_stops() {
let start = Point::new(0.0, 0.0);
let end = Point::new(100.0, 0.0);
let shading = AxialShading::new("EmptyGrad".to_string(), start, end, Vec::new());
assert!(shading.validate().is_err());
}
#[test]
fn test_axial_shading_validation_same_points() {
let point = Point::new(50.0, 50.0);
let shading = AxialShading::linear_gradient(
"SamePointGrad".to_string(),
point,
point,
Color::red(),
Color::blue(),
);
assert!(shading.validate().is_err());
}
#[test]
fn test_radial_shading_creation() {
let center = Point::new(50.0, 50.0);
let stops = vec![
ColorStop::new(0.0, Color::red()),
ColorStop::new(1.0, Color::blue()),
];
let shading =
RadialShading::new("RadialGrad".to_string(), center, 10.0, center, 50.0, stops);
assert_eq!(shading.name, "RadialGrad");
assert_eq!(shading.start_center, center);
assert_eq!(shading.start_radius, 10.0);
assert_eq!(shading.end_radius, 50.0);
}
#[test]
fn test_radial_shading_gradient() {
let center = Point::new(50.0, 50.0);
let shading = RadialShading::radial_gradient(
"SimpleRadial".to_string(),
center,
0.0,
25.0,
Color::white(),
Color::black(),
);
assert_eq!(shading.color_stops.len(), 2);
assert_eq!(shading.start_radius, 0.0);
assert_eq!(shading.end_radius, 25.0);
}
#[test]
fn test_radial_shading_radius_clamping() {
let center = Point::new(50.0, 50.0);
let stops = vec![ColorStop::new(0.0, Color::red())];
let shading = RadialShading::new(
"ClampedRadial".to_string(),
center,
-5.0, center,
10.0,
stops,
);
assert_eq!(shading.start_radius, 0.0);
}
#[test]
fn test_radial_shading_validation_valid() {
let center = Point::new(50.0, 50.0);
let shading = RadialShading::radial_gradient(
"ValidRadial".to_string(),
center,
0.0,
25.0,
Color::red(),
Color::blue(),
);
assert!(shading.validate().is_ok());
}
#[test]
fn test_function_based_shading_creation() {
let domain = [0.0, 1.0, 0.0, 1.0];
let shading = FunctionBasedShading::new("FuncShading".to_string(), domain, 1);
assert_eq!(shading.name, "FuncShading");
assert_eq!(shading.domain, domain);
assert_eq!(shading.function_id, 1);
assert!(shading.matrix.is_none());
}
#[test]
fn test_function_based_shading_with_matrix() {
let domain = [0.0, 1.0, 0.0, 1.0];
let matrix = [2.0, 0.0, 0.0, 2.0, 10.0, 20.0];
let shading =
FunctionBasedShading::new("FuncShading".to_string(), domain, 1).with_matrix(matrix);
assert_eq!(shading.matrix, Some(matrix));
}
#[test]
fn test_function_based_shading_validation_valid() {
let domain = [0.0, 1.0, 0.0, 1.0];
let shading = FunctionBasedShading::new("ValidFunc".to_string(), domain, 1);
assert!(shading.validate().is_ok());
}
#[test]
fn test_function_based_shading_validation_invalid_domain() {
let domain = [1.0, 0.0, 0.0, 1.0]; let shading = FunctionBasedShading::new("InvalidFunc".to_string(), domain, 1);
assert!(shading.validate().is_err());
}
#[test]
fn test_shading_pattern_creation() {
let start = Point::new(0.0, 0.0);
let end = Point::new(100.0, 0.0);
let axial = AxialShading::linear_gradient(
"PatternGrad".to_string(),
start,
end,
Color::red(),
Color::blue(),
);
let shading = ShadingDefinition::Axial(axial);
let pattern = ShadingPattern::new("Pattern1".to_string(), shading);
assert_eq!(pattern.name, "Pattern1");
assert!(pattern.matrix.is_none());
}
#[test]
fn test_shading_pattern_with_matrix() {
let start = Point::new(0.0, 0.0);
let end = Point::new(100.0, 0.0);
let axial = AxialShading::linear_gradient(
"PatternGrad".to_string(),
start,
end,
Color::red(),
Color::blue(),
);
let shading = ShadingDefinition::Axial(axial);
let matrix = [1.0, 0.0, 0.0, 1.0, 50.0, 50.0];
let pattern = ShadingPattern::new("Pattern1".to_string(), shading).with_matrix(matrix);
assert_eq!(pattern.matrix, Some(matrix));
}
#[test]
fn test_shading_manager_creation() {
let manager = ShadingManager::new();
assert_eq!(manager.shading_count(), 0);
assert_eq!(manager.pattern_count(), 0);
assert_eq!(manager.total_count(), 0);
}
#[test]
fn test_shading_manager_add_shading() {
let mut manager = ShadingManager::new();
let start = Point::new(0.0, 0.0);
let end = Point::new(100.0, 0.0);
let axial = AxialShading::linear_gradient(
"TestGrad".to_string(),
start,
end,
Color::red(),
Color::blue(),
);
let shading = ShadingDefinition::Axial(axial);
let name = manager.add_shading(shading).unwrap();
assert_eq!(name, "TestGrad");
assert_eq!(manager.shading_count(), 1);
let retrieved = manager.get_shading(&name).unwrap();
assert_eq!(retrieved.name(), "TestGrad");
}
#[test]
fn test_shading_manager_auto_naming() {
let mut manager = ShadingManager::new();
let start = Point::new(0.0, 0.0);
let end = Point::new(100.0, 0.0);
let axial = AxialShading::linear_gradient(
String::new(), start,
end,
Color::red(),
Color::blue(),
);
let shading = ShadingDefinition::Axial(axial);
let name = manager.add_shading(shading).unwrap();
assert_eq!(name, "Sh1");
let axial2 = AxialShading::linear_gradient(
String::new(),
start,
end,
Color::green(),
Color::yellow(),
);
let shading2 = ShadingDefinition::Axial(axial2);
let name2 = manager.add_shading(shading2).unwrap();
assert_eq!(name2, "Sh2");
}
#[test]
fn test_shading_manager_create_gradients() {
let mut manager = ShadingManager::new();
let linear_name = manager
.create_linear_gradient(
Point::new(0.0, 0.0),
Point::new(100.0, 0.0),
Color::red(),
Color::blue(),
)
.unwrap();
let radial_name = manager
.create_radial_gradient(
Point::new(50.0, 50.0),
0.0,
25.0,
Color::white(),
Color::black(),
)
.unwrap();
assert_eq!(manager.shading_count(), 2);
assert!(manager.get_shading(&linear_name).is_some());
assert!(manager.get_shading(&radial_name).is_some());
}
#[test]
fn test_shading_manager_clear() {
let mut manager = ShadingManager::new();
manager
.create_linear_gradient(
Point::new(0.0, 0.0),
Point::new(100.0, 0.0),
Color::red(),
Color::blue(),
)
.unwrap();
assert_eq!(manager.shading_count(), 1);
manager.clear();
assert_eq!(manager.shading_count(), 0);
assert_eq!(manager.total_count(), 0);
}
#[test]
fn test_axial_shading_pdf_dictionary() {
let start = Point::new(0.0, 0.0);
let end = Point::new(100.0, 50.0);
let shading = AxialShading::linear_gradient(
"TestPDF".to_string(),
start,
end,
Color::red(),
Color::blue(),
)
.with_extend(true, false);
let dict = shading.to_pdf_dictionary().unwrap();
if let Some(Object::Integer(shading_type)) = dict.get("ShadingType") {
assert_eq!(*shading_type, 2); }
if let Some(Object::Array(coords)) = dict.get("Coords") {
assert_eq!(coords.len(), 4);
}
if let Some(Object::Array(extend)) = dict.get("Extend") {
assert_eq!(extend.len(), 2);
if let (Object::Boolean(start_extend), Object::Boolean(end_extend)) =
(&extend[0], &extend[1])
{
assert!(*start_extend);
assert!(!(*end_extend));
}
}
}
#[test]
fn test_radial_shading_pdf_dictionary() {
let center = Point::new(50.0, 50.0);
let shading = RadialShading::radial_gradient(
"TestRadialPDF".to_string(),
center,
10.0,
30.0,
Color::yellow(),
Color::red(),
);
let dict = shading.to_pdf_dictionary().unwrap();
if let Some(Object::Integer(shading_type)) = dict.get("ShadingType") {
assert_eq!(*shading_type, 3); }
if let Some(Object::Array(coords)) = dict.get("Coords") {
assert_eq!(coords.len(), 6); }
}
}