cryosnap-core 0.2.11

Core rendering library for cryosnap.
Documentation
use crate::{
    Error, PngOptions, PngQuantPreset, PngStrip, Result, DEFAULT_PNG_QUANTIZE_DITHER,
    DEFAULT_PNG_QUANTIZE_QUALITY, DEFAULT_PNG_QUANTIZE_SPEED, DEFAULT_WEBP_QUALITY,
    MAX_PNG_OPT_LEVEL,
};
use std::io::Cursor;

pub(crate) fn pixmap_to_webp(pixmap: &tiny_skia::Pixmap) -> Result<Vec<u8>> {
    let width = pixmap.width();
    let height = pixmap.height();
    let rgba = unpremultiply_rgba(pixmap.data());
    let encoder = webp::Encoder::from_rgba(&rgba, width, height);
    let webp = encoder.encode(DEFAULT_WEBP_QUALITY);
    Ok(webp.to_vec())
}

pub(crate) fn unpremultiply_rgba(data: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(data.len());
    for chunk in data.chunks_exact(4) {
        let a = chunk[3] as u16;
        if a == 0 {
            out.extend_from_slice(&[0, 0, 0, 0]);
            continue;
        }
        let r = ((chunk[0] as u16 * 255 + a / 2) / a) as u8;
        let g = ((chunk[1] as u16 * 255 + a / 2) / a) as u8;
        let b = ((chunk[2] as u16 * 255 + a / 2) / a) as u8;
        out.extend_from_slice(&[r, g, b, chunk[3]]);
    }
    out
}

pub(crate) fn quantize_pixmap_to_png(
    pixmap: &tiny_skia::Pixmap,
    config: &PngOptions,
) -> Result<Vec<u8>> {
    let rgba = unpremultiply_rgba(pixmap.data());
    quantize_rgba_to_png(&rgba, pixmap.width(), pixmap.height(), config)
}

pub(crate) fn quantize_png_bytes(png: &[u8], config: &PngOptions) -> Result<Vec<u8>> {
    let (rgba, width, height) = decode_png_rgba(png)?;
    quantize_rgba_to_png(&rgba, width, height, config)
}

pub(crate) fn decode_png_rgba(png: &[u8]) -> Result<(Vec<u8>, u32, u32)> {
    let mut decoder = png::Decoder::new(Cursor::new(png));
    decoder.set_transformations(png::Transformations::normalize_to_color8());
    let mut reader = decoder
        .read_info()
        .map_err(|err| Error::Render(format!("png decode: {err}")))?;
    let buffer_size = reader
        .output_buffer_size()
        .ok_or_else(|| Error::Render("png decode: missing buffer size".to_string()))?;
    let mut buf = vec![0; buffer_size];
    let info = reader
        .next_frame(&mut buf)
        .map_err(|err| Error::Render(format!("png decode: {err}")))?;
    let data = &buf[..info.buffer_size()];
    let rgba = match info.color_type {
        png::ColorType::Rgba => data.to_vec(),
        png::ColorType::Rgb => rgb_to_rgba(data),
        png::ColorType::GrayscaleAlpha => gray_alpha_to_rgba(data),
        png::ColorType::Grayscale => gray_to_rgba(data),
        png::ColorType::Indexed => {
            return Err(Error::Render(
                "png decode: indexed color not expanded".to_string(),
            ));
        }
    };
    Ok((rgba, info.width, info.height))
}

pub(crate) fn rgb_to_rgba(data: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(data.len() / 3 * 4);
    for chunk in data.chunks_exact(3) {
        out.extend_from_slice(&[chunk[0], chunk[1], chunk[2], 255]);
    }
    out
}

pub(crate) fn gray_to_rgba(data: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(data.len() * 4);
    for &g in data {
        out.extend_from_slice(&[g, g, g, 255]);
    }
    out
}

pub(crate) fn gray_alpha_to_rgba(data: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(data.len() / 2 * 4);
    for chunk in data.chunks_exact(2) {
        let g = chunk[0];
        let a = chunk[1];
        out.extend_from_slice(&[g, g, g, a]);
    }
    out
}

#[derive(Clone, Copy)]
pub(crate) struct QuantizeSettings {
    pub(crate) quality: u8,
    pub(crate) speed: u8,
    pub(crate) dither: f32,
}

