tiny_skia/shaders/
pattern.rs

1// Copyright 2006 The Android Open Source Project
2// Copyright 2020 Yevhenii Reizner
3//
4// Use of this source code is governed by a BSD-style license that can be
5// found in the LICENSE file.
6
7use tiny_skia_path::NormalizedF32;
8
9use crate::{BlendMode, PixmapRef, Shader, SpreadMode, Transform};
10
11use crate::pipeline;
12use crate::pipeline::RasterPipelineBuilder;
13
14#[cfg(all(not(feature = "std"), feature = "no-std-float"))]
15use tiny_skia_path::NoStdFloat;
16
17/// Controls how much filtering to be done when transforming images.
18#[derive(Copy, Clone, PartialEq, Debug)]
19pub enum FilterQuality {
20    /// Nearest-neighbor. Low quality, but fastest.
21    Nearest,
22    /// Bilinear.
23    Bilinear,
24    /// Bicubic. High quality, but slow.
25    Bicubic,
26}
27
28/// Controls how a pixmap should be blended.
29///
30/// Like `Paint`, but for `Pixmap`.
31#[derive(Copy, Clone, PartialEq, Debug)]
32pub struct PixmapPaint {
33    /// Pixmap opacity.
34    ///
35    /// Must be in 0..=1 range.
36    ///
37    /// Default: 1.0
38    pub opacity: f32,
39
40    /// Pixmap blending mode.
41    ///
42    /// Default: SourceOver
43    pub blend_mode: BlendMode,
44
45    /// Specifies how much filtering to be done when transforming images.
46    ///
47    /// Default: Nearest
48    pub quality: FilterQuality,
49}
50
51impl Default for PixmapPaint {
52    fn default() -> Self {
53        PixmapPaint {
54            opacity: 1.0,
55            blend_mode: BlendMode::default(),
56            quality: FilterQuality::Nearest,
57        }
58    }
59}
60
61/// A pattern shader.
62///
63/// Essentially a `SkImageShader`.
64///
65/// Unlike Skia, we do not support FilterQuality::Medium, because it involves
66/// mipmap generation, which adds too much complexity.
67#[derive(Clone, PartialEq, Debug)]
68pub struct Pattern<'a> {
69    pub(crate) pixmap: PixmapRef<'a>,
70    quality: FilterQuality,
71    spread_mode: SpreadMode,
72    pub(crate) opacity: NormalizedF32,
73    pub(crate) transform: Transform,
74}
75
76impl<'a> Pattern<'a> {
77    /// Creates a new pattern shader.
78    ///
79    /// `opacity` will be clamped to the 0..=1 range.
80    #[allow(clippy::new_ret_no_self)]
81    pub fn new(
82        pixmap: PixmapRef<'a>,
83        spread_mode: SpreadMode,
84        quality: FilterQuality,
85        opacity: f32,
86        transform: Transform,
87    ) -> Shader {
88        Shader::Pattern(Pattern {
89            pixmap,
90            spread_mode,
91            quality,
92            opacity: NormalizedF32::new_clamped(opacity),
93            transform,
94        })
95    }
96
97    pub(crate) fn push_stages(&self, p: &mut RasterPipelineBuilder) -> bool {
98        let ts = match self.transform.invert() {
99            Some(v) => v,
100            None => {
101                log::warn!("failed to invert a pattern transform. Nothing will be rendered");
102                return false;
103            }
104        };
105
106        p.push(pipeline::Stage::SeedShader);
107
108        p.push_transform(ts);
109
110        let mut quality = self.quality;
111
112        if ts.is_identity() || ts.is_translate() {
113            quality = FilterQuality::Nearest;
114        }
115
116        if quality == FilterQuality::Bilinear {
117            if ts.is_translate() {
118                if ts.tx == ts.tx.trunc() && ts.ty == ts.ty.trunc() {
119                    // When the matrix is just an integer translate, bilerp == nearest neighbor.
120                    quality = FilterQuality::Nearest;
121                }
122            }
123        }
124
125        // TODO: minimizing scale via mipmap
126
127        match quality {
128            FilterQuality::Nearest => {
129                p.ctx.limit_x = pipeline::TileCtx {
130                    scale: self.pixmap.width() as f32,
131                    inv_scale: 1.0 / self.pixmap.width() as f32,
132                };
133
134                p.ctx.limit_y = pipeline::TileCtx {
135                    scale: self.pixmap.height() as f32,
136                    inv_scale: 1.0 / self.pixmap.height() as f32,
137                };
138
139                match self.spread_mode {
140                    SpreadMode::Pad => { /* The gather() stage will clamp for us. */ }
141                    SpreadMode::Repeat => p.push(pipeline::Stage::Repeat),
142                    SpreadMode::Reflect => p.push(pipeline::Stage::Reflect),
143                }
144
145                p.push(pipeline::Stage::Gather);
146            }
147            FilterQuality::Bilinear => {
148                p.ctx.sampler = pipeline::SamplerCtx {
149                    spread_mode: self.spread_mode,
150                    inv_width: 1.0 / self.pixmap.width() as f32,
151                    inv_height: 1.0 / self.pixmap.height() as f32,
152                };
153                p.push(pipeline::Stage::Bilinear);
154            }
155            FilterQuality::Bicubic => {
156                p.ctx.sampler = pipeline::SamplerCtx {
157                    spread_mode: self.spread_mode,
158                    inv_width: 1.0 / self.pixmap.width() as f32,
159                    inv_height: 1.0 / self.pixmap.height() as f32,
160                };
161                p.push(pipeline::Stage::Bicubic);
162
163                // Bicubic filtering naturally produces out of range values on both sides of [0,1].
164                p.push(pipeline::Stage::Clamp0);
165                p.push(pipeline::Stage::ClampA);
166            }
167        }
168
169        // Unlike Skia, we do not support global opacity and only Pattern allows it.
170        if self.opacity != NormalizedF32::ONE {
171            debug_assert_eq!(
172                core::mem::size_of_val(&self.opacity),
173                4,
174                "alpha must be f32"
175            );
176            p.ctx.current_coverage = self.opacity.get();
177            p.push(pipeline::Stage::Scale1Float);
178        }
179
180        true
181    }
182}