| 1 | use imgref::{Img, ImgRef}; |
| 2 | use rgb::{ComponentMap, RGB, RGBA8}; |
| 3 | |
| 4 | #[inline ] |
| 5 | fn 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 |
| 17 | pub(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) |
| 45 | fn 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 |
| 79 | fn 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)] |
| 103 | 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 | top.iter().chain(mid.iter()).chain(bot.iter()) |
| 105 | } |
| 106 | |
| 107 | #[inline ] |
| 108 | fn 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 ] |
| 115 | fn 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 ] |
| 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 | } |
| 136 | |