impl PngQuantPreset {
    pub(crate) fn settings(self) -> QuantizeSettings {
        match self {
            PngQuantPreset::Fast => QuantizeSettings {
                quality: 70,
                speed: 7,
                dither: 0.5,
            },
            PngQuantPreset::Balanced => QuantizeSettings {
                quality: DEFAULT_PNG_QUANTIZE_QUALITY,
                speed: DEFAULT_PNG_QUANTIZE_SPEED,
                dither: DEFAULT_PNG_QUANTIZE_DITHER,
            },
            PngQuantPreset::Best => QuantizeSettings {
                quality: 95,
                speed: 1,
                dither: 1.0,
            },
        }
    }
}

pub(crate) fn quantize_settings(config: &PngOptions) -> QuantizeSettings {
    if let Some(preset) = config.quantize_preset {
        return preset.settings();
    }
    QuantizeSettings {
        quality: config.quantize_quality,
        speed: config.quantize_speed,
        dither: config.quantize_dither,
    }
}

pub(crate) fn quantize_rgba_to_png(
    rgba: &[u8],
    width: u32,
    height: u32,
    config: &PngOptions,
) -> Result<Vec<u8>> {
    let expected = width as usize * height as usize * 4;
    if rgba.len() != expected {
        return Err(Error::Render(
            "png quantize: invalid rgba buffer".to_string(),
        ));
    }
    let mut pixels = Vec::with_capacity(width as usize * height as usize);
    for chunk in rgba.chunks_exact(4) {
        pixels.push(imagequant::RGBA::new(
            chunk[0], chunk[1], chunk[2], chunk[3],
        ));
    }

    let mut attr = imagequant::new();
    let settings = quantize_settings(config);
    let quality = settings.quality.min(100);
    let speed = settings.speed.clamp(1, 10);
    attr.set_quality(0, quality)
        .map_err(|err| Error::Render(format!("png quantize quality: {err:?}")))?;
    attr.set_speed(speed as i32)
        .map_err(|err| Error::Render(format!("png quantize speed: {err:?}")))?;
    let mut image = attr
        .new_image(pixels, width as usize, height as usize, 0.0)
        .map_err(|err| Error::Render(format!("png quantize image: {err:?}")))?;
    let mut result = attr
        .quantize(&mut image)
        .map_err(|err| Error::Render(format!("png quantize: {err:?}")))?;
    let dither = settings.dither.clamp(0.0, 1.0);
    result
        .set_dithering_level(dither)
        .map_err(|err| Error::Render(format!("png quantize dither: {err:?}")))?;
    let (palette, indices) = result
        .remapped(&mut image)
        .map_err(|err| Error::Render(format!("png quantize remap: {err:?}")))?;
    encode_indexed_png(&palette, &indices, width, height)
}

pub(crate) fn encode_indexed_png(
    palette: &[imagequant::RGBA],
    indices: &[u8],
    width: u32,
    height: u32,
) -> Result<Vec<u8>> {
    if indices.len() != width as usize * height as usize {
        return Err(Error::Render(
            "png quantize: invalid index buffer".to_string(),
        ));
    }
    let mut palette_bytes = Vec::with_capacity(palette.len() * 3);
    let mut trns = Vec::with_capacity(palette.len());
    let mut has_alpha = false;
    for color in palette {
        palette_bytes.extend_from_slice(&[color.r, color.g, color.b]);
        trns.push(color.a);
        if color.a < 255 {
            has_alpha = true;
        }
    }
    let mut out = Vec::new();
    let mut encoder = png::Encoder::new(&mut out, width, height);
    encoder.set_color(png::ColorType::Indexed);
    encoder.set_depth(png::BitDepth::Eight);
    encoder.set_palette(palette_bytes);
    if has_alpha {
        encoder.set_trns(trns);
    }
    let mut writer = encoder
        .write_header()
        .map_err(|err| Error::Render(format!("png encode: {err}")))?;
    writer
        .write_image_data(indices)
        .map_err(|err| Error::Render(format!("png encode: {err}")))?;
    drop(writer);
    Ok(out)
}

pub(crate) fn optimize_png(png: Vec<u8>, config: &PngOptions) -> Result<Vec<u8>> {
    if !config.optimize {
        return Ok(png);
    }
    let level = config.level.min(MAX_PNG_OPT_LEVEL);
    let mut options = oxipng::Options::from_preset(level);
    options.strip = match config.strip {
        PngStrip::None => oxipng::StripChunks::None,
        PngStrip::Safe => oxipng::StripChunks::Safe,
        PngStrip::All => oxipng::StripChunks::All,
    };
    oxipng::optimize_from_memory(&png, &options)
        .map_err(|err| Error::Render(format!("png optimize: {err}")))
}