1#![allow(non_snake_case)]
4#![allow(clippy::too_many_arguments)] use crate::composable;
7use crate::layout::core::{Alignment, Measurable};
8use crate::modifier::{Modifier, Rect, Size};
9use crate::widgets::Layout;
10use cranpose_core::NodeId;
11use cranpose_ui_graphics::{ColorFilter, DrawScope, ImageBitmap, ImageSampling};
12use cranpose_ui_layout::{Constraints, MeasurePolicy, MeasureResult, Placement};
13use std::hash::{Hash, Hasher};
14use std::sync::Arc;
15#[cfg(feature = "svg")]
16use std::sync::{Mutex, MutexGuard};
17use thiserror::Error;
18
19#[cfg(feature = "svg")]
20#[path = "image_svg.rs"]
21mod image_svg;
22
23pub const DEFAULT_ALPHA: f32 = 1.0;
24#[cfg(feature = "svg")]
25const SVG_RASTER_CACHE_LIMIT: usize = 8;
26
27#[derive(Clone, Copy, Debug, PartialEq)]
28pub enum ContentScale {
29 Fit,
30 Crop,
31 FillBounds,
32 FillWidth,
33 FillHeight,
34 Inside,
35 None,
36}
37
38impl ContentScale {
39 pub fn scaled_size(self, src_size: Size, dst_size: Size) -> Size {
40 if src_size.width <= 0.0
41 || src_size.height <= 0.0
42 || dst_size.width <= 0.0
43 || dst_size.height <= 0.0
44 {
45 return Size::ZERO;
46 }
47
48 let scale_x = dst_size.width / src_size.width;
49 let scale_y = dst_size.height / src_size.height;
50
51 let (factor_x, factor_y) = match self {
52 Self::Fit => {
53 let factor = scale_x.min(scale_y);
54 (factor, factor)
55 }
56 Self::Crop => {
57 let factor = scale_x.max(scale_y);
58 (factor, factor)
59 }
60 Self::FillBounds => (scale_x, scale_y),
61 Self::FillWidth => (scale_x, scale_x),
62 Self::FillHeight => (scale_y, scale_y),
63 Self::Inside => {
64 if src_size.width <= dst_size.width && src_size.height <= dst_size.height {
65 (1.0, 1.0)
66 } else {
67 let factor = scale_x.min(scale_y);
68 (factor, factor)
69 }
70 }
71 Self::None => (1.0, 1.0),
72 };
73
74 Size {
75 width: src_size.width * factor_x,
76 height: src_size.height * factor_y,
77 }
78 }
79}
80
81#[derive(Clone, Debug, PartialEq, Eq, Hash)]
82pub struct Painter {
83 kind: PainterKind,
84}
85
86#[derive(Clone, Debug, PartialEq, Eq, Hash)]
87enum PainterKind {
88 Bitmap(ImageBitmap),
89 Svg(SvgPainter),
90}
91
92impl Painter {
93 pub fn from_bitmap(bitmap: ImageBitmap) -> Self {
94 Self {
95 kind: PainterKind::Bitmap(bitmap),
96 }
97 }
98
99 pub fn from_svg(svg: SvgPainter) -> Self {
100 Self {
101 kind: PainterKind::Svg(svg),
102 }
103 }
104
105 pub fn intrinsic_size(&self) -> Size {
106 match &self.kind {
107 PainterKind::Bitmap(bitmap) => bitmap.intrinsic_size(),
108 PainterKind::Svg(svg) => svg.intrinsic_size(),
109 }
110 }
111
112 pub fn as_bitmap(&self) -> Option<&ImageBitmap> {
113 match &self.kind {
114 PainterKind::Bitmap(bitmap) => Some(bitmap),
115 PainterKind::Svg(_) => None,
116 }
117 }
118
119 pub fn bitmap(&self) -> Option<&ImageBitmap> {
121 self.as_bitmap()
122 }
123}
124
125impl From<ImageBitmap> for Painter {
126 fn from(value: ImageBitmap) -> Self {
127 Self::from_bitmap(value)
128 }
129}
130
131impl From<SvgPainter> for Painter {
132 fn from(value: SvgPainter) -> Self {
133 Self::from_svg(value)
134 }
135}
136
137pub fn BitmapPainter(bitmap: ImageBitmap) -> Painter {
138 Painter::from_bitmap(bitmap)
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Error)]
143pub enum SvgPainterError {
144 #[error("SVG support is disabled; enable the cranpose-ui `svg` feature")]
145 SvgFeatureDisabled,
146 #[error("failed to parse SVG: {0}")]
147 Parse(String),
148 #[error("SVG raster dimensions must be greater than zero")]
149 InvalidRasterDimensions,
150 #[error("SVG raster dimensions are too large")]
151 RasterDimensionsTooLarge,
152 #[error("failed to allocate SVG raster {width}x{height}")]
153 RasterAllocationFailed { width: u32, height: u32 },
154 #[error("SVG raster cache is unavailable")]
155 RasterCacheUnavailable,
156 #[error(transparent)]
157 ImageBitmap(#[from] cranpose_ui_graphics::ImageBitmapError),
158}
159
160#[derive(Clone)]
162pub struct SvgPainter {
163 inner: Arc<SvgPainterInner>,
164}
165
166struct SvgPainterInner {
167 #[cfg(feature = "svg")]
168 document: image_svg::SvgDocument,
169 intrinsic_size: Size,
170 #[cfg(feature = "svg")]
171 cache: Mutex<SvgRasterCache>,
172}
173
174#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
175#[cfg(feature = "svg")]
176struct SvgRasterKey {
177 width: u32,
178 height: u32,
179}
180
181#[derive(Clone, Debug)]
182#[cfg(feature = "svg")]
183struct SvgRasterEntry {
184 key: SvgRasterKey,
185 bitmap: ImageBitmap,
186}
187
188#[derive(Default, Debug)]
189#[cfg(feature = "svg")]
190struct SvgRasterCache {
191 entries: Vec<SvgRasterEntry>,
192}
193
194impl SvgPainter {
195 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SvgPainterError> {
196 #[cfg(not(feature = "svg"))]
197 {
198 let _ = bytes;
199 Err(SvgPainterError::SvgFeatureDisabled)
200 }
201 #[cfg(feature = "svg")]
202 {
203 let document = image_svg::parse_svg_document(bytes)?;
204 let intrinsic_size = document.intrinsic_size();
205
206 Ok(Self {
207 inner: Arc::new(SvgPainterInner {
208 document,
209 intrinsic_size,
210 cache: Mutex::new(SvgRasterCache::default()),
211 }),
212 })
213 }
214 }
215
216 pub fn id(&self) -> u64 {
217 Arc::as_ptr(&self.inner) as usize as u64
218 }
219
220 pub fn intrinsic_size(&self) -> Size {
221 self.inner.intrinsic_size
222 }
223
224 pub fn rasterize(&self, pixel_size: Size) -> Result<ImageBitmap, SvgPainterError> {
225 #[cfg(not(feature = "svg"))]
226 {
227 let _ = pixel_size;
228 Err(SvgPainterError::SvgFeatureDisabled)
229 }
230 #[cfg(feature = "svg")]
231 {
232 let key = svg_raster_key(pixel_size)?;
233 if let Some(bitmap) = self.cached_bitmap(key)? {
234 return Ok(bitmap);
235 }
236
237 let bitmap = self.rasterize_uncached(key)?;
238 self.cache_bitmap(key, bitmap.clone())?;
239 Ok(bitmap)
240 }
241 }
242
243 #[cfg(feature = "svg")]
244 fn cached_bitmap(&self, key: SvgRasterKey) -> Result<Option<ImageBitmap>, SvgPainterError> {
245 let mut cache = self.lock_cache();
246 Ok(cache.get(key))
247 }
248
249 #[cfg(feature = "svg")]
250 fn cache_bitmap(&self, key: SvgRasterKey, bitmap: ImageBitmap) -> Result<(), SvgPainterError> {
251 let mut cache = self.lock_cache();
252 cache.insert(key, bitmap);
253 Ok(())
254 }
255
256 #[cfg(feature = "svg")]
257 fn lock_cache(&self) -> MutexGuard<'_, SvgRasterCache> {
258 self.inner
259 .cache
260 .lock()
261 .unwrap_or_else(|poisoned| poisoned.into_inner())
262 }
263
264 #[cfg(feature = "svg")]
265 fn rasterize_uncached(&self, key: SvgRasterKey) -> Result<ImageBitmap, SvgPainterError> {
266 (key.width as usize)
267 .checked_mul(key.height as usize)
268 .and_then(|value| value.checked_mul(4))
269 .ok_or(SvgPainterError::RasterDimensionsTooLarge)?;
270
271 let mut pixmap = tiny_skia::Pixmap::new(key.width, key.height).ok_or(
272 SvgPainterError::RasterAllocationFailed {
273 width: key.width,
274 height: key.height,
275 },
276 )?;
277 image_svg::rasterize_svg_document(&self.inner.document, &mut pixmap);
278 let pixels = image_svg::demultiplied_rgba_pixels(&pixmap);
279 Ok(ImageBitmap::from_rgba8(key.width, key.height, pixels)?)
280 }
281}
282
283impl std::fmt::Debug for SvgPainter {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 f.debug_struct("SvgPainter")
286 .field("id", &self.id())
287 .field("intrinsic_size", &self.intrinsic_size())
288 .finish_non_exhaustive()
289 }
290}
291
292impl PartialEq for SvgPainter {
293 fn eq(&self, other: &Self) -> bool {
294 self.id() == other.id()
295 }
296}
297
298impl Eq for SvgPainter {}
299
300impl Hash for SvgPainter {
301 fn hash<H: Hasher>(&self, state: &mut H) {
302 self.id().hash(state);
303 }
304}
305
306#[cfg(feature = "svg")]
307impl SvgRasterCache {
308 fn get(&mut self, key: SvgRasterKey) -> Option<ImageBitmap> {
309 let position = self.entries.iter().position(|entry| entry.key == key)?;
310 let entry = self.entries.remove(position);
311 let bitmap = entry.bitmap.clone();
312 self.entries.push(entry);
313 Some(bitmap)
314 }
315
316 fn insert(&mut self, key: SvgRasterKey, bitmap: ImageBitmap) {
317 if let Some(position) = self.entries.iter().position(|entry| entry.key == key) {
318 self.entries.remove(position);
319 } else if self.entries.len() >= SVG_RASTER_CACHE_LIMIT {
320 self.entries.remove(0);
321 }
322 self.entries.push(SvgRasterEntry { key, bitmap });
323 }
324}
325
326#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
327struct SvgBytesKey {
328 ptr: usize,
329 len: usize,
330}
331
332impl SvgBytesKey {
333 fn new(bytes: &'static [u8]) -> Self {
334 Self {
335 ptr: bytes.as_ptr() as usize,
336 len: bytes.len(),
337 }
338 }
339}
340
341pub fn remember_svg(bytes: &'static [u8]) -> Result<SvgPainter, SvgPainterError> {
342 let key = SvgBytesKey::new(bytes);
343 cranpose_core::withCurrentComposer(|composer| {
344 composer.with_key(&key, |composer| {
345 composer
346 .remember(|| SvgPainter::from_bytes(bytes))
347 .with(|result| result.clone())
348 })
349 })
350}
351
352#[derive(Clone, Debug, PartialEq)]
359struct ImageMeasurePolicy {
360 intrinsic_size: Size,
361}
362
363impl MeasurePolicy for ImageMeasurePolicy {
364 fn measure(
365 &self,
366 _measurables: &[Box<dyn Measurable>],
367 constraints: Constraints,
368 ) -> MeasureResult {
369 let mut placements = Vec::new();
370 let size = self.measure_into(&[], constraints, &mut placements);
371 MeasureResult::new(size, placements)
372 }
373
374 fn measure_into(
375 &self,
376 _measurables: &[Box<dyn Measurable>],
377 constraints: Constraints,
378 placements: &mut Vec<Placement>,
379 ) -> Size {
380 placements.clear();
381 let iw = self.intrinsic_size.width;
382 let ih = self.intrinsic_size.height;
383
384 if iw <= 0.0 || ih <= 0.0 {
385 let (w, h) = constraints.constrain(0.0, 0.0);
386 return Size {
387 width: w,
388 height: h,
389 };
390 }
391
392 let cw = iw.clamp(constraints.min_width, constraints.max_width);
394 let ch = ih.clamp(constraints.min_height, constraints.max_height);
395
396 let scale_x = cw / iw;
399 let scale_y = ch / ih;
400
401 let (width, height) = if scale_x < 1.0 || scale_y < 1.0 {
402 let factor = scale_x.min(scale_y);
403 let w = (iw * factor).clamp(constraints.min_width, constraints.max_width);
404 let h = (ih * factor).clamp(constraints.min_height, constraints.max_height);
405 (w, h)
406 } else {
407 (cw, ch)
408 };
409
410 Size { width, height }
411 }
412
413 fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
414 self.intrinsic_size.width
415 }
416
417 fn max_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
418 self.intrinsic_size.width
419 }
420
421 fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
422 self.intrinsic_size.height
423 }
424
425 fn max_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
426 self.intrinsic_size.height
427 }
428}
429
430fn destination_rect(
431 src_size: Size,
432 dst_size: Size,
433 alignment: Alignment,
434 content_scale: ContentScale,
435) -> Rect {
436 let draw_size = content_scale.scaled_size(src_size, dst_size);
437 let allows_overflow = content_scale == ContentScale::Crop;
438 let offset_x = aligned_x_offset(
439 alignment.horizontal,
440 dst_size.width,
441 draw_size.width,
442 allows_overflow,
443 );
444 let offset_y = aligned_y_offset(
445 alignment.vertical,
446 dst_size.height,
447 draw_size.height,
448 allows_overflow,
449 );
450 Rect {
451 x: offset_x,
452 y: offset_y,
453 width: draw_size.width,
454 height: draw_size.height,
455 }
456}
457
458fn aligned_x_offset(
459 alignment: cranpose_ui_layout::HorizontalAlignment,
460 available: f32,
461 child: f32,
462 allows_overflow: bool,
463) -> f32 {
464 if !allows_overflow {
465 return alignment.align(available, child);
466 }
467
468 match alignment {
469 cranpose_ui_layout::HorizontalAlignment::Start => 0.0,
470 cranpose_ui_layout::HorizontalAlignment::CenterHorizontally => (available - child) / 2.0,
471 cranpose_ui_layout::HorizontalAlignment::End => available - child,
472 }
473}
474
475fn aligned_y_offset(
476 alignment: cranpose_ui_layout::VerticalAlignment,
477 available: f32,
478 child: f32,
479 allows_overflow: bool,
480) -> f32 {
481 if !allows_overflow {
482 return alignment.align(available, child);
483 }
484
485 match alignment {
486 cranpose_ui_layout::VerticalAlignment::Top => 0.0,
487 cranpose_ui_layout::VerticalAlignment::CenterVertically => (available - child) / 2.0,
488 cranpose_ui_layout::VerticalAlignment::Bottom => available - child,
489 }
490}
491
492fn map_destination_clip_to_source(
493 src_rect: Rect,
494 dst_rect: Rect,
495 clipped_dst_rect: Rect,
496) -> Option<Rect> {
497 if src_rect.width <= 0.0
498 || src_rect.height <= 0.0
499 || dst_rect.width <= 0.0
500 || dst_rect.height <= 0.0
501 || clipped_dst_rect.width <= 0.0
502 || clipped_dst_rect.height <= 0.0
503 {
504 return None;
505 }
506
507 let scale_x = src_rect.width / dst_rect.width;
508 let scale_y = src_rect.height / dst_rect.height;
509
510 let src_min_x = src_rect.x;
511 let src_min_y = src_rect.y;
512 let src_max_x = src_rect.x + src_rect.width;
513 let src_max_y = src_rect.y + src_rect.height;
514
515 let raw_left = src_rect.x + (clipped_dst_rect.x - dst_rect.x) * scale_x;
516 let raw_top = src_rect.y + (clipped_dst_rect.y - dst_rect.y) * scale_y;
517 let raw_right =
518 src_rect.x + ((clipped_dst_rect.x + clipped_dst_rect.width) - dst_rect.x) * scale_x;
519 let raw_bottom =
520 src_rect.y + ((clipped_dst_rect.y + clipped_dst_rect.height) - dst_rect.y) * scale_y;
521
522 let left = raw_left.clamp(src_min_x, src_max_x);
523 let top = raw_top.clamp(src_min_y, src_max_y);
524 let right = raw_right.clamp(src_min_x, src_max_x);
525 let bottom = raw_bottom.clamp(src_min_y, src_max_y);
526 let width = right - left;
527 let height = bottom - top;
528
529 if width <= 0.0 || height <= 0.0 {
530 None
531 } else {
532 Some(Rect {
533 x: left,
534 y: top,
535 width,
536 height,
537 })
538 }
539}
540
541fn image_destination_clip(
542 src_size: Size,
543 container_size: Size,
544 alignment: Alignment,
545 content_scale: ContentScale,
546) -> Option<(Rect, Rect)> {
547 let dst_rect = destination_rect(src_size, container_size, alignment, content_scale);
548 if dst_rect.width <= 0.0 || dst_rect.height <= 0.0 {
549 return None;
550 }
551
552 let container_rect = Rect::from_size(container_size);
553 let clipped_dst_rect = dst_rect.intersect(container_rect)?;
554 Some((dst_rect, clipped_dst_rect))
555}
556
557fn draw_bitmap_painter(
558 scope: &mut dyn DrawScope,
559 bitmap: ImageBitmap,
560 intrinsic_size: Size,
561 alignment: Alignment,
562 content_scale: ContentScale,
563 alpha: f32,
564 color_filter: Option<ColorFilter>,
565) {
566 let container_size = scope.size();
567 let Some((dst_rect, clipped_dst_rect)) =
568 image_destination_clip(intrinsic_size, container_size, alignment, content_scale)
569 else {
570 return;
571 };
572 let full_src_rect = Rect::from_size(Size::new(bitmap.width() as f32, bitmap.height() as f32));
573 let Some(clipped_src_rect) =
574 map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
575 else {
576 return;
577 };
578 scope.draw_image_src_sampled(
579 bitmap,
580 clipped_src_rect,
581 clipped_dst_rect,
582 alpha,
583 color_filter,
584 ImageSampling::Linear,
585 );
586}
587
588fn draw_svg_painter(
589 scope: &mut dyn DrawScope,
590 svg: SvgPainter,
591 intrinsic_size: Size,
592 alignment: Alignment,
593 content_scale: ContentScale,
594 alpha: f32,
595 color_filter: Option<ColorFilter>,
596) {
597 let container_size = scope.size();
598 let Some((dst_rect, clipped_dst_rect)) =
599 image_destination_clip(intrinsic_size, container_size, alignment, content_scale)
600 else {
601 return;
602 };
603 let density = crate::render_state::current_density();
604 let pixel_size = Size::new(dst_rect.width * density, dst_rect.height * density);
605 let bitmap = match svg.rasterize(pixel_size) {
606 Ok(bitmap) => bitmap,
607 Err(error) => {
608 log::warn!("failed to rasterize SVG painter: {error}");
609 return;
610 }
611 };
612 let full_src_rect = Rect::from_size(Size::new(bitmap.width() as f32, bitmap.height() as f32));
613 let Some(clipped_src_rect) =
614 map_destination_clip_to_source(full_src_rect, dst_rect, clipped_dst_rect)
615 else {
616 return;
617 };
618 scope.draw_image_src_sampled(
619 bitmap,
620 clipped_src_rect,
621 clipped_dst_rect,
622 alpha,
623 color_filter,
624 ImageSampling::Linear,
625 );
626}
627
628#[cfg(feature = "svg")]
629fn svg_raster_key(pixel_size: Size) -> Result<SvgRasterKey, SvgPainterError> {
630 let width = svg_raster_axis(pixel_size.width)?;
631 let height = svg_raster_axis(pixel_size.height)?;
632 width
633 .checked_mul(height)
634 .and_then(|value| value.checked_mul(4))
635 .ok_or(SvgPainterError::RasterDimensionsTooLarge)?;
636 Ok(SvgRasterKey { width, height })
637}
638
639#[cfg(feature = "svg")]
640fn svg_raster_axis(value: f32) -> Result<u32, SvgPainterError> {
641 if !value.is_finite() || value <= 0.0 {
642 return Err(SvgPainterError::InvalidRasterDimensions);
643 }
644
645 let rounded = value.ceil();
646 if rounded > u32::MAX as f32 {
647 return Err(SvgPainterError::RasterDimensionsTooLarge);
648 }
649 Ok(rounded as u32)
650}
651
652#[composable]
653pub fn Image<P>(
654 painter: P,
655 content_description: Option<String>,
656 modifier: Modifier,
657 alignment: Alignment,
658 content_scale: ContentScale,
659 alpha: f32,
660 color_filter: Option<ColorFilter>,
661) -> NodeId
662where
663 P: Into<Painter> + Clone + PartialEq + 'static,
664{
665 let painter = painter.into();
666 let intrinsic_dp = painter.intrinsic_size();
669 let draw_alpha = alpha.clamp(0.0, 1.0);
670 let draw_painter = painter.clone();
671
672 let semantics_modifier = if let Some(description) = content_description {
673 Modifier::empty().semantics(move |config| {
674 config.content_description = Some(description.clone());
675 })
676 } else {
677 Modifier::empty()
678 };
679
680 let image_modifier =
681 modifier
682 .then(semantics_modifier)
683 .draw_behind(move |scope: &mut dyn DrawScope| {
684 if draw_alpha <= 0.0 {
685 return;
686 }
687 let container_size = scope.size();
688 if container_size.width <= 0.0 || container_size.height <= 0.0 {
689 return;
690 }
691 match &draw_painter.kind {
692 PainterKind::Bitmap(bitmap) => draw_bitmap_painter(
693 scope,
694 bitmap.clone(),
695 intrinsic_dp,
696 alignment,
697 content_scale,
698 draw_alpha,
699 color_filter,
700 ),
701 PainterKind::Svg(svg) => draw_svg_painter(
702 scope,
703 svg.clone(),
704 intrinsic_dp,
705 alignment,
706 content_scale,
707 draw_alpha,
708 color_filter,
709 ),
710 }
711 });
712
713 Layout(
714 image_modifier,
715 ImageMeasurePolicy {
716 intrinsic_size: intrinsic_dp,
717 },
718 || {},
719 )
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725 use crate::layout::core::Alignment;
726
727 #[cfg(feature = "svg")]
728 const RED_RECT_SVG: &[u8] = br##"
729 <svg xmlns="http://www.w3.org/2000/svg" width="10" height="20" viewBox="0 0 10 20">
730 <rect x="0" y="0" width="10" height="20" fill="#ff0000"/>
731 </svg>
732 "##;
733
734 #[cfg(feature = "svg")]
735 const TRANSPARENT_CENTER_SVG: &[u8] = br##"
736 <svg xmlns="http://www.w3.org/2000/svg" width="4" height="4" viewBox="0 0 4 4">
737 <rect x="1" y="1" width="2" height="2" fill="#00ff00"/>
738 </svg>
739 "##;
740
741 fn sample_bitmap() -> ImageBitmap {
742 ImageBitmap::from_rgba8(4, 2, vec![255; 4 * 2 * 4]).expect("bitmap")
743 }
744
745 #[test]
746 fn svg_painter_ids_do_not_use_process_global_counter() {
747 let source = include_str!("image.rs");
748 let svg_counter = ["static ", "NEXT_SVG_PAINTER_ID"].concat();
749
750 assert!(
751 !source.contains(&svg_counter),
752 "SVG painter ids must come from retained painter identity"
753 );
754 }
755
756 #[cfg(feature = "svg")]
757 fn cache_test_bitmap(width: u32) -> ImageBitmap {
758 ImageBitmap::from_rgba8(width, 1, vec![255; width as usize * 4]).expect("bitmap")
759 }
760
761 #[cfg(feature = "svg")]
762 fn pixel_at(bitmap: &ImageBitmap, x: u32, y: u32) -> [u8; 4] {
763 let offset = ((y * bitmap.width() + x) * 4) as usize;
764 let pixels = bitmap.pixels();
765 [
766 pixels[offset],
767 pixels[offset + 1],
768 pixels[offset + 2],
769 pixels[offset + 3],
770 ]
771 }
772
773 #[test]
774 fn painter_reports_intrinsic_size_and_bitmap() {
775 let bitmap = sample_bitmap();
776 let painter = BitmapPainter(bitmap.clone());
777 assert_eq!(painter.intrinsic_size(), Size::new(4.0, 2.0));
778 assert_eq!(painter.bitmap(), Some(&bitmap));
779 }
780
781 #[test]
782 #[cfg(feature = "svg")]
783 fn svg_painter_reports_intrinsic_size() {
784 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
785 let clone = painter.clone();
786 assert_eq!(painter.intrinsic_size(), Size::new(10.0, 20.0));
787 assert_eq!(painter.id(), clone.id());
788 assert_eq!(Painter::from_svg(painter).bitmap(), None);
789 }
790
791 #[test]
792 #[cfg(feature = "svg")]
793 fn svg_painter_rasterizes_requested_dimensions() {
794 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
795 let bitmap = painter
796 .rasterize(Size::new(5.0, 10.0))
797 .expect("rasterized svg");
798
799 assert_eq!(bitmap.width(), 5);
800 assert_eq!(bitmap.height(), 10);
801 assert_eq!(pixel_at(&bitmap, 2, 5), [255, 0, 0, 255]);
802 }
803
804 #[test]
805 #[cfg(feature = "svg")]
806 fn svg_painter_preserves_transparency() {
807 let painter = SvgPainter::from_bytes(TRANSPARENT_CENTER_SVG).expect("svg painter");
808 let bitmap = painter
809 .rasterize(Size::new(4.0, 4.0))
810 .expect("rasterized svg");
811
812 assert_eq!(pixel_at(&bitmap, 0, 0), [0, 0, 0, 0]);
813 assert_eq!(pixel_at(&bitmap, 2, 2), [0, 255, 0, 255]);
814 }
815
816 #[test]
817 #[cfg(feature = "svg")]
818 fn svg_painter_reuses_cached_raster_for_same_size() {
819 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
820 let first = painter
821 .rasterize(Size::new(8.0, 8.0))
822 .expect("first raster");
823 let second = painter
824 .rasterize(Size::new(8.0, 8.0))
825 .expect("second raster");
826
827 assert_eq!(first.id(), second.id());
828 }
829
830 #[test]
831 #[cfg(feature = "svg")]
832 fn svg_raster_cache_evicts_least_recently_used_entry() {
833 let mut cache = SvgRasterCache::default();
834 let keys: Vec<SvgRasterKey> = (0..SVG_RASTER_CACHE_LIMIT)
835 .map(|index| SvgRasterKey {
836 width: index as u32 + 1,
837 height: 1,
838 })
839 .collect();
840
841 for key in &keys {
842 cache.insert(*key, cache_test_bitmap(key.width));
843 }
844
845 let recent = cache.get(keys[0]).expect("cached raster");
846 let new_key = SvgRasterKey {
847 width: SVG_RASTER_CACHE_LIMIT as u32 + 1,
848 height: 1,
849 };
850 cache.insert(new_key, cache_test_bitmap(new_key.width));
851
852 assert!(cache.get(keys[1]).is_none());
853 assert_eq!(
854 cache.get(keys[0]).expect("retained raster").id(),
855 recent.id()
856 );
857 assert!(cache.get(new_key).is_some());
858 }
859
860 #[test]
861 #[cfg(feature = "svg")]
862 fn svg_painter_rasterizes_distinct_sizes_separately() {
863 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
864 let small = painter
865 .rasterize(Size::new(8.0, 8.0))
866 .expect("small raster");
867 let large = painter
868 .rasterize(Size::new(16.0, 16.0))
869 .expect("large raster");
870
871 assert_ne!(small.id(), large.id());
872 assert_eq!(large.width(), 16);
873 assert_eq!(large.height(), 16);
874 }
875
876 #[test]
877 #[cfg(feature = "svg")]
878 fn svg_painter_cache_recovers_after_poison() {
879 let painter = SvgPainter::from_bytes(RED_RECT_SVG).expect("svg painter");
880 let poison_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
881 let _guard = painter
882 .inner
883 .cache
884 .lock()
885 .unwrap_or_else(|poisoned| poisoned.into_inner());
886 panic!("poison svg raster cache for recovery test");
887 }));
888
889 assert!(poison_result.is_err());
890
891 let bitmap = painter
892 .rasterize(Size::new(8.0, 8.0))
893 .expect("svg raster cache should recover after poisoning");
894 assert_eq!(bitmap.width(), 8);
895 assert_eq!(bitmap.height(), 8);
896 }
897
898 #[test]
899 fn svg_draw_density_has_no_outside_context_fallback() {
900 let source = include_str!("image.rs");
901 let fallback_call = ["try_current", "_density()", ".unwrap_or"].concat();
902 assert!(
903 !source.contains(&fallback_call),
904 "SVG image drawing must use the active AppContext density"
905 );
906 }
907
908 #[test]
909 #[cfg(feature = "svg")]
910 fn svg_painter_rejects_invalid_bytes() {
911 let err = SvgPainter::from_bytes(b"not svg").expect_err("invalid svg");
912 assert!(matches!(err, SvgPainterError::Parse(_)));
913 }
914
915 #[test]
916 #[cfg(not(feature = "svg"))]
917 fn svg_painter_reports_disabled_feature_without_resvg() {
918 let err = SvgPainter::from_bytes(b"not svg").expect_err("svg feature disabled");
919 assert!(matches!(err, SvgPainterError::SvgFeatureDisabled));
920 }
921
922 #[test]
923 fn fit_keeps_aspect_ratio() {
924 let src = Size::new(200.0, 100.0);
925 let dst = Size::new(300.0, 300.0);
926 let result = ContentScale::Fit.scaled_size(src, dst);
927 assert_eq!(result, Size::new(300.0, 150.0));
928 }
929
930 #[test]
931 fn crop_fills_bounds() {
932 let src = Size::new(200.0, 100.0);
933 let dst = Size::new(300.0, 300.0);
934 let result = ContentScale::Crop.scaled_size(src, dst);
935 assert_eq!(result, Size::new(600.0, 300.0));
936 }
937
938 #[test]
939 fn destination_rect_aligns_center() {
940 let src = Size::new(200.0, 100.0);
941 let dst = Size::new(300.0, 300.0);
942 let rect = destination_rect(src, dst, Alignment::CENTER, ContentScale::Fit);
943 assert_eq!(
944 rect,
945 Rect {
946 x: 0.0,
947 y: 75.0,
948 width: 300.0,
949 height: 150.0,
950 }
951 );
952 }
953
954 #[test]
955 fn crop_destination_clip_maps_centered_wide_source() {
956 let src = Size::new(200.0, 100.0);
957 let dst = Size::new(100.0, 100.0);
958 let (dst_rect, clipped_dst_rect) =
959 image_destination_clip(src, dst, Alignment::CENTER, ContentScale::Crop)
960 .expect("destination clip");
961 let rect = map_destination_clip_to_source(Rect::from_size(src), dst_rect, clipped_dst_rect)
962 .expect("source clip");
963 assert_eq!(
964 rect,
965 Rect {
966 x: 50.0,
967 y: 0.0,
968 width: 100.0,
969 height: 100.0,
970 }
971 );
972 }
973
974 #[test]
975 fn crop_destination_clip_honors_start_alignment() {
976 let src = Size::new(200.0, 100.0);
977 let dst = Size::new(100.0, 100.0);
978 let (dst_rect, clipped_dst_rect) =
979 image_destination_clip(src, dst, Alignment::TOP_START, ContentScale::Crop)
980 .expect("destination clip");
981 let rect = map_destination_clip_to_source(Rect::from_size(src), dst_rect, clipped_dst_rect)
982 .expect("source clip");
983 assert_eq!(
984 rect,
985 Rect {
986 x: 0.0,
987 y: 0.0,
988 width: 100.0,
989 height: 100.0,
990 }
991 );
992 }
993
994 fn approx_eq(left: f32, right: f32) {
995 assert!((left - right).abs() < 1e-4, "left={left}, right={right}");
996 }
997
998 #[test]
999 fn map_destination_clip_to_source_scales_proportionally() {
1000 let src = Rect {
1001 x: 0.0,
1002 y: 0.0,
1003 width: 100.0,
1004 height: 100.0,
1005 };
1006 let dst = Rect {
1007 x: -50.0,
1008 y: 0.0,
1009 width: 200.0,
1010 height: 100.0,
1011 };
1012 let clipped_dst = Rect {
1013 x: 0.0,
1014 y: 0.0,
1015 width: 100.0,
1016 height: 100.0,
1017 };
1018 let mapped = map_destination_clip_to_source(src, dst, clipped_dst).expect("mapped");
1019 approx_eq(mapped.x, 25.0);
1020 approx_eq(mapped.y, 0.0);
1021 approx_eq(mapped.width, 50.0);
1022 approx_eq(mapped.height, 100.0);
1023 }
1024
1025 #[test]
1026 fn map_destination_clip_to_source_returns_full_source_without_clipping() {
1027 let src = Rect {
1028 x: 0.0,
1029 y: 0.0,
1030 width: 120.0,
1031 height: 80.0,
1032 };
1033 let dst = Rect {
1034 x: 10.0,
1035 y: 5.0,
1036 width: 60.0,
1037 height: 40.0,
1038 };
1039 let mapped = map_destination_clip_to_source(src, dst, dst).expect("mapped");
1040 approx_eq(mapped.x, src.x);
1041 approx_eq(mapped.y, src.y);
1042 approx_eq(mapped.width, src.width);
1043 approx_eq(mapped.height, src.height);
1044 }
1045
1046 fn measure_image(intrinsic: Size, constraints: Constraints) -> Size {
1049 let policy = ImageMeasurePolicy {
1050 intrinsic_size: intrinsic,
1051 };
1052 policy.measure(&[], constraints).size
1053 }
1054
1055 #[test]
1056 fn image_measure_unconstrained() {
1057 let size = measure_image(
1058 Size::new(800.0, 600.0),
1059 Constraints::loose(f32::INFINITY, f32::INFINITY),
1060 );
1061 assert_eq!(size, Size::new(800.0, 600.0));
1062 }
1063
1064 #[test]
1065 fn image_measure_width_constrained_preserves_aspect_ratio() {
1066 let size = measure_image(
1068 Size::new(800.0, 600.0),
1069 Constraints::loose(400.0, f32::INFINITY),
1070 );
1071 assert_eq!(size, Size::new(400.0, 300.0));
1072 }
1073
1074 #[test]
1075 fn image_measure_height_constrained_preserves_aspect_ratio() {
1076 let size = measure_image(
1078 Size::new(800.0, 600.0),
1079 Constraints::loose(f32::INFINITY, 300.0),
1080 );
1081 assert_eq!(size, Size::new(400.0, 300.0));
1082 }
1083
1084 #[test]
1085 fn image_measure_both_constrained_uses_smaller_factor() {
1086 let size = measure_image(Size::new(800.0, 600.0), Constraints::loose(200.0, 400.0));
1089 assert_eq!(size, Size::new(200.0, 150.0));
1090 }
1091
1092 #[test]
1093 fn image_measure_fits_within_constraints() {
1094 let size = measure_image(Size::new(200.0, 100.0), Constraints::loose(400.0, 400.0));
1096 assert_eq!(size, Size::new(200.0, 100.0));
1097 }
1098
1099 #[test]
1100 fn image_measure_zero_intrinsic() {
1101 let size = measure_image(Size::ZERO, Constraints::loose(400.0, 400.0));
1102 assert_eq!(size, Size::new(0.0, 0.0));
1103 }
1104}