Skip to main content

cranpose_ui/widgets/
image.rs

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