Skip to main content

agg_rust/
renderer_outline_image.rs

1//! Image-patterned outline renderer.
2//!
3//! Port of `agg_renderer_outline_image.h`.
4//! Renders anti-aliased lines with image patterns applied along the stroke.
5//! The pattern is sampled from a source image and mapped onto each line segment.
6//!
7//! Copyright 2025-2026.
8
9use crate::basics::{iround, RectI};
10use crate::color::{Rgba, Rgba8};
11use crate::dda_line::Dda2LineInterpolator;
12use crate::line_aa_basics::*;
13use crate::pattern_filters_rgba::PatternFilter;
14use crate::pixfmt_rgba::PixelFormat;
15use crate::renderer_base::RendererBase;
16use crate::renderer_outline_aa::OutlineAaRenderer;
17
18// ============================================================================
19// Pattern source trait
20// ============================================================================
21
22/// Trait for image pattern sources.
23///
24/// Any type implementing this can be used as input to `LineImagePattern::create()`.
25/// The C++ equivalent uses a template parameter with `width()`, `height()`, `pixel()`.
26pub trait ImagePatternSource {
27    fn width(&self) -> f64;
28    fn height(&self) -> f64;
29    fn pixel(&self, x: i32, y: i32) -> Rgba8;
30
31    /// Full-precision (floating-point) color used by scaling filters such as
32    /// [`LineImageScale`].
33    ///
34    /// The default widens the 8-bit [`pixel`](Self::pixel) result to float.
35    /// Sources that internally carry more precision — for example an
36    /// sRGB-decoded, premultiplied pixmap whose C++ `color_type` is `rgba`
37    /// rather than `rgba8` — should override this to avoid quantizing at the
38    /// leaf before the filter runs. That early rounding otherwise costs up to
39    /// one least-significant bit per channel versus the C++ result, which
40    /// keeps the source color in floating point all the way through
41    /// `line_image_scale` and only converts to `rgba8` when the pattern is
42    /// finally stored.
43    fn pixel_rgba(&self, x: i32, y: i32) -> Rgba {
44        rgba8_to_rgba(self.pixel(x, y))
45    }
46}
47
48// ============================================================================
49// line_image_scale — scales a pattern source to a different height
50// ============================================================================
51
52/// Helper that wraps a pattern source and scales its height.
53///
54/// Port of C++ `line_image_scale<Source>`.
55/// Uses float (Rgba) arithmetic for both branches, matching C++ exactly.
56pub struct LineImageScale<'a, S: ImagePatternSource> {
57    source: &'a S,
58    height: f64,
59    scale: f64,
60    scale_inv: f64,
61}
62
63/// Convert Rgba8 to Rgba (float) — matching C++ `rgba(rgba8)` conversion.
64#[inline]
65fn rgba8_to_rgba(c: Rgba8) -> Rgba {
66    Rgba::new(
67        c.r as f64 / 255.0,
68        c.g as f64 / 255.0,
69        c.b as f64 / 255.0,
70        c.a as f64 / 255.0,
71    )
72}
73
74/// Convert Rgba (float) back to Rgba8 — matching C++ `rgba8(rgba)` conversion.
75/// Uses uround (v + 0.5) as i32, clamped to [0, 255].
76#[inline]
77fn rgba_to_rgba8(c: &Rgba) -> Rgba8 {
78    #[inline]
79    fn clamp_u8(v: f64) -> u32 {
80        let i = (v * 255.0 + 0.5) as i32;
81        i.clamp(0, 255) as u32
82    }
83    Rgba8::new(clamp_u8(c.r), clamp_u8(c.g), clamp_u8(c.b), clamp_u8(c.a))
84}
85
86impl<'a, S: ImagePatternSource> LineImageScale<'a, S> {
87    pub fn new(source: &'a S, height: f64) -> Self {
88        let sh = source.height();
89        Self {
90            source,
91            height,
92            scale: sh / height,
93            scale_inv: height / sh,
94        }
95    }
96}
97
98impl<'a, S: ImagePatternSource> ImagePatternSource for LineImageScale<'a, S> {
99    fn width(&self) -> f64 {
100        self.source.width()
101    }
102
103    fn height(&self) -> f64 {
104        self.height
105    }
106
107    /// Sample the scaled pattern at (x, y).
108    ///
109    /// Port of C++ `line_image_scale::pixel`.
110    /// Uses float (Rgba) arithmetic for both branches, matching C++ exactly:
111    /// - scale < 1.0: gradient interpolation between two rows
112    /// - scale >= 1.0: area-weighted average of multiple rows
113    fn pixel(&self, x: i32, y: i32) -> Rgba8 {
114        let h = self.source.height() as i32 - 1;
115
116        if self.scale < 1.0 {
117            // Interpolate between two nearest source rows
118            let src_y = (y as f64 + 0.5) * self.scale - 0.5;
119            let y1 = src_y.floor() as i32;
120            let y2 = y1 + 1;
121            let pix1 = if y1 < 0 {
122                Rgba::no_color()
123            } else {
124                self.source.pixel_rgba(x, y1)
125            };
126            let pix2 = if y2 > h {
127                Rgba::no_color()
128            } else {
129                self.source.pixel_rgba(x, y2)
130            };
131            let k = src_y - y1 as f64;
132            rgba_to_rgba8(&pix1.gradient(&pix2, k))
133        } else {
134            // Area-weighted average of source rows covering [src_y1, src_y2)
135            let src_y1 = (y as f64 + 0.5) * self.scale - 0.5;
136            let src_y2 = src_y1 + self.scale;
137            let mut y1 = src_y1.floor() as i32;
138            let y2 = src_y2.floor() as i32;
139
140            let mut c = Rgba::no_color();
141
142            // First partial row
143            if y1 >= 0 {
144                let weight = (y1 + 1) as f64 - src_y1;
145                let p = self.source.pixel_rgba(x, y1);
146                c += p * weight;
147            }
148
149            // Full middle rows
150            y1 += 1;
151            while y1 < y2 {
152                if y1 <= h {
153                    c += self.source.pixel_rgba(x, y1);
154                }
155                y1 += 1;
156            }
157
158            // Last partial row
159            if y2 <= h {
160                let weight = src_y2 - y2 as f64;
161                let p = self.source.pixel_rgba(x, y2);
162                c += p * weight;
163            }
164
165            c *= self.scale_inv;
166            rgba_to_rgba8(&c)
167        }
168    }
169}
170
171// ============================================================================
172// line_image_pattern — the main pattern container
173// ============================================================================
174
175/// Ceiling of float to unsigned — port of C++ `uceil`.
176#[inline]
177fn uceil(v: f64) -> u32 {
178    v.ceil() as u32
179}
180
181/// Round float to signed int — port of C++ `uround`.
182#[inline]
183fn uround(v: f64) -> i32 {
184    (v + 0.5) as i32
185}
186
187/// Image pattern for line rendering.
188///
189/// Port of C++ `line_image_pattern<Filter>`.
190/// Stores a copy of the pattern image extended with dilation borders
191/// for the filter to access neighboring pixels.
192pub struct LineImagePattern<F: PatternFilter> {
193    _phantom: std::marker::PhantomData<F>,
194    /// 2D pixel buffer: rows[y][x]
195    buf: Vec<Vec<Rgba8>>,
196    dilation: u32,
197    dilation_hr: i32,
198    width: u32,
199    height: u32,
200    width_hr: i32,
201    half_height_hr: i32,
202    offset_y_hr: i32,
203}
204
205impl<F: PatternFilter> LineImagePattern<F> {
206    /// Create an empty pattern with the specified filter type.
207    pub fn new() -> Self {
208        let dilation = F::dilation() + 1;
209        Self {
210            _phantom: std::marker::PhantomData,
211            buf: Vec::new(),
212            dilation,
213            dilation_hr: (dilation as i32) << LINE_SUBPIXEL_SHIFT,
214            width: 0,
215            height: 0,
216            width_hr: 0,
217            half_height_hr: 0,
218            offset_y_hr: 0,
219        }
220    }
221
222    /// Create a pattern initialized from a source.
223    pub fn with_source<S: ImagePatternSource>(src: &S) -> Self {
224        let mut p = Self::new();
225        p.create(src);
226        p
227    }
228
229    /// Initialize or reinitialize the pattern from a source image.
230    ///
231    /// Port of C++ `line_image_pattern::create`.
232    pub fn create<S: ImagePatternSource>(&mut self, src: &S) {
233        self.height = uceil(src.height());
234        self.width = uceil(src.width());
235        self.width_hr = uround(src.width() * LINE_SUBPIXEL_SCALE as f64);
236        self.half_height_hr = uround(src.height() * LINE_SUBPIXEL_SCALE as f64 / 2.0);
237        self.offset_y_hr =
238            self.dilation_hr + self.half_height_hr - LINE_SUBPIXEL_SCALE / 2;
239        self.half_height_hr += LINE_SUBPIXEL_SCALE / 2;
240
241        let total_w = (self.width + self.dilation * 2) as usize;
242        let total_h = (self.height + self.dilation * 2) as usize;
243
244        // Allocate buffer
245        self.buf = vec![vec![Rgba8::new(0, 0, 0, 0); total_w]; total_h];
246
247        // Copy source pixels into center region
248        for y in 0..self.height as usize {
249            let row = &mut self.buf[y + self.dilation as usize];
250            for x in 0..self.width as usize {
251                row[x + self.dilation as usize] = src.pixel(x as i32, y as i32);
252            }
253        }
254
255        // Fill top/bottom dilation borders with no_color (transparent)
256        let no_color = Rgba8::new(0, 0, 0, 0);
257        for dy in 0..self.dilation as usize {
258            // Bottom border
259            let row_bot = &mut self.buf[self.dilation as usize + self.height as usize + dy];
260            for x in 0..self.width as usize {
261                row_bot[x + self.dilation as usize] = no_color;
262            }
263            // Top border
264            let row_top = &mut self.buf[self.dilation as usize - dy - 1];
265            for x in 0..self.width as usize {
266                row_top[x + self.dilation as usize] = no_color;
267            }
268        }
269
270        // Fill left/right dilation borders (wrap from opposite side)
271        // C++ wraps: right border gets left-edge pixels, left border gets right-edge pixels
272        for y in 0..total_h {
273            for dx in 0..self.dilation as usize {
274                // Right border: copy from left edge of center
275                let src_val = self.buf[y][self.dilation as usize + dx];
276                self.buf[y][self.dilation as usize + self.width as usize + dx] = src_val;
277
278                // Left border: copy from right edge of center
279                let src_val = self.buf[y]
280                    [self.dilation as usize + self.width as usize - 1 - dx];
281                self.buf[y][self.dilation as usize - 1 - dx] = src_val;
282            }
283        }
284    }
285
286    /// Pattern width in subpixel coordinates (for repeating).
287    pub fn pattern_width(&self) -> i32 {
288        self.width_hr
289    }
290
291    /// Line width in subpixel coordinates (half-height of pattern).
292    pub fn line_width(&self) -> i32 {
293        self.half_height_hr
294    }
295
296    /// Width in floating-point (returns height, matching C++ behavior).
297    pub fn width(&self) -> f64 {
298        self.height as f64
299    }
300
301    /// Get a pixel from the pattern at the given subpixel coordinates.
302    ///
303    /// Port of C++ `line_image_pattern::pixel`.
304    #[inline]
305    pub fn pixel(&self, p: &mut Rgba8, x: i32, y: i32) {
306        F::pixel_high_res(
307            &self.buf,
308            p,
309            x % self.width_hr + self.dilation_hr,
310            y + self.offset_y_hr,
311        );
312    }
313}
314
315// ============================================================================
316// line_image_pattern_pow2 — optimized version using power-of-2 masking
317// ============================================================================
318
319/// Power-of-2 optimized image pattern for line rendering.
320///
321/// Port of C++ `line_image_pattern_pow2<Filter>`.
322/// Uses bit masking instead of modulo for pattern wrapping.
323pub struct LineImagePatternPow2<F: PatternFilter> {
324    base: LineImagePattern<F>,
325    mask: u32,
326}
327
328impl<F: PatternFilter> LineImagePatternPow2<F> {
329    pub fn new() -> Self {
330        Self {
331            base: LineImagePattern::new(),
332            mask: LINE_SUBPIXEL_MASK as u32,
333        }
334    }
335
336    pub fn with_source<S: ImagePatternSource>(src: &S) -> Self {
337        let mut p = Self::new();
338        p.create(src);
339        p
340    }
341
342    pub fn create<S: ImagePatternSource>(&mut self, src: &S) {
343        self.base.create(src);
344        self.mask = 1;
345        while self.mask < self.base.width {
346            self.mask <<= 1;
347            self.mask |= 1;
348        }
349        self.mask <<= LINE_SUBPIXEL_SHIFT as u32 - 1;
350        self.mask |= LINE_SUBPIXEL_MASK as u32;
351        self.base.width_hr = self.mask as i32 + 1;
352    }
353
354    pub fn pattern_width(&self) -> i32 {
355        self.base.width_hr
356    }
357
358    pub fn line_width(&self) -> i32 {
359        self.base.half_height_hr
360    }
361
362    pub fn width(&self) -> f64 {
363        self.base.height as f64
364    }
365
366    #[inline]
367    pub fn pixel(&self, p: &mut Rgba8, x: i32, y: i32) {
368        F::pixel_high_res(
369            &self.base.buf,
370            p,
371            (x & self.mask as i32) + self.base.dilation_hr,
372            y + self.base.offset_y_hr,
373        );
374    }
375}
376
377// ============================================================================
378// ImageLinePattern trait — common interface for line image patterns
379// ============================================================================
380
381/// Trait for image-line patterns (both standard and pow2-optimized).
382///
383/// This abstracts over `LineImagePattern` and `LineImagePatternPow2` so that
384/// `RendererOutlineImage` can work with either type.
385pub trait ImageLinePattern {
386    /// Pattern width in subpixel coordinates (for repeating).
387    fn pattern_width(&self) -> i32;
388    /// Line width in subpixel coordinates (half-height of pattern).
389    fn line_width(&self) -> i32;
390    /// Width in floating-point (returns height, matching C++ behavior).
391    fn width(&self) -> f64;
392    /// Get a pixel from the pattern at the given subpixel coordinates.
393    fn pixel(&self, p: &mut Rgba8, x: i32, y: i32);
394}
395
396impl<F: PatternFilter> ImageLinePattern for LineImagePattern<F> {
397    fn pattern_width(&self) -> i32 { self.pattern_width() }
398    fn line_width(&self) -> i32 { self.line_width() }
399    fn width(&self) -> f64 { self.width() }
400    fn pixel(&self, p: &mut Rgba8, x: i32, y: i32) { self.pixel(p, x, y) }
401}
402
403impl<F: PatternFilter> ImageLinePattern for LineImagePatternPow2<F> {
404    fn pattern_width(&self) -> i32 { self.pattern_width() }
405    fn line_width(&self) -> i32 { self.line_width() }
406    fn width(&self) -> f64 { self.width() }
407    fn pixel(&self, p: &mut Rgba8, x: i32, y: i32) { self.pixel(p, x, y) }
408}
409
410// ============================================================================
411// distance_interpolator4 — for image pattern rendering
412// ============================================================================
413
414/// Distance interpolator for image-patterned lines.
415///
416/// Port of C++ `distance_interpolator4`.
417/// Tracks perpendicular distance, start/end join distances, and pattern offset distance.
418pub struct DistanceInterpolator4 {
419    dx: i32,
420    dy: i32,
421    dx_start: i32,
422    dy_start: i32,
423    dx_pict: i32,
424    dy_pict: i32,
425    dx_end: i32,
426    dy_end: i32,
427    dist: i32,
428    dist_start: i32,
429    dist_pict: i32,
430    dist_end: i32,
431    len: i32,
432}
433
434impl DistanceInterpolator4 {
435    #[allow(clippy::too_many_arguments)]
436    pub fn new(
437        x1: i32, y1: i32, x2: i32, y2: i32,
438        sx: i32, sy: i32, ex: i32, ey: i32,
439        len: i32, scale: f64, x: i32, y: i32,
440    ) -> Self {
441        let mut dx = x2 - x1;
442        let mut dy = y2 - y1;
443        let mut dx_start = line_mr(sx) - line_mr(x1);
444        let mut dy_start = line_mr(sy) - line_mr(y1);
445        let mut dx_end = line_mr(ex) - line_mr(x2);
446        let mut dy_end = line_mr(ey) - line_mr(y2);
447
448        let dist = iround(
449            (x + LINE_SUBPIXEL_SCALE / 2 - x2) as f64 * dy as f64
450                - (y + LINE_SUBPIXEL_SCALE / 2 - y2) as f64 * dx as f64,
451        );
452
453        let dist_start = (line_mr(x + LINE_SUBPIXEL_SCALE / 2) - line_mr(sx)) * dy_start
454            - (line_mr(y + LINE_SUBPIXEL_SCALE / 2) - line_mr(sy)) * dx_start;
455
456        let dist_end = (line_mr(x + LINE_SUBPIXEL_SCALE / 2) - line_mr(ex)) * dy_end
457            - (line_mr(y + LINE_SUBPIXEL_SCALE / 2) - line_mr(ey)) * dx_end;
458
459        let ilen = uround(len as f64 / scale);
460
461        let d = len as f64 * scale;
462        let dx_f = iround(((x2 - x1) << LINE_SUBPIXEL_SHIFT) as f64 / d);
463        let dy_f = iround(((y2 - y1) << LINE_SUBPIXEL_SHIFT) as f64 / d);
464        let dx_pict = -dy_f;
465        let dy_pict = dx_f;
466        let dist_pict = ((x + LINE_SUBPIXEL_SCALE / 2 - (x1 - dy_f)) as i64
467            * dy_pict as i64
468            - (y + LINE_SUBPIXEL_SCALE / 2 - (y1 + dx_f)) as i64
469                * dx_pict as i64)
470            >> LINE_SUBPIXEL_SHIFT;
471
472        dx <<= LINE_SUBPIXEL_SHIFT;
473        dy <<= LINE_SUBPIXEL_SHIFT;
474        dx_start <<= LINE_MR_SUBPIXEL_SHIFT;
475        dy_start <<= LINE_MR_SUBPIXEL_SHIFT;
476        dx_end <<= LINE_MR_SUBPIXEL_SHIFT;
477        dy_end <<= LINE_MR_SUBPIXEL_SHIFT;
478
479        Self {
480            dx, dy, dx_start, dy_start, dx_pict, dy_pict, dx_end, dy_end,
481            dist, dist_start, dist_pict: dist_pict as i32, dist_end,
482            len: ilen,
483        }
484    }
485
486    #[inline]
487    pub fn inc_x(&mut self, dy: i32) {
488        self.dist += self.dy;
489        self.dist_start += self.dy_start;
490        self.dist_pict += self.dy_pict;
491        self.dist_end += self.dy_end;
492        if dy > 0 {
493            self.dist -= self.dx;
494            self.dist_start -= self.dx_start;
495            self.dist_pict -= self.dx_pict;
496            self.dist_end -= self.dx_end;
497        }
498        if dy < 0 {
499            self.dist += self.dx;
500            self.dist_start += self.dx_start;
501            self.dist_pict += self.dx_pict;
502            self.dist_end += self.dx_end;
503        }
504    }
505
506    #[inline]
507    pub fn dec_x(&mut self, dy: i32) {
508        self.dist -= self.dy;
509        self.dist_start -= self.dy_start;
510        self.dist_pict -= self.dy_pict;
511        self.dist_end -= self.dy_end;
512        if dy > 0 {
513            self.dist -= self.dx;
514            self.dist_start -= self.dx_start;
515            self.dist_pict -= self.dx_pict;
516            self.dist_end -= self.dx_end;
517        }
518        if dy < 0 {
519            self.dist += self.dx;
520            self.dist_start += self.dx_start;
521            self.dist_pict += self.dx_pict;
522            self.dist_end += self.dx_end;
523        }
524    }
525
526    #[inline]
527    pub fn inc_y(&mut self, dx: i32) {
528        self.dist -= self.dx;
529        self.dist_start -= self.dx_start;
530        self.dist_pict -= self.dx_pict;
531        self.dist_end -= self.dx_end;
532        if dx > 0 {
533            self.dist += self.dy;
534            self.dist_start += self.dy_start;
535            self.dist_pict += self.dy_pict;
536            self.dist_end += self.dy_end;
537        }
538        if dx < 0 {
539            self.dist -= self.dy;
540            self.dist_start -= self.dy_start;
541            self.dist_pict -= self.dy_pict;
542            self.dist_end -= self.dy_end;
543        }
544    }
545
546    #[inline]
547    pub fn dec_y(&mut self, dx: i32) {
548        self.dist += self.dx;
549        self.dist_start += self.dx_start;
550        self.dist_pict += self.dx_pict;
551        self.dist_end += self.dx_end;
552        if dx > 0 {
553            self.dist += self.dy;
554            self.dist_start += self.dy_start;
555            self.dist_pict += self.dy_pict;
556            self.dist_end += self.dy_end;
557        }
558        if dx < 0 {
559            self.dist -= self.dy;
560            self.dist_start -= self.dy_start;
561            self.dist_pict -= self.dy_pict;
562            self.dist_end -= self.dy_end;
563        }
564    }
565
566    #[inline] pub fn dist(&self) -> i32 { self.dist }
567    #[inline] pub fn dist_start(&self) -> i32 { self.dist_start }
568    #[inline] pub fn dist_pict(&self) -> i32 { self.dist_pict }
569    #[inline] pub fn dist_end(&self) -> i32 { self.dist_end }
570    #[inline] pub fn dx_start(&self) -> i32 { self.dx_start }
571    #[inline] pub fn dy_start(&self) -> i32 { self.dy_start }
572    #[inline] pub fn dx_pict(&self) -> i32 { self.dx_pict }
573    #[inline] pub fn dy_pict(&self) -> i32 { self.dy_pict }
574    #[inline] pub fn dx_end(&self) -> i32 { self.dx_end }
575    #[inline] pub fn dy_end(&self) -> i32 { self.dy_end }
576    #[inline] pub fn len(&self) -> i32 { self.len }
577}
578
579// ============================================================================
580// renderer_outline_image — renders lines with image patterns
581// ============================================================================
582
583/// Image-patterned outline renderer.
584///
585/// Port of C++ `renderer_outline_image<BaseRenderer, ImagePattern>`.
586/// Renders anti-aliased lines using an image pattern sampled along the stroke.
587///
588/// The generic parameter `P` is the image line pattern type (e.g.,
589/// `LineImagePattern<PatternFilterBilinearRgba>` or
590/// `LineImagePatternPow2<PatternFilterBilinearRgba>`).
591pub struct RendererOutlineImage<'a, PF: PixelFormat, P: ImageLinePattern> {
592    ren: &'a mut RendererBase<PF>,
593    pattern: &'a P,
594    start: i32,
595    scale_x: f64,
596    clip_box: RectI,
597    clipping: bool,
598}
599
600impl<'a, PF: PixelFormat, P: ImageLinePattern> RendererOutlineImage<'a, PF, P>
601where
602    PF::ColorType: Default + Clone + From<Rgba8>,
603{
604    pub fn new(ren: &'a mut RendererBase<PF>, pattern: &'a P) -> Self {
605        Self {
606            ren,
607            pattern,
608            start: 0,
609            scale_x: 1.0,
610            clip_box: RectI::new(0, 0, 0, 0),
611            clipping: false,
612        }
613    }
614
615    pub fn reset_clipping(&mut self) {
616        self.clipping = false;
617    }
618
619    pub fn set_clip_box(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
620        self.clip_box = RectI::new(
621            line_coord_sat(x1),
622            line_coord_sat(y1),
623            line_coord_sat(x2),
624            line_coord_sat(y2),
625        );
626        self.clipping = true;
627    }
628
629    pub fn set_scale_x(&mut self, s: f64) {
630        self.scale_x = s;
631    }
632
633    pub fn scale_x(&self) -> f64 {
634        self.scale_x
635    }
636
637    pub fn set_start_x(&mut self, s: f64) {
638        self.start = iround(s * LINE_SUBPIXEL_SCALE as f64);
639    }
640
641    pub fn start_x(&self) -> f64 {
642        self.start as f64 / LINE_SUBPIXEL_SCALE as f64
643    }
644
645    pub fn subpixel_width(&self) -> i32 {
646        self.pattern.line_width()
647    }
648
649    pub fn pattern_width(&self) -> i32 {
650        self.pattern.pattern_width()
651    }
652
653    pub fn width(&self) -> f64 {
654        self.subpixel_width() as f64 / LINE_SUBPIXEL_SCALE as f64
655    }
656
657    /// Render a line segment with image pattern (no clipping).
658    ///
659    /// Port of C++ `renderer_outline_image::line3_no_clip`.
660    fn line3_no_clip(
661        &mut self,
662        lp: &LineParameters,
663        sx: i32, sy: i32,
664        ex: i32, ey: i32,
665    ) {
666        if lp.len > LINE_MAX_LENGTH {
667            let (lp1, lp2) = lp.divide();
668            let mx = lp1.x2 + (lp1.y2 - lp1.y1);
669            let my = lp1.y2 - (lp1.x2 - lp1.x1);
670            self.line3_no_clip(
671                &lp1,
672                (lp.x1 + sx) >> 1, (lp.y1 + sy) >> 1,
673                mx, my,
674            );
675            self.line3_no_clip(
676                &lp2,
677                mx, my,
678                (lp.x2 + ex) >> 1, (lp.y2 + ey) >> 1,
679            );
680            return;
681        }
682
683        let mut sx = sx;
684        let mut sy = sy;
685        let mut ex = ex;
686        let mut ey = ey;
687        fix_degenerate_bisectrix_start(lp, &mut sx, &mut sy);
688        fix_degenerate_bisectrix_end(lp, &mut ex, &mut ey);
689
690        // Run the line interpolator inline, sampling from the pattern
691        // and blending into the renderer.
692        self.render_line_image(lp, sx, sy, ex, ey);
693
694        self.start += uround(lp.len as f64 / self.scale_x);
695    }
696
697    /// Core rendering: walk along the line segment and sample the pattern.
698    ///
699    /// This combines the C++ `line_interpolator_image` constructor and
700    /// stepping methods into a single function to avoid borrow conflicts.
701    #[allow(clippy::too_many_arguments)]
702    fn render_line_image(
703        &mut self,
704        lp: &LineParameters,
705        sx: i32, sy: i32,
706        ex: i32, ey: i32,
707    ) {
708        const MAX_HW: usize = 64;
709        const DIST_SIZE: usize = MAX_HW + 1;
710        const COLOR_SIZE: usize = MAX_HW * 2 + 4;
711
712        let li_init = if lp.vertical {
713            Dda2LineInterpolator::new_relative(
714                line_dbl_hr(lp.x2 - lp.x1),
715                (lp.y2 - lp.y1).abs(),
716            )
717        } else {
718            Dda2LineInterpolator::new_relative(
719                line_dbl_hr(lp.y2 - lp.y1),
720                (lp.x2 - lp.x1).abs() + 1,
721            )
722        };
723
724        let di_init = DistanceInterpolator4::new(
725            lp.x1, lp.y1, lp.x2, lp.y2,
726            sx, sy, ex, ey,
727            lp.len, self.scale_x,
728            lp.x1 & !LINE_SUBPIXEL_MASK,
729            lp.y1 & !LINE_SUBPIXEL_MASK,
730        );
731
732        let ix = lp.x1 >> LINE_SUBPIXEL_SHIFT;
733        let iy = lp.y1 >> LINE_SUBPIXEL_SHIFT;
734
735        let count = if lp.vertical {
736            ((lp.y2 >> LINE_SUBPIXEL_SHIFT) - iy).abs()
737        } else {
738            ((lp.x2 >> LINE_SUBPIXEL_SHIFT) - ix).abs()
739        };
740
741        let width = self.pattern.line_width();
742        let max_extent = (width + LINE_SUBPIXEL_SCALE) >> LINE_SUBPIXEL_SHIFT;
743        let start = self.start + (max_extent + 2) * self.pattern.pattern_width();
744
745        // Pre-compute distance table
746        let mut dist_pos = [0i32; DIST_SIZE];
747        {
748            let mut dd = Dda2LineInterpolator::new_forward(
749                0,
750                if lp.vertical { lp.dy << LINE_SUBPIXEL_SHIFT } else { lp.dx << LINE_SUBPIXEL_SHIFT },
751                lp.len,
752            );
753            let stop = width + LINE_SUBPIXEL_SCALE * 2;
754            let mut i = 0;
755            while i < MAX_HW {
756                dist_pos[i] = dd.y();
757                if dist_pos[i] >= stop { break; }
758                dd.inc();
759                i += 1;
760            }
761            if i <= MAX_HW {
762                dist_pos[i] = 0x7FFF_0000;
763            }
764        }
765
766        let mut li = li_init;
767        let mut di = di_init;
768        let mut x = ix;
769        let mut y = iy;
770        let mut old_x = ix;
771        let mut old_y = iy;
772        let mut step = 0i32;
773
774        // ---- Backward stepping ----
775        let mut npix = 1i32;
776        if lp.vertical {
777            loop {
778                li.dec();
779                y -= lp.inc;
780                x = (lp.x1 + li.y()) >> LINE_SUBPIXEL_SHIFT;
781                if lp.inc > 0 { di.dec_y(x - old_x); }
782                else { di.inc_y(x - old_x); }
783                old_x = x;
784
785                let mut d1 = di.dist_start();
786                let mut d2 = d1;
787                let mut dx = 0usize;
788                if d1 < 0 { npix += 1; }
789                loop {
790                    d1 += di.dy_start();
791                    d2 -= di.dy_start();
792                    if d1 < 0 { npix += 1; }
793                    if d2 < 0 { npix += 1; }
794                    dx += 1;
795                    if dist_pos[dx] > width { break; }
796                }
797                if npix == 0 { break; }
798                npix = 0;
799                step -= 1;
800                if step < -max_extent { break; }
801            }
802        } else {
803            loop {
804                li.dec();
805                x -= lp.inc;
806                y = (lp.y1 + li.y()) >> LINE_SUBPIXEL_SHIFT;
807                if lp.inc > 0 { di.dec_x(y - old_y); }
808                else { di.inc_x(y - old_y); }
809                old_y = y;
810
811                let mut d1 = di.dist_start();
812                let mut d2 = d1;
813                let mut dy = 0usize;
814                if d1 < 0 { npix += 1; }
815                loop {
816                    d1 -= di.dx_start();
817                    d2 += di.dx_start();
818                    if d1 < 0 { npix += 1; }
819                    if d2 < 0 { npix += 1; }
820                    dy += 1;
821                    if dist_pos[dy] > width { break; }
822                }
823                if npix == 0 { break; }
824                npix = 0;
825                step -= 1;
826                if step < -max_extent { break; }
827            }
828        }
829
830        li.adjust_forward();
831
832        step -= max_extent;
833
834        // ---- Forward stepping ----
835        let mut colors = [Rgba8::new(0, 0, 0, 0); COLOR_SIZE];
836
837        if lp.vertical {
838            loop {
839                // step_ver
840                li.inc();
841                y += lp.inc;
842                x = (lp.x1 + li.y()) >> LINE_SUBPIXEL_SHIFT;
843                if lp.inc > 0 { di.inc_y(x - old_x); }
844                else { di.dec_y(x - old_x); }
845                old_x = x;
846
847                let s1 = di.dist() / lp.len;
848                let s2 = -s1;
849                let s1_adj = if lp.inc > 0 { -s1 } else { s1 };
850
851                let mut dist_start = di.dist_start();
852                let mut dist_pict = di.dist_pict() + start;
853                let mut dist_end = di.dist_end();
854
855                let center = MAX_HW + 2;
856                let mut p0 = center;
857                let mut p1 = center;
858
859                let mut n = 0;
860                colors[p1] = Rgba8::new(0, 0, 0, 0);
861                if dist_end > 0 {
862                    if dist_start <= 0 {
863                        self.pattern.pixel(&mut colors[p1], dist_pict, s2);
864                    }
865                    n += 1;
866                }
867                p1 += 1;
868
869                let mut dx = 1usize;
870                while dx < DIST_SIZE && dist_pos[dx] - s1_adj <= width {
871                    dist_start += di.dy_start();
872                    dist_pict += di.dy_pict();
873                    dist_end += di.dy_end();
874                    colors[p1] = Rgba8::new(0, 0, 0, 0);
875                    if dist_end > 0 && dist_start <= 0 {
876                        let mut d = dist_pos[dx];
877                        if lp.inc > 0 { d = -d; }
878                        self.pattern.pixel(&mut colors[p1], dist_pict, s2 + d);
879                        n += 1;
880                    }
881                    p1 += 1;
882                    dx += 1;
883                }
884
885                dx = 1;
886                dist_start = di.dist_start();
887                dist_pict = di.dist_pict() + start;
888                dist_end = di.dist_end();
889                while dx < DIST_SIZE && dist_pos[dx] + s1_adj <= width {
890                    dist_start -= di.dy_start();
891                    dist_pict -= di.dy_pict();
892                    dist_end -= di.dy_end();
893                    p0 -= 1;
894                    colors[p0] = Rgba8::new(0, 0, 0, 0);
895                    if dist_end > 0 && dist_start <= 0 {
896                        let mut d = dist_pos[dx];
897                        if lp.inc > 0 { d = -d; }
898                        self.pattern.pixel(&mut colors[p0], dist_pict, s2 - d);
899                        n += 1;
900                    }
901                    dx += 1;
902                }
903
904                // Blend horizontal span
905                let len = p1 - p0;
906                if len > 0 {
907                    let cvec: Vec<PF::ColorType> = colors[p0..p1]
908                        .iter()
909                        .map(|c| PF::ColorType::from(*c))
910                        .collect();
911                    self.ren.blend_color_hspan(
912                        x - dx as i32 + 1, y, len as i32,
913                        &cvec, &[], 255,
914                    );
915                }
916
917                step += 1;
918                if n == 0 || step >= count {
919                    break;
920                }
921            }
922        } else {
923            loop {
924                // step_hor
925                li.inc();
926                x += lp.inc;
927                y = (lp.y1 + li.y()) >> LINE_SUBPIXEL_SHIFT;
928                if lp.inc > 0 { di.inc_x(y - old_y); }
929                else { di.dec_x(y - old_y); }
930                old_y = y;
931
932                let s1 = di.dist() / lp.len;
933                let s2 = -s1;
934                let s1_adj = if lp.inc < 0 { -s1 } else { s1 };
935
936                let mut dist_start = di.dist_start();
937                let mut dist_pict = di.dist_pict() + start;
938                let mut dist_end = di.dist_end();
939
940                let center = MAX_HW + 2;
941                let mut p0 = center;
942                let mut p1 = center;
943
944                let mut n = 0;
945                colors[p1] = Rgba8::new(0, 0, 0, 0);
946                if dist_end > 0 {
947                    if dist_start <= 0 {
948                        self.pattern.pixel(&mut colors[p1], dist_pict, s2);
949                    }
950                    n += 1;
951                }
952                p1 += 1;
953
954                let mut dy = 1usize;
955                while dy < DIST_SIZE && dist_pos[dy] - s1_adj <= width {
956                    dist_start -= di.dx_start();
957                    dist_pict -= di.dx_pict();
958                    dist_end -= di.dx_end();
959                    colors[p1] = Rgba8::new(0, 0, 0, 0);
960                    if dist_end > 0 && dist_start <= 0 {
961                        let mut d = dist_pos[dy];
962                        if lp.inc > 0 { d = -d; }
963                        self.pattern.pixel(&mut colors[p1], dist_pict, s2 - d);
964                        n += 1;
965                    }
966                    p1 += 1;
967                    dy += 1;
968                }
969
970                dy = 1;
971                dist_start = di.dist_start();
972                dist_pict = di.dist_pict() + start;
973                dist_end = di.dist_end();
974                while dy < DIST_SIZE && dist_pos[dy] + s1_adj <= width {
975                    dist_start += di.dx_start();
976                    dist_pict += di.dx_pict();
977                    dist_end += di.dx_end();
978                    p0 -= 1;
979                    colors[p0] = Rgba8::new(0, 0, 0, 0);
980                    if dist_end > 0 && dist_start <= 0 {
981                        let mut d = dist_pos[dy];
982                        if lp.inc > 0 { d = -d; }
983                        self.pattern.pixel(&mut colors[p0], dist_pict, s2 + d);
984                        n += 1;
985                    }
986                    dy += 1;
987                }
988
989                // Blend vertical span
990                let len = p1 - p0;
991                if len > 0 {
992                    let cvec: Vec<PF::ColorType> = colors[p0..p1]
993                        .iter()
994                        .map(|c| PF::ColorType::from(*c))
995                        .collect();
996                    self.ren.blend_color_vspan(
997                        x, y - dy as i32 + 1, len as i32,
998                        &cvec, &[], 255,
999                    );
1000                }
1001
1002                step += 1;
1003                if n == 0 || step >= count {
1004                    break;
1005                }
1006            }
1007        }
1008    }
1009}
1010
1011impl<'a, PF: PixelFormat, P: ImageLinePattern> OutlineAaRenderer
1012    for RendererOutlineImage<'a, PF, P>
1013where
1014    PF::ColorType: Default + Clone + From<Rgba8>,
1015{
1016    fn accurate_join_only(&self) -> bool {
1017        true
1018    }
1019
1020    // Image pattern renderer only supports line3 (with both joins).
1021    fn line0(&mut self, _lp: &LineParameters) {}
1022    fn line1(&mut self, _lp: &LineParameters, _sx: i32, _sy: i32) {}
1023    fn line2(&mut self, _lp: &LineParameters, _ex: i32, _ey: i32) {}
1024
1025    fn line3(&mut self, lp: &LineParameters, sx: i32, sy: i32, ex: i32, ey: i32) {
1026        if self.clipping {
1027            let mut x1 = lp.x1;
1028            let mut y1 = lp.y1;
1029            let mut x2 = lp.x2;
1030            let mut y2 = lp.y2;
1031            let flags = clip_line_segment(&mut x1, &mut y1, &mut x2, &mut y2, &self.clip_box);
1032            let start = self.start;
1033            if (flags & 4) == 0 {
1034                if flags != 0 {
1035                    let lp2 = LineParameters::new(
1036                        x1, y1, x2, y2,
1037                        uround(calc_distance_i(x1, y1, x2, y2)),
1038                    );
1039                    let mut sx = sx;
1040                    let mut sy = sy;
1041                    let mut ex = ex;
1042                    let mut ey = ey;
1043                    if flags & 1 != 0 {
1044                        self.start += uround(
1045                            calc_distance_i(lp.x1, lp.y1, x1, y1) / self.scale_x,
1046                        );
1047                        sx = x1 + (y2 - y1);
1048                        sy = y1 - (x2 - x1);
1049                    } else {
1050                        while (sx - lp.x1).abs() + (sy - lp.y1).abs() > lp2.len {
1051                            sx = (lp.x1 + sx) >> 1;
1052                            sy = (lp.y1 + sy) >> 1;
1053                        }
1054                    }
1055                    if flags & 2 != 0 {
1056                        ex = x2 + (y2 - y1);
1057                        ey = y2 - (x2 - x1);
1058                    } else {
1059                        while (ex - lp.x2).abs() + (ey - lp.y2).abs() > lp2.len {
1060                            ex = (lp.x2 + ex) >> 1;
1061                            ey = (lp.y2 + ey) >> 1;
1062                        }
1063                    }
1064                    self.line3_no_clip(&lp2, sx, sy, ex, ey);
1065                } else {
1066                    self.line3_no_clip(lp, sx, sy, ex, ey);
1067                }
1068            }
1069            self.start = start + uround(lp.len as f64 / self.scale_x);
1070        } else {
1071            self.line3_no_clip(lp, sx, sy, ex, ey);
1072        }
1073    }
1074
1075    fn semidot(&mut self, _cmp: fn(i32) -> bool, _xc1: i32, _yc1: i32, _xc2: i32, _yc2: i32) {}
1076    fn pie(&mut self, _xc: i32, _yc: i32, _x1: i32, _y1: i32, _x2: i32, _y2: i32) {}
1077}
1078
1079// ============================================================================
1080// Helper
1081// ============================================================================
1082
1083fn calc_distance_i(x1: i32, y1: i32, x2: i32, y2: i32) -> f64 {
1084    let dx = (x2 - x1) as f64;
1085    let dy = (y2 - y1) as f64;
1086    (dx * dx + dy * dy).sqrt()
1087}
1088
1089// ============================================================================
1090// Tests
1091// ============================================================================
1092
1093#[cfg(test)]
1094mod tests {
1095    use super::*;
1096
1097    /// Simple solid-color pattern source for testing.
1098    struct SolidPatternSource {
1099        w: u32,
1100        h: u32,
1101        color: Rgba8,
1102    }
1103
1104    impl ImagePatternSource for SolidPatternSource {
1105        fn width(&self) -> f64 { self.w as f64 }
1106        fn height(&self) -> f64 { self.h as f64 }
1107        fn pixel(&self, _x: i32, _y: i32) -> Rgba8 { self.color }
1108    }
1109
1110    #[test]
1111    fn test_line_image_pattern_creation() {
1112        use crate::pattern_filters_rgba::PatternFilterBilinearRgba;
1113        let src = SolidPatternSource { w: 16, h: 8, color: Rgba8::new(255, 0, 0, 255) };
1114        let pat = LineImagePattern::<PatternFilterBilinearRgba>::with_source(&src);
1115        assert!(pat.pattern_width() > 0);
1116        assert!(pat.line_width() > 0);
1117    }
1118
1119    #[test]
1120    fn test_line_image_pattern_pixel() {
1121        use crate::pattern_filters_rgba::PatternFilterNn;
1122        let src = SolidPatternSource { w: 16, h: 8, color: Rgba8::new(255, 128, 64, 200) };
1123        let pat = LineImagePattern::<PatternFilterNn>::with_source(&src);
1124        let mut p = Rgba8::new(0, 0, 0, 0);
1125        pat.pixel(&mut p, 0, 0);
1126        assert!(p.a > 0, "Expected non-zero alpha");
1127    }
1128
1129    #[test]
1130    fn test_distance_interpolator4() {
1131        let di = DistanceInterpolator4::new(
1132            0, 0, 256, 0,
1133            -256, 0, 512, 0,
1134            256, 1.0, 0, 0,
1135        );
1136        assert_ne!(di.len(), 0);
1137    }
1138
1139    #[test]
1140    fn test_line_image_pattern_pow2() {
1141        use crate::pattern_filters_rgba::PatternFilterBilinearRgba;
1142        let src = SolidPatternSource { w: 16, h: 8, color: Rgba8::new(0, 255, 0, 255) };
1143        let pat = LineImagePatternPow2::<PatternFilterBilinearRgba>::with_source(&src);
1144        assert!(pat.pattern_width() > 0);
1145        assert!(pat.line_width() > 0);
1146        let mut p = Rgba8::new(0, 0, 0, 0);
1147        pat.pixel(&mut p, 0, 0);
1148        assert!(p.a > 0);
1149    }
1150
1151    #[test]
1152    fn test_renderer_outline_image_basic() {
1153        use crate::pattern_filters_rgba::PatternFilterBilinearRgba;
1154        use crate::pixfmt_rgba::PixfmtRgba32;
1155        use crate::rendering_buffer::RowAccessor;
1156
1157        let w = 100u32;
1158        let h = 100u32;
1159        let stride = (w * 4) as i32;
1160        let mut buf = vec![255u8; (h * w * 4) as usize];
1161        let mut ra = RowAccessor::new();
1162        unsafe { ra.attach(buf.as_mut_ptr(), w, h, stride); }
1163        let pf = PixfmtRgba32::new(&mut ra);
1164        let mut rb = RendererBase::new(pf);
1165
1166        let src = SolidPatternSource { w: 32, h: 16, color: Rgba8::new(255, 0, 0, 255) };
1167        let pat = LineImagePattern::<PatternFilterBilinearRgba>::with_source(&src);
1168
1169        let mut ren = RendererOutlineImage::new(&mut rb, &pat);
1170        ren.set_scale_x(1.0);
1171        ren.set_start_x(0.0);
1172
1173        // Draw a horizontal line
1174        let lp = LineParameters::new(
1175            10 * 256, 50 * 256,
1176            90 * 256, 50 * 256,
1177            80 * 256,
1178        );
1179        ren.line3(
1180            &lp,
1181            10 * 256 + (0), 50 * 256 - (80 * 256),
1182            90 * 256 + (0), 50 * 256 - (80 * 256),
1183        );
1184
1185        // Check some pixels were drawn (red channel should be non-zero near the line)
1186        let mut found = false;
1187        let pf2 = PixfmtRgba32::new(&mut ra);
1188        let rb2 = RendererBase::new(pf2);
1189        for y in 42..=58 {
1190            for x in 10..90 {
1191                let p = rb2.pixel(x, y);
1192                if p.r > 100 {
1193                    found = true;
1194                    break;
1195                }
1196            }
1197            if found { break; }
1198        }
1199        assert!(found, "Expected red pixels near row 50");
1200    }
1201}