load_image 3.5.0-beta.1

Load PNG or JPEG with color profile support
Documentation
use bytemuck::{cast_slice, cast_slice_mut};
use rgb::FromSlice;
use std::fs;
use zune_jpeg::JpegDecoder;
use zune_jpeg::zune_core::bytestream::ZCursor;
use zune_jpeg::zune_core::colorspace::ColorSpace;

use crate::cms::Profile;
use crate::convert::ToSRGBImage;
use crate::exif::parse_exif;
use crate::format::Format;
use crate::image::{Image, ImageMeta, Rotate};
use crate::loader::Loader;
use crate::pixel_format::CMYK;

impl Loader {
    pub(crate) fn load_jpeg(&self, data: &[u8], fs_meta: Option<fs::Metadata>) -> Result<Image, crate::Error> {
        let mut dec = JpegDecoder::new(ZCursor::new(data));
        dec.decode_headers()?;
        let info = dec.info().ok_or(crate::Error::UnsupportedJpeg)?;

        let width = usize::from(info.width);
        let height = usize::from(info.height);
        self.check_dimensions(width, height)?;

        let (orientation, is_adobe_1998) = dec.exif().map_or((1, false), |exif| parse_exif(exif));

        let profile = dec.icc_profile().as_deref()
            .or(if is_adobe_1998 { Some(crate::profiles::ADOBE1998) } else { None })
            .and_then(|icc| self.process_profile(Profile::new_icc(icc)));

        // Keep raw CMYK/YCCK/grayscale pixels, so color management is done here:
        // zune-jpeg would naively convert CMYK to RGB, and it has no YCCK→CMYK
        // conversion at all (only a profile-less YCCK→RGB approximation).
        let out_cs = match dec.input_colorspace() {
            Some(ColorSpace::CMYK) => ColorSpace::CMYK,
            Some(ColorSpace::YCCK) => ColorSpace::YCCK,
            Some(ColorSpace::Luma) => ColorSpace::Luma,
            _ => ColorSpace::RGB,
        };
        if out_cs != ColorSpace::RGB {
            dec.set_options(dec.options().jpeg_set_out_colorspace(out_cs));
        }
        let mut pixels = dec.decode()?;
        let out_cs = dec.output_colorspace().unwrap_or(ColorSpace::RGB);

        // FIXME: metadata not preserved
        let meta = ImageMeta::new(Format::Jpeg, vec![], fs_meta.clone());
        let img = match out_cs {
            ColorSpace::CMYK => {
                // zune-jpeg passes through Adobe-flavored CMYK samples (255 = no ink),
                for v in &mut pixels {
                    *v = 255 - *v;
                }
                cast_slice::<u8, CMYK>(&pixels).to_image(profile, width, height, true, meta)
            }
            ColorSpace::YCCK => {
                pixels.as_chunks::<4>().0.iter()
                    .map(|px| ycck_to_cmyk(px[0], px[1], px[2], px[3]))
                    .collect::<Vec<CMYK>>()
                    .as_slice()
                    .to_image(profile, width, height, true, meta)
            },
            ColorSpace::Luma => cast_slice_mut::<_, rgb::Gray<u8>>(&mut pixels).to_image(profile, width, height, true, meta),
            _ => pixels.as_rgb_mut().to_image(profile, width, height, true, meta),
        };

        Ok(img.rotated(Rotate::from_exif_orientation(orientation)))
    }
}

/// Convert one Adobe YCCK pixel to CMYK ink coverage (0 = no ink), using the
/// same coefficients as jpeg-decoder's `ycbcr_to_rgb`.
///
/// Adobe stores YCCK with inverted channels, so the plain ycbcr→rgb formula
/// recovers the CMY ink coverage directly, and K is inverted (255 = no ink).
fn ycck_to_cmyk(y: u8, cb: u8, cr: u8, k: u8) -> CMYK {
    let y = f32::from(y);
    let cb = f32::from(cb) - 128.0;
    let cr = f32::from(cr) - 128.0;
    CMYK {
        c: (y + 1.40200 * cr).round() as u8,
        m: (y - 0.34414 * cb - 0.71414 * cr).round() as u8,
        y: (y + 1.77200 * cb).round() as u8,
        k: 255 - k,
    }
}