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 let mut img = item.to_image()?;
302 for rect in crops {
303 img = apply_reference_frame(img, rect)?;
304 }
305 Ok(img)
306}
307
308#[cfg(feature = "image")]
309fn apply_exif_orientation(
310 item: &CSIItem,
311 options: &DecodeOptions,
312 img: image::DynamicImage,
313) -> image::DynamicImage {
314 if !matches!(options.orientation_policy, OrientationPolicy::ApplyExif) {
315 return img;
316 }
317
318 match item.exif_orientation() {
319 Some(ExifOrientation::Mirrored) => img.fliph(),
320 Some(ExifOrientation::Rotated180) => img.rotate180(),
321 Some(ExifOrientation::Rotated180Mirrored) => img.rotate180().fliph(),
322 Some(ExifOrientation::Rotated90) => img.rotate90(),
323 Some(ExifOrientation::Rotated90Mirrored) => img.rotate90().fliph(),
324 Some(ExifOrientation::Rotated270) => img.rotate270(),
325 Some(ExifOrientation::Rotated270Mirrored) => img.rotate270().fliph(),
326 Some(ExifOrientation::None | ExifOrientation::Normal | ExifOrientation::Unknown(_))
327 | None => img,
328 }
329}
330
331#[cfg(feature = "image")]
332fn encode_png_bytes(item: &CSIItem, img: &image::DynamicImage) -> CarResult<Vec<u8>> {
333 let mut buf = Vec::new();
334 encode_png_with_icc(item, img, &mut buf)?;
335 Ok(buf)
336}
337
338#[cfg(feature = "image")]
339fn is_png_path(path: &Path) -> bool {
340 path.extension()
341 .and_then(|ext| ext.to_str())
342 .is_some_and(|ext| ext.eq_ignore_ascii_case("png"))
343}
344
345#[cfg(feature = "image")]
351fn save_image_to_png_direct(item: &CSIItem, path: &Path) -> CarResult<bool> {
352 use image::{ExtendedColorType, ImageEncoder};
353
354 let is_deepmap2 = is_deepmap2(item);
357 if !is_deepmap2 {
358 match item.header.encoding {
359 Encoding::ARGB | Encoding::GA8 | Encoding::ARGB16 => {}
360 _ => return Ok(false),
361 }
362 }
363
364 let w = item.header.width;
365 let h = item.header.height;
366
367 let data = match item.decode_data()? {
368 Some(RenditionData::Image { data }) => data,
369 _ => return Ok(false),
370 };
371
372 let (pixels, color_type, out_w, out_h): (Vec<u8>, ExtendedColorType, u32, u32) = if is_deepmap2
374 {
375 let row_bytes = (w as usize).saturating_mul(4);
377 if row_bytes == 0 {
378 return Ok(false);
379 }
380 let mut pixels = if data.len() == row_bytes * h as usize {
381 data
382 } else {
383 let stride = resolve_bytes_per_row(item, 4).unwrap_or(w * 4);
384 strip_stride(data, w, h, 4, stride)
385 };
386 if pixels.len() % row_bytes != 0 {
387 return Err(CarError::DecodeFailed(format!(
388 "RGBA buffer size mismatch: expected a multiple of {row_bytes} bytes, got {}",
389 pixels.len()
390 )));
391 }
392 unpremultiply_rgba8_in_place(&mut pixels);
395 let inferred_h = (pixels.len() / row_bytes) as u32;
396 (pixels, ExtendedColorType::Rgba8, w, inferred_h)
397 } else {
398 match item.header.encoding {
399 Encoding::ARGB => {
400 let stride = resolve_bytes_per_row(item, 4)? as usize;
402 let row_bytes = (w * 4) as usize;
403 let mut pixels = Vec::with_capacity(row_bytes * h as usize);
404 for row in 0..h as usize {
405 let start = row * stride;
406 let end = start + row_bytes;
407 if end > data.len() {
408 break;
409 }
410 for chunk in data[start..end].chunks_exact(4) {
411 pixels.push(chunk[2]); pixels.push(chunk[1]); pixels.push(chunk[0]); pixels.push(chunk[3]); }
416 }
417 let expected = row_bytes * h as usize;
418 if pixels.len() < expected {
419 return Err(CarError::DecodeFailed(format!(
420 "ARGB pixel buffer truncated: expected {expected} bytes, got {}",
421 pixels.len()
422 )));
423 }
424 unpremultiply_rgba8_in_place(&mut pixels);
425 (pixels, ExtendedColorType::Rgba8, w, h)
426 }
427 Encoding::GA8 => {
428 let stride = resolve_bytes_per_row(item, 2)?;
429 let pixels = strip_stride(data, w, h, 2, stride);
430 let expected = w as usize * h as usize * 2;
431 if pixels.len() < expected {
432 return Err(CarError::DecodeFailed(format!(
433 "GA8 pixel buffer truncated: expected {expected} bytes, got {}",
434 pixels.len()
435 )));
436 }
437 (pixels, ExtendedColorType::La8, w, h)
438 }
439 Encoding::ARGB16 => {
440 let stride = resolve_bytes_per_row(item, 8)? as usize;
443 let row_bytes = (w * 8) as usize; let mut pixels: Vec<u8> = Vec::with_capacity(w as usize * h as usize * 8);
445 for row in 0..h as usize {
446 let row_start = row * stride;
447 if row_start + row_bytes > data.len() {
448 break;
449 }
450 for px in data[row_start..row_start + row_bytes].chunks_exact(8) {
451 let r = u16::from_le_bytes([px[4], px[5]]);
453 let g = u16::from_le_bytes([px[2], px[3]]);
454 let b = u16::from_le_bytes([px[0], px[1]]);
455 let a = u16::from_le_bytes([px[6], px[7]]);
456 let (r, g, b) = if a == 0 || a == u16::MAX {
457 (r, g, b)
458 } else {
459 let up = |c: u16| -> u16 {
460 ((c as u64 * u16::MAX as u64 + a as u64 / 2) / a as u64)
461 .min(u16::MAX as u64) as u16
462 };
463 (up(r), up(g), up(b))
464 };
465 pixels.extend_from_slice(&r.to_ne_bytes());
466 pixels.extend_from_slice(&g.to_ne_bytes());
467 pixels.extend_from_slice(&b.to_ne_bytes());
468 pixels.extend_from_slice(&a.to_ne_bytes());
469 }
470 }
471 let expected = w as usize * h as usize * 8;
472 if pixels.len() < expected {
473 return Err(CarError::DecodeFailed(format!(
474 "ARGB16 pixel buffer truncated: expected {expected} bytes, got {}",
475 pixels.len()
476 )));
477 }
478 (pixels, ExtendedColorType::Rgba16, w, h)
479 }
480 _ => return Ok(false),
481 }
482 };
483
484 let file = std::fs::File::create(path)?;
485 let writer = std::io::BufWriter::new(file);
486 let mut encoder = image::codecs::png::PngEncoder::new(writer);
487 if let Some(profile) = png_icc_profile(item) {
488 let _ = encoder.set_icc_profile(profile);
489 }
490 encoder.write_image(&pixels, out_w, out_h, color_type)?;
491 Ok(true)
492}
493
494#[cfg(feature = "image")]
495impl CSIItem {
496 pub fn to_image(&self) -> CarResult<image::DynamicImage> {
497 self.to_image_with_options(&DecodeOptions::default())
498 }
499
500 pub fn to_image_with_options(&self, options: &DecodeOptions) -> CarResult<image::DynamicImage> {
501 let img = self.to_image_unoriented(options)?;
502 options.check_output_bytes(img.as_bytes().len(), DecodeStage::ImageConversion)?;
503 Ok(apply_exif_orientation(self, options, img))
504 }
505
506 fn to_image_unoriented(&self, options: &DecodeOptions) -> CarResult<image::DynamicImage> {
507 use image::{DynamicImage, ImageBuffer};
508
509 let data = match self.decode_data_with_options(options)? {
510 Some(RenditionData::Image { data }) => data,
511 _ => {
512 return Err(CarError::UnsupportedEncoding(self.header.encoding));
513 }
514 };
515
516 let w = self.header.width;
517 let h = self.header.height;
518
519 if is_deepmap2(self) {
521 let row_bytes = (w as usize).saturating_mul(4);
522 let header_len = row_bytes.saturating_mul(h as usize);
523 let mut pixels = if data.len() == header_len {
524 data
525 } else {
526 let stride = resolve_bytes_per_row(self, 4).unwrap_or(w * 4);
531 strip_stride(data, w, h, 4, stride)
532 };
533 if row_bytes == 0 || pixels.len() % row_bytes != 0 {
534 return Err(CarError::DecodeFailed(format!(
535 "RGBA buffer size mismatch: expected a multiple of {row_bytes} bytes, got {}",
536 pixels.len()
537 )));
538 }
539 unpremultiply_rgba8_in_place(&mut pixels);
540 let inferred_h = (pixels.len() / row_bytes) as u32;
541 let buf = ImageBuffer::from_raw(w, inferred_h, pixels)
542 .ok_or_else(|| CarError::DecodeFailed("RGBA buffer size mismatch".into()))?;
543 return Ok(DynamicImage::ImageRgba8(buf));
544 }
545
546 match self.header.encoding {
547 Encoding::ARGB => {
548 let stride = resolve_bytes_per_row(self, 4)?;
549 let mut pixels = strip_stride(data, w, h, 4, stride);
550 for chunk in pixels.chunks_exact_mut(4) {
552 chunk.swap(0, 2);
553 }
554 unpremultiply_rgba8_in_place(&mut pixels);
555 let buf = ImageBuffer::from_raw(w, h, pixels)
556 .ok_or_else(|| CarError::DecodeFailed("ARGB buffer size mismatch".into()))?;
557 Ok(DynamicImage::ImageRgba8(buf))
558 }
559 Encoding::GRAY => {
560 let stride = resolve_bytes_per_row(self, 1)?;
561 let pixels = strip_stride(data, w, h, 1, stride);
562 let buf = ImageBuffer::from_raw(w, h, pixels)
563 .ok_or_else(|| CarError::DecodeFailed("GRAY buffer size mismatch".into()))?;
564 Ok(DynamicImage::ImageLuma8(buf))
565 }
566 Encoding::GA8 => {
567 let stride = resolve_bytes_per_row(self, 2)?;
568 let pixels = strip_stride(data, w, h, 2, stride);
569 let buf = ImageBuffer::from_raw(w, h, pixels)
570 .ok_or_else(|| CarError::DecodeFailed("GA8 buffer size mismatch".into()))?;
571 Ok(DynamicImage::ImageLumaA8(buf))
572 }
573 Encoding::ARGB16 => {
574 let stride = resolve_bytes_per_row(self, 8)?;
575 let stride = stride as usize;
576 let row_bytes = (w * 8) as usize; let mut pixels: Vec<u16> = Vec::with_capacity((w * h) as usize * 4);
578 for row in 0..h as usize {
579 let row_start = row * stride;
580 if row_start + row_bytes > data.len() {
581 break;
582 }
583 for px in data[row_start..row_start + row_bytes].chunks_exact(8) {
585 let b = u16::from_le_bytes([px[0], px[1]]);
586 let g = u16::from_le_bytes([px[2], px[3]]);
587 let r = u16::from_le_bytes([px[4], px[5]]);
588 let a = u16::from_le_bytes([px[6], px[7]]);
589 pixels.push(r);
590 pixels.push(g);
591 pixels.push(b);
592 pixels.push(a);
593 }
594 }
595 unpremultiply_rgba16_in_place(&mut pixels);
596 let buf = ImageBuffer::from_raw(w, h, pixels)
597 .ok_or_else(|| CarError::DecodeFailed("ARGB16 buffer size mismatch".into()))?;
598 Ok(DynamicImage::ImageRgba16(buf))
599 }
600 Encoding::GA16 => {
601 let stride = resolve_bytes_per_row(self, 4)?;
602 let stride = stride as usize;
603 let row_bytes = (w * 4) as usize; let mut pixels: Vec<u16> = Vec::with_capacity((w * h) as usize * 2);
605 for row in 0..h as usize {
606 let row_start = row * stride;
607 if row_start + row_bytes > data.len() {
608 break;
609 }
610 for px in data[row_start..row_start + row_bytes].chunks_exact(4) {
611 pixels.push(u16::from_le_bytes([px[0], px[1]]));
612 pixels.push(u16::from_le_bytes([px[2], px[3]]));
613 }
614 }
615 let buf = ImageBuffer::from_raw(w, h, pixels)
616 .ok_or_else(|| CarError::DecodeFailed("GA16 buffer size mismatch".into()))?;
617 Ok(DynamicImage::ImageLumaA16(buf))
618 }
619 Encoding::RGB5 => {
620 let stride = resolve_bytes_per_row(self, 2)?;
621 let stripped = strip_stride(data, w, h, 2, stride);
622 let mut pixels = Vec::with_capacity((w * h * 3) as usize);
624 for pair in stripped.chunks_exact(2) {
625 let val = u16::from_le_bytes([pair[0], pair[1]]);
626 let r5 = ((val >> 10) & 0x1F) as u8;
627 let g5 = ((val >> 5) & 0x1F) as u8;
628 let b5 = (val & 0x1F) as u8;
629 pixels.push((r5 << 3) | (r5 >> 2));
630 pixels.push((g5 << 3) | (g5 >> 2));
631 pixels.push((b5 << 3) | (b5 >> 2));
632 }
633 let buf = ImageBuffer::from_raw(w, h, pixels)
634 .ok_or_else(|| CarError::DecodeFailed("RGB5 buffer size mismatch".into()))?;
635 Ok(DynamicImage::ImageRgb8(buf))
636 }
637 Encoding::JPEG | Encoding::WEBP => Ok(image::load_from_memory(&data)?),
638 _ => Err(CarError::UnsupportedEncoding(self.header.encoding)),
639 }
640 }
641
642 pub fn rgba_bytes(&self) -> CarResult<RgbaPayload> {
643 self.rgba_bytes_with_crops(&[])
644 }
645
646 pub fn rgba_bytes_with_crops(&self, crops: &[super::ReferenceRect]) -> CarResult<RgbaPayload> {
647 let img = render_image_with_crops(self, crops)?.to_rgba8();
648 Ok(RgbaPayload {
649 width: img.width(),
650 height: img.height(),
651 rgba: img.into_raw(),
652 })
653 }
654
655 pub fn png_bytes(&self) -> CarResult<Vec<u8>> {
656 self.png_bytes_with_crops(&[])
657 }
658
659 pub fn png_bytes_with_crops(&self, crops: &[super::ReferenceRect]) -> CarResult<Vec<u8>> {
660 let img = render_image_with_crops(self, crops)?;
661 encode_png_bytes(self, &img)
662 }
663
664 pub fn save_image_with_crops(
671 &self,
672 path: impl AsRef<Path>,
673 crops: &[super::ReferenceRect],
674 ) -> CarResult<()> {
675 let path = path.as_ref();
676 let img = render_image_with_crops(self, crops)?;
677
678 if is_png_path(path) {
679 write_png_with_icc(self, &img, path)?;
680 } else {
681 img.save(path)?;
682 }
683
684 Ok(())
685 }
686
687 pub fn save_image(&self, path: impl AsRef<Path>) -> CarResult<()> {
688 let path = path.as_ref();
689
690 let is_png = is_png_path(path);
692 if is_png && save_image_to_png_direct(self, path)? {
693 return Ok(());
694 }
695
696 let img = self.to_image()?;
698
699 if is_png {
700 write_png_with_icc(self, &img, path)?;
701 return Ok(());
702 }
703
704 img.save(path)?;
705 Ok(())
706 }
707}
708
709impl CSIItem {
710 pub fn source_bytes(&self) -> CarResult<Cow<'_, [u8]>> {
711 use crate::decode::RenditionDataRef;
712
713 if let Some(bytes) = hevc_source_bytes(self)? {
714 return Ok(bytes);
715 }
716
717 let preserves_source = matches!(
718 self.header.encoding,
719 Encoding::Data
720 | Encoding::HEIF
721 | Encoding::PDF
722 | Encoding::SVG
723 | Encoding::JPEG
724 | Encoding::WEBP
725 );
726 if !preserves_source {
727 return Err(CarError::UnsupportedEncoding(self.header.encoding));
728 }
729
730 match &self.header.rendition {
731 Some(Rendition::RawData(rd)) => {
732 return crate::apple_compression::maybe_decode_lzfse_with_fallback(
733 rd.raw_data.as_slice(),
734 )
735 .map_err(CarError::from);
736 }
737 Some(Rendition::ThemeCBCK(cbck))
738 if matches!(cbck.compression_type, CompressionType::Uncompressed)
739 && matches!(
740 self.header.encoding,
741 Encoding::HEIF
742 | Encoding::PDF
743 | Encoding::SVG
744 | Encoding::JPEG
745 | Encoding::WEBP
746 ) =>
747 {
748 let mut bytes = Vec::new();
749 for chunk in &cbck.chunks {
750 let data = crate::apple_compression::maybe_decode_lzfse_with_fallback(
751 chunk.raw_data.as_slice(),
752 )?;
753 bytes.extend_from_slice(data.as_ref());
754 }
755 return Ok(Cow::Owned(bytes));
756 }
757 _ => {}
758 }
759
760 match self.decode_data_ref()? {
761 Some(RenditionDataRef::RawData { data }) => Ok(data),
762 Some(RenditionDataRef::Image { data }) => Ok(data),
763 _ => Err(CarError::UnsupportedEncoding(self.header.encoding)),
764 }
765 }
766
767 pub fn source_bytes_owned(&self) -> CarResult<Vec<u8>> {
768 self.source_bytes().map(Cow::into_owned)
769 }
770
771 pub fn save_raw(&self, path: impl AsRef<Path>) -> CarResult<()> {
772 let path = path.as_ref();
773 let data = self.source_bytes()?;
774 std::fs::write(path, data.as_ref())?;
775 Ok(())
776 }
777}
778
779#[cfg(test)]
780mod test_helpers {
781 use std::io::Cursor;
782
783 use crate::car::CSIItem;
784 use crate::model::CSIHeader;
785 use deku::{DekuReader, reader::Reader};
786
787 pub fn build_csi_bytes(
788 width: u32,
789 height: u32,
790 encoding: &[u8; 4],
791 tlv: &[u8],
792 rendition: Option<&[u8]>,
793 ) -> Vec<u8> {
794 let mut b = Vec::new();
795 b.extend_from_slice(b"ISTC");
796 b.extend_from_slice(&1u32.to_le_bytes()); b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&width.to_le_bytes());
799 b.extend_from_slice(&height.to_le_bytes());
800 b.extend_from_slice(&100u32.to_le_bytes()); b.extend_from_slice(encoding);
802 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());
807 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());
810 b.extend_from_slice(tlv);
811 if let Some(r) = rendition {
812 b.extend_from_slice(r);
813 }
814 b
815 }
816
817 pub fn parse_item(bytes: Vec<u8>) -> CSIItem {
818 let mut reader = Reader::new(Cursor::new(bytes));
819 let header =
820 CSIHeader::from_reader_with_ctx(&mut reader, ()).expect("CSI header should parse");
821 CSIItem {
822 attrs: Vec::new(),
823 header,
824 key_values: Box::default(),
825 }
826 }
827
828 pub fn uncompressed_rendition(data: &[u8]) -> Vec<u8> {
829 let mut b = Vec::new();
830 b.extend_from_slice(b"MLEC");
831 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());
834 b.extend_from_slice(data);
835 b
836 }
837
838 pub fn hevc_rendition(heic_body: &[u8]) -> Vec<u8> {
839 let mut chunk = Vec::new();
840 chunk.extend_from_slice(&1u32.to_le_bytes());
841 chunk.extend_from_slice(&(heic_body.len() as u32).to_le_bytes());
842 chunk.extend_from_slice(heic_body);
843
844 let mut b = Vec::new();
845 b.extend_from_slice(b"MLEC");
846 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());
849 b.extend_from_slice(&chunk);
850 b
851 }
852
853 pub fn fake_heic_body() -> Vec<u8> {
854 let mut body = Vec::new();
855 body.extend_from_slice(&24u32.to_be_bytes());
856 body.extend_from_slice(b"ftyp");
857 body.extend_from_slice(b"heic");
858 body.extend_from_slice(&0u32.to_be_bytes());
859 body.extend_from_slice(b"mif1");
860 body.extend_from_slice(b"heic");
861 body.extend_from_slice(&8u32.to_be_bytes());
862 body.extend_from_slice(b"mdat");
863 body
864 }
865
866 pub fn raw_data_rendition(data: &[u8]) -> Vec<u8> {
867 let mut b = Vec::new();
868 b.extend_from_slice(b"DWAR");
869 b.extend_from_slice(&1u32.to_le_bytes()); b.extend_from_slice(&(data.len() as u32).to_le_bytes());
871 b.extend_from_slice(data);
872 b
873 }
874
875 pub fn bytes_per_row_tlv(bpr: u32) -> Vec<u8> {
876 let mut b = Vec::new();
877 b.extend_from_slice(&1007u32.to_le_bytes()); b.extend_from_slice(&4u32.to_le_bytes()); b.extend_from_slice(&bpr.to_le_bytes());
880 b
881 }
882
883 pub fn exif_orientation_tlv(orientation: u32) -> Vec<u8> {
884 let mut b = Vec::new();
885 b.extend_from_slice(&1006u32.to_le_bytes()); b.extend_from_slice(&4u32.to_le_bytes()); b.extend_from_slice(&orientation.to_le_bytes());
888 b
889 }
890
891 pub fn deepmap2_rendition(width: u16, height: u16, pixels: &[u8]) -> Vec<u8> {
892 let mut dmp2 = Vec::new();
894 dmp2.extend_from_slice(b"dmp2");
895 dmp2.extend_from_slice(&[0x01, 0x00, 0x00, 0x01]); dmp2.extend_from_slice(&width.to_le_bytes());
897 dmp2.extend_from_slice(&height.to_le_bytes());
898 dmp2.extend_from_slice(pixels);
899 let mut b = Vec::new();
901 b.extend_from_slice(b"MLEC");
902 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());
905 b.extend_from_slice(&dmp2);
906 b
907 }
908
909 pub fn deepmap2_rendition_ga88(width: u16, height: u16, pixels: &[u8]) -> Vec<u8> {
910 let mut dmp2 = Vec::new();
911 dmp2.extend_from_slice(b"dmp2");
912 dmp2.extend_from_slice(&[0x01, 0x00, 0x00, 0x02]); dmp2.extend_from_slice(&width.to_le_bytes());
914 dmp2.extend_from_slice(&height.to_le_bytes());
915 dmp2.extend_from_slice(pixels);
916
917 let mut b = Vec::new();
918 b.extend_from_slice(b"MLEC");
919 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());
922 b.extend_from_slice(&dmp2);
923 b
924 }
925
926 pub fn make_item(width: u32, height: u32, enc: &[u8; 4], pixels: &[u8]) -> CSIItem {
927 let r = uncompressed_rendition(pixels);
928 parse_item(build_csi_bytes(width, height, enc, &[], Some(&r)))
929 }
930
931 pub fn make_item_stride(
932 width: u32,
933 height: u32,
934 enc: &[u8; 4],
935 pixels: &[u8],
936 bpr: u32,
937 ) -> CSIItem {
938 let r = uncompressed_rendition(pixels);
939 let tlv = bytes_per_row_tlv(bpr);
940 parse_item(build_csi_bytes(width, height, enc, &tlv, Some(&r)))
941 }
942}
943
944#[cfg(test)]
945#[cfg(feature = "image")]
946mod tests {
947 use std::io::Cursor;
948
949 use super::test_helpers::*;
950 use crate::DecodeOptions;
951 use crate::car::CarError;
952
953 #[test]
956 fn to_image_argb_swizzle() {
957 let pixels = [10u8, 20, 30, 255, 32, 64, 128, 128];
960 let item = make_item(2, 1, b"BGRA", &pixels);
961 let img = item.to_image().unwrap();
962 let rgba = img.to_rgba8();
963 assert_eq!(rgba.get_pixel(0, 0).0, [30, 20, 10, 255]);
964 assert_eq!(rgba.get_pixel(1, 0).0, [255, 128, 64, 128]);
965 }
966
967 #[test]
970 fn to_image_argb16_swizzle_le() {
971 let pixels = [
975 0x00u8, 0x20, 0x00, 0x40, 0x00, 0x80, 0x00, 0x80, ];
980 let item = make_item(1, 1, b"WBGR", &pixels);
981 let img = item.to_image().unwrap();
982 let rgba16 = img.to_rgba16();
983 assert_eq!(rgba16.get_pixel(0, 0).0, [65535, 32768, 16384, 32768]);
984 }
985
986 #[test]
989 fn to_image_gray() {
990 let pixels = [100u8, 200];
991 let item = make_item(2, 1, b"YARG", &pixels);
992 let img = item.to_image().unwrap();
993 let luma = img.to_luma8();
994 assert_eq!(luma.get_pixel(0, 0).0, [100]);
995 assert_eq!(luma.get_pixel(1, 0).0, [200]);
996 }
997
998 #[test]
1001 fn to_image_ga8() {
1002 let pixels = [100u8, 200]; let item = make_item(1, 1, b" 8AG", &pixels);
1004 let img = item.to_image().unwrap();
1005 let la = img.to_luma_alpha8();
1006 assert_eq!(la.get_pixel(0, 0).0, [100, 200]);
1007 }
1008
1009 #[test]
1012 fn to_image_ga16_le() {
1013 let pixels = [0x34u8, 0x12, 0x78, 0x56];
1015 let item = make_item(1, 1, b"61AG", &pixels);
1016 let img = item.to_image().unwrap();
1017 let la16 = img.to_luma_alpha16();
1018 assert_eq!(la16.get_pixel(0, 0).0, [0x1234, 0x5678]);
1019 }
1020
1021 #[test]
1024 fn to_image_rgb5() {
1025 let pixels = 0x7C00u16.to_le_bytes();
1027 let item = make_item(1, 1, b"5BGR", &pixels);
1028 let img = item.to_image().unwrap();
1029 let rgb = img.to_rgb8();
1030 assert_eq!(rgb.get_pixel(0, 0).0, [255, 0, 0]);
1032 }
1033
1034 #[test]
1037 fn to_image_deepmap2_rgba() {
1038 let r = deepmap2_rendition(2, 1, &[100, 200]);
1041 let item = parse_item(build_csi_bytes(2, 1, b"YARG", &[], Some(&r)));
1043 let img = item.to_image().unwrap();
1044 let rgba = img.to_rgba8();
1045 assert_eq!(rgba.get_pixel(0, 0).0, [100, 100, 100, 255]);
1046 assert_eq!(rgba.get_pixel(1, 0).0, [200, 200, 200, 255]);
1047 }
1048
1049 #[test]
1050 fn to_image_deepmap2_ignores_incompatible_source_stride() {
1051 let r = deepmap2_rendition_ga88(2, 1, &[100, 255, 200, 255]);
1052 let tlv = bytes_per_row_tlv(2);
1053 let item = parse_item(build_csi_bytes(2, 1, b" 8AG", &tlv, Some(&r)));
1054 let img = item.to_image().unwrap();
1055 let rgba = img.to_rgba8();
1056 assert_eq!(rgba.get_pixel(0, 0).0, [100, 100, 100, 255]);
1057 assert_eq!(rgba.get_pixel(1, 0).0, [200, 200, 200, 255]);
1058 }
1059
1060 #[test]
1061 fn to_image_deepmap2_uses_decoded_height_when_header_mismatches() {
1062 let r = deepmap2_rendition(2, 1, &[100, 200]);
1063 let item = parse_item(build_csi_bytes(2, 2, b"YARG", &[], Some(&r)));
1064 let img = item.to_image().unwrap();
1065 assert_eq!(img.width(), 2);
1066 assert_eq!(img.height(), 1);
1067 }
1068
1069 #[test]
1072 fn to_image_exif_orientation_ignored() {
1073 let pixels = [100u8, 200];
1075 let tlv = exif_orientation_tlv(6);
1076 let r = uncompressed_rendition(&pixels);
1077 let item = parse_item(build_csi_bytes(2, 1, b"YARG", &tlv, Some(&r)));
1078 let img = item.to_image().unwrap();
1079 let luma = img.to_luma8();
1080 assert_eq!(img.width(), 2);
1082 assert_eq!(img.height(), 1);
1083 assert_eq!(luma.get_pixel(0, 0).0, [100]);
1084 assert_eq!(luma.get_pixel(1, 0).0, [200]);
1085 }
1086
1087 #[test]
1088 fn to_image_exif_orientation_can_be_applied() {
1089 let pixels = [
1090 1u8, 2, 3, 4, ];
1093 let tlv = exif_orientation_tlv(6);
1094 let r = uncompressed_rendition(&pixels);
1095 let item = parse_item(build_csi_bytes(2, 2, b"YARG", &tlv, Some(&r)));
1096
1097 let img = item
1098 .to_image_with_options(&DecodeOptions::default().apply_exif_orientation(true))
1099 .unwrap();
1100 let luma = img.to_luma8();
1101
1102 assert_eq!(img.width(), 2);
1103 assert_eq!(img.height(), 2);
1104 assert_eq!(luma.get_pixel(0, 0).0, [3]);
1105 assert_eq!(luma.get_pixel(1, 0).0, [1]);
1106 assert_eq!(luma.get_pixel(0, 1).0, [4]);
1107 assert_eq!(luma.get_pixel(1, 1).0, [2]);
1108 }
1109
1110 #[test]
1113 fn to_image_stride_strips_padding() {
1114 let pixels = [100u8, 0, 0, 0, 200, 0, 0, 0];
1116 let item = make_item_stride(1, 2, b"YARG", &pixels, 4);
1117 let img = item.to_image().unwrap();
1118 let luma = img.to_luma8();
1119 assert_eq!(luma.get_pixel(0, 0).0, [100]);
1120 assert_eq!(luma.get_pixel(0, 1).0, [200]);
1121 }
1122
1123 #[test]
1124 fn to_image_invalid_bytes_per_row() {
1125 let pixels = [0u8; 8];
1127 let item = make_item_stride(2, 1, b"BGRA", &pixels, 4);
1128 let err = item.to_image().unwrap_err();
1129 assert!(matches!(err, CarError::DecodeFailed(_)));
1130 assert!(format!("{err}").contains("bytes_per_row"));
1131 }
1132
1133 #[test]
1136 fn to_image_jpeg() {
1137 use image::{ImageBuffer, ImageFormat, RgbImage};
1138 let rgb: RgbImage = ImageBuffer::from_raw(1, 1, vec![255u8, 0, 0]).unwrap();
1140 let mut buf = Cursor::new(Vec::new());
1141 rgb.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
1142 let jpeg_bytes = buf.into_inner();
1143
1144 let item = make_item(1, 1, b"GEPJ", &jpeg_bytes);
1145 let img = item.to_image().unwrap();
1146 assert_eq!(img.width(), 1);
1147 assert_eq!(img.height(), 1);
1148 }
1149
1150 #[test]
1153 fn to_image_unsupported_encoding() {
1154 let item = make_item(1, 1, b"ATAD", &[0u8; 4]);
1155 let err = item.to_image().unwrap_err();
1156 assert!(matches!(err, CarError::UnsupportedEncoding(_)));
1157 }
1158
1159 #[test]
1162 fn save_image_png() {
1163 let pixels = [10u8, 20, 30, 255]; let item = make_item(1, 1, b"BGRA", &pixels);
1165 let dir = std::env::temp_dir().join("carparser_test_img");
1166 let _ = std::fs::create_dir_all(&dir);
1167 let path = dir.join("test_save.png");
1168 item.save_image(&path).unwrap();
1169 let img = image::open(&path).unwrap();
1170 assert_eq!(img.width(), 1);
1171 assert_eq!(img.height(), 1);
1172 let _ = std::fs::remove_file(&path);
1173 }
1174
1175 #[test]
1176 fn save_image_unsupported_format() {
1177 let pixels = [10u8, 20, 30, 255]; let item = make_item(1, 1, b"BGRA", &pixels);
1179 let dir = std::env::temp_dir().join("carparser_test_img");
1180 let _ = std::fs::create_dir_all(&dir);
1181 let path = dir.join("test_save.bmp");
1182 let err = item.save_image(&path).unwrap_err();
1183 assert!(matches!(err, CarError::Image(_)));
1184 }
1185
1186 #[test]
1189 fn save_image_png_direct_argb_pixel_correct() {
1190 let pixels = [10u8, 20, 30, 255];
1192 let item = make_item(1, 1, b"BGRA", &pixels);
1193 let dir = std::env::temp_dir().join("carparser_direct_argb");
1194 let _ = std::fs::create_dir_all(&dir);
1195 let path = dir.join("argb.png");
1196 item.save_image(&path).unwrap();
1197 let img = image::open(&path).unwrap().to_rgba8();
1198 assert_eq!(img.get_pixel(0, 0).0, [30, 20, 10, 255]);
1199 let _ = std::fs::remove_file(&path);
1200 }
1201
1202 #[test]
1203 fn save_image_png_direct_argb_with_stride() {
1204 let pixels = [1u8, 2, 3, 255, 0, 0, 0, 0, 4, 5, 6, 128, 0, 0, 0, 0];
1209 let item = make_item_stride(1, 2, b"BGRA", &pixels, 8);
1210 let dir = std::env::temp_dir().join("carparser_direct_argb_stride");
1211 let _ = std::fs::create_dir_all(&dir);
1212 let path = dir.join("argb_stride.png");
1213 item.save_image(&path).unwrap();
1214 let img = image::open(&path).unwrap().to_rgba8();
1215 assert_eq!(img.dimensions(), (1, 2));
1216 assert_eq!(img.get_pixel(0, 0).0, [3, 2, 1, 255]);
1217 assert_eq!(img.get_pixel(0, 1).0, [12, 10, 8, 128]);
1218 let _ = std::fs::remove_file(&path);
1219 }
1220
1221 #[test]
1222 fn save_image_png_direct_ga8_pixel_correct() {
1223 let pixels = [128u8, 200];
1225 let item = make_item(1, 1, b" 8AG", &pixels);
1226 let dir = std::env::temp_dir().join("carparser_direct_ga8");
1227 let _ = std::fs::create_dir_all(&dir);
1228 let path = dir.join("ga8.png");
1229 item.save_image(&path).unwrap();
1230 let img = image::open(&path).unwrap().to_luma_alpha8();
1231 assert_eq!(img.get_pixel(0, 0).0, [128, 200]);
1232 let _ = std::fs::remove_file(&path);
1233 }
1234
1235 #[test]
1236 fn save_image_png_direct_argb16_pixel_correct() {
1237 let pixels = [
1241 0x00u8, 0x20, 0x00, 0x40, 0x00, 0x80, 0x00, 0x80, ];
1246 let item = make_item(1, 1, b"WBGR", &pixels);
1247 let dir = std::env::temp_dir().join("carparser_direct_argb16");
1248 let _ = std::fs::create_dir_all(&dir);
1249 let path = dir.join("argb16.png");
1250 item.save_image(&path).unwrap();
1251 let img = image::open(&path).unwrap().to_rgba16();
1252 assert_eq!(img.get_pixel(0, 0).0, [65535, 32768, 16384, 32768]);
1253 let _ = std::fs::remove_file(&path);
1254 }
1255
1256 #[test]
1257 fn save_image_png_direct_deepmap2_pixel_correct() {
1258 let r = deepmap2_rendition(2, 1, &[100, 200]);
1260 let item = parse_item(build_csi_bytes(2, 1, b"YARG", &[], Some(&r)));
1261 let dir = std::env::temp_dir().join("carparser_direct_dmp2");
1262 let _ = std::fs::create_dir_all(&dir);
1263 let path = dir.join("deepmap2.png");
1264 item.save_image(&path).unwrap();
1265 let img = image::open(&path).unwrap().to_rgba8();
1266 assert_eq!(img.get_pixel(0, 0).0, [100, 100, 100, 255]);
1267 assert_eq!(img.get_pixel(1, 0).0, [200, 200, 200, 255]);
1268 let _ = std::fs::remove_file(&path);
1269 }
1270
1271 #[test]
1272 fn rgba_bytes_matches_rendered_rgba_output() {
1273 let pixels = [10u8, 20, 30, 255];
1274 let item = make_item(1, 1, b"BGRA", &pixels);
1275
1276 let rendered = item.to_image().unwrap().to_rgba8();
1277 let rgba = item.rgba_bytes().unwrap();
1278
1279 assert_eq!(rgba.width, 1);
1280 assert_eq!(rgba.height, 1);
1281 assert_eq!(rgba.rgba, rendered.into_raw());
1282 }
1283
1284 #[test]
1285 fn png_bytes_encodes_png_in_memory() {
1286 let pixels = [10u8, 20, 30, 255];
1287 let item = make_item(1, 1, b"BGRA", &pixels);
1288
1289 let png = item.png_bytes().unwrap();
1290 let decoded = image::load_from_memory_with_format(&png, image::ImageFormat::Png)
1291 .unwrap()
1292 .to_rgba8();
1293
1294 assert_eq!(decoded.dimensions(), (1, 1));
1295 assert_eq!(decoded.get_pixel(0, 0).0, [30, 20, 10, 255]);
1296 }
1297}
1298
1299#[cfg(test)]
1300mod save_raw_tests {
1301 use super::test_helpers::*;
1302 use crate::Encoding;
1303 use crate::car::{Car, CarError};
1304 use crate::model::rendition::Rendition;
1305
1306 #[test]
1307 fn save_raw_with_raw_data() {
1308 let payload = b"hello-raw-data";
1309 let r = raw_data_rendition(payload);
1310 let item = parse_item(build_csi_bytes(1, 1, b"ATAD", &[], Some(&r)));
1311 let dir = std::env::temp_dir().join("carparser_test_raw");
1312 let _ = std::fs::create_dir_all(&dir);
1313 let path = dir.join("test.bin");
1314 item.save_raw(&path).unwrap();
1315 assert_eq!(std::fs::read(&path).unwrap(), payload);
1316 let _ = std::fs::remove_file(&path);
1317 }
1318
1319 #[test]
1320 fn source_bytes_with_raw_data() {
1321 let payload = b"hello-raw-data";
1322 let r = raw_data_rendition(payload);
1323 let item = parse_item(build_csi_bytes(1, 1, b"ATAD", &[], Some(&r)));
1324
1325 let bytes = item.source_bytes().unwrap();
1326 assert_eq!(bytes.as_ref(), payload);
1327 }
1328
1329 #[test]
1330 fn save_raw_heif_encoding() {
1331 let payload = b"fake-heif-data";
1332 let item = make_item(1, 1, b"FIEH", payload);
1333 let dir = std::env::temp_dir().join("carparser_test_raw");
1334 let _ = std::fs::create_dir_all(&dir);
1335 let path = dir.join("test.heif");
1336 item.save_raw(&path).unwrap();
1337 assert_eq!(std::fs::read(&path).unwrap(), payload);
1338 let _ = std::fs::remove_file(&path);
1339 }
1340
1341 #[test]
1342 fn source_bytes_hevc_cbck_strips_coreui_wrapper() {
1343 let heic = fake_heic_body();
1344 let rendition = hevc_rendition(&heic);
1345 let item = parse_item(build_csi_bytes(20, 88, b"BGRA", &[], Some(&rendition)));
1346
1347 let bytes = item.source_bytes().unwrap();
1348 assert_eq!(bytes.as_ref(), heic.as_slice());
1349 }
1350
1351 #[test]
1352 fn save_raw_hevc_cbck_writes_heic_container() {
1353 let heic = fake_heic_body();
1354 let rendition = hevc_rendition(&heic);
1355 let item = parse_item(build_csi_bytes(20, 88, b"BGRA", &[], Some(&rendition)));
1356 let dir = std::env::temp_dir().join("carparser_test_raw");
1357 let _ = std::fs::create_dir_all(&dir);
1358 let path = dir.join("hevc.heic");
1359
1360 item.save_raw(&path).unwrap();
1361 assert_eq!(std::fs::read(&path).unwrap(), heic);
1362 let _ = std::fs::remove_file(&path);
1363 }
1364
1365 #[test]
1366 fn save_raw_pdf_encoding() {
1367 let payload = b"fake-pdf-data";
1368 let item = make_item(1, 1, b" FDP", payload);
1369 let dir = std::env::temp_dir().join("carparser_test_raw");
1370 let _ = std::fs::create_dir_all(&dir);
1371 let path = dir.join("test.pdf");
1372 item.save_raw(&path).unwrap();
1373 assert_eq!(std::fs::read(&path).unwrap(), payload);
1374 let _ = std::fs::remove_file(&path);
1375 }
1376
1377 #[test]
1378 fn save_raw_svg_encoding() {
1379 let payload = b"<svg></svg>";
1380 let item = make_item(1, 1, b" GVS", payload);
1381 let dir = std::env::temp_dir().join("carparser_test_raw");
1382 let _ = std::fs::create_dir_all(&dir);
1383 let path = dir.join("test.svg");
1384 item.save_raw(&path).unwrap();
1385 assert_eq!(std::fs::read(&path).unwrap(), payload);
1386 let _ = std::fs::remove_file(&path);
1387 }
1388
1389 #[test]
1390 fn source_bytes_preserves_svg_payload_in_memory() {
1391 let payload = b"<svg></svg>";
1392 let item = make_item(1, 1, b" GVS", payload);
1393
1394 let bytes = item.source_bytes().unwrap();
1395 assert_eq!(bytes.as_ref(), payload);
1396 }
1397
1398 #[test]
1399 fn save_raw_rejects_pixel_format_encoding() {
1400 let item = make_item(1, 1, b"BGRA", &[0u8; 4]);
1401 let dir = std::env::temp_dir().join("carparser_test_raw");
1402 let _ = std::fs::create_dir_all(&dir);
1403 let path = dir.join("test_reject.bin");
1404 let err = item.save_raw(&path).unwrap_err();
1405 assert!(matches!(err, CarError::UnsupportedEncoding(_)));
1406 }
1407
1408 #[test]
1411 fn save_raw_heif_uncompressed_cbck_direct() {
1412 let payload = b"heif-cbck-direct-write";
1415 let item = make_item(1, 1, b"FIEH", payload);
1416 let dir = std::env::temp_dir().join("carparser_raw_direct");
1417 let _ = std::fs::create_dir_all(&dir);
1418 let path = dir.join("heif_direct.heic");
1419 item.save_raw(&path).unwrap();
1420 assert_eq!(std::fs::read(&path).unwrap(), payload);
1421 let _ = std::fs::remove_file(&path);
1422 }
1423
1424 #[test]
1425 fn save_raw_pdf_uncompressed_cbck_direct() {
1426 let payload = b"%PDF-1.4 direct-write";
1427 let item = make_item(1, 1, b" FDP", payload);
1428 let dir = std::env::temp_dir().join("carparser_raw_direct");
1429 let _ = std::fs::create_dir_all(&dir);
1430 let path = dir.join("pdf_direct.pdf");
1431 item.save_raw(&path).unwrap();
1432 assert_eq!(std::fs::read(&path).unwrap(), payload);
1433 let _ = std::fs::remove_file(&path);
1434 }
1435
1436 #[test]
1437 fn save_raw_rawdata_direct_write() {
1438 let payload = b"rawdata-no-clone-path";
1440 let r = raw_data_rendition(payload);
1441 let item = parse_item(build_csi_bytes(1, 1, b"ATAD", &[], Some(&r)));
1442 let dir = std::env::temp_dir().join("carparser_raw_direct");
1443 let _ = std::fs::create_dir_all(&dir);
1444 let path = dir.join("rawdata_direct.bin");
1445 item.save_raw(&path).unwrap();
1446 assert_eq!(std::fs::read(&path).unwrap(), payload);
1447 let _ = std::fs::remove_file(&path);
1448 }
1449
1450 fn fixture_ios_car() -> Option<Car> {
1451 let full_fixtures_enabled = matches!(
1452 std::env::var("CAR_TEST_FULL").as_deref(),
1453 Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES")
1454 );
1455 if !full_fixtures_enabled {
1456 eprintln!("skipping full fixture test: set CAR_TEST_FULL=1 to enable Assets_iOS.car");
1457 return None;
1458 }
1459
1460 let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1461 .join("../car-tests/data")
1462 .join("Assets_iOS.car");
1463 if !path.exists() {
1464 eprintln!("skipping full fixture test: missing {}", path.display());
1465 return None;
1466 }
1467
1468 Some(Car::new(path).expect("load test Assets_iOS.car"))
1469 }
1470
1471 #[test]
1472 fn save_raw_assets_ios_svg_rawdata_decodes_apple_compression_stream() {
1473 let Some(car) = fixture_ios_car() else {
1474 return;
1475 };
1476 let item = car
1477 .rendtions_with_name("icon_device_migration")
1478 .and_then(|items| {
1479 items.iter().find(|item| {
1480 matches!(item.header.rendition, Some(Rendition::RawData(_)))
1481 && matches!(item.header.encoding, Encoding::SVG)
1482 })
1483 })
1484 .expect("missing icon_device_migration SVG RawData rendition");
1485
1486 let dir = std::env::temp_dir().join("carparser_raw_fixture");
1487 let _ = std::fs::create_dir_all(&dir);
1488 let path = dir.join("icon_device_migration.svg");
1489
1490 item.save_raw(&path).unwrap();
1491 let saved = std::fs::read(&path).unwrap();
1492
1493 assert!(
1494 saved.starts_with(b"<?xml"),
1495 "expected decoded SVG XML, got magic {:?}",
1496 saved.get(..4)
1497 );
1498 assert!(saved.windows(b"<svg".len()).any(|w| w == b"<svg"));
1499 assert!(
1500 !saved.starts_with(b"bvx"),
1501 "saved file should not keep Apple compression stream magic"
1502 );
1503
1504 let _ = std::fs::remove_file(&path);
1505 }
1506
1507 #[cfg(feature = "image")]
1517 #[test]
1518 fn apply_reference_frame_crops_atlas_subregion() {
1519 use super::apply_reference_frame;
1520 use crate::car::ReferenceRect;
1521
1522 let source = image::RgbaImage::from_fn(12, 4, |x, y| {
1523 if y < 2 {
1524 if x < 6 {
1525 image::Rgba([255u8, 0, 0, 255]) } else {
1527 image::Rgba([0u8, 0, 255, 255]) }
1529 } else {
1530 image::Rgba([0u8, 255, 0, 255]) }
1532 });
1533 let img = image::DynamicImage::ImageRgba8(source);
1534
1535 let rect = ReferenceRect {
1536 x: 6,
1537 y: 2,
1538 width: 6,
1539 height: 2,
1540 };
1541 let result = apply_reference_frame(img, &rect).unwrap();
1542
1543 assert_eq!(result.width(), 6);
1544 assert_eq!(result.height(), 2);
1545 let rgba = result.to_rgba8();
1546 assert_eq!(
1548 rgba.get_pixel(0, 0).0,
1549 [0, 0, 255, 255],
1550 "top-left should be blue"
1551 );
1552 assert_eq!(
1553 rgba.get_pixel(5, 1).0,
1554 [0, 0, 255, 255],
1555 "bottom-right should be blue"
1556 );
1557 assert_ne!(rgba.get_pixel(0, 0).0[0], 255, "no red channel leak");
1559 }
1560
1561 #[cfg(feature = "image")]
1564 #[test]
1565 fn apply_reference_frame_rejects_out_of_bounds_rect() {
1566 use super::apply_reference_frame;
1567 use crate::car::ReferenceRect;
1568
1569 let img = image::DynamicImage::ImageRgba8(image::RgbaImage::new(4, 4));
1570
1571 let rect = ReferenceRect {
1572 x: 0,
1573 y: 2,
1574 width: 4,
1575 height: 8,
1576 };
1577 let err = apply_reference_frame(img, &rect).unwrap_err();
1578 assert!(matches!(err, CarError::DecodeFailed(_)));
1579 assert!(format!("{err}").contains("exceeds source image"));
1580 }
1581}