@@ -7,48 +7,116 @@ pub struct Yiq {
77 q : f32 , // saturation of color
88}
99
10- impl Yiq {
11- #[ allow( clippy:: many_single_char_names, clippy:: excessive_precision) ]
12- pub fn rgb2y ( rgb : & image:: Rgb < u8 > ) -> f32 {
13- let rgb = rgb. channels ( ) ;
14- let r = f32:: from ( rgb[ 0 ] ) ;
15- let g = f32:: from ( rgb[ 1 ] ) ;
16- let b = f32:: from ( rgb[ 2 ] ) ;
10+ /// Calculate background color components for blending transparent pixels.
11+ /// Uses position-dependent colors (like pixelmatch) to ensure transparent
12+ /// and opaque versions of the same color compare as different.
13+ ///
14+ /// Design considerations from https://github.com/mapbox/pixelmatch/pull/142:
15+ /// - Non-uniform color (no solid background)
16+ /// - No large areas of uniform color
17+ /// - High perceptual variability
18+ /// - Deterministic computation
19+ /// - Function of pixel index only
20+ /// - Avoids common colors (especially white and black)
21+ /// - Contains no lines or patterns expected in test images
22+ #[ allow( clippy:: excessive_precision) ]
23+ fn background_color ( k : usize ) -> ( f32 , f32 , f32 ) {
24+ let r = 48.0 + 159.0 * ( ( k % 2 ) as f32 ) ;
25+ let g = 48.0 + 159.0 * ( ( k as f32 / 1.618033988749895 ) . floor ( ) as u32 % 2 ) as f32 ;
26+ let b = 48.0 + 159.0 * ( ( k as f32 / 2.618033988749895 ) . floor ( ) as u32 % 2 ) as f32 ;
27+ ( r, g, b)
28+ }
1729
30+ impl Yiq {
31+ #[ allow( clippy:: excessive_precision) ]
32+ pub fn rgb2y ( r : f32 , g : f32 , b : f32 ) -> f32 {
1833 0.298_895_31 * r + 0.586_622_47 * g + 0.114_482_23 * b
1934 }
2035
21- #[ allow( clippy:: many_single_char_names, clippy:: excessive_precision) ]
22- fn rgb2i ( rgb : & image:: Rgb < u8 > ) -> f32 {
23- let rgb = rgb. channels ( ) ;
24- let r = f32:: from ( rgb[ 0 ] ) ;
25- let g = f32:: from ( rgb[ 1 ] ) ;
26- let b = f32:: from ( rgb[ 2 ] ) ;
27-
36+ #[ allow( clippy:: excessive_precision) ]
37+ pub fn rgb2i ( r : f32 , g : f32 , b : f32 ) -> f32 {
2838 0.595_977_99 * r - 0.274_171_6 * g - 0.321_801_89 * b
2939 }
3040
31- #[ allow( clippy:: many_single_char_names, clippy:: excessive_precision) ]
32- fn rgb2q ( rgb : & image:: Rgb < u8 > ) -> f32 {
33- let rgb = rgb. channels ( ) ;
34- let r = f32:: from ( rgb[ 0 ] ) ;
35- let g = f32:: from ( rgb[ 1 ] ) ;
36- let b = f32:: from ( rgb[ 2 ] ) ;
37-
41+ #[ allow( clippy:: excessive_precision) ]
42+ pub fn rgb2q ( r : f32 , g : f32 , b : f32 ) -> f32 {
3843 0.211_470_19 * r - 0.522_617_11 * g + 0.311_146_94 * b
3944 }
4045
46+ /// Convert RGBA to YIQ with position-dependent background blending for
47+ /// transparent pixels. This ensures transparent and opaque versions of the
48+ /// same color compare as different.
49+ pub fn from_rgba_with_pos ( rgba : & image:: Rgba < u8 > , pos : usize ) -> Self {
50+ let rgba_channels = rgba. channels ( ) ;
51+ let r = f32:: from ( rgba_channels[ 0 ] ) ;
52+ let g = f32:: from ( rgba_channels[ 1 ] ) ;
53+ let b = f32:: from ( rgba_channels[ 2 ] ) ;
54+ let a = f32:: from ( rgba_channels[ 3 ] ) ;
55+
56+ let ( r, g, b) = if a < 255.0 {
57+ // Blend with position-dependent background for transparent/semi-transparent pixels
58+ let alpha = a / 255.0 ;
59+ let ( bg_r, bg_g, bg_b) = background_color ( pos) ;
60+ // Alpha blending: result = background + (foreground - background) * alpha
61+ // When alpha=0: pure background; when alpha=1: pure foreground
62+ (
63+ bg_r + ( r - bg_r) * alpha,
64+ bg_g + ( g - bg_g) * alpha,
65+ bg_b + ( b - bg_b) * alpha,
66+ )
67+ } else {
68+ // Fully opaque - use RGB values as-is
69+ ( r, g, b)
70+ } ;
71+
72+ let y = Self :: rgb2y ( r, g, b) ;
73+ let i = Self :: rgb2i ( r, g, b) ;
74+ let q = Self :: rgb2q ( r, g, b) ;
75+
76+ Self { y, i, q }
77+ }
78+
79+ /// Convert RGBA to YIQ without transparency handling.
80+ ///
81+ /// # Deprecated
82+ ///
83+ /// This method does not handle transparency correctly. Use [`from_rgba_with_pos`]
84+ /// instead, which properly handles transparent and semi-transparent pixels
85+ /// by blending with a position-dependent background.
86+ #[ deprecated(
87+ since = "0.8.0" ,
88+ note = "Use from_rgba_with_pos instead for correct transparency handling"
89+ ) ]
90+ #[ allow( dead_code) ]
4191 pub fn from_rgba ( rgba : & image:: Rgba < u8 > ) -> Self {
4292 let rgb = rgba. to_rgb ( ) ;
43- let y = Self :: rgb2y ( & rgb) ;
44- let i = Self :: rgb2i ( & rgb) ;
45- let q = Self :: rgb2q ( & rgb) ;
93+ let rgb_channels = rgb. channels ( ) ;
94+ let r = f32:: from ( rgb_channels[ 0 ] ) ;
95+ let g = f32:: from ( rgb_channels[ 1 ] ) ;
96+ let b = f32:: from ( rgb_channels[ 2 ] ) ;
97+ let y = Self :: rgb2y ( r, g, b) ;
98+ let i = Self :: rgb2i ( r, g, b) ;
99+ let q = Self :: rgb2q ( r, g, b) ;
46100
47101 Self { y, i, q }
48102 }
49103
50104 pub fn delta_y ( left : & image:: Rgb < u8 > , right : & image:: Rgb < u8 > ) -> f32 {
51- Self :: rgb2y ( left) - Self :: rgb2y ( right)
105+ let left_channels = left. channels ( ) ;
106+ let ( left_r, left_g, left_b) = (
107+ f32:: from ( left_channels[ 0 ] ) ,
108+ f32:: from ( left_channels[ 1 ] ) ,
109+ f32:: from ( left_channels[ 2 ] ) ,
110+ ) ;
111+
112+ let right_channels = right. channels ( ) ;
113+ let ( right_r, right_g, right_b) = (
114+ f32:: from ( right_channels[ 0 ] ) ,
115+ f32:: from ( right_channels[ 1 ] ) ,
116+ f32:: from ( right_channels[ 2 ] ) ,
117+ ) ;
118+
119+ Self :: rgb2y ( left_r, left_g, left_b) - Self :: rgb2y ( right_r, right_g, right_b)
52120 }
53121
54122 // in the performance critical applications, square root can be omiitted
@@ -60,6 +128,10 @@ impl Yiq {
60128
61129 if self . y > other. y { -delta } else { delta }
62130 }
131+
132+ pub fn blend_with_white ( & self , alpha : f32 ) -> f32 {
133+ 255.0 + ( self . y - 255.0 ) * alpha
134+ }
63135}
64136
65137#[ cfg( test) ]
@@ -73,7 +145,7 @@ mod tests {
73145 i : 0.0 ,
74146 q : 0.0 ,
75147 } ;
76- let actual = Yiq :: from_rgba ( & image:: Rgba ( [ 0 , 0 , 0 , 0 ] ) ) ;
148+ let actual = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 0 , 0 , 0 , 255 ] ) , 0 ) ;
77149 assert_eq ! ( expected, actual) ;
78150 }
79151
@@ -91,4 +163,99 @@ mod tests {
91163 } ;
92164 assert_eq ! ( a. squared_distance( & b) , 0.0 ) ;
93165 }
166+
167+ #[ test]
168+ fn test_issue_32_transparent_vs_opaque_black ( ) {
169+ // https://github.com/jihchi/dify/issues/32
170+ // Transparent black (#00000000) and opaque black (#000000FF)
171+ // should NOT compare as equal since they appear different visually.
172+ let opaque_black = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 0 , 0 , 0 , 255 ] ) , 0 ) ;
173+ let transparent_black = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 0 , 0 , 0 , 0 ] ) , 0 ) ;
174+
175+ assert_ne ! (
176+ opaque_black. squared_distance( & transparent_black) ,
177+ 0.0 ,
178+ "Transparent black and opaque black should have different YIQ values"
179+ ) ;
180+ }
181+
182+ #[ test]
183+ fn test_semi_transparent_pixels ( ) {
184+ // Semi-transparent pixels (alpha between 0 and 255) should be handled
185+ // by blending with the background color
186+ let opaque = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 100 , 50 , 25 , 255 ] ) , 0 ) ;
187+ let semi_transparent = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 100 , 50 , 25 , 128 ] ) , 0 ) ;
188+ let transparent = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 100 , 50 , 25 , 0 ] ) , 0 ) ;
189+
190+ // All three should have different YIQ values due to different blending
191+ assert_ne ! ( opaque. y, semi_transparent. y) ;
192+ assert_ne ! ( opaque. y, transparent. y) ;
193+ assert_ne ! ( semi_transparent. y, transparent. y) ;
194+ }
195+
196+ #[ test]
197+ fn test_position_dependent_background ( ) {
198+ // Same transparent color at different positions should have different
199+ // YIQ values due to position-dependent background blending
200+ let transparent_red_pos0 = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 255 , 0 , 0 , 0 ] ) , 0 ) ;
201+ let transparent_red_pos1 = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 255 , 0 , 0 , 0 ] ) , 1 ) ;
202+
203+ assert_ne ! (
204+ transparent_red_pos0, transparent_red_pos1,
205+ "Same transparent color at different positions should differ"
206+ ) ;
207+ }
208+
209+ #[ test]
210+ fn test_fully_transparent_pixels_with_different_rgb_compare_equal ( ) {
211+ // Fully transparent pixels (alpha=0) should compare equal regardless of RGB values
212+ // because they are visually identical (completely invisible)
213+ let pos = 42 ;
214+ let transparent_black = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 0 , 0 , 0 , 0 ] ) , pos) ;
215+ let transparent_red = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 255 , 0 , 0 , 0 ] ) , pos) ;
216+ let transparent_white = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 255 , 255 , 255 , 0 ] ) , pos) ;
217+
218+ assert_eq ! ( transparent_black, transparent_red) ;
219+ assert_eq ! ( transparent_black, transparent_white) ;
220+ assert_eq ! (
221+ transparent_black. squared_distance( & transparent_red) ,
222+ 0.0 ,
223+ "Fully transparent pixels should have zero distance regardless of RGB"
224+ ) ;
225+ }
226+
227+ #[ test]
228+ fn test_opaque_pixels_position_independent ( ) {
229+ // Opaque pixels should NOT be affected by position
230+ let opaque_red_pos0 = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 255 , 0 , 0 , 255 ] ) , 0 ) ;
231+ let opaque_red_pos1 = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ 255 , 0 , 0 , 255 ] ) , 100 ) ;
232+
233+ assert_eq ! (
234+ opaque_red_pos0, opaque_red_pos1,
235+ "Opaque pixels should be position-independent"
236+ ) ;
237+ }
238+
239+ #[ test]
240+ fn test_various_colors_with_transparency ( ) {
241+ // Test that transparency handling works for different colors
242+ let color_rgb = [
243+ [ 255 , 0 , 0 ] , // red
244+ [ 0 , 255 , 0 ] , // green
245+ [ 0 , 0 , 255 ] , // blue
246+ [ 255 , 255 , 255 ] , // white
247+ ] ;
248+
249+ // All transparent colors should differ from their opaque equivalents
250+ for rgb in color_rgb {
251+ let transparent = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ rgb[ 0 ] , rgb[ 1 ] , rgb[ 2 ] , 0 ] ) , 0 ) ;
252+ let opaque = Yiq :: from_rgba_with_pos ( & image:: Rgba ( [ rgb[ 0 ] , rgb[ 1 ] , rgb[ 2 ] , 255 ] ) , 0 ) ;
253+
254+ assert_ne ! (
255+ transparent, opaque,
256+ "Transparent {:?} should differ from opaque" ,
257+ rgb
258+ ) ;
259+ }
260+ }
94261}
0 commit comments