1use kurbo::{Affine, Point, Shape};
11use pdfrum_object::{Array, ByteSpan, Dict, Name, Object, Stream, names as pdf_names};
12use peniko::Color;
13
14use super::{Canvas, Fill, as_f32};
15use crate::write_matrix;
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct Gradient {
20 pub kind: GradientKind,
22 pub stops: Vec<GradientStop>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq)]
30pub enum GradientKind {
31 Linear {
33 start: Point,
35 end: Point,
37 },
38 Radial {
41 start_center: Point,
43 start_radius: f64,
45 end_center: Point,
47 end_radius: f64,
49 },
50}
51
52#[derive(Debug, Clone, Copy, PartialEq)]
54pub struct GradientStop {
55 pub offset: f64,
57 pub color: Color,
59}
60
61impl Canvas<'_, '_> {
62 pub fn fill_gradient(
88 &mut self,
89 shape: impl Shape,
90 rule: Fill,
91 gradient: &Gradient,
92 transform: Affine,
93 ) {
94 let stops = ordered(&gradient.stops);
95 let path = shape.into_path(0.1);
96 match stops.as_slice() {
97 [] => return,
98 [only] => return self.draw(path, super::Paint::Fill(only.color), rule),
99 _ => {}
100 }
101 if path.elements().is_empty() {
102 return;
103 }
104 let shading = shading(gradient.kind, &stops, Channel::Colour);
105 let alphas: Vec<f32> = stops.iter().map(|stop| stop.color.components[3]).collect();
106 let uniform = alphas
107 .windows(2)
108 .all(|pair| matches!(pair, [a, b] if (a - b).abs() < 1e-4));
109 let bounds = transform.inverse().transform_rect_bbox(path.bounding_box());
110 let name = self.realize(pdf_names::SHADING, Object::Dict(shading));
111 self.out.push_str("q\n");
112 self.write_path(&path);
113 self.out.push_str(match rule {
114 Fill::NonZero => " W n\n",
115 Fill::EvenOdd => " W* n\n",
116 });
117 write_matrix(&mut self.out, transform);
118 self.out.push_str(" cm\n");
119 match (uniform, alphas.first()) {
120 (true, Some(&alpha)) if alpha < 1.0 => self.opacity(f64::from(alpha)),
121 (true, _) => {}
122 (false, _) => self.alpha_mask(gradient.kind, &stops, bounds),
123 }
124 self.out.push('/');
125 self.push_name(&name);
126 self.out.push_str(" sh\nQ\n");
127 }
128
129 fn alpha_mask(&mut self, kind: GradientKind, stops: &[GradientStop], bounds: kurbo::Rect) {
132 let grey = shading(kind, stops, Channel::Alpha);
133 let form = Dict::from_pairs([
134 (
135 pdf_names::TYPE.clone(),
136 Object::Name(pdf_names::XOBJECT.clone()),
137 ),
138 (pdf_names::SUBTYPE.clone(), Object::Name(Name::from("Form"))),
139 (
140 Name::from("BBox"),
141 Object::Array(Array::of(
142 [bounds.x0, bounds.y0, bounds.x1, bounds.y1].map(|v| Object::Real(as_f32(v))),
143 )),
144 ),
145 (
146 Name::from("Group"),
147 Object::Dict(Dict::from_pairs([
148 (Name::from("S"), Object::Name(Name::from("Transparency"))),
149 (Name::from("CS"), Object::Name(Name::from("DeviceGray"))),
150 ])),
151 ),
152 (
153 pdf_names::RESOURCES.clone(),
154 Object::Dict(Dict::from_pairs([(
155 pdf_names::SHADING.clone(),
156 Object::Dict(Dict::from_pairs([(Name::from("A"), Object::Dict(grey))])),
157 )])),
158 ),
159 ]);
160 let form = self.edit.add(Object::Stream(Box::new(Stream::new(
161 form,
162 ByteSpan::from(b"/A sh\n".to_vec()),
163 ))));
164 let state = Dict::from_pairs([(
165 Name::from("SMask"),
166 Object::Dict(Dict::from_pairs([
167 (pdf_names::TYPE.clone(), Object::Name(Name::from("Mask"))),
168 (Name::from("S"), Object::Name(Name::from("Luminosity"))),
169 (Name::from("G"), Object::Ref(form)),
170 ])),
171 )]);
172 let name = self.realize(pdf_names::EXT_G_STATE, Object::Dict(state));
173 self.out.push('/');
174 self.push_name(&name);
175 self.out.push_str(" gs\n");
176 }
177}
178
179fn ordered(stops: &[GradientStop]) -> Vec<GradientStop> {
181 let mut out: Vec<GradientStop> = Vec::with_capacity(stops.len());
182 for stop in stops {
183 let floor = out.last().map_or(0.0, |last| last.offset);
184 let offset = if stop.offset.is_finite() {
185 stop.offset
186 } else {
187 0.0
188 };
189 out.push(GradientStop {
190 offset: offset.clamp(floor, 1.0),
191 color: stop.color,
192 });
193 }
194 out
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199enum Channel {
200 Colour,
202 Alpha,
204}
205
206fn shading(kind: GradientKind, stops: &[GradientStop], channel: Channel) -> Dict {
208 let (shading_type, coords) = match kind {
209 GradientKind::Linear { start, end } => (2, vec![start.x, start.y, end.x, end.y]),
210 GradientKind::Radial {
211 start_center,
212 start_radius,
213 end_center,
214 end_radius,
215 } => (
216 3,
217 vec![
218 start_center.x,
219 start_center.y,
220 start_radius.max(0.0),
221 end_center.x,
222 end_center.y,
223 end_radius.max(0.0),
224 ],
225 ),
226 };
227 let space = match channel {
228 Channel::Colour => "DeviceRGB",
229 Channel::Alpha => "DeviceGray",
230 };
231 Dict::from_pairs([
232 (Name::from("ShadingType"), Object::Int(shading_type)),
233 (Name::from("ColorSpace"), Object::Name(Name::from(space))),
234 (
235 Name::from("Coords"),
236 Object::Array(Array::of(
237 coords.into_iter().map(|v| Object::Real(as_f32(v))),
238 )),
239 ),
240 (
241 Name::from("Function"),
242 Object::Dict(stitched(stops, channel)),
243 ),
244 (
245 Name::from("Extend"),
246 Object::Array(Array::of([Object::Bool(true), Object::Bool(true)])),
247 ),
248 ])
249}
250
251fn stitched(stops: &[GradientStop], channel: Channel) -> Dict {
255 let values = |stop: &GradientStop| -> Object {
256 let [r, g, b, a] = stop.color.components;
257 let components: Vec<f32> = match channel {
258 Channel::Colour => vec![r, g, b],
259 Channel::Alpha => vec![a],
260 };
261 Object::Array(Array::of(
262 components
263 .into_iter()
264 .map(|c| Object::Real(c.clamp(0.0, 1.0))),
265 ))
266 };
267 let mut ends: Vec<GradientStop> = stops.to_vec();
268 if let Some(first) = stops.first().filter(|first| first.offset > 0.0) {
269 ends.insert(
270 0,
271 GradientStop {
272 offset: 0.0,
273 ..*first
274 },
275 );
276 }
277 if let Some(last) = stops.last().filter(|last| last.offset < 1.0) {
278 ends.push(GradientStop {
279 offset: 1.0,
280 ..*last
281 });
282 }
283 let mut functions = Vec::new();
284 let mut bounds = Vec::new();
285 let mut encode = Vec::new();
286 for pair in ends.windows(2) {
287 let [from, to] = pair else { continue };
288 functions.push(Object::Dict(Dict::from_pairs([
289 (Name::from("FunctionType"), Object::Int(2)),
290 (Name::from("Domain"), unit_domain()),
291 (Name::from("C0"), values(from)),
292 (Name::from("C1"), values(to)),
293 (Name::from("N"), Object::Real(1.0)),
294 ])));
295 encode.push(Object::Real(0.0));
296 encode.push(Object::Real(1.0));
297 bounds.push(Object::Real(as_f32(to.offset)));
298 }
299 bounds.pop();
301 Dict::from_pairs([
302 (Name::from("FunctionType"), Object::Int(3)),
303 (Name::from("Domain"), unit_domain()),
304 (Name::from("Functions"), Object::Array(Array::of(functions))),
305 (Name::from("Bounds"), Object::Array(Array::of(bounds))),
306 (Name::from("Encode"), Object::Array(Array::of(encode))),
307 ])
308}
309
310fn unit_domain() -> Object {
311 Object::Array(Array::of([Object::Real(0.0), Object::Real(1.0)]))
312}
313
314#[cfg(test)]
315mod tests {
316 use super::{Channel, GradientStop, ordered, stitched};
317 use peniko::Color;
318
319 fn stop(offset: f64) -> GradientStop {
320 GradientStop {
321 offset,
322 color: Color::BLACK,
323 }
324 }
325
326 #[test]
327 fn stops_are_clamped_into_order() {
328 let offsets: Vec<f64> = ordered(&[stop(-1.0), stop(0.6), stop(0.4), stop(2.0)])
329 .iter()
330 .map(|stop| stop.offset)
331 .collect();
332 assert_eq!(offsets, vec![0.0, 0.6, 0.6, 1.0]);
333 }
334
335 #[test]
336 fn inner_stops_are_extended_to_the_ends() {
337 let function = stitched(&[stop(0.25), stop(0.75)], Channel::Colour);
338 let functions = function
339 .raw(&pdfrum_object::Name::from("Functions"))
340 .and_then(pdfrum_object::Object::as_array)
341 .map(pdfrum_object::Array::len);
342 assert_eq!(functions, Some(3), "flat, ramp, flat");
343 }
344}