1#![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
29pub const DEFAULT_ALPHA: f32 = 1.0;
31#[cfg(feature = "svg")]
32const SVG_RASTER_CACHE_LIMIT: usize = 8;
33
34#[derive(Clone, Copy, Debug, PartialEq)]
36pub enum ContentScale {
37 Fit,
40 Crop,
42 FillBounds,
44 FillWidth,
46 FillHeight,
48 Inside,
50 None,
52}
53
54impl ContentScale {
55 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#[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 BitmapTiled {
118 bitmap: ImageBitmap,
119 source: Quad,
120 sampling: ImageSampling,
121 },
122 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#[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 pub fn from_bitmap(bitmap: ImageBitmap) -> Self {
194 Self {
195 kind: PainterKind::Bitmap(bitmap),
196 }
197 }
198
199 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 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 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 pub fn from_svg(svg: SvgPainter) -> Self {
256 Self {
257 kind: PainterKind::Svg(svg),
258 }
259 }
260
261 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 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 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
306pub fn BitmapPainter(bitmap: ImageBitmap) -> Painter {
308 Painter::from_bitmap(bitmap)
309}
310
311pub fn BitmapRegionPainter(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Painter {
313 Painter::from_bitmap_region(bitmap, source, sampling)
314}
315
316pub fn TiledPainter(bitmap: ImageBitmap, source: Rect, sampling: ImageSampling) -> Painter {
318 Painter::from_bitmap_tiled(bitmap, source, sampling)
319}
320
321pub 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#[derive(Debug, Clone, PartialEq, Eq, Error)]
335pub enum SvgPainterError {
336 #[error("SVG support is disabled; enable the cranpose-ui `svg` feature")]
338 SvgFeatureDisabled,
339 #[error("failed to parse SVG: {0}")]
341 Parse(String),
342 #[error("SVG raster dimensions must be greater than zero")]
344 InvalidRasterDimensions,
345 #[error("SVG raster dimensions are too large")]
347 RasterDimensionsTooLarge,
348 #[error("failed to allocate SVG raster {width}x{height}")]
350 RasterAllocationFailed {
351 width: u32,
353 height: u32,
355 },
356 #[error("SVG raster cache is unavailable")]
358 RasterCacheUnavailable,
359 #[error(transparent)]
361 ImageBitmap(#[from] cranpose_ui_graphics::ImageBitmapError),
362}
363
364#[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 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 pub fn id(&self) -> u64 {
425 Arc::as_ptr(&self.inner) as usize as u64
426 }
427
428 pub fn intrinsic_size(&self) -> Size {
430 self.inner.intrinsic_size
431 }
432
433 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
552pub 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#[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
643fn 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
738struct 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
810fn 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
859fn 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#[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 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;