load_image 3.5.0-beta.1

Load PNG or JPEG with color profile support
Documentation
use crate::pixel_format::{ColorSpace, PixelFormat};
use bytemuck::{Pod, cast_slice, cast_slice_mut};
pub use moxcms::CmsError;
use moxcms::{ColorProfile, DataColorSpace, Layout, ProfileText, RenderingIntent, TransformExecutor, TransformOptions};
use std::marker::PhantomData;
use std::sync::Arc;

pub(crate) struct Profile(moxcms::ColorProfile);

impl Profile {
    pub(crate) fn new_icc(data: &[u8]) -> Result<Self, CmsError> {
        ColorProfile::new_from_slice(data).map(Profile)
    }

    pub(crate) fn new_srgb() -> Self {
        Self(ColorProfile::new_srgb())
    }

    pub(crate) fn color_space(&self) -> ColorSpace {
        match self.0.color_space {
            DataColorSpace::Rgb => ColorSpace::Rgb,
            DataColorSpace::Gray => ColorSpace::Gray,
            DataColorSpace::Cmyk => ColorSpace::Cmyk,
            _ => ColorSpace::Other,
        }
    }

    pub(crate) fn description(&self) -> Option<String> {
        let text = self.0.description.as_ref()?;
        Some(match text {
            ProfileText::PlainString(s) => s.clone(),
            ProfileText::Localizable(locales) => locales.iter().find(|l| l.language == "en").or_else(|| locales.first())?.value.clone(),
            ProfileText::Description(d) => {
                if !d.ascii_string.is_empty() {
                    d.ascii_string.clone()
                } else if !d.unicode_string.is_empty() {
                    d.unicode_string.clone()
                } else {
                    d.mac_string.clone()
                }
            },
        })
    }
}

fn to_layout(format: PixelFormat) -> Layout {
    match format {
        PixelFormat::RGBA8 | PixelFormat::RGBA16 => Layout::Rgba,
        // CMYK can't occur without a JPEG decoder.
        #[cfg(any(feature = "jpeg", feature = "mozjpeg"))]
        PixelFormat::CMYK8 => Layout::Rgba,
        PixelFormat::GRAY8 | PixelFormat::GRAY16 => Layout::Gray,
        PixelFormat::GRAYA8 | PixelFormat::GRAYA16 => Layout::GrayAlpha,
        PixelFormat::RGB8 | PixelFormat::RGB16 => Layout::Rgb,
    }
}

fn is_8bit(format: PixelFormat) -> bool {
    match format {
        PixelFormat::RGB8 | PixelFormat::RGBA8 | PixelFormat::GRAY8 | PixelFormat::GRAYA8 => true,
        #[cfg(any(feature = "jpeg", feature = "mozjpeg"))]
        PixelFormat::CMYK8 => true,
        _ => false,
    }
}

/// moxcms has no mixed-depth transforms, so all conversions run in 16-bit.
/// 8-bit input is upscaled with `v * 257`, which is exactly how lcms2
/// quantizes 8-bit samples to 16-bit.
pub(crate) struct Transform<In: Pod, Out: Pod> {
    executor: Arc<dyn TransformExecutor<u16> + Send + Sync>,
    src_8bit: bool,
    _marker: PhantomData<fn(In, Out)>,
}

impl<In: Pod, Out: Pod> Transform<In, Out> {
    pub(crate) fn new(src: &Profile, src_format: PixelFormat, dst: &Profile, dst_format: PixelFormat) -> Result<Self, CmsError> {
        let options = TransformOptions {
            rendering_intent: RenderingIntent::RelativeColorimetric,
            ..Default::default()
        };
        let executor = src.0.create_transform_16bit(to_layout(src_format), &dst.0, to_layout(dst_format), options)?;
        Ok(Self {
            executor,
            src_8bit: is_8bit(src_format),
            _marker: PhantomData,
        })
    }

    pub(crate) fn transform_pixels(&self, src: &[In], dst: &mut [Out]) -> Result<(), CmsError> {
        let dst: &mut [u16] = cast_slice_mut(dst);
        if self.src_8bit {
            let src: &[u8] = cast_slice(src);
            let src: Vec<u16> = src
                .iter()
                .map(|&v| {
                    u16::from(v) * 257
                })
                .collect();
            self.executor.transform(&src, dst)?;
        } else {
            let src: &[u16] = cast_slice(src);
            self.executor.transform(src, dst)?;
        }
        Ok(())
    }
}