1use std::fmt::Display;
9use std::sync::Arc;
10
11use valo::DisplayList;
12use valo::DisplayListBuilder;
13use valo::Op;
14
15pub type Canvas = DisplayListBuilder;
17
18pub type Picture = DisplayList;
21
22pub fn flattened_ops(picture: &Picture) -> Vec<Op> {
26 let mut ops = Vec::new();
27 flatten_into(picture, &mut ops);
28 ops
29}
30
31fn flatten_into(picture: &Picture, ops: &mut Vec<Op>) {
32 for op in picture.ops() {
33 ops.push(op.clone());
34 if let Op::DrawDisplayList { list, .. } = op {
35 flatten_into(list, ops);
36 }
37 }
38}
39
40pub use valo::{
41 Backdrop, BlendMode, BlurStyle, ClipOp, FillRule, Image, MaskBlur, Paint, PaintStyle, Path,
42 PathBuilder, Stroke,
43};
44
45pub use valo;
48
49#[derive(Clone, Copy, Debug, PartialEq)]
58pub enum ColorFilter {
59 Mode {
62 color: crate::Color,
64 blend_mode: BlendMode,
66 },
67
68 Matrix([f64; 20]),
84
85 LinearToSrgbGamma,
88
89 SrgbToLinearGamma,
92}
93
94impl ColorFilter {
95 pub const fn mode(color: crate::Color, blend_mode: BlendMode) -> ColorFilter {
103 ColorFilter::Mode { color, blend_mode }
104 }
105
106 pub const fn matrix(matrix: [f64; 20]) -> ColorFilter {
122 ColorFilter::Matrix(matrix)
123 }
124
125 pub const fn linear_to_srgb_gamma() -> ColorFilter {
128 ColorFilter::LinearToSrgbGamma
129 }
130
131 pub const fn srgb_to_linear_gamma() -> ColorFilter {
134 ColorFilter::SrgbToLinearGamma
135 }
136
137 pub fn saturation(saturation: f64) -> ColorFilter {
140 const R_LUMINANCE: f64 = 0.2126;
141 const G_LUMINANCE: f64 = 0.7152;
142 const B_LUMINANCE: f64 = 0.0722;
143 let inv_sat = 1.0 - saturation;
144
145 ColorFilter::matrix([
146 inv_sat * R_LUMINANCE + saturation,
147 inv_sat * G_LUMINANCE,
148 inv_sat * B_LUMINANCE,
149 0.0,
150 0.0,
151 inv_sat * R_LUMINANCE,
152 inv_sat * G_LUMINANCE + saturation,
153 inv_sat * B_LUMINANCE,
154 0.0,
155 0.0,
156 inv_sat * R_LUMINANCE,
157 inv_sat * G_LUMINANCE,
158 inv_sat * B_LUMINANCE + saturation,
159 0.0,
160 0.0,
161 0.0,
162 0.0,
163 0.0,
164 1.0,
165 0.0,
166 ])
167 }
168
169 pub fn debug_short_description(&self) -> String {
171 match self {
172 ColorFilter::Mode { color, blend_mode } => {
173 format!("ColorFilter.mode({color:?}, {blend_mode:?})")
174 }
175 ColorFilter::Matrix(matrix) => format!("ColorFilter.matrix({matrix:?})"),
176 ColorFilter::LinearToSrgbGamma => String::from("ColorFilter.linearToSrgbGamma()"),
177 ColorFilter::SrgbToLinearGamma => String::from("ColorFilter.srgbToLinearGamma()"),
178 }
179 }
180}
181
182impl std::fmt::Display for ColorFilter {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 f.write_str(&self.debug_short_description())
185 }
186}
187
188impl ColorFilter {
189 pub fn to_valo(&self) -> valo::ColorFilter {
191 match *self {
192 ColorFilter::Mode { color, blend_mode } => {
193 valo::ColorFilter::Blend(color.into(), blend_mode)
194 }
195 ColorFilter::Matrix(matrix) => valo::ColorFilter::Matrix(valo_matrix(matrix)),
196 ColorFilter::LinearToSrgbGamma => valo::ColorFilter::LinearToSrgbGamma,
197 ColorFilter::SrgbToLinearGamma => valo::ColorFilter::SrgbToLinearGamma,
198 }
199 }
200}
201
202fn valo_matrix(matrix: [f64; 20]) -> [f32; 20] {
205 const ROW_LENGTH: usize = 5;
206 const TRANSLATION_COLUMN: usize = 4;
207 std::array::from_fn(|index| {
208 let entry = matrix[index];
209 let entry = if index % ROW_LENGTH == TRANSLATION_COLUMN {
210 entry / 255.0
211 } else {
212 entry
213 };
214 entry as f32
215 })
216}
217
218#[derive(Clone, Debug, PartialEq)]
229pub enum ImageFilter {
230 Blur {
232 sigma_x: f64,
234 sigma_y: f64,
236 bounds: Option<crate::Rect>,
238 },
239
240 Color(ColorFilter),
243
244 Compose {
246 outer: Arc<ImageFilter>,
248 inner: Arc<ImageFilter>,
250 },
251}
252
253impl ImageFilter {
254 pub const fn blur(sigma_x: f64, sigma_y: f64) -> ImageFilter {
256 ImageFilter::Blur {
257 sigma_x,
258 sigma_y,
259 bounds: None,
260 }
261 }
262
263 pub fn bounds(self, bounds: crate::Rect) -> ImageFilter {
265 match self {
266 ImageFilter::Blur {
267 sigma_x, sigma_y, ..
268 } => ImageFilter::Blur {
269 sigma_x,
270 sigma_y,
271 bounds: Some(bounds),
272 },
273 other => other,
274 }
275 }
276
277 pub fn compose(outer: ImageFilter, inner: ImageFilter) -> ImageFilter {
283 ImageFilter::Compose {
284 outer: Arc::new(outer),
285 inner: Arc::new(inner),
286 }
287 }
288
289 pub fn debug_short_description(&self) -> String {
291 match self {
292 ImageFilter::Blur {
293 sigma_x,
294 sigma_y,
295 bounds,
296 } => {
297 let bounds = match bounds {
298 Some(bounds) => format!(", bounds: {bounds:?}"),
299 None => String::new(),
300 };
301 format!("blur({sigma_x}, {sigma_y}, unspecified{bounds})")
302 }
303 ImageFilter::Color(filter) => filter.debug_short_description(),
304 ImageFilter::Compose { outer, inner } => format!(
305 "{} -> {}",
306 inner.debug_short_description(),
307 outer.debug_short_description()
308 ),
309 }
310 }
311}
312
313impl std::fmt::Display for ImageFilter {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 match self {
316 ImageFilter::Blur { .. } => write!(f, "ImageFilter.{}", self.debug_short_description()),
317 ImageFilter::Color(filter) => Display::fmt(filter, f),
318 ImageFilter::Compose { .. } => write!(
319 f,
320 "ImageFilter.compose(source -> {} -> result)",
321 self.debug_short_description()
322 ),
323 }
324 }
325}
326
327impl ImageFilter {
328 pub fn to_valo(&self) -> valo::ImageFilter {
330 match self {
331 ImageFilter::Blur {
332 sigma_x,
333 sigma_y,
334 bounds,
335 } => {
336 let (sigma_x, sigma_y) = (*sigma_x as f32, *sigma_y as f32);
337 match bounds {
338 Some(bounds) => {
339 valo::ImageFilter::bounded_blur(sigma_x, sigma_y, (*bounds).into())
340 }
341 None => valo::ImageFilter::blur(sigma_x, sigma_y),
342 }
343 }
344 ImageFilter::Color(filter) => valo::ImageFilter::Color(filter.to_valo()),
345 ImageFilter::Compose { outer, inner } => {
346 valo::ImageFilter::compose(outer.to_valo(), inner.to_valo())
347 }
348 }
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::{ColorFilter, ImageFilter, valo};
355 use crate::{BlendMode, Color, Rect};
356
357 #[test]
358 fn a_saturation_of_one_is_the_identity_matrix() {
359 let ColorFilter::Matrix(matrix) = ColorFilter::saturation(1.0) else {
360 panic!("saturation is a matrix filter");
361 };
362 let identity = [
363 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, ];
368 for (value, expected) in matrix.iter().zip(identity) {
369 assert!((value - expected).abs() < 1e-12, "{matrix:?}");
370 }
371 }
372
373 #[test]
374 fn a_blur_reports_its_bounds_and_a_composition_reads_inner_then_outer() {
375 let blur = ImageFilter::blur(2.0, 3.0).bounds(Rect::from_ltwh(0.0, 0.0, 4.0, 5.0));
376 assert_eq!(
377 blur.debug_short_description(),
378 "blur(2, 3, unspecified, bounds: Rect.fromLTRB(0.0, 0.0, 4.0, 5.0))"
379 );
380
381 let tint = ImageFilter::Color(ColorFilter::mode(
382 Color::new(0xFF000000),
383 BlendMode::SrcOver,
384 ));
385 let composed = ImageFilter::compose(tint, ImageFilter::blur(2.0, 3.0));
386 assert_eq!(
387 composed.to_string(),
388 "ImageFilter.compose(source -> blur(2, 3, unspecified) -> \
389 ColorFilter.mode(Color(alpha: 1.0000, red: 0.0000, green: 0.0000, blue: 0.0000, \
390 colorSpace: ColorSpace.sRGB), SrcOver) -> result)"
391 );
392 }
393
394 #[test]
395 fn a_colour_matrix_reaches_valo_with_its_translation_in_the_channels_range() {
396 let mut matrix = [0.0; 20];
397 matrix[0] = 2.0;
398 matrix[4] = 51.0;
399 matrix[18] = 1.0;
400 matrix[19] = 255.0;
401 let valo::ColorFilter::Matrix(replayed) = ColorFilter::matrix(matrix).to_valo() else {
402 panic!("a matrix reaches valo as a matrix");
403 };
404 assert_eq!(
405 [replayed[0], replayed[4], replayed[18], replayed[19]],
406 [2.0, 0.2, 1.0, 1.0]
407 );
408 }
409
410 #[test]
411 fn a_mode_filter_reaches_valo_as_a_blend_and_a_gamma_curve_as_the_same_curve() {
412 let green = Color::new(0x8C00FF00);
413 assert_eq!(
414 ColorFilter::mode(green, BlendMode::Darken).to_valo(),
415 valo::ColorFilter::Blend(green.into(), BlendMode::Darken)
416 );
417 assert_eq!(
418 ColorFilter::linear_to_srgb_gamma().to_valo(),
419 valo::ColorFilter::LinearToSrgbGamma
420 );
421 assert_eq!(
422 ColorFilter::srgb_to_linear_gamma().to_valo(),
423 valo::ColorFilter::SrgbToLinearGamma
424 );
425 }
426
427 #[test]
428 fn a_composition_reaches_valo_in_its_order_with_both_sigmas_and_the_bounds() {
429 let tint = ColorFilter::mode(Color::new(0xFF0000FF), BlendMode::Multiply);
430 let bounds = Rect::from_ltwh(0.0, 0.0, 4.0, 4.0);
431 let filter = ImageFilter::compose(
432 ImageFilter::Color(tint),
433 ImageFilter::blur(2.0, 5.0).bounds(bounds),
434 );
435 assert_eq!(
436 filter.to_valo(),
437 valo::ImageFilter::compose(
438 valo::ImageFilter::Color(valo::ColorFilter::Blend(
439 Color::new(0xFF0000FF).into(),
440 BlendMode::Multiply
441 )),
442 valo::ImageFilter::bounded_blur(2.0, 5.0, bounds.into()),
443 )
444 );
445 }
446
447 #[test]
448 fn a_gamma_curve_reaches_valo_in_its_place_in_a_composition() {
449 let filter = ImageFilter::compose(
450 ImageFilter::Color(ColorFilter::linear_to_srgb_gamma()),
451 ImageFilter::blur(3.0, 3.0),
452 );
453 assert_eq!(
454 filter.to_valo(),
455 valo::ImageFilter::compose(
456 valo::ImageFilter::Color(valo::ColorFilter::LinearToSrgbGamma),
457 valo::ImageFilter::blur(3.0, 3.0),
458 )
459 );
460 }
461}