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