1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
use imgref::ImgRef;
use imgref::Img;
use rgb::ComponentMap;
use rgb::RGB;
use rgb::RGBA8;

#[inline]
fn weighed_pixel(px: RGBA8) -> (u16, RGB<u32>) {
    if px.a == 0 {
        return (0, RGB::new(0,0,0))
    }
    let weight = 256 - px.a as u16;
    (weight, RGB::new(
        px.r as u32 * weight as u32,
        px.g as u32 * weight as u32,
        px.b as u32 * weight as u32))
}

/// Clear/change RGB components of fully-transparent RGBA pixels to make them cheaper to encode with AV1
pub fn cleared_alpha(mut img: Img<Vec<RGBA8>>) -> Img<Vec<RGBA8>> {
    // get dominant visible transparent color (excluding opaque pixels)
    let mut sum = RGB::new(0,0,0);
    let mut weights = 0;

    // Only consider colors around transparent images
    // (e.g. solid semitransparent area doesn't need to contribute)
    loop9::loop9_img(img.as_ref(), |_, _, top, mid, bot| {
        if mid.curr.a == 255 || mid.curr.a == 0 {
            return;
        }
        if chain(&top, &mid, &bot).any(|px| px.a == 0) {
            let (w, px) = weighed_pixel(mid.curr);
            weights += w as u64;
            sum += px.map(|c| c as u64);
        }
    });
    if weights == 0 {
        return img; // opaque image
    }

    let neutral_alpha = RGBA8::new((sum.r / weights) as u8, (sum.g / weights) as u8, (sum.b / weights) as u8, 0);
    img.pixels_mut().filter(|px| px.a == 0).for_each(|px| *px = neutral_alpha);
    let img2 = bleed_opaque_color(img.as_ref());
    drop(img);
    blur_transparent_pixels(img2.as_ref())
}

/// copy color from opaque pixels to transparent pixels
/// (so that when edges get crushed by compression, the distortion will be away from visible edge)
fn bleed_opaque_color(img: ImgRef<RGBA8>) -> Img<Vec<RGBA8>> {
    let mut out = Vec::with_capacity(img.width() * img.height());
    loop9::loop9_img(img, |_, _, top, mid, bot| {
        out.push(if mid.curr.a == 255 {
            mid.curr
        } else {
            let (weights, sum) = chain(&top, &mid, &bot)
                .map(|c| weighed_pixel(*c))
                .fold((0u32, RGB::new(0,0,0)), |mut sum, item| {
                    sum.0 += item.0 as u32;
                    sum.1 += item.1;
                    sum
                });
            if weights != 0 {
                let mut avg = sum.map(|c| (c / weights) as u8);
                if mid.curr.a == 0 {
                    avg.alpha(0)
                } else {
                    // also change non-transparent colors, but only within range where
                    // rounding caused by premultiplied alpha would land on the same color
                    avg.r = clamp(avg.r, premultiplied_minmax(mid.curr.r, mid.curr.a));
                    avg.g = clamp(avg.g, premultiplied_minmax(mid.curr.g, mid.curr.a));
                    avg.b = clamp(avg.b, premultiplied_minmax(mid.curr.b, mid.curr.a));
                    avg.alpha(mid.curr.a)
                }
            } else {
                mid.curr
            }
        });
    });
    Img::new(out, img.width(), img.height())
}

/// ensure there are no sharp edges created by the cleared alpha
fn blur_transparent_pixels(img: ImgRef<RGBA8>) -> Img<Vec<RGBA8>> {
    let mut out = Vec::with_capacity(img.width() * img.height());
    loop9::loop9_img(img, |_, _, top, mid, bot| {
        out.push(if mid.curr.a == 255 {
            mid.curr
        } else {
            let sum: RGB<u16> =
                chain(&top, &mid, &bot).map(|px| px.rgb().map(|c| c as u16)).sum();
            let mut avg = sum.map(|c| (c / 9) as u8);
            if mid.curr.a == 0 {
                avg.alpha(0)
            } else {
                // also change non-transparent colors, but only within range where
                // rounding caused by premultiplied alpha would land on the same color
                avg.r = clamp(avg.r, premultiplied_minmax(mid.curr.r, mid.curr.a));
                avg.g = clamp(avg.g, premultiplied_minmax(mid.curr.g, mid.curr.a));
                avg.b = clamp(avg.b, premultiplied_minmax(mid.curr.b, mid.curr.a));
                avg.alpha(mid.curr.a)
            }
        });
    });
    Img::new(out, img.width(), img.height())
}

#[inline(always)]
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 {
    top.iter().chain(mid.iter()).chain(bot.iter())
}

#[inline]
fn clamp(px: u8, (min, max): (u8, u8)) -> u8 {
    px.max(min).min(max)
}

/// safe range to change px color given its alpha
/// (mostly-transparent colors tolerate more variation)
#[inline]
fn premultiplied_minmax(px: u8, alpha: u8) -> (u8, u8) {
    let alpha = alpha as u16;
    let rounded = (px as u16) * alpha / 255 * 255;

    // leave some spare room for rounding
    let low = ((rounded + 16) / alpha) as u8;
    let hi = ((rounded + 239) / alpha) as u8;

    (low.min(px), hi.max(px))
}

#[test]
fn preminmax() {
    assert_eq!((100,100), premultiplied_minmax(100, 255));
    assert_eq!((78,100), premultiplied_minmax(100, 10));
    assert_eq!(100*10/255, 78*10/255);
    assert_eq!(100*10/255, 100*10/255);
    assert_eq!((8,119), premultiplied_minmax(100, 2));
    assert_eq!((16,239), premultiplied_minmax(100, 1));
    assert_eq!((15,255), premultiplied_minmax(255, 1));
}