use serde_derive::{Deserialize, Serialize};
use lopdf::Dictionary as LoDictionary;
#[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ShadingId(pub String);
impl ShadingId {
pub fn new() -> Self {
Self(crate::utils::random_character_string_32())
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct GradientStop {
pub offset: f32,
pub color: [f32; 3],
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub enum ShadingGeometry {
Axial { coords: [f32; 4] },
Radial { coords: [f32; 6] },
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct Shading {
pub geometry: ShadingGeometry,
pub stops: Vec<GradientStop>,
pub extend: (bool, bool),
}
impl Shading {
pub fn to_dict(&self) -> LoDictionary {
use lopdf::Object::*;
let (shading_type, coords): (i64, Vec<lopdf::Object>) = match &self.geometry {
ShadingGeometry::Axial { coords } => (2, coords.iter().map(|v| Real(*v)).collect()),
ShadingGeometry::Radial { coords } => (3, coords.iter().map(|v| Real(*v)).collect()),
};
let mut dict = LoDictionary::new();
dict.set("ShadingType", Integer(shading_type));
dict.set("ColorSpace", Name(b"DeviceRGB".to_vec()));
dict.set("Coords", Array(coords));
dict.set("Function", self.build_function());
dict.set(
"Extend",
Array(vec![Boolean(self.extend.0), Boolean(self.extend.1)]),
);
dict
}
fn build_function(&self) -> lopdf::Object {
let n = self.stops.len();
if n == 0 {
return lopdf::Object::Dictionary(exponential_fn([0.0; 3], [0.0; 3]));
}
if n == 1 {
let c = self.stops[0].color;
return lopdf::Object::Dictionary(exponential_fn(c, c));
}
let mut segs: Vec<(f32, f32, [f32; 3], [f32; 3])> = Vec::new();
let first = &self.stops[0];
let last = &self.stops[n - 1];
let o_first = first.offset.clamp(0.0, 1.0);
let o_last = last.offset.clamp(0.0, 1.0);
if o_first > 0.0 {
segs.push((0.0, o_first, first.color, first.color));
}
for w in self.stops.windows(2) {
let ta = w[0].offset.clamp(0.0, 1.0);
let tb = w[1].offset.clamp(0.0, 1.0);
if tb > ta {
segs.push((ta, tb, w[0].color, w[1].color));
}
}
if o_last < 1.0 {
segs.push((o_last, 1.0, last.color, last.color));
}
if segs.is_empty() {
let c = first.color;
return lopdf::Object::Dictionary(exponential_fn(c, c));
}
if segs.len() == 1 {
let s = &segs[0];
return lopdf::Object::Dictionary(exponential_fn(s.2, s.3));
}
use lopdf::Object::*;
let functions: Vec<lopdf::Object> = segs
.iter()
.map(|s| Dictionary(exponential_fn(s.2, s.3)))
.collect();
let bounds: Vec<lopdf::Object> = segs[..segs.len() - 1]
.iter()
.map(|s| Real(s.1))
.collect();
let encode: Vec<lopdf::Object> = segs.iter().flat_map(|_| [Real(0.0), Real(1.0)]).collect();
let mut dict = LoDictionary::new();
dict.set("FunctionType", Integer(3));
dict.set("Domain", Array(vec![Real(0.0), Real(1.0)]));
dict.set("Functions", Array(functions));
dict.set("Bounds", Array(bounds));
dict.set("Encode", Array(encode));
Dictionary(dict)
}
}
fn exponential_fn(c0: [f32; 3], c1: [f32; 3]) -> LoDictionary {
use lopdf::Object::*;
let mut dict = LoDictionary::new();
dict.set("FunctionType", Integer(2));
dict.set("Domain", Array(vec![Real(0.0), Real(1.0)]));
dict.set("C0", Array(c0.iter().map(|v| Real(*v)).collect()));
dict.set("C1", Array(c1.iter().map(|v| Real(*v)).collect()));
dict.set("N", Real(1.0));
dict
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn axial_two_stop_builds_type2_function() {
let sh = Shading {
geometry: ShadingGeometry::Axial { coords: [0.0, 0.0, 100.0, 0.0] },
stops: vec![
GradientStop { offset: 0.0, color: [1.0, 0.0, 0.0] },
GradientStop { offset: 1.0, color: [0.0, 0.0, 1.0] },
],
extend: (true, true),
};
let d = sh.to_dict();
assert_eq!(d.get(b"ShadingType").unwrap().as_i64().unwrap(), 2);
let f = d.get(b"Function").unwrap().as_dict().unwrap();
assert_eq!(f.get(b"FunctionType").unwrap().as_i64().unwrap(), 2);
}
#[test]
fn multi_stop_builds_stitching_function() {
let sh = Shading {
geometry: ShadingGeometry::Axial { coords: [0.0, 0.0, 0.0, 100.0] },
stops: vec![
GradientStop { offset: 0.0, color: [1.0, 0.0, 0.0] },
GradientStop { offset: 0.5, color: [0.0, 1.0, 0.0] },
GradientStop { offset: 1.0, color: [0.0, 0.0, 1.0] },
],
extend: (true, true),
};
let d = sh.to_dict();
let f = d.get(b"Function").unwrap().as_dict().unwrap();
assert_eq!(f.get(b"FunctionType").unwrap().as_i64().unwrap(), 3);
assert_eq!(f.get(b"Functions").unwrap().as_array().unwrap().len(), 2);
assert_eq!(f.get(b"Bounds").unwrap().as_array().unwrap().len(), 1);
}
#[test]
fn leading_constant_segment_for_offset_gradient() {
let sh = Shading {
geometry: ShadingGeometry::Axial { coords: [0.0, 0.0, 100.0, 0.0] },
stops: vec![
GradientStop { offset: 0.3, color: [1.0, 0.0, 0.0] },
GradientStop { offset: 1.0, color: [0.0, 0.0, 1.0] },
],
extend: (true, true),
};
let d = sh.to_dict();
let f = d.get(b"Function").unwrap().as_dict().unwrap();
assert_eq!(f.get(b"FunctionType").unwrap().as_i64().unwrap(), 3);
assert_eq!(f.get(b"Functions").unwrap().as_array().unwrap().len(), 2);
let bounds = f.get(b"Bounds").unwrap().as_array().unwrap();
assert_eq!(bounds.len(), 1);
assert!((bounds[0].as_float().unwrap() - 0.3).abs() < 1e-4);
}
}