shadertoy-cli 2.5.1

Agent-friendly ShaderToy project, rendering, debugging, and live-preview CLI
use super::*;

pub(super) struct BufferOverride {
    pub name: String,
    pub width: u32,
    pub height: u32,
    pub rgba: Vec<u8>,
}

pub(super) fn load_overrides(
    assignments: &[String],
    manifest: &crate::manifest::Manifest,
    output_width: u32,
    output_height: u32,
) -> Result<Vec<BufferOverride>> {
    let mut result = Vec::new();
    for assignment in assignments {
        let (name, path) = split_assignment(assignment)?;
        let pass = manifest
            .passes
            .iter()
            .find(|pass| pass.name == name)
            .with_context(|| format!("unknown buffer override pass '{name}'"))?;
        if !matches!(pass.kind, PassKind::Buffer | PassKind::Compute) {
            bail!("buffer override '{}' is not a 2D buffer/compute pass", name);
        }
        let (width, height) = manifest.pass_dimensions(pass, output_width, output_height);
        let image = ImageReader::open(path)
            .with_context(|| format!("failed to open buffer override {}", path.display()))?
            .decode()
            .with_context(|| format!("failed to decode buffer override {}", path.display()))?
            .to_rgba8();
        let dimensions = image.dimensions();
        if dimensions != (width, height) {
            bail!(
                "buffer override '{}' is {}x{} but pass '{}' is {}x{}; use an exact-size image",
                name,
                dimensions.0,
                dimensions.1,
                name,
                width,
                height
            );
        }
        let mut rgba = image.into_raw();
        flip_rgba_rows(&mut rgba, width, height);
        result.push(BufferOverride {
            name: name.to_string(),
            width,
            height,
            rgba,
        });
    }
    Ok(result)
}

pub(super) fn split_assignment(value: &str) -> Result<(&str, &Path)> {
    let (name, path) = value
        .split_once('=')
        .with_context(|| format!("expected BUFFER=IMAGE, got '{value}'"))?;
    if name.trim().is_empty() || path.trim().is_empty() {
        bail!("expected non-empty BUFFER=IMAGE assignment, got '{value}'");
    }
    Ok((name.trim(), Path::new(path.trim())))
}

pub(super) fn save_rgb_png(image: &RgbImage, output: &Path) -> Result<()> {
    let mut pixels = image.pixels.clone();
    flip_rgb_rows(&mut pixels, image.width, image.height);
    ::image::save_buffer(
        output,
        &pixels,
        image.width,
        image.height,
        ::image::ColorType::Rgb8,
    )
    .with_context(|| format!("failed to write PNG {}", output.display()))
}

pub fn preview_raw_rgb_bytes(image: &RgbImage) -> Result<Vec<u8>> {
    let payload_len = image
        .pixels
        .len()
        .checked_add(8)
        .context("preview raw frame size overflow")?;
    let mut payload = Vec::with_capacity(payload_len);
    payload.extend_from_slice(&image.width.to_le_bytes());
    payload.extend_from_slice(&image.height.to_le_bytes());
    payload.extend_from_slice(&image.pixels);
    Ok(payload)
}

pub fn preview_png_bytes(image: &RgbImage) -> Result<Vec<u8>> {
    use ::image::ImageEncoder;
    use ::image::codecs::png::{CompressionType, FilterType, PngEncoder};

    let mut pixels = image.pixels.clone();
    flip_rgb_rows(&mut pixels, image.width, image.height);
    let mut encoded = Vec::new();
    PngEncoder::new_with_quality(
        &mut encoded,
        CompressionType::Level(1),
        FilterType::Adaptive,
    )
    .write_image(
        &pixels,
        image.width,
        image.height,
        ::image::ExtendedColorType::Rgb8,
    )
    .context("failed to encode preview PNG")?;
    Ok(encoded)
}

pub fn preview_jpeg_bytes(image: &RgbImage) -> Result<Vec<u8>> {
    use ::image::codecs::jpeg::JpegEncoder;

    let mut pixels = image.pixels.clone();
    flip_rgb_rows(&mut pixels, image.width, image.height);
    let mut encoded = Vec::new();
    JpegEncoder::new_with_quality(&mut encoded, 90)
        .encode(
            &pixels,
            image.width,
            image.height,
            ::image::ExtendedColorType::Rgb8,
        )
        .context("failed to encode preview JPEG")?;
    Ok(encoded)
}

pub fn flip_rgb_rows(data: &mut [u8], width: u32, height: u32) {
    flip_rows(data, width, height, 3);
}

pub(super) fn flip_rgba_rows(data: &mut [u8], width: u32, height: u32) {
    flip_rows(data, width, height, 4);
}

fn flip_rows(data: &mut [u8], width: u32, height: u32, channels: usize) {
    let Some(row) = (width as usize).checked_mul(channels) else {
        debug_assert!(false, "row size overflow");
        return;
    };
    let Some(expected_len) = row.checked_mul(height as usize) else {
        debug_assert!(false, "image size overflow");
        return;
    };
    debug_assert_eq!(data.len(), expected_len);
    for y in 0..(height as usize / 2) {
        let opposite = height as usize - 1 - y;
        let (before_opposite, opposite_and_after) = data.split_at_mut(opposite * row);
        let top = &mut before_opposite[y * row..(y + 1) * row];
        let bottom = &mut opposite_and_after[..row];
        top.swap_with_slice(bottom);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn test_image() -> RgbImage {
        RgbImage::new(2, 2, vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255])
    }

    #[test]
    fn preview_jpeg_is_valid() {
        let jpeg = preview_jpeg_bytes(&test_image()).unwrap();
        let decoded = ::image::load_from_memory_with_format(&jpeg, ::image::ImageFormat::Jpeg)
            .unwrap()
            .to_rgb8();
        assert_eq!(decoded.dimensions(), (2, 2));
    }

    #[test]
    fn preview_png_is_valid_and_flips_gl_rows() {
        let png = preview_png_bytes(&test_image()).unwrap();
        let decoded = ::image::load_from_memory_with_format(&png, ::image::ImageFormat::Png)
            .unwrap()
            .to_rgb8();
        assert_eq!(
            decoded.into_raw(),
            vec![0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 0,]
        );
    }

    #[test]
    fn preview_raw_rgb_preserves_gl_pixels_and_dimensions() {
        let image = test_image();
        let raw = preview_raw_rgb_bytes(&image).unwrap();
        assert_eq!(&raw[..4], &2u32.to_le_bytes());
        assert_eq!(&raw[4..8], &2u32.to_le_bytes());
        assert_eq!(&raw[8..], image.pixels.as_slice());
    }
}