/rust/registry/src/index.crates.io-6f17d22bba15001f/ravif-0.11.20/src/dirtyalpha.rs
Line | Count | Source (jump to first uncovered line) |
1 | | use imgref::{Img, ImgRef}; |
2 | | use rgb::{ComponentMap, RGB, RGBA8}; |
3 | | |
4 | | #[inline] |
5 | 0 | fn weighed_pixel(px: RGBA8) -> (u16, RGB<u32>) { |
6 | 0 | if px.a == 0 { |
7 | 0 | return (0, RGB::new(0, 0, 0)); |
8 | 0 | } |
9 | 0 | let weight = 256 - u16::from(px.a); |
10 | 0 | (weight, RGB::new( |
11 | 0 | u32::from(px.r) * u32::from(weight), |
12 | 0 | u32::from(px.g) * u32::from(weight), |
13 | 0 | u32::from(px.b) * u32::from(weight))) |
14 | 0 | } |
15 | | |
16 | | /// Clear/change RGB components of fully-transparent RGBA pixels to make them cheaper to encode with AV1 |
17 | 0 | pub(crate) fn blurred_dirty_alpha(img: ImgRef<RGBA8>) -> Option<Img<Vec<RGBA8>>> { |
18 | 0 | // get dominant visible transparent color (excluding opaque pixels) |
19 | 0 | let mut sum = RGB::new(0, 0, 0); |
20 | 0 | let mut weights = 0; |
21 | 0 |
|
22 | 0 | // Only consider colors around transparent images |
23 | 0 | // (e.g. solid semitransparent area doesn't need to contribute) |
24 | 0 | loop9::loop9_img(img, |_, _, top, mid, bot| { |
25 | 0 | if mid.curr.a == 255 || mid.curr.a == 0 { |
26 | 0 | return; |
27 | 0 | } |
28 | 0 | if chain(&top, &mid, &bot).any(|px| px.a == 0) { |
29 | 0 | let (w, px) = weighed_pixel(mid.curr); |
30 | 0 | weights += u64::from(w); |
31 | 0 | sum += px.map(u64::from); |
32 | 0 | } |
33 | 0 | }); |
34 | 0 | if weights == 0 { |
35 | 0 | return None; // opaque image |
36 | 0 | } |
37 | 0 |
|
38 | 0 | let neutral_alpha = RGBA8::new((sum.r / weights) as u8, (sum.g / weights) as u8, (sum.b / weights) as u8, 0); |
39 | 0 | let img2 = bleed_opaque_color(img, neutral_alpha); |
40 | 0 | Some(blur_transparent_pixels(img2.as_ref())) |
41 | 0 | } |
42 | | |
43 | | /// copy color from opaque pixels to transparent pixels |
44 | | /// (so that when edges get crushed by compression, the distortion will be away from visible edge) |
45 | 0 | fn bleed_opaque_color(img: ImgRef<RGBA8>, bg: RGBA8) -> Img<Vec<RGBA8>> { |
46 | 0 | let mut out = Vec::with_capacity(img.width() * img.height()); |
47 | 0 | loop9::loop9_img(img, |_, _, top, mid, bot| { |
48 | 0 | out.push(if mid.curr.a == 255 { |
49 | 0 | mid.curr |
50 | | } else { |
51 | 0 | let (weights, sum) = chain(&top, &mid, &bot) |
52 | 0 | .map(|c| weighed_pixel(*c)) |
53 | 0 | .fold((0u32, RGB::new(0,0,0)), |mut sum, item| { |
54 | 0 | sum.0 += u32::from(item.0); |
55 | 0 | sum.1 += item.1; |
56 | 0 | sum |
57 | 0 | }); |
58 | 0 | if weights == 0 { |
59 | 0 | bg |
60 | | } else { |
61 | 0 | let mut avg = sum.map(|c| (c / weights) as u8); |
62 | 0 | if mid.curr.a == 0 { |
63 | 0 | avg.with_alpha(0) |
64 | | } else { |
65 | | // also change non-transparent colors, but only within range where |
66 | | // rounding caused by premultiplied alpha would land on the same color |
67 | 0 | avg.r = clamp(avg.r, premultiplied_minmax(mid.curr.r, mid.curr.a)); |
68 | 0 | avg.g = clamp(avg.g, premultiplied_minmax(mid.curr.g, mid.curr.a)); |
69 | 0 | avg.b = clamp(avg.b, premultiplied_minmax(mid.curr.b, mid.curr.a)); |
70 | 0 | avg.with_alpha(mid.curr.a) |
71 | | } |
72 | | } |
73 | | }); |
74 | 0 | }); |
75 | 0 | Img::new(out, img.width(), img.height()) |
76 | 0 | } |
77 | | |
78 | | /// ensure there are no sharp edges created by the cleared alpha |
79 | 0 | fn blur_transparent_pixels(img: ImgRef<RGBA8>) -> Img<Vec<RGBA8>> { |
80 | 0 | let mut out = Vec::with_capacity(img.width() * img.height()); |
81 | 0 | loop9::loop9_img(img, |_, _, top, mid, bot| { |
82 | 0 | out.push(if mid.curr.a == 255 { |
83 | 0 | mid.curr |
84 | | } else { |
85 | 0 | let sum: RGB<u16> = chain(&top, &mid, &bot).map(|px| px.rgb().map(u16::from)).sum(); |
86 | 0 | let mut avg = sum.map(|c| (c / 9) as u8); |
87 | 0 | if mid.curr.a == 0 { |
88 | 0 | avg.with_alpha(0) |
89 | | } else { |
90 | | // also change non-transparent colors, but only within range where |
91 | | // rounding caused by premultiplied alpha would land on the same color |
92 | 0 | avg.r = clamp(avg.r, premultiplied_minmax(mid.curr.r, mid.curr.a)); |
93 | 0 | avg.g = clamp(avg.g, premultiplied_minmax(mid.curr.g, mid.curr.a)); |
94 | 0 | avg.b = clamp(avg.b, premultiplied_minmax(mid.curr.b, mid.curr.a)); |
95 | 0 | avg.with_alpha(mid.curr.a) |
96 | | } |
97 | | }); |
98 | 0 | }); |
99 | 0 | Img::new(out, img.width(), img.height()) |
100 | 0 | } |
101 | | |
102 | | #[inline(always)] |
103 | 0 | fn chain<'a, T>(top: &'a loop9::Triple<T>, mid: &'a loop9::Triple<T>, bot: &'a loop9::Triple<T>) -> impl Iterator<Item = &'a T> + 'a { |
104 | 0 | top.iter().chain(mid.iter()).chain(bot.iter()) |
105 | 0 | } |
106 | | |
107 | | #[inline] |
108 | 0 | fn clamp(px: u8, (min, max): (u8, u8)) -> u8 { |
109 | 0 | px.max(min).min(max) |
110 | 0 | } |
111 | | |
112 | | /// safe range to change px color given its alpha |
113 | | /// (mostly-transparent colors tolerate more variation) |
114 | | #[inline] |
115 | 0 | fn premultiplied_minmax(px: u8, alpha: u8) -> (u8, u8) { |
116 | 0 | let alpha = u16::from(alpha); |
117 | 0 | let rounded = u16::from(px) * alpha / 255 * 255; |
118 | 0 |
|
119 | 0 | // leave some spare room for rounding |
120 | 0 | let low = ((rounded + 16) / alpha) as u8; |
121 | 0 | let hi = ((rounded + 239) / alpha) as u8; |
122 | 0 |
|
123 | 0 | (low.min(px), hi.max(px)) |
124 | 0 | } |
125 | | |
126 | | #[test] |
127 | | fn preminmax() { |
128 | | assert_eq!((100, 100), premultiplied_minmax(100, 255)); |
129 | | assert_eq!((78, 100), premultiplied_minmax(100, 10)); |
130 | | assert_eq!(100 * 10 / 255, 78 * 10 / 255); |
131 | | assert_eq!(100 * 10 / 255, 100 * 10 / 255); |
132 | | assert_eq!((8, 119), premultiplied_minmax(100, 2)); |
133 | | assert_eq!((16, 239), premultiplied_minmax(100, 1)); |
134 | | assert_eq!((15, 255), premultiplied_minmax(255, 1)); |
135 | | } |