Skip to content

Commit a79233a

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 a79233a

File tree

5 files changed

+340
-21
lines changed

5 files changed

+340
-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: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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 downsample = match (src_width & 1 == 0, src_height & 1 == 0) {
76+
(true, true) => downsample_2_2,
77+
(true, false) => downsample_2_3,
78+
(false, true) => downsample_3_2,
79+
(false, false) => downsample_3_3,
80+
};
81+
82+
let mut src_y = 0;
83+
for dst_y in 0..dst_height {
84+
downsample(src_pixels, src_y, src_width, dst_pixels, dst_y, dst_width);
85+
src_y += 2;
86+
}
87+
88+
self.levels.push(dst_pixmap);
89+
src_pixmap = self.levels.get(src_level).unwrap().as_ref();
90+
src_level += 1;
91+
}
92+
93+
src_level
94+
}
95+
}
96+
97+
/// Determine how many Mipmap levels will be needed for a given source and
98+
/// a given (approximate) scaling being applied to the source
99+
///
100+
/// Return the number of levels, and a pre-scale that should be applied to
101+
/// a transform that will 'correct' it to the right size of source
102+
///
103+
/// Note that this is different from Skia since only required levels will
104+
/// be generated
105+
pub fn compute_required_levels(
106+
base_pixmap: PixmapRef,
107+
scale_x: f32,
108+
scale_y: f32,
109+
) -> (usize, f32, f32) {
110+
let mut required_levels: usize = 0;
111+
let mut level_width = base_pixmap.width();
112+
let mut level_height = base_pixmap.height();
113+
let mut prescale_x: f32 = 1.0;
114+
let mut prescale_y: f32 = 1.0;
115+
116+
// Keep generating levels whilst required scale is
117+
// smaller than half of previous level size
118+
while scale_x * prescale_x < 0.5
119+
&& level_width > 1
120+
&& scale_y * prescale_y < 0.5
121+
&& level_height > 1
122+
{
123+
required_levels += 1;
124+
level_width = (level_width as f32 / 2.0).floor() as u32;
125+
level_height = (level_height as f32 / 2.0).floor() as u32;
126+
prescale_x = base_pixmap.width() as f32 / level_width as f32;
127+
prescale_y = base_pixmap.height() as f32 / level_height as f32;
128+
}
129+
130+
(required_levels, prescale_x, prescale_y)
131+
}
132+
133+
// Downsamples to match Skia (non-SIMD)
134+
macro_rules! sum_channel {
135+
($channel:ident, $($p:ident),+ ) => {
136+
0u16 $( + $p.$channel() as u16 )+
137+
};
138+
}
139+
140+
fn downsample_2_2(
141+
src_pixels: &[PremultipliedColorU8],
142+
src_y: usize,
143+
src_width: usize,
144+
dst_pixels: &mut [PremultipliedColorU8],
145+
dst_y: usize,
146+
dst_width: usize,
147+
) {
148+
let mut src_x = 0;
149+
for dst_x in 0..dst_width {
150+
let p1 = src_pixels[src_y * src_width + src_x];
151+
let p2 = src_pixels[src_y * src_width + src_x + 1];
152+
let p3 = src_pixels[(src_y + 1) * src_width + src_x];
153+
let p4 = src_pixels[(src_y + 1) * src_width + src_x + 1];
154+
155+
let r = (sum_channel!(red, p1, p2, p3, p4) >> 2) as u8;
156+
let g = (sum_channel!(green, p1, p2, p3, p4) >> 2) as u8;
157+
let b = (sum_channel!(blue, p1, p2, p3, p4) >> 2) as u8;
158+
let a = (sum_channel!(alpha, p1, p2, p3, p4) >> 2) as u8;
159+
dst_pixels[dst_y * dst_width + dst_x] =
160+
PremultipliedColorU8::from_rgba_unchecked(r, g, b, a);
161+
162+
src_x += 2;
163+
}
164+
}
165+
166+
fn downsample_2_3(
167+
src_pixels: &[PremultipliedColorU8],
168+
src_y: usize,
169+
src_width: usize,
170+
dst_pixels: &mut [PremultipliedColorU8],
171+
dst_y: usize,
172+
dst_width: usize,
173+
) {
174+
// Given pixels:
175+
// a0 b0 c0 d0 ...
176+
// a1 b1 c1 d1 ...
177+
// a2 b2 c2 d2 ...
178+
// We want:
179+
// (a0 + 2*a1 + a2 + b0 + 2*b1 + b2) / 8
180+
// (c0 + 2*c1 + c2 + d0 + 2*d1 + d2) / 8
181+
// ...
182+
183+
let mut src_x = 0;
184+
for dst_x in 0..dst_width {
185+
let p1 = src_pixels[src_y * src_width + src_x];
186+
let p2 = src_pixels[src_y * src_width + src_x + 1];
187+
let p3 = src_pixels[(src_y + 1) * src_width + src_x];
188+
let p4 = src_pixels[(src_y + 1) * src_width + src_x + 1];
189+
let p5 = src_pixels[(src_y + 2) * src_width + src_x];
190+
let p6 = src_pixels[(src_y + 2) * src_width + src_x + 1];
191+
192+
let r = (sum_channel!(red, p1, p3, p3, p5, p2, p4, p4, p6) >> 3) as u8;
193+
let g = (sum_channel!(green, p1, p3, p3, p5, p2, p4, p4, p6) >> 3) as u8;
194+
let b = (sum_channel!(blue, p1, p3, p3, p5, p2, p4, p4, p6) >> 3) as u8;
195+
let a = (sum_channel!(alpha, p1, p3, p3, p5, p2, p4, p4, p6) >> 3) as u8;
196+
dst_pixels[dst_y * dst_width + dst_x] =
197+
PremultipliedColorU8::from_rgba_unchecked(r, g, b, a);
198+
199+
src_x += 2;
200+
}
201+
}
202+
203+
fn downsample_3_2(
204+
src_pixels: &[PremultipliedColorU8],
205+
src_y: usize,
206+
src_width: usize,
207+
dst_pixels: &mut [PremultipliedColorU8],
208+
dst_y: usize,
209+
dst_width: usize,
210+
) {
211+
// Given pixels:
212+
// a0 b0 c0 d0 e0 ...
213+
// a1 b1 c1 d1 e1 ...
214+
// We want:
215+
// (a0 + 2*b0 + c0 + a1 + 2*b1 + c1) / 8
216+
// (c0 + 2*d0 + e0 + c1 + 2*d1 + e1) / 8
217+
// ...
218+
219+
let mut src_x = 0;
220+
for dst_x in 0..dst_width {
221+
let p1 = src_pixels[src_y * src_width + src_x];
222+
let p2 = src_pixels[src_y * src_width + src_x + 1];
223+
let p3 = src_pixels[src_y * src_width + src_x + 2];
224+
let p4 = src_pixels[(src_y + 1) * src_width + src_x];
225+
let p5 = src_pixels[(src_y + 1) * src_width + src_x + 1];
226+
let p6 = src_pixels[(src_y + 1) * src_width + src_x + 2];
227+
228+
let r = (sum_channel!(red, p1, p2, p2, p3, p4, p5, p5, p6) >> 3) as u8;
229+
let g = (sum_channel!(green, p1, p2, p2, p3, p4, p5, p5, p6) >> 3) as u8;
230+
let b = (sum_channel!(blue, p1, p2, p2, p3, p4, p5, p5, p6) >> 3) as u8;
231+
let a = (sum_channel!(alpha, p1, p2, p2, p3, p4, p5, p5, p6) >> 3) as u8;
232+
dst_pixels[dst_y * dst_width + dst_x] =
233+
PremultipliedColorU8::from_rgba_unchecked(r, g, b, a);
234+
235+
src_x += 2;
236+
}
237+
}
238+
239+
fn downsample_3_3(
240+
src_pixels: &[PremultipliedColorU8],
241+
src_y: usize,
242+
src_width: usize,
243+
dst_pixels: &mut [PremultipliedColorU8],
244+
dst_y: usize,
245+
dst_width: usize,
246+
) {
247+
// Given pixels:
248+
// a0 b0 c0 d0 e0 ...
249+
// a1 b1 c1 d1 e1 ...
250+
// a2 b2 c2 d2 e2 ...
251+
// We want:
252+
// (a0 + 2*b0 + c0 + 2*a1 + 4*b1 + 2*c1 + a2 + 2*b2 + c2) / 16
253+
// (c0 + 2*d0 + e0 + 2*c1 + 4*d1 + 2*e1 + c2 + 2*d2 + e2) / 16
254+
// ...
255+
256+
let mut src_x = 0;
257+
for dst_x in 0..dst_width {
258+
let p1 = src_pixels[src_y * src_width + src_x];
259+
let p2 = src_pixels[src_y * src_width + src_x + 1];
260+
let p3 = src_pixels[src_y * src_width + src_x + 2];
261+
let p4 = src_pixels[(src_y + 1) * src_width + src_x];
262+
let p5 = src_pixels[(src_y + 1) * src_width + src_x + 1];
263+
let p6 = src_pixels[(src_y + 1) * src_width + src_x + 2];
264+
let p7 = src_pixels[(src_y + 2) * src_width + src_x];
265+
let p8 = src_pixels[(src_y + 2) * src_width + src_x + 1];
266+
let p9 = src_pixels[(src_y + 2) * src_width + src_x + 2];
267+
268+
let r = (sum_channel!(red, p1, p2, p2, p3, p4, p4, p5, p5, p5, p5, p6, p6, p7, p8, p8, p9)
269+
>> 4) as u8;
270+
let g =
271+
(sum_channel!(green, p1, p2, p2, p3, p4, p4, p5, p5, p5, p5, p6, p6, p7, p8, p8, p9)
272+
>> 4) as u8;
273+
let b = (sum_channel!(blue, p1, p2, p2, p3, p4, p4, p5, p5, p5, p5, p6, p6, p7, p8, p8, p9)
274+
>> 4) as u8;
275+
let a =
276+
(sum_channel!(alpha, p1, p2, p2, p3, p4, p4, p5, p5, p5, p5, p6, p6, p7, p8, p8, p9)
277+
>> 4) as u8;
278+
dst_pixels[dst_y * dst_width + dst_x] =
279+
PremultipliedColorU8::from_rgba_unchecked(r, g, b, a);
280+
281+
src_x += 2;
282+
}
283+
}

