Skip to content

Commit 4cffa27

Browse files
committed
Add basic mimpmap downscaling support
This is small patch to fix quality issues when scaling images down. It'll create a number of mipmap/half-scaled versions of a given source as required and use the next largest version as the source for bilinear or bicubic scaling. This uses the same downsampling functions as Skia, but similarly doesn't run SIMD versions of these. As they are integer-based, these should be reasonably quick.
1 parent cf6530d commit 4cffa27

File tree

5 files changed

+354
-21
lines changed

5 files changed

+354
-21
lines changed

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ mod geom;
4646
mod line_clipper;
4747
mod mask;
4848
mod math;
49+
mod mipmap;
4950
mod path64;
5051
mod path_geometry;
5152
mod pipeline;
@@ -60,6 +61,7 @@ pub use blend_mode::BlendMode;
6061
pub use color::{Color, ColorSpace, ColorU8, PremultipliedColor, PremultipliedColorU8};
6162
pub use color::{ALPHA_OPAQUE, ALPHA_TRANSPARENT, ALPHA_U8_OPAQUE, ALPHA_U8_TRANSPARENT};
6263
pub use mask::{Mask, MaskType};
64+
pub use mipmap::Mipmaps;
6365
pub use painter::{FillRule, Paint};
6466
pub use pixmap::{Pixmap, PixmapMut, PixmapRef, BYTES_PER_PIXEL};
6567
pub use shaders::{FilterQuality, GradientStop, PixmapPaint, SpreadMode};

