Coverage Report

Created: 2025-07-18 06:49

/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
}