1use std::borrow::Cow;
2use std::path::Path;
3
4use crate::car::{CSIItem, CarError, CarResult};
5#[cfg(feature = "image")]
6use crate::decode::{DecodeOptions, DecodeStage, OrientationPolicy, RenditionData};
7#[cfg(feature = "image")]
8use crate::metadata::ExifOrientation;
9use crate::model::Encoding;
10use crate::model::rendition::{CompressionType, Rendition};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct RgbaPayload {
14 pub width: u32,
15 pub height: u32,
16 pub rgba: Vec<u8>,
17}
18
19#[cfg(all(test, feature = "image", target_arch = "wasm32"))]
20mod wasm_image_tests {
21 use std::io::Cursor;
22
23 use image::ImageDecoder;
24 use wasm_bindgen_test::wasm_bindgen_test;
25
26 use crate::Car;
27
28 const ASSETS_CAR_BYTES: &[u8] = include_bytes!(concat!(
29 env!("CARGO_MANIFEST_DIR"),
30 "/../car-tests/data/Assets.car"
31 ));
32
33 #[wasm_bindgen_test]
34 fn png_bytes_omit_host_icc_profiles_on_wasm() {
35 let car = Car::from_bytes(ASSETS_CAR_BYTES.to_vec()).expect("load archive from bytes");
36 let item = car
37 .rendtions_with_name("2016_coin1")
38 .and_then(|items| items.first())
39 .expect("missing 2016_coin1 rendition");
40
41 let png = item.png_bytes().expect("encode png bytes");
42 let mut decoder =
43 image::codecs::png::PngDecoder::new(Cursor::new(png)).expect("decode png bytes");
44
45 assert!(
46 decoder
47 .icc_profile()
48 .expect("reading ICC metadata should succeed")
49 .is_none(),
50 "wasm path should not read host ICC profile files"
51 );
52 }
53}
54
55#[cfg(feature = "image")]
56fn resolve_bytes_per_row(item: &CSIItem, bpp: u32) -> CarResult<u32> {
57 let min_row = item.header.width * bpp;
58 if let Some(bytes_per_row) = item.bytes_per_row_tlv() {
59 if bytes_per_row < min_row {
60 return Err(CarError::DecodeFailed(
61 "bytes_per_row smaller than minimum row size".to_string(),
62 ));
63 }
64 return Ok(bytes_per_row);
65 }
66 Ok(min_row)
67}
68
69#[cfg(feature = "image")]
70fn strip_stride(data: Vec<u8>, width: u32, height: u32, bpp: u32, stride: u32) -> Vec<u8> {
71 let row_bytes = (width * bpp) as usize;
72 let stride = stride as usize;
73 if row_bytes == stride {
74 return data;
75 }
76 let mut out = Vec::with_capacity(row_bytes * height as usize);
77 for row in 0..height as usize {
78 let start = row * stride;
79 let end = start + row_bytes;
80 if end <= data.len() {
81 out.extend_from_slice(&data[start..end]);
82 }
83 }
84 out
85}
86
87#[cfg(feature = "image")]
88fn is_deepmap2(item: &CSIItem) -> bool {
89 matches!(
90 &item.header.rendition,
91 Some(Rendition::ThemeCBCK(cbck)) if matches!(cbck.compression_type, CompressionType::Deepmap2)
92 )
93}
94
95pub(crate) fn is_hevc_compressed(item: &CSIItem) -> bool {
96 matches!(
97 &item.header.rendition,
98 Some(Rendition::ThemeCBCK(cbck)) if matches!(cbck.compression_type, CompressionType::HEVC)
99 )
100}
101
102fn hevc_source_bytes(item: &CSIItem) -> CarResult<Option<Cow<'_, [u8]>>> {
103 let Some(Rendition::ThemeCBCK(cbck)) = &item.header.rendition else {
104 return Ok(None);
105 };
106 if !matches!(cbck.compression_type, CompressionType::HEVC) {
107 return Ok(None);
108 }
109 if cbck.chunks.len() != 1 {
110 return Err(CarError::DecodeFailed(format!(
111 "HEVC raw export expects a single chunk, got {}",
112 cbck.chunks.len()
113 )));
114 }
115
116 heic_container_from_hevc_chunk(cbck.chunks[0].raw_data.as_slice())
117 .map(|data| Some(Cow::Borrowed(data)))
118}
119
120fn heic_container_from_hevc_chunk(raw: &[u8]) -> CarResult<&[u8]> {
121 const WRAPPER_LEN: usize = 8;
122 const MIN_FTYP_LEN: usize = 16;
123
124 if raw.len() < WRAPPER_LEN + MIN_FTYP_LEN {
125 return Err(CarError::DecodeFailed(
126 "HEVC chunk too short to contain HEIC payload".to_string(),
127 ));
128 }
129
130 let declared_len = u32::from_le_bytes(raw[4..8].try_into().unwrap()) as usize;
131 let body_len = raw.len() - WRAPPER_LEN;
132 if declared_len != body_len {
133 return Err(CarError::DecodeFailed(format!(
134 "HEVC chunk length mismatch: declared {}, got {}",
135 declared_len, body_len
136 )));
137 }
138
139 let body = &raw[WRAPPER_LEN..];
140 let ftyp_len = u32::from_be_bytes(body[0..4].try_into().unwrap()) as usize;
141 if ftyp_len < MIN_FTYP_LEN || ftyp_len > body.len() {
142 return Err(CarError::DecodeFailed(format!(
143 "HEVC chunk has invalid ftyp box length: {ftyp_len}"
144 )));
145 }
146 if &body[4..8] != b"ftyp" {
147 return Err(CarError::DecodeFailed(
148 "HEVC chunk does not start with HEIC ftyp box".to_string(),
149 ));
150 }
151 if !ftyp_has_heif_brand(&body[..ftyp_len]) {
152 return Err(CarError::DecodeFailed(
153 "HEVC chunk ftyp box does not advertise HEIC/HEIF brand".to_string(),
154 ));
155 }
156
157 Ok(body)
158}
159
160fn ftyp_has_heif_brand(ftyp_box: &[u8]) -> bool {
161 is_heif_brand(&ftyp_box[8..12]) || ftyp_box[16..].chunks_exact(4).any(is_heif_brand)
162}
163
164fn is_heif_brand(brand: &[u8]) -> bool {
165 matches!(
166 brand,
167 b"heic"
168 | b"heix"
169 | b"hevc"
170 | b"hevx"
171 | b"heim"
172 | b"heis"
173 | b"mif1"
174 | b"msf1"
175 | b"MiHE"
176 | b"MiHB"
177 )
178}
179
180#[cfg(feature = "image")]
181fn unpremultiply_rgba8_in_place(pixels: &mut [u8]) {
182 for px in pixels.chunks_exact_mut(4) {
183 let alpha = px[3];
184 if alpha == 0 || alpha == u8::MAX {
185 continue;
186 }
187
188 for channel in &mut px[..3] {
189 let value = (((*channel as u32) * u8::MAX as u32) + (alpha as u32 / 2)) / alpha as u32;
190 *channel = value.min(u8::MAX as u32) as u8;
191 }
192 }
193}
194
195#[cfg(feature = "image")]
196fn unpremultiply_rgba16_in_place(pixels: &mut [u16]) {
197 for px in pixels.chunks_exact_mut(4) {
198 let alpha = px[3];
199 if alpha == 0 || alpha == u16::MAX {
200 continue;
201 }
202
203 for channel in &mut px[..3] {
204 let value = (((*channel as u64) * u16::MAX as u64) + (alpha as u64 / 2)) / alpha as u64;
205 *channel = value.min(u16::MAX as u64) as u16;
206 }
207 }
208}
209
210#[cfg(feature = "image")]
216fn apply_reference_frame(
217 img: image::DynamicImage,
218 rect: &super::ReferenceRect,
219) -> CarResult<image::DynamicImage> {
220 let img_width = img.width();
221 let img_height = img.height();
222 let right = rect.x.checked_add(rect.width);
223 let top = rect.y.checked_add(rect.height);
224 match (right, top) {
225 (Some(right), Some(top)) if right <= img_width && top <= img_height => {
226 let crop_y = img_height - top;
227 Ok(img.crop_imm(rect.x, crop_y, rect.width, rect.height))
228 }
229 _ => Err(CarError::DecodeFailed(format!(
230 "reference crop rect (x={}, y={}, w={}, h={}) exceeds source image {}x{}",
231 rect.x, rect.y, rect.width, rect.height, img_width, img_height
232 ))),
233 }
234}
235
236#[cfg(feature = "image")]
237#[cfg(not(target_arch = "wasm32"))]
238fn png_icc_profile(item: &CSIItem) -> Option<Vec<u8>> {
239 use crate::model::{ColorModel, rendition::AttributeType};
240 use std::sync::OnceLock;
241
242 static SRGB_PROFILE: OnceLock<Option<Vec<u8>>> = OnceLock::new();
243 static P3_PROFILE: OnceLock<Option<Vec<u8>>> = OnceLock::new();
244
245 let is_display_p3 = item.header.color_model == ColorModel::RGBP3
246 || item
247 .attrs
248 .iter()
249 .any(|attr| attr.name == AttributeType::DisplayGamut && attr.val == 1);
250
251 if is_display_p3 {
252 P3_PROFILE
253 .get_or_init(|| std::fs::read("/System/Library/ColorSync/Profiles/Display P3.icc").ok())
254 .clone()
255 } else {
256 SRGB_PROFILE
257 .get_or_init(|| {
258 std::fs::read("/System/Library/ColorSync/Profiles/sRGB Profile.icc").ok()
259 })
260 .clone()
261 }
262}
263
264#[cfg(feature = "image")]
265#[cfg(target_arch = "wasm32")]
266fn png_icc_profile(_item: &CSIItem) -> Option<Vec<u8>> {
267 None
268}
269
270#[cfg(feature = "image")]
271fn encode_png_with_icc<W>(item: &CSIItem, img: &image::DynamicImage, writer: W) -> CarResult<()>
272where
273 W: std::io::Write,
274{
275 use image::ImageEncoder;
276 let mut encoder = image::codecs::png::PngEncoder::new(writer);
277 if let Some(profile) = png_icc_profile(item) {
278 let _ = encoder.set_icc_profile(profile);
279 }
280 encoder.write_image(
281 img.as_bytes(),
282 img.width(),
283 img.height(),
284 img.color().into(),
285 )?;
286 Ok(())
287}
288
289#[cfg(feature = "image")]
290fn write_png_with_icc(item: &CSIItem, img: &image::DynamicImage, path: &Path) -> CarResult<()> {
291 let file = std::fs::File::create(path)?;
292 let writer = std::io::BufWriter::new(file);
293 encode_png_with_icc(item, img, writer)
294}
295
296#[cfg(feature = "image")]
297fn render_image_with_crops(
298 item: &CSIItem,
299 crops: &[super::ReferenceRect],
300) -> CarResult<image::DynamicImage> {
301 item.to_image_with_crops_and_options(crops, &DecodeOptions::default())
302}
303
304#[cfg(feature = "image")]
305fn apply_exif_orientation(
306 item: &CSIItem,
307 options: &DecodeOptions,
308 img: image::DynamicImage,
309) -> image::DynamicImage {
310 if !matches!(options.orientation_policy, OrientationPolicy::ApplyExif) {
311 return img;
312 }
313
314 match item.exif_orientation() {
315 Some(ExifOrientation::Mirrored) => img.fliph(),
316 Some(ExifOrientation::Rotated180) => img.rotate180(),
317 Some(ExifOrientation::Rotated180Mirrored) => img.rotate180().fliph(),
318 Some(ExifOrientation::Rotated90) => img.rotate90(),
319 Some(ExifOrientation::Rotated90Mirrored) => img.rotate90().fliph(),
320 Some(ExifOrientation::Rotated270) => img.rotate270(),
321 Some(ExifOrientation::Rotated270Mirrored) => img.rotate270().fliph(),
322 Some(ExifOrientation::None | ExifOrientation::Normal | ExifOrientation::Unknown(_))
323 | None => img,
324 }
325}
326
327#[cfg(feature = "image")]
328fn encode_png_bytes(item: &CSIItem, img: &image::DynamicImage) -> CarResult<Vec<u8>> {
329 let mut buf = Vec::new();
330 encode_png_with_icc(item, img, &mut buf)?;
331 Ok(buf)
332}
333
334#[cfg(feature = "image")]
335fn is_png_path(path: &Path) -> bool {
336 path.extension()
337 .and_then(|ext| ext.to_str())
338 .is_some_and(|ext| ext.eq_ignore_ascii_case("png"))
339}
340
341#[cfg(feature = "image")]
347fn save_image_to_png_direct(item: &CSIItem, path: &Path) -> CarResult<bool> {
348 use image::{ExtendedColorType, ImageEncoder};
349
350 let is_deepmap2 = is_deepmap2(item);
353 if !is_deepmap2 {
354 match item.header.encoding {
355 Encoding::ARGB | Encoding::GA8 | Encoding::ARGB16 => {}
356 _ => return Ok(false),
357 }
358 }
359
360 let w = item.header.width;
361 let h = item.header.height;
362
363 let data = match item.decode_data()? {
364 Some(RenditionData::Image { data }) => data,
365 _ => return Ok(false),
366 };
367
368 let (pixels, color_type, out_w, out_h): (Vec<u8>, ExtendedColorType, u32, u32) = if is_deepmap2
370 {
371 let row_bytes = (w as usize).saturating_mul(4);
373 if row_bytes == 0 {
374 return Ok(false);
375 }
376 let mut pixels = if data.len() == row_bytes * h as usize {
377 data
378 } else {
379 let stride = resolve_bytes_per_row(item, 4).unwrap_or(w * 4);
380 strip_stride(data, w, h, 4, stride)
381 };
382 if pixels.len() % row_bytes != 0 {
383 return Err(CarError::DecodeFailed(format!(
384 "RGBA buffer size mismatch: expected a multiple of {row_bytes} bytes, got {}",
385 pixels.len()
386 )));
387 }
388 unpremultiply_rgba8_in_place(&mut pixels);
391 let inferred_h = (pixels.len() / row_bytes) as u32;
392 (pixels, ExtendedColorType::Rgba8, w, inferred_h)
393 } else {
394 match item.header.encoding {
395 Encoding::ARGB => {
396 let stride = resolve_bytes_per_row(item, 4)? as usize;
398 let row_bytes = (w * 4) as usize;
399 let mut pixels = Vec::with_capacity(row_bytes * h as usize);
400 for row in 0..h as usize {
401 let start = row * stride;
402 let end = start + row_bytes;
403 if end > data.len() {
404 break;
405 }
406 for chunk in data[start..end].chunks_exact(4) {
407 pixels.push(chunk[2]); pixels.push(chunk[1]); pixels.push(chunk[0]); pixels.push(chunk[3]); }
412 }
413 let expected = row_bytes * h as usize;
414 if pixels.len() < expected {
415 return Err(CarError::DecodeFailed(format!(
416 "ARGB pixel buffer truncated: expected {expected} bytes, got {}",
417 pixels.len()
418 )));
419 }
420 unpremultiply_rgba8_in_place(&mut pixels);
421 (pixels, ExtendedColorType::Rgba8, w, h)
422 }
423 Encoding::GA8 => {
424 let stride = resolve_bytes_per_row(item, 2)?;
425 let pixels = strip_stride(data, w, h, 2, stride);
426 let expected = w as usize * h as usize * 2;
427 if pixels.len() < expected {
428 return Err(CarError::DecodeFailed(format!(
429 "GA8 pixel buffer truncated: expected {expected} bytes, got {}",
430 pixels.len()
431 )));
432 }
433 (pixels, ExtendedColorType::La8, w, h)
434 }
435 Encoding::ARGB16 => {
436 let stride = resolve_bytes_per_row(item, 8)? as usize;
439 let row_bytes = (w * 8) as usize; let mut pixels: Vec<u8> = Vec::with_capacity(w as usize * h as usize * 8);
441 for row in 0..h as usize {
442 let row_start = row * stride;
443 if row_start + row_bytes > data.len() {
444 break;
445 }
446 for px in data[row_start..row_start + row_bytes].chunks_exact(8) {
447 let r = u16::from_le_bytes([px[4], px[5]]);
449 let g = u16::from_le_bytes([px[2], px[3]]);
450 let b = u16::from_le_bytes([px[0], px[1]]);
451 let a = u16::from_le_bytes([px[6], px[7]]);
452 let (r, g, b) = if a == 0 || a == u16::MAX {
453 (r, g, b)
454 } else {
455 let up = |c: u16| -> u16 {
456 ((c as u64 * u16::MAX as u64 + a as u64 / 2) / a as u64)
457 .min(u16::MAX as u64) as u16
458 };
459 (up(r), up(g), up(b))
460 };
461 pixels.extend_from_slice(&r.to_ne_bytes());
462 pixels.extend_from_slice(&g.to_ne_bytes());
463 pixels.extend_from_slice(&b.to_ne_bytes());
464 pixels.extend_from_slice(&a.to_ne_bytes());
465 }
466 }
467 let expected = w as usize * h as usize * 8;
468 if pixels.len() < expected {
469 return Err(CarError::DecodeFailed(format!(
470 "ARGB16 pixel buffer truncated: expected {expected} bytes, got {}",
471 pixels.len()
472 )));
473 }
474 (pixels, ExtendedColorType::Rgba16, w, h)
475 }
476 _ => return Ok(false),
477 }
478 };
479
480 let file = std::fs::File::create(path)?;
481 let writer = std::io::BufWriter::new(file);
482 let mut encoder = image::codecs::png::PngEncoder::new(writer);
483 if let Some(profile) = png_icc_profile(item) {
484 let _ = encoder.set_icc_profile(profile);
485 }
486 encoder.write_image(&pixels, out_w, out_h, color_type)?;
487 Ok(true)
488}
489
490#[cfg(feature = "image")]
491impl CSIItem {
492 pub fn to_image(&self) -> CarResult<image::DynamicImage> {
493 self.to_image_with_options(&DecodeOptions::default())
494 }
495
496 pub fn to_image_with_options(&self, options: &DecodeOptions) -> CarResult<image::DynamicImage> {
497 self.to_image_with_crops_and_options(&[], options)
498 }
499
500 pub fn to_image_with_crops_and_options(
505 &self,
506 crops: &[super::ReferenceRect],
507 options: &DecodeOptions,
508 ) -> CarResult<image::DynamicImage> {
509 let source_options = DecodeOptions {
510 orientation_policy: OrientationPolicy::Ignore,
511 ..options.clone()
512 };
513 let img = self.to_image_unoriented(&source_options)?;
514 options.check_output_bytes(img.as_bytes().len(), DecodeStage::ImageConversion)?;
515 let mut img = img;
516 for rect in crops {
517 img = apply_reference_frame(img, rect)?;
518 }
519 Ok(apply_exif_orientation(self, options, img))
520 }
521
522 fn to_image_unoriented(&self, options: &DecodeOptions) -> CarResult<image::DynamicImage> {
523 use image::{DynamicImage, ImageBuffer};
524
525 let data = match self.decode_data_with_options(options)? {
526 Some(RenditionData::Image { data }) => data,
527 _ => {
528 return Err(CarError::UnsupportedEncoding(self.header.encoding));
529 }
530 };
531
532 let w = self.header.width;
533 let h = self.header.height;
534
535 if is_deepmap2(self) {
537 let row_bytes = (w as usize).saturating_mul(4);
538 let header_len = row_bytes.saturating_mul(h as usize);
539 let mut pixels = if data.len() == header_len {
540 data
541 } else {
542 let stride = resolve_bytes_per_row(self, 4).unwrap_or(w * 4);
547 strip_stride(data, w, h, 4, stride)
548 };
549 if row_bytes == 0 || pixels.len() % row_bytes != 0 {
550 return Err(CarError::DecodeFailed(format!(
551 "RGBA buffer size mismatch: expected a multiple of {row_bytes} bytes, got {}",
552 pixels.len()
553 )));
554 }
555 unpremultiply_rgba8_in_place(&mut pixels);
556 let inferred_h = (pixels.len() / row_bytes) as u32;
557 let buf = ImageBuffer::from_raw(w, inferred_h, pixels)
558 .ok_or_else(|| CarError::DecodeFailed("RGBA buffer size mismatch".into()))?;
559 return Ok(DynamicImage::ImageRgba8(buf));
560 }
561
562 match self.header.encoding {
563 Encoding::ARGB => {
564 let stride = resolve_bytes_per_row(self, 4)?;
565 let mut pixels = strip_stride(data, w, h, 4, stride);
566 for chunk in pixels.chunks_exact_mut(4) {
568 chunk.swap(0, 2);
569 }
570 unpremultiply_rgba8_in_place(&mut pixels);
571 let buf = ImageBuffer::from_raw(w, h, pixels)
572 .ok_or_else(|| CarError::DecodeFailed("ARGB buffer size mismatch".into()))?;
573 Ok(DynamicImage::ImageRgba8(buf))
574 }
575 Encoding::GRAY => {
576 let stride = resolve_bytes_per_row(self, 1)?;
577 let pixels = strip_stride(data, w, h, 1, stride);
578 let buf = ImageBuffer::from_raw(w, h, pixels)
579 .ok_or_else(|| CarError::DecodeFailed("GRAY buffer size mismatch".into()))?;
580 Ok(DynamicImage::ImageLuma8(buf))
581 }
582 Encoding::GA8 => {
583 let stride = resolve_bytes_per_row(self, 2)?;
584 let pixels = strip_stride(data, w, h, 2, stride);
585 let buf = ImageBuffer::from_raw(w, h, pixels)
586 .ok_or_else(|| CarError::DecodeFailed("GA8 buffer size mismatch".into()))?;
587 Ok(DynamicImage::ImageLumaA8(buf))
588 }
589 Encoding::ARGB16 => {
590 let stride = resolve_bytes_per_row(self, 8)?;
591 let stride = stride as usize;
592 let row_bytes = (w * 8) as usize; let mut pixels: Vec<u16> = Vec::with_capacity((w * h) as usize * 4);
594 for row in 0..h as usize {
595 let row_start = row * stride;
596 if row_start + row_bytes > data.len() {
597 break;
598 }
599 for px in data[row_start..row_start + row_bytes].chunks_exact(8) {
601 let b = u16::from_le_bytes([px[0], px[1]]);
602 let g = u16::from_le_bytes([px[2], px[3]]);
603 let r = u16::from_le_bytes([px[4], px[5]]);
604 let a = u16::from_le_bytes([px[6], px[7]]);
605 pixels.push(r);
606 pixels.push(g);
607 pixels.push(b);
608 pixels.push(a);
609 }
610 }
611 unpremultiply_rgba16_in_place(&mut pixels);
612 let buf = ImageBuffer::from_raw(w, h, pixels)
613 .ok_or_else(|| CarError::DecodeFailed("ARGB16 buffer size mismatch".into()))?;
614 Ok(DynamicImage::ImageRgba16(buf))
615 }
616 Encoding::GA16 => {
617 let stride = resolve_bytes_per_row(self, 4)?;
618 let stride = stride as usize;
619 let row_bytes = (w * 4) as usize; let mut pixels: Vec<u16> = Vec::with_capacity((w * h) as usize * 2);
621 for row in 0..h as usize {
622 let row_start = row * stride;
623 if row_start + row_bytes > data.len() {
624 break;
625 }
626 for px in data[row_start..row_start + row_bytes].chunks_exact(4) {
627 pixels.push(u16::from_le_bytes([px[0], px[1]]));
628 pixels.push(u16::from_le_bytes([px[2], px[3]]));
629 }
630 }
631 let buf = ImageBuffer::from_raw(w, h, pixels)
632 .ok_or_else(|| CarError::DecodeFailed("GA16 buffer size mismatch".into()))?;
633 Ok(DynamicImage::ImageLumaA16(buf))
634 }
635 Encoding::RGB5 => {
636 let stride = resolve_bytes_per_row(self, 2)?;
637 let stripped = strip_stride(data, w, h, 2, stride);
638 let mut pixels = Vec::with_capacity((w * h * 3) as usize);
640 for pair in stripped.chunks_exact(2) {
641 let val = u16::from_le_bytes([pair[0], pair[1]]);
642 let r5 = ((val >> 10) & 0x1F) as u8;
643 let g5 = ((val >> 5) & 0x1F) as u8;
644 let b5 = (val & 0x1F) as u8;
645 pixels.push((r5 << 3) | (r5 >> 2));
646 pixels.push((g5 << 3) | (g5 >> 2));
647 pixels.push((b5 << 3) | (b5 >> 2));
648 }
649 let buf = ImageBuffer::from_raw(w, h, pixels)
650 .ok_or_else(|| CarError::DecodeFailed("RGB5 buffer size mismatch".into()))?;
651 Ok(DynamicImage::ImageRgb8(buf))
652 }
653 Encoding::JPEG | Encoding::WEBP => Ok(image::load_from_memory(&data)?),
654 _ => Err(CarError::UnsupportedEncoding(self.header.encoding)),
655 }
656 }
657
658 pub fn rgba_bytes(&self) -> CarResult<RgbaPayload> {
659 self.rgba_bytes_with_crops(&[])
660 }
661
662 pub fn rgba_bytes_with_crops(&self, crops: &[super::ReferenceRect]) -> CarResult<RgbaPayload> {
663 self.rgba_bytes_with_crops_and_options(crops, &DecodeOptions::default())
664 }
665
666 pub fn rgba_bytes_with_crops_and_options(
667 &self,
668 crops: &[super::ReferenceRect],
669 options: &DecodeOptions,
670 ) -> CarResult<RgbaPayload> {
671 let img = self.to_image_with_crops_and_options(crops, options)?;
672 let rgba_len = (img.width() as usize)
673 .checked_mul(img.height() as usize)
674 .and_then(|len| len.checked_mul(4))
675 .ok_or_else(|| CarError::DecodeFailed("RGBA output size overflow".into()))?;
676 options.check_output_bytes(rgba_len, DecodeStage::ImageConversion)?;
677 let img = img.to_rgba8();
678 Ok(RgbaPayload {
679 width: img.width(),
680 height: img.height(),
681 rgba: img.into_raw(),
682 })
683 }
684
685 pub fn png_bytes(&self) -> CarResult<Vec<u8>> {
686 self.png_bytes_with_crops(&[])
687 }
688
689 pub fn png_bytes_with_crops(&self, crops: &[super::ReferenceRect]) -> CarResult<Vec<u8>> {
690 self.png_bytes_with_crops_and_options(crops, &DecodeOptions::default())
691 }
692
693 pub fn png_bytes_with_crops_and_options(
694 &self,
695 crops: &[super::ReferenceRect],
696 options: &DecodeOptions,
697 ) -> CarResult<Vec<u8>> {
698 let img = self.to_image_with_crops_and_options(crops, options)?;
699 encode_png_bytes(self, &img)
700 }
701
702 pub fn save_image_with_crops(
709 &self,
710 path: impl AsRef<Path>,
711 crops: &[super::ReferenceRect],
712 ) -> CarResult<()> {
713 let path = path.as_ref();
714 let img = render_image_with_crops(self, crops)?;
715
716 if is_png_path(path) {
717 write_png_with_icc(self, &img, path)?;
718 } else {
719 img.save(path)?;
720 }
721
722 Ok(())
723 }
724
725 pub fn save_image(&self, path: impl AsRef<Path>) -> CarResult<()> {
726 let path = path.as_ref();
727
728 let is_png = is_png_path(path);
730 if is_png && save_image_to_png_direct(self, path)? {
731 return Ok(());
732 }
733
734 let img = self.to_image()?;
736
737 if is_png {
738 write_png_with_icc(self, &img, path)?;
739 return Ok(());
740 }
741
742 img.save(path)?;
743 Ok(())
744 }
745}
746
747impl CSIItem {
748 pub fn source_bytes(&self) -> CarResult<Cow<'_, [u8]>> {
749 use crate::decode::RenditionDataRef;
750
751 if let Some(bytes) = hevc_source_bytes(self)? {
752 return Ok(bytes);
753 }
754
755 let preserves_source = matches!(
756 self.header.encoding,
757 Encoding::Data
758 | Encoding::HEIF
759 | Encoding::PDF
760 | Encoding::SVG
761 | Encoding::JPEG
762 | Encoding::WEBP
763 );
764 if !preserves_source {
765 return Err(CarError::UnsupportedEncoding(self.header.encoding));
766 }
767
768 match &self.header.rendition {
769 Some(Rendition::RawData(rd)) => {
770 return crate::apple_compression::maybe_decode_lzfse_with_fallback(
771 rd.raw_data.as_slice(),
772 )
773 .map_err(CarError::from);
774 }
775 Some(Rendition::ThemeCBCK(cbck))
776 if matches!(cbck.compression_type, CompressionType::Uncompressed)
777 && matches!(
778 self.header.encoding,
779 Encoding::HEIF
780 | Encoding::PDF
781 | Encoding::SVG
782 | Encoding::JPEG
783 | Encoding::WEBP
784 ) =>
785 {
786 let mut bytes = Vec::new();
787 for chunk in &cbck.chunks {
788 let data = crate::apple_compression::maybe_decode_lzfse_with_fallback(
789 chunk.raw_data.as_slice(),
790 )?;
791 bytes.extend_from_slice(data.as_ref());
792 }
793 return Ok(Cow::Owned(bytes));
794 }
795 _ => {}
796 }
797
798 match self.decode_data_ref()? {
799 Some(RenditionDataRef::RawData { data }) => Ok(data),
800 Some(RenditionDataRef::Image { data }) => Ok(data),
801 _ => Err(CarError::UnsupportedEncoding(self.header.encoding)),
802 }
803 }
804
805 pub fn source_bytes_owned(&self) -> CarResult<Vec<u8>> {
806 self.source_bytes().map(Cow::into_owned)
807 }
808
809 pub fn save_raw(&self, path: impl AsRef<Path>) -> CarResult<()> {
810 let path = path.as_ref();
811 let data = self.source_bytes()?;
812 std::fs::write(path, data.as_ref())?;
813 Ok(())
814 }
815}
816
817#[cfg(test)]
818mod test_helpers {
819 use std::io::Cursor;
820
821 use crate::car::CSIItem;
822 use crate::model::CSIHeader;
823 use deku::{DekuReader, reader::Reader};
824
825 pub fn build_csi_bytes(
826 width: u32,
827 height: u32,
828 encoding: &[u8; 4],
829 tlv: &[u8],
830 rendition: Option<&[u8]>,
831 ) -> Vec<u8> {
832 let mut b = Vec::new();
833 b.extend_from_slice(b"ISTC");
834 b.extend_from_slice(&1u32.to_le_bytes()); b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&width.to_le_bytes());
837 b.extend_from_slice(&height.to_le_bytes());
838 b.extend_from_slice(&100u32.to_le_bytes()); b.extend_from_slice(encoding);
840 b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&1000u32.to_le_bytes()); b.extend_from_slice(&[0u8; 128]); b.extend_from_slice(&(tlv.len() as u32).to_le_bytes());
845 b.extend_from_slice(&1u32.to_le_bytes()); b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&(rendition.map_or(0, |d| d.len()) as u32).to_le_bytes());
848 b.extend_from_slice(tlv);
849 if let Some(r) = rendition {
850 b.extend_from_slice(r);
851 }
852 b
853 }
854
855 pub fn parse_item(bytes: Vec<u8>) -> CSIItem {
856 let mut reader = Reader::new(Cursor::new(bytes));
857 let header =
858 CSIHeader::from_reader_with_ctx(&mut reader, ()).expect("CSI header should parse");
859 CSIItem {
860 attrs: Vec::new(),
861 header,
862 key_values: Box::default(),
863 }
864 }
865
866 pub fn uncompressed_rendition(data: &[u8]) -> Vec<u8> {
867 let mut b = Vec::new();
868 b.extend_from_slice(b"MLEC");
869 b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&(data.len() as u32).to_le_bytes());
872 b.extend_from_slice(data);
873 b
874 }
875
876 pub fn hevc_rendition(heic_body: &[u8]) -> Vec<u8> {
877 let mut chunk = Vec::new();
878 chunk.extend_from_slice(&1u32.to_le_bytes());
879 chunk.extend_from_slice(&(heic_body.len() as u32).to_le_bytes());
880 chunk.extend_from_slice(heic_body);
881
882 let mut b = Vec::new();
883 b.extend_from_slice(b"MLEC");
884 b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&9u32.to_le_bytes()); b.extend_from_slice(&(chunk.len() as u32).to_le_bytes());
887 b.extend_from_slice(&chunk);
888 b
889 }
890
891 pub fn fake_heic_body() -> Vec<u8> {
892 let mut body = Vec::new();
893 body.extend_from_slice(&24u32.to_be_bytes());
894 body.extend_from_slice(b"ftyp");
895 body.extend_from_slice(b"heic");
896 body.extend_from_slice(&0u32.to_be_bytes());
897 body.extend_from_slice(b"mif1");
898 body.extend_from_slice(b"heic");
899 body.extend_from_slice(&8u32.to_be_bytes());
900 body.extend_from_slice(b"mdat");
901 body
902 }
903
904 pub fn raw_data_rendition(data: &[u8]) -> Vec<u8> {
905 let mut b = Vec::new();
906 b.extend_from_slice(b"DWAR");
907 b.extend_from_slice(&1u32.to_le_bytes()); b.extend_from_slice(&(data.len() as u32).to_le_bytes());
909 b.extend_from_slice(data);
910 b
911 }
912
913 pub fn bytes_per_row_tlv(bpr: u32) -> Vec<u8> {
914 let mut b = Vec::new();
915 b.extend_from_slice(&1007u32.to_le_bytes()); b.extend_from_slice(&4u32.to_le_bytes()); b.extend_from_slice(&bpr.to_le_bytes());
918 b
919 }
920
921 pub fn exif_orientation_tlv(orientation: u32) -> Vec<u8> {
922 let mut b = Vec::new();
923 b.extend_from_slice(&1006u32.to_le_bytes()); b.extend_from_slice(&4u32.to_le_bytes()); b.extend_from_slice(&orientation.to_le_bytes());
926 b
927 }
928
929 pub fn deepmap2_rendition(width: u16, height: u16, pixels: &[u8]) -> Vec<u8> {
930 let mut dmp2 = Vec::new();
932 dmp2.extend_from_slice(b"dmp2");
933 dmp2.extend_from_slice(&[0x01, 0x00, 0x00, 0x01]); dmp2.extend_from_slice(&width.to_le_bytes());
935 dmp2.extend_from_slice(&height.to_le_bytes());
936 dmp2.extend_from_slice(pixels);
937 let mut b = Vec::new();
939 b.extend_from_slice(b"MLEC");
940 b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&11u32.to_le_bytes()); b.extend_from_slice(&(dmp2.len() as u32).to_le_bytes());
943 b.extend_from_slice(&dmp2);
944 b
945 }
946
947 pub fn deepmap2_rendition_ga88(width: u16, height: u16, pixels: &[u8]) -> Vec<u8> {
948 let mut dmp2 = Vec::new();
949 dmp2.extend_from_slice(b"dmp2");
950 dmp2.extend_from_slice(&[0x01, 0x00, 0x00, 0x02]); dmp2.extend_from_slice(&width.to_le_bytes());
952 dmp2.extend_from_slice(&height.to_le_bytes());
953 dmp2.extend_from_slice(pixels);
954
955 let mut b = Vec::new();
956 b.extend_from_slice(b"MLEC");
957 b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&11u32.to_le_bytes()); b.extend_from_slice(&(dmp2.len() as u32).to_le_bytes());
960 b.extend_from_slice(&dmp2);
961 b
962 }
963
964 pub fn make_item(width: u32, height: u32, enc: &[u8; 4], pixels: &[u8]) -> CSIItem {
965 let r = uncompressed_rendition(pixels);
966 parse_item(build_csi_bytes(width, height, enc, &[], Some(&r)))
967 }
968
969 pub fn make_item_stride(
970 width: u32,
971 height: u32,
972 enc: &[u8; 4],
973 pixels: &[u8],
974 bpr: u32,
975 ) -> CSIItem {
976 let r = uncompressed_rendition(pixels);
977 let tlv = bytes_per_row_tlv(bpr);
978 parse_item(build_csi_bytes(width, height, enc, &tlv, Some(&r)))
979 }
980}
981
982#[cfg(test)]
983#[cfg(feature = "image")]
984mod tests {
985 use std::io::Cursor;
986
987 use super::test_helpers::*;
988 use crate::DecodeOptions;
989 use crate::car::CarError;
990
991 #[test]
994 fn to_image_argb_swizzle() {
995 let pixels = [10u8, 20, 30, 255, 32, 64, 128, 128];
998 let item = make_item(2, 1, b"BGRA", &pixels);
999 let img = item.to_image().unwrap();
1000 let rgba = img.to_rgba8();
1001 assert_eq!(rgba.get_pixel(0, 0).0, [30, 20, 10, 255]);
1002 assert_eq!(rgba.get_pixel(1, 0).0, [255, 128, 64, 128]);
1003 }
1004
1005 #[test]
1008 fn to_image_argb16_swizzle_le() {
1009 let pixels = [
1013 0x00u8, 0x20, 0x00, 0x40, 0x00, 0x80, 0x00, 0x80, ];
1018 let item = make_item(1, 1, b"WBGR", &pixels);
1019 let img = item.to_image().unwrap();
1020 let rgba16 = img.to_rgba16();
1021 assert_eq!(rgba16.get_pixel(0, 0).0, [65535, 32768, 16384, 32768]);
1022 }
1023
1024 #[test]
1027 fn to_image_gray() {
1028 let pixels = [100u8, 200];
1029 let item = make_item(2, 1, b"YARG", &pixels);
1030 let img = item.to_image().unwrap();
1031 let luma = img.to_luma8();
1032 assert_eq!(luma.get_pixel(0, 0).0, [100]);
1033 assert_eq!(luma.get_pixel(1, 0).0, [200]);
1034 }
1035
1036 #[test]
1039 fn to_image_ga8() {
1040 let pixels = [100u8, 200]; let item = make_item(1, 1, b" 8AG", &pixels);
1042 let img = item.to_image().unwrap();
1043 let la = img.to_luma_alpha8();
1044 assert_eq!(la.get_pixel(0, 0).0, [100, 200]);
1045 }
1046
1047 #[test]
1050 fn to_image_ga16_le() {
1051 let pixels = [0x34u8, 0x12, 0x78, 0x56];
1053 let item = make_item(1, 1, b"61AG", &pixels);
1054 let img = item.to_image().unwrap();
1055 let la16 = img.to_luma_alpha16();
1056 assert_eq!(la16.get_pixel(0, 0).0, [0x1234, 0x5678]);
1057 }
1058
1059 #[test]
1062 fn to_image_rgb5() {
1063 let pixels = 0x7C00u16.to_le_bytes();
1065 let item = make_item(1, 1, b"5BGR", &pixels);
1066 let img = item.to_image().unwrap();
1067 let rgb = img.to_rgb8();
1068 assert_eq!(rgb.get_pixel(0, 0).0, [255, 0, 0]);
1070 }
1071
1072 #[test]
1075 fn to_image_deepmap2_rgba() {
1076 let r = deepmap2_rendition(2, 1, &[100, 200]);
1079 let item = parse_item(build_csi_bytes(2, 1, b"YARG", &[], Some(&r)));
1081 let img = item.to_image().unwrap();
1082 let rgba = img.to_rgba8();
1083 assert_eq!(rgba.get_pixel(0, 0).0, [100, 100, 100, 255]);
1084 assert_eq!(rgba.get_pixel(1, 0).0, [200, 200, 200, 255]);
1085 }
1086
1087 #[test]
1088 fn to_image_deepmap2_ignores_incompatible_source_stride() {
1089 let r = deepmap2_rendition_ga88(2, 1, &[100, 255, 200, 255]);
1090 let tlv = bytes_per_row_tlv(2);
1091 let item = parse_item(build_csi_bytes(2, 1, b" 8AG", &tlv, Some(&r)));
1092 let img = item.to_image().unwrap();
1093 let rgba = img.to_rgba8();
1094 assert_eq!(rgba.get_pixel(0, 0).0, [100, 100, 100, 255]);
1095 assert_eq!(rgba.get_pixel(1, 0).0, [200, 200, 200, 255]);
1096 }
1097
1098 #[test]
1099 fn to_image_deepmap2_uses_decoded_height_when_header_mismatches() {
1100 let r = deepmap2_rendition(2, 1, &[100, 200]);
1101 let item = parse_item(build_csi_bytes(2, 2, b"YARG", &[], Some(&r)));
1102 let img = item.to_image().unwrap();
1103 assert_eq!(img.width(), 2);
1104 assert_eq!(img.height(), 1);
1105 }
1106
1107 #[test]
1110 fn to_image_exif_orientation_ignored() {
1111 let pixels = [100u8, 200];
1113 let tlv = exif_orientation_tlv(6);
1114 let r = uncompressed_rendition(&pixels);
1115 let item = parse_item(build_csi_bytes(2, 1, b"YARG", &tlv, Some(&r)));
1116 let img = item.to_image().unwrap();
1117 let luma = img.to_luma8();
1118 assert_eq!(img.width(), 2);
1120 assert_eq!(img.height(), 1);
1121 assert_eq!(luma.get_pixel(0, 0).0, [100]);
1122 assert_eq!(luma.get_pixel(1, 0).0, [200]);
1123 }
1124
1125 #[test]
1126 fn to_image_exif_orientation_can_be_applied() {
1127 let pixels = [
1128 1u8, 2, 3, 4, ];
1131 let tlv = exif_orientation_tlv(6);
1132 let r = uncompressed_rendition(&pixels);
1133 let item = parse_item(build_csi_bytes(2, 2, b"YARG", &tlv, Some(&r)));
1134
1135 let img = item
1136 .to_image_with_options(&DecodeOptions::default().apply_exif_orientation(true))
1137 .unwrap();
1138 let luma = img.to_luma8();
1139
1140 assert_eq!(img.width(), 2);
1141 assert_eq!(img.height(), 2);
1142 assert_eq!(luma.get_pixel(0, 0).0, [3]);
1143 assert_eq!(luma.get_pixel(1, 0).0, [1]);
1144 assert_eq!(luma.get_pixel(0, 1).0, [4]);
1145 assert_eq!(luma.get_pixel(1, 1).0, [2]);
1146 }
1147
1148 #[test]
1149 fn crops_precede_exif_and_report_oriented_output_dimensions() {
1150 use crate::ReferenceRect;
1151
1152 let pixels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
1153 let tlv = exif_orientation_tlv(6);
1154 let r = uncompressed_rendition(&pixels);
1155 let item = parse_item(build_csi_bytes(4, 3, b"YARG", &tlv, Some(&r)));
1156 let crops = [
1158 ReferenceRect {
1159 x: 1,
1160 y: 0,
1161 width: 3,
1162 height: 2,
1163 },
1164 ReferenceRect {
1165 x: 1,
1166 y: 1,
1167 width: 2,
1168 height: 1,
1169 },
1170 ];
1171 let options = DecodeOptions::default().apply_exif_orientation(true);
1172 let img = crate::image::to_image_with_crops_and_options(&item, &crops, &options)
1173 .unwrap()
1174 .to_luma8();
1175 assert_eq!(img.dimensions(), (1, 2));
1176 assert_eq!(img.into_raw(), [7, 8]);
1177
1178 let rgba =
1179 crate::image::rgba_bytes_with_crops_and_options(&item, &crops, &options).unwrap();
1180 assert_eq!((rgba.width, rgba.height), (1, 2));
1181 assert_eq!(rgba.rgba, [7, 7, 7, 255, 8, 8, 8, 255]);
1182 let default_rgba = item.rgba_bytes_with_crops(&crops).unwrap();
1183 assert_eq!((default_rgba.width, default_rgba.height), (2, 1));
1184 assert_eq!(default_rgba.rgba, rgba.rgba);
1185 }
1186
1187 #[test]
1188 fn crops_do_not_bypass_image_conversion_budget() {
1189 use crate::{DecodeBudgetError, DecodeStage, ReferenceRect};
1190
1191 let item = make_item(3, 2, b"5BGR", &[0; 12]);
1193 let crops = [ReferenceRect {
1194 x: 0,
1195 y: 0,
1196 width: 1,
1197 height: 1,
1198 }];
1199 let options = DecodeOptions::default().with_max_output_bytes(12);
1200 let err = item
1201 .to_image_with_crops_and_options(&crops, &options)
1202 .unwrap_err();
1203 assert!(matches!(
1204 err,
1205 CarError::DecodeBudgetExceeded(DecodeBudgetError::OutputBytesExceeded {
1206 limit: 12,
1207 requested: 18,
1208 stage: DecodeStage::ImageConversion
1209 })
1210 ));
1211 }
1212
1213 #[test]
1214 fn rgba_conversion_checks_expanded_output_budget() {
1215 use crate::{DecodeBudgetError, DecodeStage};
1216
1217 let item = make_item(2, 1, b"YARG", &[1, 2]);
1218 let options = DecodeOptions::default().with_max_output_bytes(2);
1219 let err = item
1220 .rgba_bytes_with_crops_and_options(&[], &options)
1221 .unwrap_err();
1222 assert!(matches!(
1223 err,
1224 CarError::DecodeBudgetExceeded(DecodeBudgetError::OutputBytesExceeded {
1225 limit: 2,
1226 requested: 8,
1227 stage: DecodeStage::ImageConversion
1228 })
1229 ));
1230 }
1231
1232 #[test]
1233 fn png_crops_and_orientation_preserve_sixteen_bit_pixels() {
1234 use crate::ReferenceRect;
1235
1236 let channels: [u16; 24] = [
1237 1, 2, 3, 65535, 101, 102, 103, 65535, 201, 202, 203, 65535, 301, 302, 303, 65535, 401,
1238 402, 403, 65535, 501, 502, 503, 65535,
1239 ];
1240 let pixels: Vec<u8> = channels
1241 .iter()
1242 .flat_map(|value| value.to_le_bytes())
1243 .collect();
1244 let tlv = exif_orientation_tlv(6);
1245 let r = uncompressed_rendition(&pixels);
1246 let item = parse_item(build_csi_bytes(3, 2, b"WBGR", &tlv, Some(&r)));
1247 let crops = [ReferenceRect {
1248 x: 1,
1249 y: 0,
1250 width: 2,
1251 height: 1,
1252 }];
1253 let png = crate::image::png_bytes_with_crops_and_options(
1254 &item,
1255 &crops,
1256 &DecodeOptions::default().apply_exif_orientation(true),
1257 )
1258 .unwrap();
1259 let decoded = image::load_from_memory_with_format(&png, image::ImageFormat::Png).unwrap();
1260 assert_eq!(decoded.color(), image::ColorType::Rgba16);
1261 let rgba = decoded.to_rgba16();
1262 assert_eq!(rgba.dimensions(), (1, 2));
1263 assert_eq!(rgba.get_pixel(0, 0).0, [403, 402, 401, 65535]);
1264 assert_eq!(rgba.get_pixel(0, 1).0, [503, 502, 501, 65535]);
1265 }
1266
1267 #[test]
1270 fn to_image_stride_strips_padding() {
1271 let pixels = [100u8, 0, 0, 0, 200, 0, 0, 0];
1273 let item = make_item_stride(1, 2, b"YARG", &pixels, 4);
1274 let img = item.to_image().unwrap();
1275 let luma = img.to_luma8();
1276 assert_eq!(luma.get_pixel(0, 0).0, [100]);
1277 assert_eq!(luma.get_pixel(0, 1).0, [200]);
1278 }
1279
1280 #[test]
1281 fn to_image_invalid_bytes_per_row() {
1282 let pixels = [0u8; 8];
1284 let item = make_item_stride(2, 1, b"BGRA", &pixels, 4);
1285 let err = item.to_image().unwrap_err();
1286 assert!(matches!(err, CarError::DecodeFailed(_)));
1287 assert!(format!("{err}").contains("bytes_per_row"));
1288 }
1289
1290 #[test]
1293 fn to_image_jpeg() {
1294 use image::{ImageBuffer, ImageFormat, RgbImage};
1295 let rgb: RgbImage = ImageBuffer::from_raw(1, 1, vec![255u8, 0, 0]).unwrap();
1297 let mut buf = Cursor::new(Vec::new());
1298 rgb.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
1299 let jpeg_bytes = buf.into_inner();
1300
1301 let item = make_item(1, 1, b"GEPJ", &jpeg_bytes);
1302 let img = item.to_image().unwrap();
1303 assert_eq!(img.width(), 1);
1304 assert_eq!(img.height(), 1);
1305 }
1306
1307 #[test]
1310 fn to_image_unsupported_encoding() {
1311 let item = make_item(1, 1, b"ATAD", &[0u8; 4]);
1312 let err = item.to_image().unwrap_err();
1313 assert!(matches!(err, CarError::UnsupportedEncoding(_)));
1314 }
1315
1316 #[test]
1319 fn save_image_png() {
1320 let pixels = [10u8, 20, 30, 255]; let item = make_item(1, 1, b"BGRA", &pixels);
1322 let dir = std::env::temp_dir().join("carparser_test_img");
1323 let _ = std::fs::create_dir_all(&dir);
1324 let path = dir.join("test_save.png");
1325 item.save_image(&path).unwrap();
1326 let img = image::open(&path).unwrap();
1327 assert_eq!(img.width(), 1);
1328 assert_eq!(img.height(), 1);
1329 let _ = std::fs::remove_file(&path);
1330 }
1331
1332 #[test]
1333 fn save_image_unsupported_format() {
1334 let pixels = [10u8, 20, 30, 255]; let item = make_item(1, 1, b"BGRA", &pixels);
1336 let dir = std::env::temp_dir().join("carparser_test_img");
1337 let _ = std::fs::create_dir_all(&dir);
1338 let path = dir.join("test_save.bmp");
1339 let err = item.save_image(&path).unwrap_err();
1340 assert!(matches!(err, CarError::Image(_)));
1341 }
1342
1343 #[test]
1346 fn save_image_png_direct_argb_pixel_correct() {
1347 let pixels = [10u8, 20, 30, 255];
1349 let item = make_item(1, 1, b"BGRA", &pixels);
1350 let dir = std::env::temp_dir().join("carparser_direct_argb");
1351 let _ = std::fs::create_dir_all(&dir);
1352 let path = dir.join("argb.png");
1353 item.save_image(&path).unwrap();
1354 let img = image::open(&path).unwrap().to_rgba8();
1355 assert_eq!(img.get_pixel(0, 0).0, [30, 20, 10, 255]);
1356 let _ = std::fs::remove_file(&path);
1357 }
1358
1359 #[test]
1360 fn save_image_png_direct_argb_with_stride() {
1361 let pixels = [1u8, 2, 3, 255, 0, 0, 0, 0, 4, 5, 6, 128, 0, 0, 0, 0];
1366 let item = make_item_stride(1, 2, b"BGRA", &pixels, 8);
1367 let dir = std::env::temp_dir().join("carparser_direct_argb_stride");
1368 let _ = std::fs::create_dir_all(&dir);
1369 let path = dir.join("argb_stride.png");
1370 item.save_image(&path).unwrap();
1371 let img = image::open(&path).unwrap().to_rgba8();
1372 assert_eq!(img.dimensions(), (1, 2));
1373 assert_eq!(img.get_pixel(0, 0).0, [3, 2, 1, 255]);
1374 assert_eq!(img.get_pixel(0, 1).0, [12, 10, 8, 128]);
1375 let _ = std::fs::remove_file(&path);
1376 }
1377
1378 #[test]
1379 fn save_image_png_direct_ga8_pixel_correct() {
1380 let pixels = [128u8, 200];
1382 let item = make_item(1, 1, b" 8AG", &pixels);
1383 let dir = std::env::temp_dir().join("carparser_direct_ga8");
1384 let _ = std::fs::create_dir_all(&dir);
1385 let path = dir.join("ga8.png");
1386 item.save_image(&path).unwrap();
1387 let img = image::open(&path).unwrap().to_luma_alpha8();
1388 assert_eq!(img.get_pixel(0, 0).0, [128, 200]);
1389 let _ = std::fs::remove_file(&path);
1390 }
1391
1392 #[test]
1393 fn save_image_png_direct_argb16_pixel_correct() {
1394 let pixels = [
1398 0x00u8, 0x20, 0x00, 0x40, 0x00, 0x80, 0x00, 0x80, ];
1403 let item = make_item(1, 1, b"WBGR", &pixels);
1404 let dir = std::env::temp_dir().join("carparser_direct_argb16");
1405 let _ = std::fs::create_dir_all(&dir);
1406 let path = dir.join("argb16.png");
1407 item.save_image(&path).unwrap();
1408 let img = image::open(&path).unwrap().to_rgba16();
1409 assert_eq!(img.get_pixel(0, 0).0, [65535, 32768, 16384, 32768]);
1410 let _ = std::fs::remove_file(&path);
1411 }
1412
1413 #[test]
1414 fn save_image_png_direct_deepmap2_pixel_correct() {
1415 let r = deepmap2_rendition(2, 1, &[100, 200]);
1417 let item = parse_item(build_csi_bytes(2, 1, b"YARG", &[], Some(&r)));
1418 let dir = std::env::temp_dir().join("carparser_direct_dmp2");
1419 let _ = std::fs::create_dir_all(&dir);
1420 let path = dir.join("deepmap2.png");
1421 item.save_image(&path).unwrap();
1422 let img = image::open(&path).unwrap().to_rgba8();
1423 assert_eq!(img.get_pixel(0, 0).0, [100, 100, 100, 255]);
1424 assert_eq!(img.get_pixel(1, 0).0, [200, 200, 200, 255]);
1425 let _ = std::fs::remove_file(&path);
1426 }
1427
1428 #[test]
1429 fn rgba_bytes_matches_rendered_rgba_output() {
1430 let pixels = [10u8, 20, 30, 255];
1431 let item = make_item(1, 1, b"BGRA", &pixels);
1432
1433 let rendered = item.to_image().unwrap().to_rgba8();
1434 let rgba = item.rgba_bytes().unwrap();
1435
1436 assert_eq!(rgba.width, 1);
1437 assert_eq!(rgba.height, 1);
1438 assert_eq!(rgba.rgba, rendered.into_raw());
1439 }
1440
1441 #[test]
1442 fn png_bytes_encodes_png_in_memory() {
1443 let pixels = [10u8, 20, 30, 255];
1444 let item = make_item(1, 1, b"BGRA", &pixels);
1445
1446 let png = item.png_bytes().unwrap();
1447 let decoded = image::load_from_memory_with_format(&png, image::ImageFormat::Png)
1448 .unwrap()
1449 .to_rgba8();
1450
1451 assert_eq!(decoded.dimensions(), (1, 1));
1452 assert_eq!(decoded.get_pixel(0, 0).0, [30, 20, 10, 255]);
1453 }
1454}
1455
1456#[cfg(test)]
1457mod save_raw_tests {
1458 use super::test_helpers::*;
1459 use crate::Encoding;
1460 use crate::car::{Car, CarError};
1461 use crate::model::rendition::Rendition;
1462
1463 #[test]
1464 fn save_raw_with_raw_data() {
1465 let payload = b"hello-raw-data";
1466 let r = raw_data_rendition(payload);
1467 let item = parse_item(build_csi_bytes(1, 1, b"ATAD", &[], Some(&r)));
1468 let dir = std::env::temp_dir().join("carparser_test_raw");
1469 let _ = std::fs::create_dir_all(&dir);
1470 let path = dir.join("test.bin");
1471 item.save_raw(&path).unwrap();
1472 assert_eq!(std::fs::read(&path).unwrap(), payload);
1473 let _ = std::fs::remove_file(&path);
1474 }
1475
1476 #[test]
1477 fn source_bytes_with_raw_data() {
1478 let payload = b"hello-raw-data";
1479 let r = raw_data_rendition(payload);
1480 let item = parse_item(build_csi_bytes(1, 1, b"ATAD", &[], Some(&r)));
1481
1482 let bytes = item.source_bytes().unwrap();
1483 assert_eq!(bytes.as_ref(), payload);
1484 }
1485
1486 #[test]
1487 fn save_raw_heif_encoding() {
1488 let payload = b"fake-heif-data";
1489 let item = make_item(1, 1, b"FIEH", payload);
1490 let dir = std::env::temp_dir().join("carparser_test_raw");
1491 let _ = std::fs::create_dir_all(&dir);
1492 let path = dir.join("test.heif");
1493 item.save_raw(&path).unwrap();
1494 assert_eq!(std::fs::read(&path).unwrap(), payload);
1495 let _ = std::fs::remove_file(&path);
1496 }
1497
1498 #[test]
1499 fn source_bytes_hevc_cbck_strips_coreui_wrapper() {
1500 let heic = fake_heic_body();
1501 let rendition = hevc_rendition(&heic);
1502 let item = parse_item(build_csi_bytes(20, 88, b"BGRA", &[], Some(&rendition)));
1503
1504 let bytes = item.source_bytes().unwrap();
1505 assert_eq!(bytes.as_ref(), heic.as_slice());
1506 }
1507
1508 #[test]
1509 fn save_raw_hevc_cbck_writes_heic_container() {
1510 let heic = fake_heic_body();
1511 let rendition = hevc_rendition(&heic);
1512 let item = parse_item(build_csi_bytes(20, 88, b"BGRA", &[], Some(&rendition)));
1513 let dir = std::env::temp_dir().join("carparser_test_raw");
1514 let _ = std::fs::create_dir_all(&dir);
1515 let path = dir.join("hevc.heic");
1516
1517 item.save_raw(&path).unwrap();
1518 assert_eq!(std::fs::read(&path).unwrap(), heic);
1519 let _ = std::fs::remove_file(&path);
1520 }
1521
1522 #[test]
1523 fn save_raw_pdf_encoding() {
1524 let payload = b"fake-pdf-data";
1525 let item = make_item(1, 1, b" FDP", payload);
1526 let dir = std::env::temp_dir().join("carparser_test_raw");
1527 let _ = std::fs::create_dir_all(&dir);
1528 let path = dir.join("test.pdf");
1529 item.save_raw(&path).unwrap();
1530 assert_eq!(std::fs::read(&path).unwrap(), payload);
1531 let _ = std::fs::remove_file(&path);
1532 }
1533
1534 #[test]
1535 fn save_raw_svg_encoding() {
1536 let payload = b"<svg></svg>";
1537 let item = make_item(1, 1, b" GVS", payload);
1538 let dir = std::env::temp_dir().join("carparser_test_raw");
1539 let _ = std::fs::create_dir_all(&dir);
1540 let path = dir.join("test.svg");
1541 item.save_raw(&path).unwrap();
1542 assert_eq!(std::fs::read(&path).unwrap(), payload);
1543 let _ = std::fs::remove_file(&path);
1544 }
1545
1546 #[test]
1547 fn source_bytes_preserves_svg_payload_in_memory() {
1548 let payload = b"<svg></svg>";
1549 let item = make_item(1, 1, b" GVS", payload);
1550
1551 let bytes = item.source_bytes().unwrap();
1552 assert_eq!(bytes.as_ref(), payload);
1553 }
1554
1555 #[test]
1556 fn save_raw_rejects_pixel_format_encoding() {
1557 let item = make_item(1, 1, b"BGRA", &[0u8; 4]);
1558 let dir = std::env::temp_dir().join("carparser_test_raw");
1559 let _ = std::fs::create_dir_all(&dir);
1560 let path = dir.join("test_reject.bin");
1561 let err = item.save_raw(&path).unwrap_err();
1562 assert!(matches!(err, CarError::UnsupportedEncoding(_)));
1563 }
1564
1565 #[test]
1568 fn save_raw_heif_uncompressed_cbck_direct() {
1569 let payload = b"heif-cbck-direct-write";
1572 let item = make_item(1, 1, b"FIEH", payload);
1573 let dir = std::env::temp_dir().join("carparser_raw_direct");
1574 let _ = std::fs::create_dir_all(&dir);
1575 let path = dir.join("heif_direct.heic");
1576 item.save_raw(&path).unwrap();
1577 assert_eq!(std::fs::read(&path).unwrap(), payload);
1578 let _ = std::fs::remove_file(&path);
1579 }
1580
1581 #[test]
1582 fn save_raw_pdf_uncompressed_cbck_direct() {
1583 let payload = b"%PDF-1.4 direct-write";
1584 let item = make_item(1, 1, b" FDP", payload);
1585 let dir = std::env::temp_dir().join("carparser_raw_direct");
1586 let _ = std::fs::create_dir_all(&dir);
1587 let path = dir.join("pdf_direct.pdf");
1588 item.save_raw(&path).unwrap();
1589 assert_eq!(std::fs::read(&path).unwrap(), payload);
1590 let _ = std::fs::remove_file(&path);
1591 }
1592
1593 #[test]
1594 fn save_raw_rawdata_direct_write() {
1595 let payload = b"rawdata-no-clone-path";
1597 let r = raw_data_rendition(payload);
1598 let item = parse_item(build_csi_bytes(1, 1, b"ATAD", &[], Some(&r)));
1599 let dir = std::env::temp_dir().join("carparser_raw_direct");
1600 let _ = std::fs::create_dir_all(&dir);
1601 let path = dir.join("rawdata_direct.bin");
1602 item.save_raw(&path).unwrap();
1603 assert_eq!(std::fs::read(&path).unwrap(), payload);
1604 let _ = std::fs::remove_file(&path);
1605 }
1606
1607 fn fixture_ios_car() -> Option<Car> {
1608 let full_fixtures_enabled = matches!(
1609 std::env::var("CAR_TEST_FULL").as_deref(),
1610 Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES")
1611 );
1612 if !full_fixtures_enabled {
1613 eprintln!("skipping full fixture test: set CAR_TEST_FULL=1 to enable Assets_iOS.car");
1614 return None;
1615 }
1616
1617 let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1618 .join("../car-tests/data")
1619 .join("Assets_iOS.car");
1620 if !path.exists() {
1621 eprintln!("skipping full fixture test: missing {}", path.display());
1622 return None;
1623 }
1624
1625 Some(Car::new(path).expect("load test Assets_iOS.car"))
1626 }
1627
1628 #[test]
1629 fn save_raw_assets_ios_svg_rawdata_decodes_apple_compression_stream() {
1630 let Some(car) = fixture_ios_car() else {
1631 return;
1632 };
1633 let item = car
1634 .rendtions_with_name("icon_device_migration")
1635 .and_then(|items| {
1636 items.iter().find(|item| {
1637 matches!(item.header.rendition, Some(Rendition::RawData(_)))
1638 && matches!(item.header.encoding, Encoding::SVG)
1639 })
1640 })
1641 .expect("missing icon_device_migration SVG RawData rendition");
1642
1643 let dir = std::env::temp_dir().join("carparser_raw_fixture");
1644 let _ = std::fs::create_dir_all(&dir);
1645 let path = dir.join("icon_device_migration.svg");
1646
1647 item.save_raw(&path).unwrap();
1648 let saved = std::fs::read(&path).unwrap();
1649
1650 assert!(
1651 saved.starts_with(b"<?xml"),
1652 "expected decoded SVG XML, got magic {:?}",
1653 saved.get(..4)
1654 );
1655 assert!(saved.windows(b"<svg".len()).any(|w| w == b"<svg"));
1656 assert!(
1657 !saved.starts_with(b"bvx"),
1658 "saved file should not keep Apple compression stream magic"
1659 );
1660
1661 let _ = std::fs::remove_file(&path);
1662 }
1663
1664 #[cfg(feature = "image")]
1674 #[test]
1675 fn apply_reference_frame_crops_atlas_subregion() {
1676 use super::apply_reference_frame;
1677 use crate::car::ReferenceRect;
1678
1679 let source = image::RgbaImage::from_fn(12, 4, |x, y| {
1680 if y < 2 {
1681 if x < 6 {
1682 image::Rgba([255u8, 0, 0, 255]) } else {
1684 image::Rgba([0u8, 0, 255, 255]) }
1686 } else {
1687 image::Rgba([0u8, 255, 0, 255]) }
1689 });
1690 let img = image::DynamicImage::ImageRgba8(source);
1691
1692 let rect = ReferenceRect {
1693 x: 6,
1694 y: 2,
1695 width: 6,
1696 height: 2,
1697 };
1698 let result = apply_reference_frame(img, &rect).unwrap();
1699
1700 assert_eq!(result.width(), 6);
1701 assert_eq!(result.height(), 2);
1702 let rgba = result.to_rgba8();
1703 assert_eq!(
1705 rgba.get_pixel(0, 0).0,
1706 [0, 0, 255, 255],
1707 "top-left should be blue"
1708 );
1709 assert_eq!(
1710 rgba.get_pixel(5, 1).0,
1711 [0, 0, 255, 255],
1712 "bottom-right should be blue"
1713 );
1714 assert_ne!(rgba.get_pixel(0, 0).0[0], 255, "no red channel leak");
1716 }
1717
1718 #[cfg(feature = "image")]
1721 #[test]
1722 fn apply_reference_frame_rejects_out_of_bounds_rect() {
1723 use super::apply_reference_frame;
1724 use crate::car::ReferenceRect;
1725
1726 let img = image::DynamicImage::ImageRgba8(image::RgbaImage::new(4, 4));
1727
1728 let rect = ReferenceRect {
1729 x: 0,
1730 y: 2,
1731 width: 4,
1732 height: 8,
1733 };
1734 let err = apply_reference_frame(img, &rect).unwrap_err();
1735 assert!(matches!(err, CarError::DecodeFailed(_)));
1736 assert!(format!("{err}").contains("exceeds source image"));
1737 }
1738}