use kurbo::{Affine, Point, Shape};
use pdfrum_object::{Array, ByteSpan, Dict, Name, Object, Stream, names as pdf_names};
use peniko::Color;
use super::{Canvas, Fill, as_f32};
use crate::write_matrix;
#[derive(Debug, Clone, PartialEq)]
pub struct Gradient {
pub kind: GradientKind,
pub stops: Vec<GradientStop>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GradientKind {
Linear {
start: Point,
end: Point,
},
Radial {
start_center: Point,
start_radius: f64,
end_center: Point,
end_radius: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GradientStop {
pub offset: f64,
pub color: Color,
}
impl Canvas<'_, '_> {
pub fn fill_gradient(
&mut self,
shape: impl Shape,
rule: Fill,
gradient: &Gradient,
transform: Affine,
) {
let stops = ordered(&gradient.stops);
let path = shape.into_path(0.1);
match stops.as_slice() {
[] => return,
[only] => return self.draw(path, super::Paint::Fill(only.color), rule),
_ => {}
}
if path.elements().is_empty() {
return;
}
let shading = shading(gradient.kind, &stops, Channel::Colour);
let alphas: Vec<f32> = stops.iter().map(|stop| stop.color.components[3]).collect();
let uniform = alphas
.windows(2)
.all(|pair| matches!(pair, [a, b] if (a - b).abs() < 1e-4));
let bounds = transform.inverse().transform_rect_bbox(path.bounding_box());
let name = self.realize(pdf_names::SHADING, Object::Dict(shading));
self.out.push_str("q\n");
self.write_path(&path);
self.out.push_str(match rule {
Fill::NonZero => " W n\n",
Fill::EvenOdd => " W* n\n",
});
write_matrix(&mut self.out, transform);
self.out.push_str(" cm\n");
match (uniform, alphas.first()) {
(true, Some(&alpha)) if alpha < 1.0 => self.opacity(f64::from(alpha)),
(true, _) => {}
(false, _) => self.alpha_mask(gradient.kind, &stops, bounds),
}
self.out.push('/');
self.push_name(&name);
self.out.push_str(" sh\nQ\n");
}
fn alpha_mask(&mut self, kind: GradientKind, stops: &[GradientStop], bounds: kurbo::Rect) {
let grey = shading(kind, stops, Channel::Alpha);
let form = Dict::from_pairs([
(
pdf_names::TYPE.clone(),
Object::Name(pdf_names::XOBJECT.clone()),
),
(pdf_names::SUBTYPE.clone(), Object::Name(Name::from("Form"))),
(
Name::from("BBox"),
Object::Array(Array::of(
[bounds.x0, bounds.y0, bounds.x1, bounds.y1].map(|v| Object::Real(as_f32(v))),
)),
),
(
Name::from("Group"),
Object::Dict(Dict::from_pairs([
(Name::from("S"), Object::Name(Name::from("Transparency"))),
(Name::from("CS"), Object::Name(Name::from("DeviceGray"))),
])),
),
(
pdf_names::RESOURCES.clone(),
Object::Dict(Dict::from_pairs([(
pdf_names::SHADING.clone(),
Object::Dict(Dict::from_pairs([(Name::from("A"), Object::Dict(grey))])),
)])),
),
]);
let form = self.edit.add(Object::Stream(Box::new(Stream::new(
form,
ByteSpan::from(b"/A sh\n".to_vec()),
))));
let state = Dict::from_pairs([(
Name::from("SMask"),
Object::Dict(Dict::from_pairs([
(pdf_names::TYPE.clone(), Object::Name(Name::from("Mask"))),
(Name::from("S"), Object::Name(Name::from("Luminosity"))),
(Name::from("G"), Object::Ref(form)),
])),
)]);
let name = self.realize(pdf_names::EXT_G_STATE, Object::Dict(state));
self.out.push('/');
self.push_name(&name);
self.out.push_str(" gs\n");
}
}
fn ordered(stops: &[GradientStop]) -> Vec<GradientStop> {
let mut out: Vec<GradientStop> = Vec::with_capacity(stops.len());
for stop in stops {
let floor = out.last().map_or(0.0, |last| last.offset);
let offset = if stop.offset.is_finite() {
stop.offset
} else {
0.0
};
out.push(GradientStop {
offset: offset.clamp(floor, 1.0),
color: stop.color,
});
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Channel {
Colour,
Alpha,
}
fn shading(kind: GradientKind, stops: &[GradientStop], channel: Channel) -> Dict {
let (shading_type, coords) = match kind {
GradientKind::Linear { start, end } => (2, vec![start.x, start.y, end.x, end.y]),
GradientKind::Radial {
start_center,
start_radius,
end_center,
end_radius,
} => (
3,
vec![
start_center.x,
start_center.y,
start_radius.max(0.0),
end_center.x,
end_center.y,
end_radius.max(0.0),
],
),
};
let space = match channel {
Channel::Colour => "DeviceRGB",
Channel::Alpha => "DeviceGray",
};
Dict::from_pairs([
(Name::from("ShadingType"), Object::Int(shading_type)),
(Name::from("ColorSpace"), Object::Name(Name::from(space))),
(
Name::from("Coords"),
Object::Array(Array::of(
coords.into_iter().map(|v| Object::Real(as_f32(v))),
)),
),
(
Name::from("Function"),
Object::Dict(stitched(stops, channel)),
),
(
Name::from("Extend"),
Object::Array(Array::of([Object::Bool(true), Object::Bool(true)])),
),
])
}
fn stitched(stops: &[GradientStop], channel: Channel) -> Dict {
let values = |stop: &GradientStop| -> Object {
let [r, g, b, a] = stop.color.components;
let components: Vec<f32> = match channel {
Channel::Colour => vec![r, g, b],
Channel::Alpha => vec![a],
};
Object::Array(Array::of(
components
.into_iter()
.map(|c| Object::Real(c.clamp(0.0, 1.0))),
))
};
let mut ends: Vec<GradientStop> = stops.to_vec();
if let Some(first) = stops.first().filter(|first| first.offset > 0.0) {
ends.insert(
0,
GradientStop {
offset: 0.0,
..*first
},
);
}
if let Some(last) = stops.last().filter(|last| last.offset < 1.0) {
ends.push(GradientStop {
offset: 1.0,
..*last
});
}
let mut functions = Vec::new();
let mut bounds = Vec::new();
let mut encode = Vec::new();
for pair in ends.windows(2) {
let [from, to] = pair else { continue };
functions.push(Object::Dict(Dict::from_pairs([
(Name::from("FunctionType"), Object::Int(2)),
(Name::from("Domain"), unit_domain()),
(Name::from("C0"), values(from)),
(Name::from("C1"), values(to)),
(Name::from("N"), Object::Real(1.0)),
])));
encode.push(Object::Real(0.0));
encode.push(Object::Real(1.0));
bounds.push(Object::Real(as_f32(to.offset)));
}
bounds.pop();
Dict::from_pairs([
(Name::from("FunctionType"), Object::Int(3)),
(Name::from("Domain"), unit_domain()),
(Name::from("Functions"), Object::Array(Array::of(functions))),
(Name::from("Bounds"), Object::Array(Array::of(bounds))),
(Name::from("Encode"), Object::Array(Array::of(encode))),
])
}
fn unit_domain() -> Object {
Object::Array(Array::of([Object::Real(0.0), Object::Real(1.0)]))
}
#[cfg(test)]
mod tests {
use super::{Channel, GradientStop, ordered, stitched};
use peniko::Color;
fn stop(offset: f64) -> GradientStop {
GradientStop {
offset,
color: Color::BLACK,
}
}
#[test]
fn stops_are_clamped_into_order() {
let offsets: Vec<f64> = ordered(&[stop(-1.0), stop(0.6), stop(0.4), stop(2.0)])
.iter()
.map(|stop| stop.offset)
.collect();
assert_eq!(offsets, vec![0.0, 0.6, 0.6, 1.0]);
}
#[test]
fn inner_stops_are_extended_to_the_ends() {
let function = stitched(&[stop(0.25), stop(0.75)], Channel::Colour);
let functions = function
.raw(&pdfrum_object::Name::from("Functions"))
.and_then(pdfrum_object::Object::as_array)
.map(pdfrum_object::Array::len);
assert_eq!(functions, Some(3), "flat, ramp, flat");
}
}