src/mipmap.rs

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
// Copyright 2006 The Android Open Source Project
2+
// Copyright 2020 Yevhenii Reizner
3+
// Copyright 2024 Jeremy James
4+
//
5+
// Use of this source code is governed by a BSD-style license that can be
6+
// found in the LICENSE file.
7+
8+
use alloc::vec::Vec;
9+
10+
use crate::pixmap::{Pixmap, PixmapRef};
11+
use crate::PremultipliedColorU8;
12+
13+
#[cfg(all(not(feature = "std"), feature = "no-std-float"))]
14+
use tiny_skia_path::NoStdFloat;
15+
16+
/// Mipmaps are used to scaling down source images quickly to be used instead
17+
/// of a pixmap as source for bilinear or bicubic scaling
18+
///
19+
/// These are created from a `PixmapRef` as a base source which can be fetched
20+
/// using level `0`
21+
///
22+
#[derive(Debug)]
23+
pub struct Mipmaps<'a> {
24+
levels: Vec<Pixmap>,
25+
base_pixmap: PixmapRef<'a>,
26+
}
27+
28+
impl<'a> Mipmaps<'a> {
29+
/// Allocates a new set of mipmaps from a base pixmap
30+
pub fn new(p: PixmapRef<'a>) -> Self {
31+
Mipmaps {
32+
levels: Vec::new(),
33+
base_pixmap: p,
34+
}
35+
}
36+
37+
/// Fetch a mipmap to be used - or base pixmap if zero is given
38+
pub fn get(&self, level: usize) -> PixmapRef {
39+
return if level > 0 {
40+
self.levels.get(level - 1).unwrap().as_ref()
41+
} else {
42+
self.base_pixmap
43+
};
44+
}
45+
46+
/// Ensure this many levels of mipmap are available, returning
47+
/// an index to be used with get()
48+
pub fn build(&mut self, required_levels: usize) -> usize {
49+
let mut src_level = self.levels.len();
50+
let mut src_pixmap = self.get(src_level);
51+
let mut level_width = src_pixmap.width();
52+
let mut level_height = src_pixmap.height();
53+
54+
while src_level < required_levels {
55+
level_width = (level_width as f32 / 2.0).floor() as u32;
56+
level_height = (level_height as f32 / 2.0).floor() as u32;
57+
58+
// Scale image down
59+
let mut dst_pixmap = Pixmap::new(level_width, level_height).unwrap();
60+
let dst_width = dst_pixmap.width() as usize;
61+
let dst_height = dst_pixmap.height() as usize;
62+
let dst_pixels = dst_pixmap.pixels_mut();
63+
64+
let src_pixels = src_pixmap.pixels();
65+
let src_width = src_pixmap.width() as usize;
66+
let src_height = src_pixmap.height() as usize;
67+
68+
// To produce each mip level, we need to filter down by 1/2 (e.g. 100x100 -> 50,50)
69+
// If the starting dimension is odd, we floor the size of the lower level (e.g. 101 -> 50)
70+
// In those (odd) cases, we use a triangle filter, with 1-pixel overlap between samplings,
71+
// else for even cases, we just use a 2x box filter.
72+
//
73+
// This produces 4 possible isotropic filters: 2x2 2x3 3x2 3x3 where WxH indicates the number of
74+
// src pixels we need to sample in each dimension to produce 1 dst pixel.
75+
let mut downsample: fn(
76+
&[PremultipliedColorU8],
77+
usize,
78+
usize,
79+
&mut [PremultipliedColorU8],
80+
usize,
81+
usize,
82+
) = downsample_2_2;
83+
84+
if src_height & 1 == 1 {
85+
if src_width & 1 == 1 {
86+
downsample = downsample_3_3;
87+
} else {
88+
downsample = downsample_2_3;
89+
}
90+
} else {
91+
if src_width & 1 == 1 {
92+
downsample = downsample_3_2;
93+
}
94+
}
95+
96+
let mut src_y = 0;
97+
for dst_y in 0..dst_height {
98+
downsample(src_pixels, src_y, src_width, dst_pixels, dst_y, dst_width);
99+
src_y += 2;
100+
}
101+
102+
self.levels.push(dst_pixmap);
103+
src_pixmap = self.levels.get(src_level).unwrap().as_ref();
104+
src_level += 1;
105+
}
106+
107+
src_level
108+
}
109+
}
110+
111+
/// Determine how many Mipmap levels will be needed for a given source and
112+
/// a given (approximate) scaling being applied to the source
113+
///
114+
/// Return the number of levels, and a pre-scale that should be applied to
115+
/// a transform that will 'correct' it to the right size of source
116+
///
117+
/// Note that this is different from Skia since only required levels will
118+
/// be generated
119+
pub fn compute_required_levels(
120+
base_pixmap: PixmapRef,
121+
scale_x: f32,
122+
scale_y: f32,
123+
) -> (usize, f32, f32) {
124+
let mut required_levels: usize = 0;
125+
let mut level_width = base_pixmap.width();
126+
let mut level_height = base_pixmap.height();
127+
let mut prescale_x: f32 = 1.0;
128+
let mut prescale_y: f32 = 1.0;
129+
130+
// Keep generating levels whilst required scale is
131+
// smaller than half of previous level size
132+
while scale_x * prescale_x < 0.5
133+
&& level_width > 1
134+
&& scale_y * prescale_y < 0.5
135+
&& level_height > 1
136+
{
137+
required_levels += 1;
138+
level_width = (level_width as f32 / 2.0).floor() as u32;
139+
level_height = (level_height as f32 / 2.0).floor() as u32;
140+
prescale_x = base_pixmap.width() as f32 / level_width as f32;
141+
prescale_y = base_pixmap.height() as f32 / level_height as f32;
142+
}
143+
144+
(required_levels, prescale_x, prescale_y)
145+
}
146+
147+
// Downsamples to match Skia (non-SIMD)
148+
macro_rules! sum_channel {
149+
($channel:ident, $($p:ident),+ ) => {
150+
0u16 $( + $p.$channel() as u16 )+
151+
};
152+
}
153+
154+
fn downsample_2_2(
155+
src_pixels: &[PremultipliedColorU8],
156+
src_y: usize,
157+
src_width: usize,
158+
dst_pixels: &mut [PremultipliedColorU8],
159+
dst_y: usize,
160+
dst_width: usize,
161+
) {
162+
let mut src_x = 0;
163+
for dst_x in 0..dst_width {
164+
let p1 = src_pixels[src_y * src_width + src_x];
165+
let p2 = src_pixels[src_y * src_width + src_x + 1];
166+
let p3 = src_pixels[(src_y + 1) * src_width + src_x];
167+
let p4 = src_pixels[(src_y + 1) * src_width + src_x + 1];
168+
169+
let r = (sum_channel!(red, p1, p2, p3, p4) >> 2) as u8;
170+
let g = (sum_channel!(green, p1, p2, p3, p4) >> 2) as u8;
171+
let b = (sum_channel!(blue, p1, p2, p3, p4) >> 2) as u8;
172+
let a = (sum_channel!(alpha, p1, p2, p3, p4) >> 2) as u8;
173+
dst_pixels[dst_y * dst_width + dst_x] =
174+
PremultipliedColorU8::from_rgba_unchecked(r, g, b, a);
175+
176+
src_x += 2;
177+
}
178+
}
179+
180+
fn downsample_2_3(
181+
src_pixels: &[PremultipliedColorU8],
182+
src_y: usize,
183+
src_width: usize,
184+
dst_pixels: &mut [PremultipliedColorU8],
185+
dst_y: usize,
186+
dst_width: usize,
187+
) {
188+
// Given pixels:
189+
// a0 b0 c0 d0 ...
190+
// a1 b1 c1 d1 ...
191+
// a2 b2 c2 d2 ...
192+
// We want:
193+
// (a0 + 2*a1 + a2 + b0 + 2*b1 + b2) / 8
194+
// (c0 + 2*c1 + c2 + d0 + 2*d1 + d2) / 8
195+
// ...
196+
197+
let mut src_x = 0;
198+
for dst_x in 0..dst_width {
199+
let p1 = src_pixels[src_y * src_width + src_x];
200+
let p2 = src_pixels[src_y * src_width + src_x + 1];
201+
let p3 = src_pixels[(src_y + 1) * src_width + src_x];
202+
let p4 = src_pixels[(src_y + 1) * src_width + src_x + 1];
203+
let p5 = src_pixels[(src_y + 2) * src_width + src_x];
204+
let p6 = src_pixels[(src_y + 2) * src_width + src_x + 1];
205+
206+
let r = (sum_channel!(red, p1, p3, p3, p5, p2, p4, p4, p6) >> 3) as u8;
207+
let g = (sum_channel!(green, p1, p3, p3, p5, p2, p4, p4, p6) >> 3) as u8;
208+
let b = (sum_channel!(blue, p1, p3, p3, p5, p2, p4, p4, p6) >> 3) as u8;
209+
let a = (sum_channel!(alpha, p1, p3, p3, p5, p2, p4, p4, p6) >> 3) as u8;
210+
dst_pixels[dst_y * dst_width + dst_x] =
211+
PremultipliedColorU8::from_rgba_unchecked(r, g, b, a);
212+
213+
src_x += 2;
214+
}
215+
}
216+
217+
fn downsample_3_2(
218+
src_pixels: &[PremultipliedColorU8],
219+
src_y: usize,
220+
src_width: usize,
221+
dst_pixels: &mut [PremultipliedColorU8],
222+
dst_y: usize,
223+
dst_width: usize,
224+
) {
225+
// Given pixels:
226+
// a0 b0 c0 d0 e0 ...
227+
// a1 b1 c1 d1 e1 ...
228+
// We want:
229+
// (a0 + 2*b0 + c0 + a1 + 2*b1 + c1) / 8
230+
// (c0 + 2*d0 + e0 + c1 + 2*d1 + e1) / 8
231+
// ...
232+
233+
let mut src_x = 0;
234+
for dst_x in 0..dst_width {
235+
let p1 = src_pixels[src_y * src_width + src_x];
236+
let p2 = src_pixels[src_y * src_width + src_x + 1];
237+
let p3 = src_pixels[src_y * src_width + src_x + 2];
238+
let p4 = src_pixels[(src_y + 1) * src_width + src_x];
239+
let p5 = src_pixels[(src_y + 1) * src_width + src_x + 1];
240+
let p6 = src_pixels[(src_y + 1) * src_width + src_x + 2];
241+
242+
let r = (sum_channel!(red, p1, p2, p2, p3, p4, p5, p5, p6) >> 3) as u8;
243+
let g = (sum_channel!(green, p1, p2, p2, p3, p4, p5, p5, p6) >> 3) as u8;
244+
let b = (sum_channel!(blue, p1, p2, p2, p3, p4, p5, p5, p6) >> 3) as u8;
245+
let a = (sum_channel!(alpha, p1, p2, p2, p3, p4, p5, p5, p6) >> 3) as u8;
246+
dst_pixels[dst_y * dst_width + dst_x] =
247+
PremultipliedColorU8::from_rgba_unchecked(r, g, b, a);
248+
249+
src_x += 2;
250+
}
251+
}
252+
253+
fn downsample_3_3(
254+
src_pixels: &[PremultipliedColorU8],
255+
src_y: usize,
256+
src_width: usize,
257+
dst_pixels: &mut [PremultipliedColorU8],
258+
dst_y: usize,
259+
dst_width: usize,
260+
) {
261+
// Given pixels:
262+
// a0 b0 c0 d0 e0 ...
263+
// a1 b1 c1 d1 e1 ...
264+
// a2 b2 c2 d2 e2 ...
265+
// We want:
266+
// (a0 + 2*b0 + c0 + 2*a1 + 4*b1 + 2*c1 + a2 + 2*b2 + c2) / 16
267+
// (c0 + 2*d0 + e0 + 2*c1 + 4*d1 + 2*e1 + c2 + 2*d2 + e2) / 16
268+
// ...
269+
270+
let mut src_x = 0;
271+
for dst_x in 0..dst_width {
272+
let p1 = src_pixels[src_y * src_width + src_x];
273+
let p2 = src_pixels[src_y * src_width + src_x + 1];
274+
let p3 = src_pixels[src_y * src_width + src_x + 2];
275+
let p4 = src_pixels[(src_y + 1) * src_width + src_x];
276+
let p5 = src_pixels[(src_y + 1) * src_width + src_x + 1];
277+
let p6 = src_pixels[(src_y + 1) * src_width + src_x + 2];
278+
let p7 = src_pixels[(src_y + 2) * src_width + src_x];
279+
let p8 = src_pixels[(src_y + 2) * src_width + src_x + 1];
280+
let p9 = src_pixels[(src_y + 2) * src_width + src_x + 2];
281+
282+
let r = (sum_channel!(red, p1, p2, p2, p3, p4, p4, p5, p5, p5, p5, p6, p6, p7, p8, p8, p9)
283+
>> 4) as u8;
284+
let g =
285+
(sum_channel!(green, p1, p2, p2, p3, p4, p4, p5, p5, p5, p5, p6, p6, p7, p8, p8, p9)
286+
>> 4) as u8;
287+
let b = (sum_channel!(blue, p1, p2, p2, p3, p4, p4, p5, p5, p5, p5, p6, p6, p7, p8, p8, p9)
288+
>> 4) as u8;
289+
let a =
290+
(sum_channel!(alpha, p1, p2, p2, p3, p4, p4, p5, p5, p5, p5, p6, p6, p7, p8, p8, p9)
291+
>> 4) as u8;
292+
dst_pixels[dst_y * dst_width + dst_x] =
293+
PremultipliedColorU8::from_rgba_unchecked(r, g, b, a);
294+
295+
src_x += 2;
296+
}
297+
}

0 commit comments

Comments
 (0)