Skip to main content

cranpose_ui/widgets/
image.rs

1//! Image composable and painter primitives.
2
3#![expect(non_snake_case)]
4#![expect(clippy::too_many_arguments)]
5
6#[cfg(feature = "svg")]
7use std::sync::{Mutex, MutexGuard};
8use std::{
9    hash::{Hash, Hasher},
10    sync::Arc,
11};
12
13use cranpose_core::NodeId;
14use cranpose_ui_graphics::{ColorFilter, DrawScope, ImageBitmap, ImageSampling};
15use cranpose_ui_layout::{Constraints, MeasurePolicy, MeasureResult, MeasureScope, Placement};
16use thiserror::Error;
17
18use crate::{
19    composable,
20    layout::core::{Alignment, Measurable},
21    modifier::{Modifier, Rect, Size},
22    nine_patch::{NinePatchInsets, PatchFill, PatchQuad, nine_patch_quads, tile_quads},
23    widgets::Layout,
24};
25
26#[cfg(feature = "svg")]
27#[path = "image_svg.rs"]
28mod image_svg;
29
30/// Alpha value of a fully opaque image, the default for [`Image`].
31pub const DEFAULT_ALPHA: f32 = 1.0;
32#[cfg(feature = "svg")]
33const SVG_RASTER_CACHE_LIMIT: usize = 8;
34
35/// How an image is scaled to fit the bounds it is drawn into. Compose's `ContentScale`.
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub enum ContentScale {
38    /// Scales both axes by one factor so the whole image fits inside the bounds, leaving space on
39    /// one axis.
40    Fit,
41    /// Scales both axes by one factor so the image covers the bounds, cropping what overflows.
42    Crop,
43    /// Scales each axis separately so the image fills the bounds, which distorts its aspect ratio.
44    FillBounds,
45    /// Scales both axes by one factor so the image width matches the bounds width.
46    FillWidth,
47    /// Scales both axes by one factor so the image height matches the bounds height.
48    FillHeight,
49    /// Like `Fit`, but never enlarges an image that already fits inside the bounds.
50    Inside,
51    /// Draws the image at its own size without scaling.
52    None,
53}
54
55impl ContentScale {
56    /// Returns the size of an image of `src_size` after this scale fits it to `dst_size`. For
57    /// `Crop` the result can exceed `dst_size`. Returns zero size if a side of either size is zero
58    /// or negative.
59    pub fn scaled_size(self, src_size: Size, dst_size: Size) -> Size {
60        if src_size.width <= 0.0
61            || src_size.height <= 0.0
62            || dst_size.width <= 0.0
63            || dst_size.height <= 0.0
64        {
65            return Size::ZERO;
66        }
67
68        let scale_x = dst_size.width / src_size.width;
69        let scale_y = dst_size.height / src_size.height;
70
71        let (factor_x, factor_y) = match self {
72            Self::Fit => {
73                let factor = scale_x.min(scale_y);
74                (factor, factor)
75            }
76            Self::Crop => {
77                let factor = scale_x.max(scale_y);
78                (factor, factor)
79            }
80            Self::FillBounds => (scale_x, scale_y),
81            Self::FillWidth => (scale_x, scale_x),
82            Self::FillHeight => (scale_y, scale_y),
83            Self::Inside => {
84                if src_size.width <= dst_size.width && src_size.height <= dst_size.height {
85                    (1.0, 1.0)
86                } else {
87                    let factor = scale_x.min(scale_y);
88                    (factor, factor)
89                }
90            }
91            Self::None => (1.0, 1.0),
92        };
93
94        Size {
95            width: src_size.width * factor_x,
96            height: src_size.height * factor_y,
97        }
98    }
99}
100
101/// The pixels that an [`Image`] draws: a bitmap, a region of a bitmap, a tiled or nine-patch
102/// region, or an SVG. Compose's `Painter`.
103#[derive(Clone, Debug, PartialEq, Eq, Hash)]
104pub struct Painter {
105    kind: PainterKind,
106}
107
108#[derive(Clone, Debug, PartialEq, Eq, Hash)]
109enum PainterKind {
110    Bitmap(ImageBitmap),
111    BitmapRegion {
112        bitmap: ImageBitmap,
113        source: Quad,
114        sampling: ImageSampling,
115    },
116    /// A source repeated at its own size across whatever space it is given,
117    /// rather than scaled to fit it.
118    BitmapTiled {
119        bitmap: ImageBitmap,
120        source: Quad,
121        sampling: ImageSampling,
122    },
123    /// A source whose corners keep their size while its edges and middle grow.
124    NinePatch {
125        bitmap: ImageBitmap,
126        source: Quad,
127        insets: Quad,
128        center: PatchFill,
129        edges: PatchFill,
130        sampling: ImageSampling,
131    },
132    Svg(SvgPainter),
133}
134
135/// Four `f32`s carried as bits.
136///
137/// A painter is compared and hashed so a composable can skip when it has not
138/// changed, and floats are neither `Eq` nor `Hash`. Keeping the bits makes the
139/// comparison exact — two painters built from the same numbers are the same
140/// painter — without asking the caller to think about it.
141#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
142struct Quad {
143    a: u32,
144    b: u32,
145    c: u32,
146    d: u32,
147}
148
149impl Quad {
150    fn of(a: f32, b: f32, c: f32, d: f32) -> Self {
151        Self {
152            a: a.to_bits(),
153            b: b.to_bits(),
154            c: c.to_bits(),
155            d: d.to_bits(),
156        }
157    }
158
159    fn values(self) -> (f32, f32, f32, f32) {
160        (
161            f32::from_bits(self.a),
162            f32::from_bits(self.b),
163            f32::from_bits(self.c),
164            f32::from_bits(self.d),
165        )
166    }
167
168    fn from_rect(source: Rect) -> Self {
169        Self::of(source.x, source.y, source.width, source.height)
170    }
171
172    fn rect(self) -> Rect {
173        let (x, y, width, height) = self.values();
174        Rect {
175            x,
176            y,
177            width,
178            height,
179        }
180    }
181
182    fn from_insets(insets: NinePatchInsets) -> Self {
183        Self::of(insets.left, insets.top, insets.right, insets.bottom)
184    }
185
186    fn insets(self) -> NinePatchInsets {
187        let (left, top, right, bottom) = self.values();
188        NinePatchInsets::new(left, top, right, bottom)
189    }
190}
191
192impl Painter {
193    /// Creates a painter that draws the whole bitmap.
194    pub fn from_bitmap(bitmap: ImageBitmap) -> Self {
195        Self {
196            kind: PainterKind::Bitmap(bitmap),
197        }
198    }
199
200    /// Creates a painter for one source region of a bitmap atlas.
201    pub fn from_bitmap_region(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Self {
202        Self {
203            kind: PainterKind::BitmapRegion {
204                bitmap,
205                source: Quad::from_rect(source),
206                sampling,
207            },
208        }
209    }
210
211    /// Creates a painter that repeats `source` at its own size across whatever
212    /// space it is given, instead of scaling it to fit.
213    ///
214    /// A texture, a hatch or a skin background covers an area of any size
215    /// without going soft. `source` is a region of `bitmap`, so an application
216    /// tiles one sprite out of an atlas and never the whole sheet.
217    pub fn from_bitmap_tiled(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Self {
218        Self {
219            kind: PainterKind::BitmapTiled {
220                bitmap,
221                source: Quad::from_rect(source),
222                sampling,
223            },
224        }
225    }
226
227    /// Creates a painter whose corners keep their own size while its edges and
228    /// middle grow — one drawing of a button, panel or trough used at every
229    /// size.
230    ///
231    /// `center` and `edges` decide whether the growing parts are stretched or
232    /// tiled. Insets that leave no middle, or a destination with no room for
233    /// the corners, fall back to a plain scale rather than drawing corners over
234    /// each other.
235    pub fn from_nine_patch(
236        bitmap: ImageBitmap,
237        source: Rect,
238        insets: NinePatchInsets,
239        center: PatchFill,
240        edges: PatchFill,
241        sampling: ImageSampling,
242    ) -> Self {
243        Self {
244            kind: PainterKind::NinePatch {
245                bitmap,
246                source: Quad::from_rect(source),
247                insets: Quad::from_insets(insets),
248                center,
249                edges,
250                sampling,
251            },
252        }
253    }
254
255    /// Creates a painter that draws a parsed SVG.
256    pub fn from_svg(svg: SvgPainter) -> Self {
257        Self {
258            kind: PainterKind::Svg(svg),
259        }
260    }
261
262    /// Returns the size in pixels that the painter has without constraints: the bitmap size, the
263    /// source region size, or the size the SVG declares.
264    pub fn intrinsic_size(&self) -> Size {
265        match &self.kind {
266            PainterKind::Bitmap(bitmap) => bitmap.intrinsic_size(),
267            PainterKind::BitmapRegion { source, .. }
268            | PainterKind::BitmapTiled { source, .. }
269            | PainterKind::NinePatch { source, .. } => {
270                let source = source.rect();
271                Size::new(source.width.max(0.0), source.height.max(0.0))
272            }
273            PainterKind::Svg(svg) => svg.intrinsic_size(),
274        }
275    }
276
277    /// Returns the bitmap that the painter draws from, or `None` for an SVG painter. Region, tiled
278    /// and nine-patch painters return their whole source bitmap.
279    pub fn as_bitmap(&self) -> Option<&ImageBitmap> {
280        match &self.kind {
281            PainterKind::Bitmap(bitmap) => Some(bitmap),
282            PainterKind::BitmapRegion { bitmap, .. }
283            | PainterKind::BitmapTiled { bitmap, .. }
284            | PainterKind::NinePatch { bitmap, .. } => Some(bitmap),
285            PainterKind::Svg(_) => None,
286        }
287    }
288
289    /// Returns the underlying bitmap when this painter is bitmap-backed.
290    pub fn bitmap(&self) -> Option<&ImageBitmap> {
291        self.as_bitmap()
292    }
293}
294
295impl From<ImageBitmap> for Painter {
296    fn from(value: ImageBitmap) -> Self {
297        Self::from_bitmap(value)
298    }
299}
300
301impl From<SvgPainter> for Painter {
302    fn from(value: SvgPainter) -> Self {
303        Self::from_svg(value)
304    }
305}
306
307/// Creates a painter that draws the whole bitmap.
308pub fn BitmapPainter(bitmap: ImageBitmap) -> Painter {
309    Painter::from_bitmap(bitmap)
310}
311
312/// Creates a painter that draws one region from a bitmap atlas.
313pub fn BitmapRegionPainter(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Painter {
314    Painter::from_bitmap_region(bitmap, source, sampling)
315}
316
317/// Creates a painter that repeats one region of a bitmap across its bounds.
318pub fn TiledPainter(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Painter {
319    Painter::from_bitmap_tiled(bitmap, source, sampling)
320}
321
322/// Creates a painter whose corners stay put while its edges and middle grow.
323pub fn NinePatchPainter(
324    bitmap: ImageBitmap,
325    source: Rect,
326    insets: NinePatchInsets,
327    center: PatchFill,
328    edges: PatchFill,
329    sampling: ImageSampling,
330) -> Painter {
331    Painter::from_nine_patch(bitmap, source, insets, center, edges, sampling)
332}
333
334/// Errors returned while parsing or rasterizing an SVG painter.
335#[derive(Debug, Clone, PartialEq, Eq, Error)]
336pub enum SvgPainterError {
337    /// The crate was built without the `svg` feature.
338    #[error("SVG support is disabled; enable the cranpose-ui `svg` feature")]
339    SvgFeatureDisabled,
340    /// The SVG data could not be parsed. Holds the parser message.
341    #[error("failed to parse SVG: {0}")]
342    Parse(String),
343    /// A requested raster side was zero, negative or not a finite number.
344    #[error("SVG raster dimensions must be greater than zero")]
345    InvalidRasterDimensions,
346    /// The requested raster is too large to address.
347    #[error("SVG raster dimensions are too large")]
348    RasterDimensionsTooLarge,
349    /// Allocating the memory for the raster failed.
350    #[error("failed to allocate SVG raster {width}x{height}")]
351    RasterAllocationFailed {
352        /// Requested raster width in pixels.
353        width: u32,
354        /// Requested raster height in pixels.
355        height: u32,
356    },
357    /// The raster cache could not be locked.
358    #[error("SVG raster cache is unavailable")]
359    RasterCacheUnavailable,
360    /// Creating the bitmap from the rasterized pixels failed.
361    #[error(transparent)]
362    ImageBitmap(#[from] cranpose_ui_graphics::ImageBitmapError),
363}
364
365/// Parsed SVG image data that rasterizes on demand for the requested draw size.
366#[derive(Clone)]
367pub struct SvgPainter {
368    inner: Arc<SvgPainterInner>,
369}
370
371struct SvgPainterInner {
372    #[cfg(feature = "svg")]
373    document: image_svg::SvgDocument,
374    intrinsic_size: Size,
375    #[cfg(feature = "svg")]
376    cache: Mutex<SvgRasterCache>,
377}
378
379#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
380#[cfg(feature = "svg")]
381struct SvgRasterKey {
382    width: u32,
383    height: u32,
384}
385
386#[derive(Clone, Debug)]
387#[cfg(feature = "svg")]
388struct SvgRasterEntry {
389    key: SvgRasterKey,
390    bitmap: ImageBitmap,
391}
392
393#[derive(Default, Debug)]
394#[cfg(feature = "svg")]
395struct SvgRasterCache {
396    entries: Vec<SvgRasterEntry>,
397}
398
399impl SvgPainter {
400    /// Parses SVG `bytes`. Fails with `SvgFeatureDisabled` when the `svg` feature is off and with
401    /// `Parse` when the data is not valid SVG.
402    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SvgPainterError> {
403        #[cfg(not(feature = "svg"))]
404        {
405            let _ = bytes;
406            Err(SvgPainterError::SvgFeatureDisabled)
407        }
408        #[cfg(feature = "svg")]
409        {
410            let document = image_svg::parse_svg_document(bytes)?;
411            let intrinsic_size = document.intrinsic_size();
412
413            Ok(Self {
414                inner: Arc::new(SvgPainterInner {
415                    document,
416                    intrinsic_size,
417                    cache: Mutex::new(SvgRasterCache::default()),
418                }),
419            })
420        }
421    }
422
423    /// Returns an id that clones of this painter share. Painters parsed separately have different
424    /// ids while both are alive.
425    pub fn id(&self) -> u64 {
426        Arc::as_ptr(&self.inner) as usize as u64
427    }
428
429    /// Returns the size in pixels that the SVG document declares.
430    pub fn intrinsic_size(&self) -> Size {
431        self.inner.intrinsic_size
432    }
433
434    /// Rasterizes the SVG into a bitmap of `pixel_size` pixels, rounded up to whole pixels. A few
435    /// recent sizes are cached.
436    pub fn rasterize(&self, pixel_size: Size) -> Result<ImageBitmap, SvgPainterError> {
437        #[cfg(not(feature = "svg"))]
438        {
439            let _ = pixel_size;
440            Err(SvgPainterError::SvgFeatureDisabled)
441        }
442        #[cfg(feature = "svg")]
443        {
444            let key = svg_raster_key(pixel_size)?;
445            if let Some(bitmap) = self.cached_bitmap(key)? {
446                return Ok(bitmap);
447            }
448
449            let bitmap = self.rasterize_uncached(key)?;
450            self.cache_bitmap(key, bitmap.clone())?;
451            Ok(bitmap)
452        }
453    }
454
455    #[cfg(feature = "svg")]
456    fn cached_bitmap(&self, key: SvgRasterKey) -> Result<Option<ImageBitmap>, SvgPainterError> {
457        let mut cache = self.lock_cache();
458        Ok(cache.get(key))
459    }
460
461    #[cfg(feature = "svg")]
462    fn cache_bitmap(&self, key: SvgRasterKey, bitmap: ImageBitmap) -> Result<(), SvgPainterError> {
463        let mut cache = self.lock_cache();
464        cache.insert(key, bitmap);
465        Ok(())
466    }
467
468    #[cfg(feature = "svg")]
469    fn lock_cache(&self) -> MutexGuard<'_, SvgRasterCache> {
470        self.inner
471            .cache
472            .lock()
473            .unwrap_or_else(std::sync::PoisonError::into_inner)
474    }
475
476    #[cfg(feature = "svg")]
477    fn rasterize_uncached(&self, key: SvgRasterKey) -> Result<ImageBitmap, SvgPainterError> {
478        (key.width as usize)
479            .checked_mul(key.height as usize)
480            .and_then(|value| value.checked_mul(4))
481            .ok_or(SvgPainterError::RasterDimensionsTooLarge)?;
482
483        let mut pixmap = tiny_skia::Pixmap::new(key.width, key.height).ok_or(
484            SvgPainterError::RasterAllocationFailed {
485                width: key.width,
486                height: key.height,
487            },
488        )?;
489        image_svg::rasterize_svg_document(&self.inner.document, &mut pixmap);
490        let pixels = image_svg::demultiplied_rgba_pixels(&pixmap);
491        Ok(ImageBitmap::from_rgba8(key.width, key.height, pixels)?)
492    }
493}
494
495impl std::fmt::Debug for SvgPainter {
496    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497        f.debug_struct("SvgPainter")
498            .field("id", &self.id())
499            .field("intrinsic_size", &self.intrinsic_size())
500            .finish_non_exhaustive()
501    }
502}
503
504impl PartialEq for SvgPainter {
505    fn eq(&self, other: &Self) -> bool {
506        self.id() == other.id()
507    }
508}
509
510impl Eq for SvgPainter {}
511
512impl Hash for SvgPainter {
513    fn hash<H: Hasher>(&self, state: &mut H) {
514        self.id().hash(state);
515    }
516}
517
518#[cfg(feature = "svg")]
519impl SvgRasterCache {
520    fn get(&mut self, key: SvgRasterKey) -> Option<ImageBitmap> {
521        let position = self.entries.iter().position(|entry| entry.key == key)?;
522        let entry = self.entries.remove(position);
523        let bitmap = entry.bitmap.clone();
524        self.entries.push(entry);
525        Some(bitmap)
526    }
527
528    fn insert(&mut self, key: SvgRasterKey, bitmap: ImageBitmap) {
529        if let Some(position) = self.entries.iter().position(|entry| entry.key == key) {
530            self.entries.remove(position);
531        } else if self.entries.len() >= SVG_RASTER_CACHE_LIMIT {
532            self.entries.remove(0);
533        }
534        self.entries.push(SvgRasterEntry { key, bitmap });
535    }
536}
537
538#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
539struct SvgBytesKey {
540    ptr: usize,
541    len: usize,
542}
543
544impl SvgBytesKey {
545    fn new(bytes: &'static [u8]) -> Self {
546        Self {
547            ptr: bytes.as_ptr() as usize,
548            len: bytes.len(),
549        }
550    }
551}
552
553/// Parses `bytes` once for the calling composition position and returns the same painter on later
554/// recompositions. The cache key is the address and length of `bytes`.
555pub fn rememberSvg(bytes: &'static [u8]) -> Result<SvgPainter, SvgPainterError> {
556    let key = SvgBytesKey::new(bytes);
557    cranpose_core::withCurrentComposer(|composer| {
558        composer.with_key(&key, |composer| {
559            composer
560                .remember(|| SvgPainter::from_bytes(bytes))
561                .with(Clone::clone)
562        })
563    })
564}
565
566/// Measure policy for Image that preserves aspect ratio when constraints
567/// force the image smaller than its intrinsic size.
568///
569/// Unlike [`LeafMeasurePolicy`] which clamps width and height independently,
570/// this scales both dimensions by the same factor so the image is never
571/// distorted by layout constraints.
572#[derive(Clone, Debug, PartialEq)]
573struct ImageMeasurePolicy {
574    intrinsic_size: Size,
575}
576
577impl MeasurePolicy for ImageMeasurePolicy {
578    fn measure(
579        &self,
580        scope: &dyn MeasureScope,
581        _measurables: &[Box<dyn Measurable>],
582        constraints: Constraints,
583    ) -> MeasureResult {
584        let mut placements = Vec::new();
585        let measurement = self.measure_into(scope, &[], constraints, &mut placements);
586        MeasureResult::new(measurement, placements)
587    }
588
589    fn measure_into(
590        &self,
591        _scope: &dyn MeasureScope,
592        _measurables: &[Box<dyn Measurable>],
593        constraints: Constraints,
594        placements: &mut Vec<Placement>,
595    ) -> cranpose_ui_layout::Measurement {
596        placements.clear();
597        let iw = self.intrinsic_size.width;
598        let ih = self.intrinsic_size.height;
599
600        if iw <= 0.0 || ih <= 0.0 {
601            let (w, h) = constraints.constrain(0.0, 0.0);
602            return Size {
603                width: w,
604                height: h,
605            }
606            .into();
607        }
608
609        let cw = iw.clamp(constraints.min_width, constraints.max_width);
610        let ch = ih.clamp(constraints.min_height, constraints.max_height);
611
612        let scale_x = cw / iw;
613        let scale_y = ch / ih;
614
615        let (width, height) = if scale_x < 1.0 || scale_y < 1.0 {
616            let factor = scale_x.min(scale_y);
617            let w = (iw * factor).clamp(constraints.min_width, constraints.max_width);
618            let h = (ih * factor).clamp(constraints.min_height, constraints.max_height);
619            (w, h)
620        } else {
621            (cw, ch)
622        };
623
624        Size { width, height }.into()
625    }
626
627    fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
628        self.intrinsic_size.width
629    }
630
631    fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
632        self.intrinsic_size.width
633    }
634
635    fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
636        self.intrinsic_size.height
637    }
638
639    fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
640        self.intrinsic_size.height
641    }
642}
643
644/// Where the painter draws in the container, as Compose's painter modifier
645/// places it: scaled, then aligned on sizes rounded to whole device pixels
646/// of `density`, overflowing the container where the scale makes it larger.
647fn destination_rect(
648    src_size: Size,
649    dst_size: Size,
650    alignment: Alignment,
651    content_scale: ContentScale,
652    density: f32,
653) -> Rect {
654    use cranpose_ui_layout::round_to_px;
655
656    let draw_size = content_scale.scaled_size(src_size, dst_size);
657    Rect {
658        x: alignment.horizontal.align(
659            round_to_px(dst_size.width, density),
660            round_to_px(draw_size.width, density),
661            density,
662        ),
663        y: alignment.vertical.align(
664            round_to_px(dst_size.height, density),
665            round_to_px(draw_size.height, density),
666            density,
667        ),
668        width: draw_size.width,
669        height: draw_size.height,
670    }
671}
672
673fn map_destination_clip_to_source(
674    src_rect: Rect,
675    dst_rect: Rect,
676    clipped_dst_rect: Rect,
677) -> Option<Rect> {
678    if src_rect.width <= 0.0
679        || src_rect.height <= 0.0
680        || dst_rect.width <= 0.0
681        || dst_rect.height <= 0.0
682        || clipped_dst_rect.width <= 0.0
683        || clipped_dst_rect.height <= 0.0
684    {
685        return None;
686    }
687
688    let scale_x = src_rect.width / dst_rect.width;
689    let scale_y = src_rect.height / dst_rect.height;
690
691    let src_min_x = src_rect.x;
692    let src_min_y = src_rect.y;
693    let src_max_x = src_rect.x + src_rect.width;
694    let src_max_y = src_rect.y + src_rect.height;
695
696    let raw_left = src_rect.x + (clipped_dst_rect.x - dst_rect.x) * scale_x;
697    let raw_top = src_rect.y + (clipped_dst_rect.y - dst_rect.y) * scale_y;
698    let raw_right =
699        src_rect.x + ((clipped_dst_rect.x + clipped_dst_rect.width) - dst_rect.x) * scale_x;
700    let raw_bottom =
701        src_rect.y + ((clipped_dst_rect.y + clipped_dst_rect.height) - dst_rect.y) * scale_y;
702
703    let left = raw_left.clamp(src_min_x, src_max_x);
704    let top = raw_top.clamp(src_min_y, src_max_y);
705    let right = raw_right.clamp(src_min_x, src_max_x);
706    let bottom = raw_bottom.clamp(src_min_y, src_max_y);
707    let width = right - left;
708    let height = bottom - top;
709
710    if width <= 0.0 || height <= 0.0 {
711        None
712    } else {
713        Some(Rect {
714            x: left,
715            y: top,
716            width,
717            height,
718        })
719    }
720}
721
722fn image_destination_clip(
723    src_size: Size,
724    container_size: Size,
725    alignment: Alignment,
726    content_scale: ContentScale,
727    density: f32,
728) -> Option<(Rect, Rect)> {
729    let dst_rect = destination_rect(src_size, container_size, alignment, content_scale, density);
730    if dst_rect.width <= 0.0 || dst_rect.height <= 0.0 {
731        return None;
732    }
733
734    let container_rect = Rect::from_size(container_size);
735    let clipped_dst_rect = dst_rect.intersect(container_rect)?;
736    Some((dst_rect, clipped_dst_rect))
737}
738
739fn draw_bitmap_painter(
740    scope: &mut dyn DrawScope,
741    bitmap: ImageBitmap,
742    intrinsic_size: Size,
743    alignment: Alignment,
744    content_scale: ContentScale,
745    alpha: f32,
746    color_filter: Option<ColorFilter>,
747    density: f32,
748) {
749    let container_size = scope.size();
750    let Some((dst_rect, clipped_dst_rect)) = image_destination_clip(
751        intrinsic_size,
752        container_size,
753        alignment,
754        content_scale,
755        density,
756    ) else {
757        return;
758    };
759    let full_src_rect = Rect::from_size(Size::new(bitmap.width() as f32, bitmap.height() as f32));
760    let Some(clipped_src_rect) =
761        map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
762    else {
763        return;
764    };
765    scope.draw_image_src_sampled(
766        bitmap,
767        clipped_src_rect,
768        clipped_dst_rect,
769        alpha,
770        color_filter,
771        ImageSampling::Linear,
772    );
773}
774
775fn draw_bitmap_region_painter(
776    scope: &mut dyn DrawScope,
777    bitmap: ImageBitmap,
778    source: Rect,
779    alignment: Alignment,
780    content_scale: ContentScale,
781    alpha: f32,
782    color_filter: Option<ColorFilter>,
783    sampling: ImageSampling,
784    density: f32,
785) {
786    let source_size = Size::new(source.width.max(0.0), source.height.max(0.0));
787    let Some((dst_rect, clipped_dst_rect)) =
788        image_destination_clip(source_size, scope.size(), alignment, content_scale, density)
789    else {
790        return;
791    };
792    let Some(clipped_source) = map_destination_clip_to_source(source, dst_rect, clipped_dst_rect)
793    else {
794        return;
795    };
796    scope.draw_image_src_sampled(
797        bitmap,
798        clipped_source,
799        clipped_dst_rect,
800        alpha,
801        color_filter,
802        sampling,
803    );
804}
805
806/// Draws a list of source-to-destination quads, clipped to the container.
807///
808/// Every fill that is not a plain scale — tiling, nine-patch — reduces to this,
809/// so the clipping rule and the sampling choice are stated once.
810fn draw_patch_quads(
811    scope: &mut dyn DrawScope,
812    bitmap: &ImageBitmap,
813    quads: &[PatchQuad],
814    container: Rect,
815    alpha: f32,
816    color_filter: Option<ColorFilter>,
817    sampling: ImageSampling,
818) {
819    for quad in quads {
820        let Some(clipped_destination) = intersect(quad.destination, container) else {
821            continue;
822        };
823        let Some(clipped_source) =
824            map_destination_clip_to_source(quad.source, quad.destination, clipped_destination)
825        else {
826            continue;
827        };
828        scope.draw_image_src_sampled(
829            bitmap.clone(),
830            clipped_source,
831            clipped_destination,
832            alpha,
833            color_filter,
834            sampling,
835        );
836    }
837}
838
839fn intersect(rect: Rect, bounds: Rect) -> Option<Rect> {
840    let left = rect.x.max(bounds.x);
841    let top = rect.y.max(bounds.y);
842    let right = (rect.x + rect.width).min(bounds.x + bounds.width);
843    let bottom = (rect.y + rect.height).min(bounds.y + bounds.height);
844    if right <= left || bottom <= top {
845        return None;
846    }
847    Some(Rect {
848        x: left,
849        y: top,
850        width: right - left,
851        height: bottom - top,
852    })
853}
854
855/// Draws a tiled painter across the whole container.
856///
857/// Content scale and alignment do not apply: a tiled fill covers its bounds by
858/// definition, and scaling the source would defeat the reason for tiling it.
859fn draw_bitmap_tiled_painter(
860    scope: &mut dyn DrawScope,
861    bitmap: ImageBitmap,
862    source: Rect,
863    alpha: f32,
864    color_filter: Option<ColorFilter>,
865    sampling: ImageSampling,
866) {
867    let container = Rect::from_size(scope.size());
868    let quads = tile_quads(source, container);
869    draw_patch_quads(
870        scope,
871        &bitmap,
872        &quads,
873        container,
874        alpha,
875        color_filter,
876        sampling,
877    );
878}
879
880/// Draws a nine-patch painter across the whole container.
881///
882/// Like tiling, this fills its bounds by construction, so content scale and
883/// alignment have nothing left to decide.
884#[expect(clippy::too_many_arguments)]
885fn draw_nine_patch_painter(
886    scope: &mut dyn DrawScope,
887    bitmap: ImageBitmap,
888    source: Rect,
889    insets: NinePatchInsets,
890    center: PatchFill,
891    edges: PatchFill,
892    alpha: f32,
893    color_filter: Option<ColorFilter>,
894    sampling: ImageSampling,
895) {
896    let container = Rect::from_size(scope.size());
897    let quads = nine_patch_quads(source, container, insets, center, edges);
898    draw_patch_quads(
899        scope,
900        &bitmap,
901        &quads,
902        container,
903        alpha,
904        color_filter,
905        sampling,
906    );
907}
908
909fn draw_svg_painter(
910    scope: &mut dyn DrawScope,
911    svg: SvgPainter,
912    intrinsic_size: Size,
913    alignment: Alignment,
914    content_scale: ContentScale,
915    alpha: f32,
916    color_filter: Option<ColorFilter>,
917    density: f32,
918) {
919    let container_size = scope.size();
920    let Some((dst_rect, clipped_dst_rect)) = image_destination_clip(
921        intrinsic_size,
922        container_size,
923        alignment,
924        content_scale,
925        density,
926    ) else {
927        return;
928    };
929    let pixel_size = Size::new(dst_rect.width * density, dst_rect.height * density);
930    let bitmap = match svg.rasterize(pixel_size) {
931        Ok(bitmap) => bitmap,
932        Err(error) => {
933            log::warn!("failed to rasterize SVG painter: {error}");
934            return;
935        }
936    };
937    let full_src_rect = Rect::from_size(Size::new(bitmap.width() as f32, bitmap.height() as f32));
938    let Some(clipped_src_rect) =
939        map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
940    else {
941        return;
942    };
943    scope.draw_image_src_sampled(
944        bitmap,
945        clipped_src_rect,
946        clipped_dst_rect,
947        alpha,
948        color_filter,
949        ImageSampling::Linear,
950    );
951}
952
953#[cfg(feature = "svg")]
954fn svg_raster_key(pixel_size: Size) -> Result<SvgRasterKey, SvgPainterError> {
955    let width = svg_raster_axis(pixel_size.width)?;
956    let height = svg_raster_axis(pixel_size.height)?;
957    width
958        .checked_mul(height)
959        .and_then(|value| value.checked_mul(4))
960        .ok_or(SvgPainterError::RasterDimensionsTooLarge)?;
961    Ok(SvgRasterKey { width, height })
962}
963
964#[cfg(feature = "svg")]
965fn svg_raster_axis(value: f32) -> Result<u32, SvgPainterError> {
966    if !value.is_finite() || value <= 0.0 {
967        return Err(SvgPainterError::InvalidRasterDimensions);
968    }
969
970    let rounded = value.ceil();
971    if rounded > u32::MAX as f32 {
972        return Err(SvgPainterError::RasterDimensionsTooLarge);
973    }
974    Ok(rounded as u32)
975}
976
977/// Displays a painter with an optional accessible description.
978///
979/// Images use the image accessibility role unless `modifier` supplies another role.
980/// Without a description or modifier semantics, the image is decorative.
981#[composable]
982pub fn Image<P>(
983    painter: P,
984    content_description: Option<String>,
985    modifier: Modifier,
986    alignment: Alignment,
987    content_scale: ContentScale,
988    alpha: f32,
989    color_filter: Option<ColorFilter>,
990) -> NodeId
991where
992    P: Into<Painter> + Clone + PartialEq + 'static,
993{
994    let painter = painter.into();
995    let intrinsic_dp = painter.intrinsic_size();
996    let draw_alpha = alpha.clamp(0.0, 1.0);
997
998    let semantics_modifier = Modifier::empty().stable_semantics(move |config| {
999        config.content_description.clone_from(&content_description);
1000    });
1001
1002    // The grid the image is aligned on: the composition's, as Text keeps it.
1003    let density = crate::density::density().density();
1004    let image_modifier = semantics_modifier
1005        .then(modifier)
1006        .stable_semantics(|config| {
1007            if config.role.is_none()
1008                && config
1009                    .content_description
1010                    .as_ref()
1011                    .is_some_and(|label| !label.trim().is_empty())
1012            {
1013                config.role = Some(crate::SemanticsWidgetRole::Image);
1014            }
1015        })
1016        .draw_behind(move |scope: &mut dyn DrawScope| {
1017            if draw_alpha <= 0.0 {
1018                return;
1019            }
1020            let container_size = scope.size();
1021            if container_size.width <= 0.0 || container_size.height <= 0.0 {
1022                return;
1023            }
1024            match &painter.kind {
1025                PainterKind::Bitmap(bitmap) => draw_bitmap_painter(
1026                    scope,
1027                    bitmap.clone(),
1028                    intrinsic_dp,
1029                    alignment,
1030                    content_scale,
1031                    draw_alpha,
1032                    color_filter,
1033                    density,
1034                ),
1035                PainterKind::BitmapRegion {
1036                    bitmap,
1037                    source,
1038                    sampling,
1039                } => draw_bitmap_region_painter(
1040                    scope,
1041                    bitmap.clone(),
1042                    source.rect(),
1043                    alignment,
1044                    content_scale,
1045                    draw_alpha,
1046                    color_filter,
1047                    *sampling,
1048                    density,
1049                ),
1050                PainterKind::BitmapTiled {
1051                    bitmap,
1052                    source,
1053                    sampling,
1054                } => draw_bitmap_tiled_painter(
1055                    scope,
1056                    bitmap.clone(),
1057                    source.rect(),
1058                    draw_alpha,
1059                    color_filter,
1060                    *sampling,
1061                ),
1062                PainterKind::NinePatch {
1063                    bitmap,
1064                    source,
1065                    insets,
1066                    center,
1067                    edges,
1068                    sampling,
1069                } => draw_nine_patch_painter(
1070                    scope,
1071                    bitmap.clone(),
1072                    source.rect(),
1073                    insets.insets(),
1074                    *center,
1075                    *edges,
1076                    draw_alpha,
1077                    color_filter,
1078                    *sampling,
1079                ),
1080                PainterKind::Svg(svg) => draw_svg_painter(
1081                    scope,
1082                    svg.clone(),
1083                    intrinsic_dp,
1084                    alignment,
1085                    content_scale,
1086                    draw_alpha,
1087                    color_filter,
1088                    density,
1089                ),
1090            }
1091        });
1092
1093    Layout(
1094        image_modifier,
1095        ImageMeasurePolicy {
1096            intrinsic_size: intrinsic_dp,
1097        },
1098        || {},
1099    )
1100}
1101
1102#[cfg(test)]
1103#[path = "tests/image_tests.rs"]
1104mod tests;