1#[cfg(feature = "image")]
29use std::io::Cursor;
30use std::{io, sync::Arc};
31
32use half::f16;
33#[cfg(feature = "image")]
34use image::{DynamicImage, ImageDecoder, ImageFormat, ImageReader, Limits};
35
36pub use motion_core::pixels::Sampling;
37
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
42#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
43pub enum Fit {
44 Fill,
46 #[default]
48 Contain,
49 Cover,
51 None,
53}
54
55#[derive(Clone, Debug)]
56enum Input {
57 Source(motion_core::Source),
58 Lazy(motion_core::Handle<ImageAsset>),
59 Loaded(Arc<ImageAsset>),
60}
61
62#[derive(Clone, Debug)]
66pub struct Image {
67 input: Input,
68 size: Option<motion_core::Size>,
69 fit: Fit,
70 radius: f64,
71 sampling: Sampling,
72}
73
74impl Image {
75 pub fn open(source: impl Into<motion_core::Source>) -> Self {
77 Self {
78 input: Input::Source(source.into()),
79 size: None,
80 fit: Fit::Contain,
81 radius: 0.0,
82 sampling: Sampling::Trilinear,
83 }
84 }
85
86 pub fn new(handle: motion_core::Handle<ImageAsset>) -> Self {
88 Self {
89 input: Input::Lazy(handle),
90 size: None,
91 fit: Fit::Contain,
92 radius: 0.0,
93 sampling: Sampling::Trilinear,
94 }
95 }
96
97 pub fn from_asset(image: Arc<ImageAsset>) -> Self {
99 Self {
100 input: Input::Loaded(image),
101 size: None,
102 fit: Fit::Contain,
103 radius: 0.0,
104 sampling: Sampling::Trilinear,
105 }
106 }
107
108 pub fn size(mut self, width: f64, height: f64) -> Self {
110 self.size = Some(motion_core::Size::new(width, height));
111 self
112 }
113
114 pub fn fit(mut self, fit: Fit) -> Self {
116 self.fit = fit;
117 self
118 }
119
120 pub fn corner_radius(mut self, radius: f64) -> Self {
122 self.radius = radius;
123 self
124 }
125
126 pub fn sampling(mut self, sampling: Sampling) -> Self {
128 self.sampling = sampling;
129 self
130 }
131
132 fn image(&self, context: &motion_core::DrawContext) -> motion_core::Result<Arc<ImageAsset>> {
133 match &self.input {
134 Input::Source(source) => context.assets().load::<ImageAsset>(source.clone()).get(),
135 Input::Lazy(handle) => handle.get(),
136 Input::Loaded(image) => Ok(image.clone()),
137 }
138 }
139
140 fn layout_bounds(
141 &self,
142 context: &motion_core::DrawContext,
143 ) -> Result<motion_core::Rect, motion_core::ValidationError> {
144 if !self.radius.is_finite() || self.radius < 0.0 {
145 return Err(motion_core::ValidationError::invalid(
146 "image",
147 "corner_radius",
148 "Image corner radius must be finite and nonnegative",
149 ));
150 }
151 let size = self.size.unwrap_or_else(|| {
152 self.image(context)
153 .map(|image| {
154 motion_core::Size::new(f64::from(image.width()), f64::from(image.height()))
155 })
156 .unwrap_or(motion_core::Size::new(64.0, 64.0))
157 });
158 Ok(size.to_rect())
159 }
160}
161
162impl motion_core::Drawable for Image {
163 fn bounds(
164 &self,
165 context: &motion_core::DrawContext,
166 ) -> Result<motion_core::Rect, motion_core::ValidationError> {
167 self.layout_bounds(context)
168 }
169
170 fn draw(
171 &self,
172 context: &motion_core::DrawContext,
173 canvas: &mut motion_core::Canvas<'_>,
174 ) -> Result<(), motion_core::ValidationError> {
175 let bounds = self.layout_bounds(context)?;
176 let shape = motion_core::RoundedRect::new(bounds, self.radius)?;
177 match self.image(context) {
178 Ok(image) => {
179 if bounds.width() == 0.0 || bounds.height() == 0.0 {
180 return Ok(());
181 }
182 let horizontal = bounds.width() / f64::from(image.width());
183 let vertical = bounds.height() / f64::from(image.height());
184 let (horizontal, vertical) = match self.fit {
185 Fit::Fill => (horizontal, vertical),
186 Fit::Contain => (horizontal.min(vertical), horizontal.min(vertical)),
187 Fit::Cover => (horizontal.max(vertical), horizontal.max(vertical)),
188 Fit::None => (1.0, 1.0),
189 };
190 let width = f64::from(image.width()) * horizontal;
191 let height = f64::from(image.height()) * vertical;
192 let transform = motion_core::Transform::scale_non_uniform(horizontal, vertical)
193 .then(motion_core::Transform::translate(
194 (bounds.width() - width) * 0.5,
195 (bounds.height() - height) * 0.5,
196 ));
197 let mut pattern = motion_core::ImagePattern::from_asset(image)
198 .transform(transform)
199 .extend(motion_core::GradientExtend::Pad)
200 .sampling(self.sampling);
201 pattern.decal = true;
202 canvas.fill(&shape, pattern, motion_core::FillRule::NonZero)
203 }
204 Err(_) => canvas.fill(
205 &shape,
206 motion_core::Color::rgb(255, 0, 255),
207 motion_core::FillRule::NonZero,
208 ),
209 }
210 }
211}
212
213#[derive(Clone, Debug, PartialEq)]
215pub struct ImageLevel {
216 width: u32,
217 height: u32,
218 pixels: Arc<[[f16; 4]]>,
219}
220
221impl ImageLevel {
222 pub fn width(&self) -> u32 {
224 self.width
225 }
226
227 pub fn height(&self) -> u32 {
229 self.height
230 }
231
232 pub fn pixel(&self, column: u32, row: u32) -> Option<[f32; 4]> {
234 (column < self.width && row < self.height)
235 .then(|| self.pixels[(row * self.width + column) as usize].map(f16::to_f32))
236 }
237}
238
239#[derive(Clone, Debug, PartialEq)]
243pub struct ImageAsset {
244 levels: Arc<[ImageLevel]>,
245}
246
247impl motion_core::pixels::ImagePixels for ImageAsset {
248 fn width(&self) -> u32 {
249 self.width()
250 }
251
252 fn height(&self) -> u32 {
253 self.height()
254 }
255
256 fn level_count(&self) -> usize {
257 self.levels.len()
258 }
259
260 fn level_size(&self, level: usize) -> (u32, u32) {
261 (self.levels[level].width(), self.levels[level].height())
262 }
263
264 fn texel(&self, level: usize, column: usize, row: usize) -> [f32; 4] {
265 self.levels[level]
266 .pixel(column as u32, row as u32)
267 .expect("bounded image texel")
268 }
269
270 fn extend_level(&self, level: usize, out: &mut Vec<f32>) {
271 let pixels = &self.levels[level].pixels;
272 out.reserve(pixels.len() * 4);
273 for pixel in pixels.iter() {
274 out.extend(pixel.map(f16::to_f32));
275 }
276 }
277
278 fn content_eq(&self, other: &dyn motion_core::pixels::ImagePixels) -> bool {
279 other
280 .as_any()
281 .downcast_ref::<Self>()
282 .is_some_and(|other| self == other)
283 }
284
285 fn as_any(&self) -> &dyn std::any::Any {
286 self
287 }
288}
289
290fn invalid(message: &str) -> motion_core::Error {
291 io::Error::new(io::ErrorKind::InvalidInput, message).into()
292}
293
294impl ImageAsset {
295 #[cfg(not(feature = "image"))]
297 pub fn from_bytes(_: &[u8]) -> motion_core::Result<Self> {
298 Err(motion_core::Error::from(
299 motion_core::ValidationError::unsupported(
300 "image",
301 "codec",
302 "Image decoding requires the image feature",
303 ),
304 ))
305 }
306
307 #[cfg(feature = "image")]
309 pub fn from_bytes(bytes: &[u8]) -> motion_core::Result<Self> {
310 if bytes.len() > super::MAX_ENCODED {
311 return Err(invalid("Encoded image exceeds 64 MiB"));
312 }
313 let mut reader = ImageReader::new(Cursor::new(bytes)).with_guessed_format()?;
314 if !matches!(
315 reader.format(),
316 Some(ImageFormat::Png | ImageFormat::Jpeg | ImageFormat::WebP)
317 ) {
318 return Err(invalid("images require static PNG, JPEG or WebP data"));
319 }
320 if reader.format() == Some(ImageFormat::Png)
321 && image::codecs::png::PngDecoder::new(Cursor::new(bytes))
322 .map_err(crate::image_error)?
323 .is_apng()
324 .map_err(crate::image_error)?
325 {
326 return Err(invalid("Animated PNG requires the animated-image contract"));
327 }
328 if reader.format() == Some(ImageFormat::WebP)
329 && image::codecs::webp::WebPDecoder::new(Cursor::new(bytes))
330 .map_err(crate::image_error)?
331 .has_animation()
332 {
333 return Err(invalid(
334 "Animated WebP requires the animated-image contract",
335 ));
336 }
337 let mut limits = Limits::default();
338 limits.max_image_width = Some(16384);
339 limits.max_image_height = Some(16384);
340 limits.max_alloc = Some(256 * 1024 * 1024);
341 reader.limits(limits);
342 let mut decoder = reader.into_decoder().map_err(crate::image_error)?;
343 let (width, height) = decoder.dimensions();
344 super::pixel_count(width, height)?;
345 let orientation = decoder.orientation().map_err(crate::image_error)?;
346 let mut decoded = DynamicImage::from_decoder(decoder).map_err(crate::image_error)?;
347 decoded.apply_orientation(orientation);
348 let rgba = decoded.into_rgba32f();
349 let pixels = rgba
350 .pixels()
351 .map(|pixel| {
352 let alpha = f64::from(pixel.0[3]);
353 let rgb = [pixel.0[0], pixel.0[1], pixel.0[2]]
354 .map(|value| motion_core::color::srgb_to_linear(f64::from(value)));
355 [rgb[0], rgb[1], rgb[2], alpha]
356 })
357 .collect();
358 Self::from_rgba_float(rgba.width(), rgba.height(), pixels)
359 }
360
361 pub fn from_rgba8(width: u32, height: u32, pixels: Vec<u8>) -> motion_core::Result<Self> {
363 if super::pixel_count(width, height)? * 4 != pixels.len() {
364 return Err(invalid("RGBA8 byte count must equal width*height*4"));
365 }
366 let values = pixels
367 .as_chunks::<4>()
368 .0
369 .iter()
370 .map(|pixel| {
371 [
372 motion_core::color::srgb_to_linear(f64::from(pixel[0]) / 255.0),
373 motion_core::color::srgb_to_linear(f64::from(pixel[1]) / 255.0),
374 motion_core::color::srgb_to_linear(f64::from(pixel[2]) / 255.0),
375 f64::from(pixel[3]) / 255.0,
376 ]
377 })
378 .collect();
379 Self::from_rgba_float(width, height, values)
380 }
381
382 pub fn from_rgba_float(
386 width: u32,
387 height: u32,
388 pixels: Vec<[f64; 4]>,
389 ) -> motion_core::Result<Self> {
390 if super::pixel_count(width, height)? != pixels.len() {
391 return Err(invalid("RGBA float count must equal width*height"));
392 }
393 let mut base = Vec::with_capacity(pixels.len());
394 for pixel in pixels {
395 if !pixel[3].is_finite() || !(0.0..=1.0).contains(&pixel[3]) {
396 return Err(invalid("Image alpha must be finite in 0..=1"));
397 }
398 if pixel[3] == 0.0 {
399 base.push([f16::ZERO; 4]);
400 continue;
401 }
402 let pixel = [
403 pixel[0] * pixel[3],
404 pixel[1] * pixel[3],
405 pixel[2] * pixel[3],
406 pixel[3],
407 ];
408 if pixel
409 .iter()
410 .any(|value| !value.is_finite() || value.abs() > 65504.0)
411 {
412 return Err(invalid(
413 "Premultiplied image values must fit finite RGBA16F",
414 ));
415 }
416 let mut stored = pixel.map(f16::from_f64);
417 if stored[3] == f16::ZERO {
418 stored = [f16::ZERO; 4];
419 }
420 base.push(stored);
421 }
422 let mut levels = vec![ImageLevel {
423 width,
424 height,
425 pixels: base.into(),
426 }];
427 let mut bytes = u64::from(width) * u64::from(height) * 8;
428 while let Some(previous) = levels
429 .last()
430 .filter(|level| level.width > 1 || level.height > 1)
431 {
432 let width = previous.width.div_ceil(2);
433 let height = previous.height.div_ceil(2);
434 bytes = bytes
435 .checked_add(u64::from(width) * u64::from(height) * 8)
436 .filter(|bytes| *bytes <= 256 * 1024 * 1024)
437 .ok_or_else(|| invalid("Image mip chain exceeds 256 MiB"))?;
438 let mut pixels = Vec::with_capacity(width as usize * height as usize);
439 for row in 0..height {
440 for column in 0..width {
441 let left = f64::from(column) * f64::from(previous.width) / f64::from(width);
442 let right =
443 f64::from(column + 1) * f64::from(previous.width) / f64::from(width);
444 let top = f64::from(row) * f64::from(previous.height) / f64::from(height);
445 let bottom =
446 f64::from(row + 1) * f64::from(previous.height) / f64::from(height);
447 let mut sum = [0.0_f64; 4];
448 let mut count = 0.0;
449 for vertical in
450 libm::floor(top) as u32..(libm::ceil(bottom) as u32).min(previous.height)
451 {
452 for horizontal in
453 libm::floor(left) as u32..(libm::ceil(right) as u32).min(previous.width)
454 {
455 let pixel = previous
456 .pixel(horizontal, vertical)
457 .expect("mip source texel in bounds");
458 let weight = (right.min(f64::from(horizontal + 1))
459 - left.max(f64::from(horizontal)))
460 * (bottom.min(f64::from(vertical + 1))
461 - top.max(f64::from(vertical)));
462 for channel in 0..4 {
463 sum[channel] += f64::from(pixel[channel]) * weight;
464 }
465 count += weight;
466 }
467 }
468 let mut stored = sum.map(|value| f16::from_f64(value / count));
469 if stored[3] == f16::ZERO {
470 stored = [f16::ZERO; 4];
471 }
472 pixels.push(stored);
473 }
474 }
475 levels.push(ImageLevel {
476 width,
477 height,
478 pixels: pixels.into(),
479 });
480 }
481 Ok(Self {
482 levels: levels.into(),
483 })
484 }
485
486 pub fn width(&self) -> u32 {
488 self.levels[0].width
489 }
490
491 pub fn height(&self) -> u32 {
493 self.levels[0].height
494 }
495
496 pub fn levels(&self) -> &[ImageLevel] {
498 &self.levels
499 }
500
501 pub fn retained_bytes(&self) -> usize {
503 self.levels.iter().map(|level| level.pixels.len() * 8).sum()
504 }
505}
506
507impl motion_core::Asset for ImageAsset {
508 const KIND: &'static str = "image";
509
510 fn decode(bytes: &[u8], _: &motion_core::LoadContext) -> motion_core::Result<Self> {
511 Self::from_bytes(bytes)
512 }
513
514 fn retained_bytes(&self) -> usize {
515 self.retained_bytes()
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 #[cfg(feature = "image")]
523 use crate::asset::AssetsExt;
524 use motion_render::RenderPrepare;
525
526 #[test]
527 #[cfg(feature = "gpu")]
528 #[ignore = "requires native GPU; no font discovery, encoder or window"]
529 fn images_match_gpu_for_all_fit_sampling_and_fractional_mips() -> motion_core::Result {
530 use motion_core::{Node, NodeModifiers, PixelSize, RasterLimits, Size, ValidationLimits};
531 let limits = RasterLimits::default();
532 let mut gpu = motion_render::gpu::Gpu::new()?;
533 let info = gpu.info().clone();
534 let pixels = (0..9)
535 .flat_map(|row| {
536 (0..17).map(move |column| {
537 [
538 column as f64 / 16.0,
539 row as f64 / 8.0,
540 if column % 2 == 0 { 1.5 } else { 0.1 },
541 if row % 3 == 0 { 0.0 } else { 0.8 },
542 ]
543 })
544 })
545 .collect();
546 let asset = Arc::new(ImageAsset::from_rgba_float(17, 9, pixels)?);
547 let mut maximum = 0.0_f32;
548 let mut cases = 0;
549 for size in [2.3, 5.5, 12.0] {
550 for fit in [Fit::Fill, Fit::Contain, Fit::Cover, Fit::None] {
551 for sampling in [Sampling::Nearest, Sampling::Bilinear, Sampling::Trilinear] {
552 let image = Image::from_asset(asset.clone())
553 .size(size, size * 0.8)
554 .fit(fit)
555 .corner_radius(size * 0.15)
556 .sampling(sampling);
557 let prepared = Node::from(image)
558 .rotate(0.13)
559 .translate(1.1, 2.1)
560 .compile(
561 Size::new(16.0, 16.0),
562 PixelSize::new(16, 16),
563 ValidationLimits::default(),
564 )?
565 .prepare(ValidationLimits::default(), limits)?;
566 let cpu = prepared.render_linear(limits)?;
567 let output = gpu.render_linear(&prepared, limits)?;
568 for row in 0..16 {
569 for column in 0..16 {
570 for (first, second) in cpu
571 .linear_pixel(column, row)
572 .unwrap()
573 .into_iter()
574 .zip(output.linear_pixel(column, row).unwrap())
575 {
576 maximum = maximum.max((first - second).abs());
577 }
578 }
579 }
580 let cpu = cpu.to_format(
581 motion_render::PixelFormat::SrgbRgba8 {
582 premultiplied: false,
583 },
584 limits,
585 )?;
586 let output = output.to_format(
587 motion_render::PixelFormat::SrgbRgba8 {
588 premultiplied: false,
589 },
590 limits,
591 )?;
592 for (first, second) in cpu.bytes().iter().zip(output.bytes()) {
593 assert!(first.abs_diff(*second) <= 1);
594 }
595 let replay = gpu.render_linear(&prepared, limits)?.to_format(
596 motion_render::PixelFormat::SrgbRgba8 {
597 premultiplied: false,
598 },
599 limits,
600 )?;
601 assert_eq!(output, replay);
602 cases += 1;
603 }
604 }
605 }
606 assert!(maximum <= 5e-6, "image linear difference {maximum}");
607 eprintln!(
608 "_image_gpu adapter={} backend={:?} cases={cases} source=RGBA16F odd_mips=shared max_linear_difference={maximum} rgba_tolerance=1 replay=exact",
609 info.name, info.backend
610 );
611 Ok(())
612 }
613
614 #[test]
615 #[cfg(feature = "image")]
616 fn decoded_image_handles_survive_source_file_removal() -> motion_core::Result {
617 use image::ImageEncoder;
618 let directory = tempfile::tempdir()?;
619 let path = directory.path().join("pixel.png");
620 let mut encoded = Vec::new();
621 image::codecs::png::PngEncoder::new(&mut encoded)
622 .write_image(&[255, 0, 0, 255], 1, 1, image::ExtendedColorType::Rgba8)
623 .map_err(crate::image_error)?;
624 std::fs::write(&path, encoded)?;
625 let assets = motion_core::Assets::new(directory.path())?;
626 let handle = assets.image("pixel.png");
627 let loaded = handle.get()?;
628 std::fs::remove_file(path)?;
629 assert!(Arc::ptr_eq(&loaded, &handle.get()?));
630 assert_eq!(
631 render(Image::new(handle).size(1.0, 1.0), 1)?.linear_pixel(0, 0),
632 Some([1.0, 0.0, 0.0, 1.0])
633 );
634 Ok(())
635 }
636
637 fn render(image: Image, size: u32) -> motion_core::Result<motion_render::FrameBuffer> {
638 Ok(motion_core::Node::from(image)
639 .compile(
640 motion_core::Size::new(f64::from(size), f64::from(size)),
641 motion_core::PixelSize::new(size, size),
642 motion_core::ValidationLimits::default(),
643 )?
644 .prepare(
645 motion_core::ValidationLimits::default(),
646 motion_core::RasterLimits::default(),
647 )?
648 .render_linear(motion_core::RasterLimits::default())?)
649 }
650
651 #[test]
652 #[cfg(feature = "image")]
653 fn static_decoders_preserve_png_precision_webp_and_exif_orientation() -> motion_core::Result {
654 use image::ImageEncoder;
655 let mut png = Cursor::new(Vec::new());
656 let image = image::ImageBuffer::from_vec(1, 1, vec![32768_u16, 0, 0, 65535]).unwrap();
657 DynamicImage::ImageRgba16(image)
658 .write_to(&mut png, ImageFormat::Png)
659 .map_err(crate::image_error)?;
660 let decoded = ImageAsset::from_bytes(png.get_ref())?;
661 let expected = motion_core::color::srgb_to_linear(32768.0 / 65535.0);
662 assert!((f64::from(decoded.levels()[0].pixel(0, 0).unwrap()[0]) - expected).abs() < 0.0002);
663 let exif = b"II\x2a\0\x08\0\0\0\x01\0\x12\x01\x03\0\x01\0\0\0\x06\0\0\0\0\0\0\0".to_vec();
664 let mut webp = Vec::new();
665 let mut encoder = image::codecs::webp::WebPEncoder::new_lossless(&mut webp);
666 encoder
667 .set_exif_metadata(exif.clone())
668 .map_err(io::Error::other)?;
669 encoder
670 .encode(
671 &[255, 0, 0, 255, 0, 255, 0, 255],
672 2,
673 1,
674 image::ExtendedColorType::Rgba8,
675 )
676 .map_err(crate::image_error)?;
677 let decoded = ImageAsset::from_bytes(&webp)?;
678 assert_eq!((decoded.width(), decoded.height()), (1, 2));
679 assert_eq!(decoded.levels()[0].pixel(0, 0), Some([1.0, 0.0, 0.0, 1.0]));
680 assert_eq!(decoded.levels()[0].pixel(0, 1), Some([0.0, 1.0, 0.0, 1.0]));
681 let mut jpeg = Vec::new();
682 let mut encoder = image::codecs::jpeg::JpegEncoder::new(&mut jpeg);
683 encoder.set_exif_metadata(exif).map_err(io::Error::other)?;
684 encoder
685 .encode(
686 &[
687 255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255,
688 ],
689 3,
690 2,
691 image::ExtendedColorType::Rgb8,
692 )
693 .map_err(crate::image_error)?;
694 let decoded = ImageAsset::from_bytes(&jpeg)?;
695 assert_eq!((decoded.width(), decoded.height()), (2, 3));
696 Ok(())
697 }
698
699 #[test]
700 fn image_fit_rounding_and_mip_minification_have_independent_pixels() -> motion_core::Result {
701 let asset = Arc::new(ImageAsset::from_rgba8(
702 3,
703 1,
704 vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255],
705 )?);
706 let image = Image::from_asset(asset)
707 .size(8.0, 8.0)
708 .sampling(Sampling::Nearest);
709 let contain = render(image.clone().fit(Fit::Contain), 8)?;
710 assert_eq!(contain.linear_pixel(4, 0), Some([0.0; 4]));
711 assert_eq!(contain.linear_pixel(4, 3), Some([0.0, 1.0, 0.0, 1.0]));
712 let cover = render(image.clone().fit(Fit::Cover), 8)?;
713 assert_eq!(cover.linear_pixel(0, 0), Some([0.0, 1.0, 0.0, 1.0]));
714 let fill = render(image.clone().fit(Fit::Fill), 8)?;
715 assert_eq!(fill.linear_pixel(0, 0), Some([1.0, 0.0, 0.0, 1.0]));
716 let original = render(image.clone().fit(Fit::None), 8)?;
717 assert_eq!(original.linear_pixel(0, 0), Some([0.0; 4]));
718 assert_eq!(original.linear_pixel(3, 3), Some([0.0, 1.0, 0.0, 1.0]));
719 let rounded = render(image.fit(Fit::Fill).corner_radius(4.0), 8)?;
720 assert_eq!(rounded.linear_pixel(0, 0), Some([0.0; 4]));
721 assert!(rounded.linear_pixel(4, 4).unwrap()[3] > 0.9);
722 let mut pixels = vec![[0.0, 0.0, 0.0, 1.0]; 64];
723 pixels[0] = [1.0; 4];
724 let asset = Arc::new(ImageAsset::from_rgba_float(8, 8, pixels)?);
725 let nearest = render(
726 Image::from_asset(asset.clone())
727 .size(1.0, 1.0)
728 .sampling(Sampling::Nearest),
729 1,
730 )?;
731 let trilinear = render(
732 Image::from_asset(asset)
733 .size(1.0, 1.0)
734 .sampling(Sampling::Trilinear),
735 1,
736 )?;
737 assert_eq!(nearest.linear_pixel(0, 0).unwrap()[0], 0.0);
738 assert_eq!(trilinear.linear_pixel(0, 0).unwrap()[0], 1.0 / 64.0);
739 Ok(())
740 }
741
742 #[test]
743 fn half_images_build_linear_premultiplied_odd_mips_without_hidden_color() -> motion_core::Result
744 {
745 let image = ImageAsset::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 0])?;
746 assert_eq!(image.levels()[1].pixel(0, 0), Some([0.5, 0.0, 0.0, 0.5]));
747 assert_eq!(image.retained_bytes(), 24);
748 let odd = ImageAsset::from_rgba8(3, 1, vec![255; 12])?;
749 assert_eq!(
750 odd.levels()
751 .iter()
752 .map(|level| (level.width(), level.height()))
753 .collect::<Vec<_>>(),
754 [(3, 1), (2, 1), (1, 1)]
755 );
756 assert_eq!(odd.levels().last().unwrap().pixel(0, 0), Some([1.0; 4]));
757 let weighted =
758 ImageAsset::from_rgba8(3, 1, vec![255, 0, 0, 255, 255, 0, 0, 255, 0, 0, 255, 255])?;
759 let final_pixel = weighted.levels().last().unwrap().pixel(0, 0).unwrap();
760 assert!((final_pixel[0] - 2.0 / 3.0).abs() < 0.0005);
761 assert!((final_pixel[2] - 1.0 / 3.0).abs() < 0.0005);
762 let transparent =
763 ImageAsset::from_rgba_float(1, 1, vec![[f64::NAN, f64::INFINITY, 1.0, 0.0]])?;
764 assert_eq!(transparent.levels()[0].pixel(0, 0), Some([0.0; 4]));
765 assert!(ImageAsset::from_rgba_float(1, 1, vec![[1e9, 0.0, 0.0, 1.0]]).is_err());
766 Ok(())
767 }
768}