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