src/pipeline/blitter.rs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// Use of this source code is governed by a BSD-style license that can be
55
// found in the LICENSE file.
66

7-
use crate::{BlendMode, Color, LengthU32, Paint, PixmapRef, PremultipliedColorU8, Shader};
7+
use crate::{BlendMode, Color, LengthU32, Mipmaps, Paint, PixmapRef, PremultipliedColorU8, Shader};
88
use crate::{ALPHA_U8_OPAQUE, ALPHA_U8_TRANSPARENT};
99

1010
use crate::alpha_runs::AlphaRun;
@@ -18,7 +18,7 @@ use crate::pixmap::SubPixmapMut;
1818

1919
pub struct RasterPipelineBlitter<'a, 'b: 'a> {
2020
mask: Option<SubMaskRef<'a>>,
21-
pixmap_src: PixmapRef<'a>,
21+
mipmaps: Mipmaps<'a>,
2222
pixmap: &'a mut SubPixmapMut<'b>,
2323
memset2d_color: Option<PremultipliedColorU8>,
2424
blit_anti_h_rp: RasterPipeline,
@@ -201,7 +201,7 @@ impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b> {
201201

202202
Some(RasterPipelineBlitter {
203203
mask,
204-
pixmap_src,
204+
mipmaps: Mipmaps::new(pixmap_src),
205205
pixmap,
206206
memset2d_color,
207207
blit_anti_h_rp,
@@ -241,9 +241,10 @@ impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b> {
241241
p.compile()
242242
};
243243

244+
let pixmap_src = PixmapRef::from_bytes(&[0, 0, 0, 0], 1, 1).unwrap();
244245
Some(RasterPipelineBlitter {
245246
mask: None,
246-
pixmap_src: PixmapRef::from_bytes(&[0, 0, 0, 0], 1, 1).unwrap(),
247+
mipmaps: Mipmaps::new(pixmap_src),
247248
pixmap,
248249
memset2d_color,
249250
blit_anti_h_rp,
@@ -276,13 +277,18 @@ impl Blitter for RasterPipelineBlitter<'_, '_> {
276277
}
277278
alpha => {
278279
self.blit_anti_h_rp.ctx.current_coverage = alpha as f32 * (1.0 / 255.0);
279-
280280
let rect = ScreenIntRect::from_xywh_safe(x, y, width, LENGTH_U32_ONE);
281+
282+
let mipmap_index = self
283+
.mipmaps
284+
.build(self.blit_anti_h_rp.ctx.required_mipmap_levels);
285+
let pixmap_src = self.mipmaps.get(mipmap_index);
286+
281287
self.blit_anti_h_rp.run(
282288
&rect,
283289
pipeline::AAMaskCtx::default(),
284290
mask_ctx,
285-
self.pixmap_src,
291+
pixmap_src,
286292
self.pixmap,
287293
);
288294
}
@@ -360,11 +366,16 @@ impl Blitter for RasterPipelineBlitter<'_, '_> {
360366

361367
let mask_ctx = self.mask.map(|c| c.mask_ctx()).unwrap_or_default();
362368

369+
let mipmap_index = self
370+
.mipmaps
371+
.build(self.blit_rect_rp.ctx.required_mipmap_levels);
372+
let pixmap_src = self.mipmaps.get(mipmap_index);
373+
363374
self.blit_rect_rp.run(
364375
rect,
365376
pipeline::AAMaskCtx::default(),
366377
mask_ctx,
367-
self.pixmap_src,
378+
pixmap_src,
368379
self.pixmap,
369380
);
370381
}
@@ -378,7 +389,12 @@ impl Blitter for RasterPipelineBlitter<'_, '_> {
378389

379390
let mask_ctx = self.mask.map(|c| c.mask_ctx()).unwrap_or_default();
380391

392+
let mipmap_index = self
393+
.mipmaps
394+
.build(self.blit_mask_rp.ctx.required_mipmap_levels);
395+
let pixmap_src = self.mipmaps.get(mipmap_index);
396+
381397
self.blit_mask_rp
382-
.run(clip, aa_mask_ctx, mask_ctx, self.pixmap_src, self.pixmap);
398+
.run(clip, aa_mask_ctx, mask_ctx, pixmap_src, self.pixmap);
383399
}
384400
}

0 commit comments

Comments
 (0)