Skip to main content

car/
image_conv.rs

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/// Crop `img` to the sub-region described by `rect`.
211///
212/// `RenditionTypeReference.(x, y, width, height)` stores a CGRect-style frame whose
213/// origin is measured from the source image's bottom-left corner. `image` uses a
214/// top-left origin, so `y` must be flipped before cropping.
215#[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/// 热路径 PNG 直写:对 Deepmap2/ARGB/GA8/ARGB16 跳过 DynamicImage 中间层,
346/// 将解码后的像素缓冲直接送入 PngEncoder,减少中间分配与多遍扫描。
347///
348/// 返回 `Ok(true)` 表示已处理,`Ok(false)` 表示当前编码不在直接路径上,
349/// 调用方应回退到 `to_image()` + `save_image()` 通用路径。
350#[cfg(feature = "image")]
351fn save_image_to_png_direct(item: &CSIItem, path: &Path) -> CarResult<bool> {
352    use image::{ExtendedColorType, ImageEncoder};
353
354    // 提前检查编码是否在直写白名单中,避免对 GRAY/GA16/RGB5/JPEG/WEBP 等编码调用
355    // decode_data(),随后又回退到 to_image() 导致整段数据被二次解码。
356    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    // 每种编码产出 (像素字节, 颜色类型, 输出宽, 输出高)
373    let (pixels, color_type, out_w, out_h): (Vec<u8>, ExtendedColorType, u32, u32) = if is_deepmap2
374    {
375        // Deepmap2 解码输出恒为 RGBA8888;仅在长度与期望不符时才做 stride 剥离
376        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        // CoreUI stores Deepmap2 RGBA payloads with premultiplied alpha on
393        // semi-transparent edge pixels; convert to straight alpha before saving.
394        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                // 单遍:stride 剥离 + BGRA(预乘 alpha)→RGBA(直 alpha)
401                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]); // R (BGRA→RGBA)
412                        pixels.push(chunk[1]); // G
413                        pixels.push(chunk[0]); // B
414                        pixels.push(chunk[3]); // A
415                    }
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                // 单遍:BGRA16LE(预乘 alpha)→RGBA16NE(直 alpha)
441                // PngEncoder 的 Rgba16 颜色类型要求本机字节序 u16,不能直接透传 LE 字节。
442                let stride = resolve_bytes_per_row(item, 8)? as usize;
443                let row_bytes = (w * 8) as usize; // 4 通道 × 2 字节
444                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                        // BGRA16LE → RGBA16NE,通道重排与反预乘在同一遍完成
452                        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        // Deepmap2 special case: always RGBA8888
520        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                // Deepmap2 already returns a packed RGBA buffer. Some assets still carry a
527                // BytesPerRow TLV for the original source pixel format, which is not applicable
528                // after deepmap2 expansion. Only use stride stripping as a recovery path when the
529                // decoded byte count does not already match the target RGBA image size.
530                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                // BGRA(预乘 alpha) → RGBA(直 alpha)
551                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; // 4 channels × 2 bytes each (BGRA16)
577                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                    // BGRA16 → RGBA16: write channels in R G B A order
584                    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; // 2 channels × 2 bytes each
604                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                // XRGB1555: bit layout [x r4 r3 r2 r1 r0 g4 g3 | g2 g1 g0 b4 b3 b2 b1 b0]
623                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    /// Decode the image, apply reference crop rectangles in order, then save to `path`.
665    ///
666    /// `crops` must be ordered **innermost-first** (as produced by
667    /// `Car::resolve_internal_reference`).  Each rect is applied via
668    /// `apply_reference_frame`: crop the current image at
669    /// `(rect.x, rect.y, rect.width, rect.height)`.
670    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        // PNG 热路径:对已知像素格式直接编码,跳过 DynamicImage 中间层
691        let is_png = is_png_path(path);
692        if is_png && save_image_to_png_direct(self, path)? {
693            return Ok(());
694        }
695
696        // 通用回退路径:其他格式或 PNG 中非热路径编码(GRAY/GA16/RGB5/JPEG/WEBP)
697        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()); // version
797        b.extend_from_slice(&0u32.to_le_bytes()); // flags
798        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()); // scale_factor
801        b.extend_from_slice(encoding);
802        b.extend_from_slice(&0u32.to_le_bytes()); // color_model
803        b.extend_from_slice(&0u32.to_le_bytes()); // modification_time
804        b.extend_from_slice(&1000u32.to_le_bytes()); // layout_type
805        b.extend_from_slice(&[0u8; 128]); // name
806        b.extend_from_slice(&(tlv.len() as u32).to_le_bytes());
807        b.extend_from_slice(&1u32.to_le_bytes()); // bitmap_count
808        b.extend_from_slice(&0u32.to_le_bytes()); // zero
809        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()); // version=0 single chunk
832        b.extend_from_slice(&0u32.to_le_bytes()); // Uncompressed
833        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()); // version=0 single chunk
847        b.extend_from_slice(&9u32.to_le_bytes()); // HEVC
848        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()); // version
870        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()); // tag
878        b.extend_from_slice(&4u32.to_le_bytes()); // length
879        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()); // tag
886        b.extend_from_slice(&4u32.to_le_bytes()); // length
887        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        // Build dmp2 payload: "dmp2" + header + pixel data
893        let mut dmp2 = Vec::new();
894        dmp2.extend_from_slice(b"dmp2");
895        dmp2.extend_from_slice(&[0x01, 0x00, 0x00, 0x01]); // version=1, compression=none, format=0, pixel_type=g8
896        dmp2.extend_from_slice(&width.to_le_bytes());
897        dmp2.extend_from_slice(&height.to_le_bytes());
898        dmp2.extend_from_slice(pixels);
899        // Wrap in ThemeCBCK with compression_type=11 (Deepmap2)
900        let mut b = Vec::new();
901        b.extend_from_slice(b"MLEC");
902        b.extend_from_slice(&0u32.to_le_bytes()); // version=0 single chunk
903        b.extend_from_slice(&11u32.to_le_bytes()); // Deepmap2
904        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]); // pixel_type=ga88
913        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()); // version=0 single chunk
920        b.extend_from_slice(&11u32.to_le_bytes()); // Deepmap2
921        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    // ── ARGB (BGRA → RGBA swizzle) ─────────────────────────────────
954
955    #[test]
956    fn to_image_argb_swizzle() {
957        // 2×1 BGRA pixels; second pixel uses premultiplied alpha:
958        // straight RGBA [255, 128, 64, 128] -> stored BGRA [32, 64, 128, 128]
959        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    // ── ARGB16 (BGRA16 → RGBA16, little-endian) ────────────────────
968
969    #[test]
970    fn to_image_argb16_swizzle_le() {
971        // 1×1 BGRA16 (premultiplied alpha):
972        // straight RGBA16 [65535, 32768, 16384, 32768]
973        // -> stored BGRA16 [8192, 16384, 32768, 32768]
974        let pixels = [
975            0x00u8, 0x20, // B=8192
976            0x00, 0x40, // G=16384
977            0x00, 0x80, // R=32768
978            0x00, 0x80, // A=32768
979        ];
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    // ── GRAY ────────────────────────────────────────────────────────
987
988    #[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    // ── GA8 ─────────────────────────────────────────────────────────
999
1000    #[test]
1001    fn to_image_ga8() {
1002        let pixels = [100u8, 200]; // L=100, A=200
1003        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    // ── GA16 (little-endian) ────────────────────────────────────────
1010
1011    #[test]
1012    fn to_image_ga16_le() {
1013        // L=0x1234, A=0x5678 in LE
1014        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    // ── RGB5 (XRGB1555) ────────────────────────────────────────────
1022
1023    #[test]
1024    fn to_image_rgb5() {
1025        // R=31 G=0 B=0 → 0b0_11111_00000_00000 = 0x7C00
1026        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        // R: (31<<3)|(31>>2) = 255, G: 0, B: 0
1031        assert_eq!(rgb.get_pixel(0, 0).0, [255, 0, 0]);
1032    }
1033
1034    // ── Deepmap2 special case ───────────────────────────────────────
1035
1036    #[test]
1037    fn to_image_deepmap2_rgba() {
1038        // 2×1 Deepmap2 with grayscale pixels [100, 200]
1039        // Deepmap2 always produces RGBA: [v, v, v, 0xFF] per pixel
1040        let r = deepmap2_rendition(2, 1, &[100, 200]);
1041        // Use GRAY encoding to prove Deepmap2 overrides it
1042        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    // ── EXIF orientation ────────────────────────────────────────────
1070
1071    #[test]
1072    fn to_image_exif_orientation_ignored() {
1073        // 2×1 GRAY with EXIF orientation TLV (Rotate90CW=6) present
1074        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        // Pixels unchanged — no rotation applied
1081        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, // row 0
1091            3, 4, // row 1
1092        ];
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    // ── stride padding ──────────────────────────────────────────────
1111
1112    #[test]
1113    fn to_image_stride_strips_padding() {
1114        // 1×2 GRAY, min row = 1 byte, stride = 4 (3 bytes padding per row)
1115        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        // 2×1 ARGB: min row = 8 bytes, but BytesPerRow = 4 → error
1126        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    // ── JPEG ────────────────────────────────────────────────────────
1134
1135    #[test]
1136    fn to_image_jpeg() {
1137        use image::{ImageBuffer, ImageFormat, RgbImage};
1138        // Create a minimal 1×1 JPEG in memory
1139        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    // ── unsupported encoding ────────────────────────────────────────
1151
1152    #[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    // ── save_image ──────────────────────────────────────────────────
1160
1161    #[test]
1162    fn save_image_png() {
1163        let pixels = [10u8, 20, 30, 255]; // 1×1 BGRA pixel
1164        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]; // 1×1 BGRA pixel
1178        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    // ── 直接 PNG 编码路径 ──────────────────────────────────────────────
1187
1188    #[test]
1189    fn save_image_png_direct_argb_pixel_correct() {
1190        // 1×1 BGRA: B=10 G=20 R=30 A=255 → PNG RGBA: R=30 G=20 B=10 A=255
1191        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        // 1×2 BGRA,stride=8(每行 4 字节有效 + 4 字节填充)
1205        // Row0: [B=1 G=2 R=3 A=255 PAD PAD PAD PAD]
1206        // Row1 stores premultiplied alpha for straight RGBA [12, 10, 8, 128]:
1207        // [B=4 G=5 R=6 A=128 PAD PAD PAD PAD]
1208        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        // 1×1 GA8: L=128 A=200
1224        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        // 1×1 BGRA16 LE (premultiplied alpha):
1238        // straight RGBA16 [65535, 32768, 16384, 32768]
1239        // -> stored BGRA16 [8192, 16384, 32768, 32768]
1240        let pixels = [
1241            0x00u8, 0x20, // B=8192
1242            0x00, 0x40, // G=16384
1243            0x00, 0x80, // R=32768
1244            0x00, 0x80, // A=32768
1245        ];
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        // 2×1 Deepmap2 灰度 → RGBA [v v v 255] per pixel
1259        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    // ── 直接写路径:ThemeCBCK Uncompressed HEIF/PDF/SVG ───────────────
1409
1410    #[test]
1411    fn save_raw_heif_uncompressed_cbck_direct() {
1412        // HEIF 存储为 ThemeCBCK Uncompressed(make_item 构造的正是这种结构)
1413        // 验证直接写路径产出与 payload 完全一致
1414        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        // RawData 渲染类型直写:不经 decode_data() clone
1439        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    /// Verify that `apply_reference_frame` crops a sub-region from the source atlas.
1508    ///
1509    /// Atlas layout (12×4):
1510    ///   cols 0-5, rows 0-1: red
1511    ///   cols 6-11, rows 0-1: blue
1512    ///   all cols,  rows 2-3: green
1513    ///
1514    /// Crop rect (x=6, y=2, w=6, h=2) uses a bottom-left origin and should
1515    /// therefore extract the blue top-right region.
1516    #[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]) // red
1526                } else {
1527                    image::Rgba([0u8, 0, 255, 255]) // blue
1528                }
1529            } else {
1530                image::Rgba([0u8, 255, 0, 255]) // green
1531            }
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        // Every pixel in the cropped result must be blue
1547        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        // No red or green pixels leaked in
1558        assert_ne!(rgba.get_pixel(0, 0).0[0], 255, "no red channel leak");
1559    }
1560
1561    /// Out-of-bounds crop rects must surface as `CarError::DecodeFailed` rather than
1562    /// triggering an unsigned underflow / debug panic inside `crop_imm`.
1563    #[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}