@@ -4,14 +4,35 @@ use crate::{
44 tracks:: video_frame_cache:: VideoImage ,
55} ;
66use image:: { RgbaImage , imageops} ;
7+ use rayon:: prelude:: * ;
78use video_utils:: convert:: resize_rgba_image_contain;
89
10+ #[ derive(
11+ Debug ,
12+ Clone ,
13+ Copy ,
14+ PartialEq ,
15+ Eq ,
16+ Default ,
17+ serde:: Serialize ,
18+ serde:: Deserialize ,
19+ num_enum:: TryFromPrimitive ,
20+ num_enum:: IntoPrimitive ,
21+ ) ]
22+ #[ repr( u8 ) ]
23+ pub enum CropShape {
24+ #[ default]
25+ Rectangle = 0 ,
26+ Circle = 1 ,
27+ }
28+
929#[ derive( Debug , Clone , serde:: Serialize , serde:: Deserialize ) ]
1030pub struct CropFilter {
1131 pub left : f32 , // 左边缘偏移(0, 1)
1232 pub top : f32 , // 上边缘偏移(0, 1)
1333 pub width : f32 , // 裁剪宽度(0, 1)
1434 pub height : f32 , // 裁剪高度(0, 1)
35+ pub shape : CropShape ,
1536}
1637
1738impl Default for CropFilter {
@@ -21,19 +42,21 @@ impl Default for CropFilter {
2142 top : 0. ,
2243 width : 1.0 ,
2344 height : 1.0 ,
45+ shape : CropShape :: default ( ) ,
2446 }
2547 }
2648}
2749
2850impl CropFilter {
2951 pub const NAME : & ' static str = "crop" ;
3052
31- pub fn new ( left : f32 , top : f32 , width : f32 , height : f32 ) -> Self {
53+ pub fn new ( left : f32 , top : f32 , width : f32 , height : f32 , shape : CropShape ) -> Self {
3254 Self {
3355 left,
3456 top,
3557 width,
3658 height,
59+ shape,
3760 }
3861 }
3962
@@ -62,11 +85,109 @@ impl CropFilter {
6285 return Ok ( ( ) ) ;
6386 }
6487
88+ match self . shape {
89+ CropShape :: Rectangle => {
90+ let cropped = imageops:: crop ( image, px_left, px_top, px_width, px_height) ;
91+ let cropped_image = cropped. to_image ( ) ;
92+ * image =
93+ resize_rgba_image_contain ( cropped_image, target_width, target_height, false ) ?;
94+ }
95+ CropShape :: Circle => {
96+ self . apply_circular_crop (
97+ image,
98+ px_left,
99+ px_top,
100+ px_width,
101+ px_height,
102+ target_width,
103+ target_height,
104+ ) ?;
105+ }
106+ }
107+
108+ Ok ( ( ) )
109+ }
110+
111+ /// Apply circular crop with anti-aliasing for smooth edges.
112+ /// The circle/ellipse is centered within the crop bounds and scaled to fill the area.
113+ fn apply_circular_crop (
114+ & self ,
115+ image : & mut RgbaImage ,
116+ px_left : u32 ,
117+ px_top : u32 ,
118+ px_width : u32 ,
119+ px_height : u32 ,
120+ target_width : u32 ,
121+ target_height : u32 ,
122+ ) -> Result < ( ) > {
123+ // First, extract the rectangular crop region
65124 let cropped = imageops:: crop ( image, px_left, px_top, px_width, px_height) ;
66125 let cropped_image = cropped. to_image ( ) ;
67126
68- * image = resize_rgba_image_contain ( cropped_image, target_width, target_height, false ) ?;
127+ // Create output image at target size
128+ let mut result = RgbaImage :: new ( target_width, target_height) ;
129+
130+ // Calculate center and radii for the ellipse scaled to target dimensions
131+ // Use the smaller dimension to create a proper circle if width/height are similar
132+ // Otherwise create an ellipse that fills the crop area
133+ let center_x = target_width as f32 / 2.0 ;
134+ let center_y = target_height as f32 / 2.0 ;
135+ let radius_x = target_width as f32 / 2.0 ;
136+ let radius_y = target_height as f32 / 2.0 ;
137+
138+ // Anti-aliasing edge distance (in pixels)
139+ let aa_edge_distance = 1.0 ;
140+
141+ // Sample from cropped image and apply elliptical mask
142+ result
143+ . par_enumerate_pixels_mut ( )
144+ . for_each ( |( px, py, pixel) | {
145+ // Calculate normalized position in target image (0-1)
146+ let norm_x = px as f32 / target_width as f32 ;
147+ let norm_y = py as f32 / target_height as f32 ;
148+
149+ // Map to cropped image coordinates
150+ let src_x = ( norm_x * px_width as f32 ) . round ( ) as u32 ;
151+ let src_y = ( norm_y * px_height as f32 ) . round ( ) as u32 ;
152+
153+ // Clamp to valid range
154+ let src_x = src_x. min ( px_width. saturating_sub ( 1 ) ) ;
155+ let src_y = src_y. min ( px_height. saturating_sub ( 1 ) ) ;
156+
157+ // Get source pixel
158+ let src_pixel = cropped_image. get_pixel ( src_x, src_y) ;
159+
160+ // Check if inside the ellipse using parametric equation
161+ // (x-cx)^2/rx^2 + (y-cy)^2/ry^2 <= 1 means inside
162+ let dx = px as f32 - center_x;
163+ let dy = py as f32 - center_y;
164+ let ellipse_dist =
165+ ( dx * dx) / ( radius_x * radius_x) + ( dy * dy) / ( radius_y * radius_y) ;
166+
167+ // Calculate alpha based on distance from edge
168+ // ellipse_dist < 1 means inside, > 1 means outside
169+ if ellipse_dist <= 1.0 - aa_edge_distance / radius_x. min ( radius_y) {
170+ // Fully inside the ellipse
171+ * pixel = * src_pixel;
172+ } else if ellipse_dist <= 1.0 + aa_edge_distance / radius_x. min ( radius_y) {
173+ // Anti-aliasing zone near the edge
174+ let edge_threshold = 1.0 ;
175+ let aa_factor = ( edge_threshold
176+ - ( ellipse_dist - 1.0 ) * radius_x. min ( radius_y) / aa_edge_distance)
177+ . clamp ( 0.0 , 1.0 ) ;
178+
179+ // Blend with transparency
180+ pixel[ 0 ] = src_pixel[ 0 ] ;
181+ pixel[ 1 ] = src_pixel[ 1 ] ;
182+ pixel[ 2 ] = src_pixel[ 2 ] ;
183+ pixel[ 3 ] = ( ( src_pixel[ 3 ] as f32 * aa_factor) as u8 ) . min ( src_pixel[ 3 ] ) ;
184+ } else {
185+ // Outside the ellipse - transparent
186+ pixel[ 3 ] = 0 ;
187+ }
188+ } ) ;
69189
190+ * image = result;
70191 Ok ( ( ) )
71192 }
72193}
0 commit comments