1mod path;
13use path::{flatten_path, make_dashed_path, make_stroke_path};
14
15use crate::bitmap::Bitmap;
16use crate::clip::{Clip, ClipResult};
17use crate::fill::fill;
18use crate::path::Path;
19use crate::pipe::{PipeSrc, PipeState};
20use crate::types::{LineCap, LineJoin, splash_floor};
21use crate::xpath::XPath;
22use color::Pixel;
23
24pub struct StrokeParams<'a> {
30 pub line_width: f64,
32 pub line_cap: LineCap,
34 pub line_join: LineJoin,
36 pub miter_limit: f64,
38 pub flatness: f64,
40 pub stroke_adjust: bool,
42 pub line_dash: &'a [f64],
44 pub line_dash_phase: f64,
46 pub vector_antialias: bool,
48}
49
50pub fn stroke<P: Pixel>(
59 bitmap: &mut Bitmap<P>,
60 clip: &Clip,
61 path: &Path,
62 pipe: &PipeState<'_>,
63 src: &PipeSrc<'_>,
64 matrix: &[f64; 6],
65 params: &StrokeParams<'_>,
66) {
67 if path.pts.is_empty() {
68 return;
69 }
70
71 let mut path2 = flatten_path(path, matrix, params.flatness);
72
73 if !params.line_dash.is_empty() {
74 path2 = make_dashed_path(&path2, params.line_dash, params.line_dash_phase);
75 if path2.pts.is_empty() {
76 return;
77 }
78 }
79
80 if params.line_width == 0.0 {
85 stroke_narrow::<P>(bitmap, clip, &path2, pipe, src, matrix, params.flatness);
86 } else {
87 stroke_wide::<P>(bitmap, clip, &path2, pipe, src, matrix, params);
88 }
89}
90
91fn stroke_narrow<P: Pixel>(
96 bitmap: &mut Bitmap<P>,
97 clip: &Clip,
98 path: &Path,
99 pipe: &PipeState<'_>,
100 src: &PipeSrc<'_>,
101 matrix: &[f64; 6],
102 flatness: f64,
103) {
104 let xpath = XPath::new(path, matrix, flatness, false);
106
107 for seg in &xpath.segs {
108 let (sx0, sy0, sx1, sy1) = if seg.y0 <= seg.y1 {
110 (seg.x0, seg.y0, seg.x1, seg.y1)
111 } else {
112 (seg.x1, seg.y1, seg.x0, seg.y0)
113 };
114
115 let y0 = splash_floor(sy0);
116 let y1 = splash_floor(sy1);
117 let x0 = splash_floor(sx0);
118 let x1 = splash_floor(sx1);
119
120 let (xl, xr) = if x0 <= x1 { (x0, x1) } else { (x1, x0) };
121 let clip_res = clip.test_rect(xl, y0, xr, y1);
122 if clip_res == ClipResult::AllOutside {
123 continue;
124 }
125
126 if y0 == y1 {
127 let (span_x0, span_x1) = if x0 <= x1 { (x0, x1) } else { (x1, x0) };
129 draw_narrow_span::<P>(bitmap, clip, pipe, src, span_x0, span_x1, y0, clip_res);
130 } else {
131 let dxdy = seg.dxdy;
133
134 let (mut cy0, mut cx0) = (y0, x0);
136 let (mut cy1, mut cx1) = (y1, x1);
137
138 if cy0 < clip.y_min_i {
139 cy0 = clip.y_min_i;
140 cx0 = splash_floor((clip.y_min - sy0).mul_add(dxdy, sx0));
141 }
142 if cy1 > clip.y_max_i {
143 cy1 = clip.y_max_i;
144 cx1 = splash_floor((clip.y_max - sy0).mul_add(dxdy, sx0));
145 }
146
147 let mut xa = cx0;
149 let left_to_right = cx0 <= cx1;
150 for y in cy0..=cy1 {
151 let xb = if y < cy1 {
152 splash_floor((f64::from(y) + 1.0 - sy0).mul_add(dxdy, sx0))
153 } else if left_to_right {
154 cx1 + 1
155 } else {
156 cx1 - 1
157 };
158 let (span_x0, span_x1) = if left_to_right {
159 if xa == xb { (xa, xa) } else { (xa, xb - 1) }
160 } else if xa == xb {
161 (xa, xa)
162 } else {
163 (xb + 1, xa)
164 };
165 draw_narrow_span::<P>(bitmap, clip, pipe, src, span_x0, span_x1, y, clip_res);
166 xa = xb;
167 }
168 }
169 }
170}
171
172fn stroke_wide<P: Pixel>(
174 bitmap: &mut Bitmap<P>,
175 clip: &Clip,
176 path: &Path,
177 pipe: &PipeState<'_>,
178 src: &PipeSrc<'_>,
179 matrix: &[f64; 6],
180 params: &StrokeParams<'_>,
181) {
182 let outline = make_stroke_path(path, params.line_width, params);
183 fill::<P>(
184 bitmap,
185 clip,
186 &outline,
187 pipe,
188 src,
189 matrix,
190 params.flatness,
191 params.vector_antialias,
192 );
193}
194
195#[expect(
201 clippy::too_many_arguments,
202 reason = "all parameters are required; splitting would add indirection"
203)]
204fn draw_narrow_span<P: Pixel>(
205 bitmap: &mut Bitmap<P>,
206 clip: &Clip,
207 pipe: &PipeState<'_>,
208 src: &PipeSrc<'_>,
209 x0: i32,
210 x1: i32,
211 y: i32,
212 clip_res: ClipResult,
213) {
214 if y < 0 {
215 return;
216 }
217 #[expect(clippy::cast_sign_loss, reason = "y >= 0 checked above")]
218 if (y as u32) >= bitmap.height {
219 return;
220 }
221 #[expect(
223 clippy::cast_possible_wrap,
224 reason = "bitmap width fits in i32 in practice"
225 )]
226 let width_i = bitmap.width as i32;
227
228 let (sx0, sx1) = if clip_res == ClipResult::AllInside {
229 (x0.max(0), x1.min(width_i - 1))
230 } else {
231 (x0.max(clip.x_min_i), x1.min(clip.x_max_i))
232 };
233
234 if sx0 > sx1 {
235 return;
236 }
237
238 if clip_res == ClipResult::AllInside {
239 draw_span_unchecked::<P>(bitmap, pipe, src, sx0, sx1, y);
240 } else {
241 let mut run_start: Option<i32> = None;
243 for x in sx0..=sx1 {
244 if clip.test(x, y) {
245 if run_start.is_none() {
246 run_start = Some(x);
247 }
248 } else if let Some(rs) = run_start.take() {
249 draw_span_unchecked::<P>(bitmap, pipe, src, rs, x - 1, y);
250 }
251 }
252 if let Some(rs) = run_start {
253 draw_span_unchecked::<P>(bitmap, pipe, src, rs, sx1, y);
254 }
255 }
256}
257
258fn draw_span_unchecked<P: Pixel>(
260 bitmap: &mut Bitmap<P>,
261 pipe: &PipeState<'_>,
262 src: &PipeSrc<'_>,
263 x0: i32,
264 x1: i32,
265 y: i32,
266) {
267 debug_assert!(x0 <= x1);
268 debug_assert!(y >= 0);
269 #[expect(clippy::cast_sign_loss, reason = "y >= 0")]
270 let y_u = y as u32;
271 #[expect(clippy::cast_sign_loss, reason = "x0 >= 0 after clamping")]
272 let byte_off = x0 as usize * P::BYTES;
273 #[expect(clippy::cast_sign_loss, reason = "x1 >= x0 >= 0")]
274 let byte_end = (x1 as usize + 1) * P::BYTES;
275 #[expect(clippy::cast_sign_loss, reason = "x0 >= 0, x1 >= x0")]
276 let alpha_range = x0 as usize..=x1 as usize;
277
278 let (row, alpha) = bitmap.row_and_alpha_mut(y_u);
279 let dst_pixels = &mut row[byte_off..byte_end];
280 let dst_alpha = alpha.map(|a| &mut a[alpha_range]);
281
282 crate::pipe::render_span::<P>(pipe, src, dst_pixels, dst_alpha, None, x0, x1, y);
283}
284
285#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::bitmap::Bitmap;
291 use crate::path::PathBuilder;
292 use crate::pipe::PipeSrc;
293 use crate::testutil::{identity_matrix, make_clip, simple_pipe};
294 use color::Rgb8;
295
296 fn default_params<'a>() -> StrokeParams<'a> {
297 StrokeParams {
298 line_width: 0.0,
299 line_cap: LineCap::Butt,
300 line_join: LineJoin::Miter,
301 miter_limit: 10.0,
302 flatness: 1.0,
303 stroke_adjust: false,
304 line_dash: &[],
305 line_dash_phase: 0.0,
306 vector_antialias: false,
307 }
308 }
309
310 #[test]
312 fn stroke_narrow_draws_diagonal() {
313 let mut bmp: Bitmap<Rgb8> = Bitmap::new(16, 16, 4, false);
314 let clip = make_clip(16, 16);
315 let pipe = simple_pipe();
316 let color = [255u8, 0, 0];
317 let src = PipeSrc::Solid(&color);
318
319 let mut b = PathBuilder::new();
321 b.move_to(1.0, 1.0).unwrap();
322 b.line_to(8.0, 8.0).unwrap();
323 let path = b.build();
324
325 let flat = flatten_path(&path, &identity_matrix(), 1.0);
327 stroke_narrow::<Rgb8>(&mut bmp, &clip, &flat, &pipe, &src, &identity_matrix(), 1.0);
328
329 let mut any_painted = false;
331 for i in 1u32..9 {
332 if bmp.row(i)[i as usize].r == 255 {
333 any_painted = true;
334 break;
335 }
336 }
337 assert!(
338 any_painted,
339 "stroke_narrow should paint at least one diagonal pixel"
340 );
341 }
342
343 #[test]
346 fn make_stroke_path_non_degenerate() {
347 let mut b = PathBuilder::new();
349 b.move_to(0.0, 0.0).unwrap();
350 b.line_to(10.0, 0.0).unwrap();
351 let path = b.build();
352
353 let params = StrokeParams {
354 line_width: 2.0,
355 ..default_params()
356 };
357 let outline = make_stroke_path(&path, 2.0, ¶ms);
358 assert!(
359 !outline.pts.is_empty(),
360 "make_stroke_path should return a non-empty path for a non-degenerate segment"
361 );
362 assert!(
364 outline.pts.len() >= 4,
365 "stroke outline should have at least 4 points, got {}",
366 outline.pts.len()
367 );
368 }
369
370 #[test]
372 fn make_dashed_path_respects_dash_array() {
373 let mut b = PathBuilder::new();
375 b.move_to(0.0, 0.0).unwrap();
376 b.line_to(20.0, 0.0).unwrap();
377 let path = b.build();
378
379 let dash = [4.0_f64, 2.0];
380 let dashed = make_dashed_path(&path, &dash, 0.0);
381
382 assert!(
384 !dashed.pts.is_empty(),
385 "dashed path should not be empty for a long segment"
386 );
387
388 for pt in &dashed.pts {
390 assert!(
391 pt.x >= -1e-9 && pt.x <= 20.0 + 1e-9,
392 "dashed point x={} out of [0, 20]",
393 pt.x
394 );
395 }
396
397 let first_count = dashed.flags.iter().filter(|f| f.is_first()).count();
399 assert!(
400 first_count >= 2,
401 "dashed path should have at least 2 subpaths (on segments), got {first_count}"
402 );
403 }
404
405 #[test]
407 fn make_dashed_path_zero_dash_is_empty() {
408 let mut b = PathBuilder::new();
409 b.move_to(0.0, 0.0).unwrap();
410 b.line_to(10.0, 0.0).unwrap();
411 let path = b.build();
412
413 let dash = [0.0_f64];
414 let dashed = make_dashed_path(&path, &dash, 0.0);
415 assert!(
416 dashed.pts.is_empty(),
417 "zero dash array should produce empty path"
418 );
419 }
420
421 #[test]
423 fn flatten_path_removes_curves() {
424 let mut b = PathBuilder::new();
425 b.move_to(0.0, 0.0).unwrap();
426 b.curve_to(1.0, 2.0, 3.0, 4.0, 4.0, 0.0).unwrap();
427 let path = b.build();
428
429 assert!(path.flags.iter().any(|f| f.is_curve()));
431
432 let flat = flatten_path(&path, &identity_matrix(), 1.0);
433 assert!(
435 flat.flags.iter().all(|f| !f.is_curve()),
436 "flatten_path must remove all CURVE flags"
437 );
438 assert!(
440 flat.pts.len() >= 2,
441 "flattened curve should have at least 2 points"
442 );
443 }
444}