1use imgref::{Img, ImgRef};
2use rgb::{ComponentMap, RGB, RGBA8};
3
4#[inline]
5fn weighed_pixel(px: RGBA8) -> (u16, RGB<u32>) {
6 if px.a == 0 {
7 return (0, RGB::new(red:0, green:0, blue:0));
8 }
9 let weight: u16 = 256 - u16::from(px.a);
10 (weight, RGB::new(
11 red:u32::from(px.r) * u32::from(weight),
12 green:u32::from(px.g) * u32::from(weight),
13 blue:u32::from(px.b) * u32::from(weight)))
14}
15
16/// Clear/change RGB components of fully-transparent RGBA pixels to make them cheaper to encode with AV1
17pub(crate) fn blurred_dirty_alpha(img: ImgRef<RGBA8>) -> Option<Img<Vec<RGBA8>>> {
18 // get dominant visible transparent color (excluding opaque pixels)
19 let mut sum = RGB::new(0, 0, 0);
20 let mut weights = 0;
21
22 // Only consider colors around transparent images
23 // (e.g. solid semitransparent area doesn't need to contribute)
24 loop9::loop9_img(img, |_, _, top, mid, bot| {
25 if mid.curr.a == 255 || mid.curr.a == 0 {
26 return;
27 }
28 if chain(&top, &mid, &bot).any(|px| px.a == 0) {
29 let (w, px) = weighed_pixel(mid.curr);
30 weights += u64::from(w);
31 sum += px.map(u64::from);
32 }
33 });
34 if weights == 0 {
35 return None; // opaque image
36 }
37
38 let neutral_alpha = RGBA8::new((sum.r / weights) as u8, (sum.g / weights) as u8, (sum.b / weights) as u8, 0);
39 let img2 = bleed_opaque_color(img, neutral_alpha);
40 Some(blur_transparent_pixels(img2.as_ref()))
41}
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)
45fn bleed_opaque_color(img: ImgRef<RGBA8>, bg: RGBA8) -> Img<Vec<RGBA8>> {
46 let mut out = Vec::with_capacity(img.width() * img.height());
47 loop9::loop9_img(img, |_, _, top, mid, bot| {
48 out.push(if mid.curr.a == 255 {
49 mid.curr
50 } else {
51 let (weights, sum) = chain(&top, &mid, &bot)
52 .map(|c| weighed_pixel(*c))
53 .fold((0u32, RGB::new(0,0,0)), |mut sum, item| {
54 sum.0 += u32::from(item.0);
55 sum.1 += item.1;
56 sum
57 });
58 if weights == 0 {
59 bg
60 } else {
61 let mut avg = sum.map(|c| (c / weights) as u8);
62 if mid.curr.a == 0 {
63 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 avg.r = clamp(avg.r, premultiplied_minmax(mid.curr.r, mid.curr.a));
68 avg.g = clamp(avg.g, premultiplied_minmax(mid.curr.g, mid.curr.a));
69 avg.b = clamp(avg.b, premultiplied_minmax(mid.curr.b, mid.curr.a));
70 avg.with_alpha(mid.curr.a)
71 }
72 }
73 });
74 });
75 Img::new(out, img.width(), img.height())
76}
77
78/// ensure there are no sharp edges created by the cleared alpha
79fn blur_transparent_pixels(img: ImgRef<RGBA8>) -> Img<Vec<RGBA8>> {
80 let mut out: Vec> = Vec::with_capacity(img.width() * img.height());
81 loop9::loop9_img(img, |_, _, top: Triple>, mid: Triple>, bot: Triple>| {
82 out.push(if mid.curr.a == 255 {
83 mid.curr
84 } else {
85 let sum: RGB<u16> = chain(&top, &mid, &bot).map(|px: &Rgba| px.rgb().map(u16::from)).sum();
86 let mut avg: Rgb = sum.map(|c: u16| (c / 9) as u8);
87 if mid.curr.a == 0 {
88 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 avg.r = clamp(px:avg.r, premultiplied_minmax(px:mid.curr.r, alpha:mid.curr.a));
93 avg.g = clamp(px:avg.g, premultiplied_minmax(px:mid.curr.g, alpha:mid.curr.a));
94 avg.b = clamp(px:avg.b, premultiplied_minmax(px:mid.curr.b, alpha:mid.curr.a));
95 avg.with_alpha(mid.curr.a)
96 }
97 });
98 });
99 Img::new(buf:out, img.width(), img.height())
100}
101
102#[inline(always)]
103fn 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 top.iter().chain(mid.iter()).chain(bot.iter())
105}
106
107#[inline]
108fn clamp(px: u8, (min: u8, max: u8): (u8, u8)) -> u8 {
109 px.max(min).min(max)
110}
111
112/// safe range to change px color given its alpha
113/// (mostly-transparent colors tolerate more variation)
114#[inline]
115fn premultiplied_minmax(px: u8, alpha: u8) -> (u8, u8) {
116 let alpha: u16 = u16::from(alpha);
117 let rounded: u16 = u16::from(px) * alpha / 255 * 255;
118
119 // leave some spare room for rounding
120 let low: u8 = ((rounded + 16) / alpha) as u8;
121 let hi: u8 = ((rounded + 239) / alpha) as u8;
122
123 (low.min(px), hi.max(px))
124}
125
126#[test]
127fn 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}
136