diff --git a/Cargo.lock b/Cargo.lock index 4b671fb2e..94076f131 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2106,6 +2106,7 @@ dependencies = [ name = "craft-raster-codecs" version = "0.1.0" dependencies = [ + "base64 0.23.1", "image", "rayon", "thiserror 2.0.21", diff --git a/crates/raster-codecs/Cargo.toml b/crates/raster-codecs/Cargo.toml index cadf9cbd0..786e9d55c 100644 --- a/crates/raster-codecs/Cargo.toml +++ b/crates/raster-codecs/Cargo.toml @@ -11,6 +11,7 @@ repository.workspace = true workspace = true [dependencies] +base64 = "0.23" thiserror = { workspace = true } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "gif", "tiff", "webp"] } diff --git a/crates/raster-codecs/src/common.rs b/crates/raster-codecs/src/common.rs index fd8c1be60..357123e7c 100644 --- a/crates/raster-codecs/src/common.rs +++ b/crates/raster-codecs/src/common.rs @@ -6,6 +6,7 @@ use std::io::Cursor; +use base64::{Engine as _, engine::general_purpose::STANDARD}; use image::{ImageReader, Limits as ImageLimits, RgbaImage}; use crate::CodecError; @@ -62,6 +63,29 @@ pub fn encode_png(pixels: &RgbaImage) -> Result, CodecError> { Ok(out.into_inner()) } +/// Encode an RGBA image as standard Base64 containing PNG bytes, for a document field that stores +/// an inline PNG image. +pub fn encode_png_base64(pixels: &RgbaImage) -> Result { + encode_png(pixels).map(|bytes| STANDARD.encode(bytes)) +} + +/// Decode standard Base64 containing one PNG into straight-alpha RGBA pixels. +/// +/// The encoded length is checked before Base64 allocates the decoded buffer, and the decoded PNG +/// then passes through the same file, dimension, pixel-count, and allocation limits as any other +/// raster input. +pub fn decode_png_base64(text: &str, limits: DecodeLimits) -> Result { + let max_encoded_bytes = limits.max_file_bytes.div_ceil(3).saturating_mul(4); + if text.len() > max_encoded_bytes { + return Err(CodecError::LimitExceeded(format!("{} Base64 characters exceed max {max_encoded_bytes}", text.len()))); + } + let bytes = STANDARD.decode(text).map_err(|e| CodecError::malformed("PNG Base64", e))?; + if !bytes.starts_with(b"\x89PNG\r\n\x1a\n") { + return Err(CodecError::malformed("PNG Base64", "decoded data has no PNG signature")); + } + decode_rgba(&bytes, limits) +} + /// Encode RGBA pixels as JPEG at the requested quality (1–100). pub fn encode_jpeg(pixels: &RgbaImage, quality: u8) -> Result, CodecError> { if !(1..=100).contains(&quality) {