use std::io::Cursor;
use image::{GrayImage, ImageFormat, ImageReader, Limits, Luma};
const LEN_PREFIX: usize = 4;
const MAX_IMG_DIM: u32 = 8192;
const MAX_IMG_ALLOC: u64 = 128 * 1024 * 1024;
pub(crate) fn decode_png_luma(png: &[u8]) -> Option<GrayImage> {
decode_png_luma_limited(png, MAX_IMG_DIM, MAX_IMG_ALLOC)
}
fn decode_png_luma_limited(png: &[u8], max_dim: u32, max_alloc: u64) -> Option<GrayImage> {
let mut reader = ImageReader::new(Cursor::new(png));
reader.set_format(ImageFormat::Png);
let mut limits = Limits::no_limits();
limits.max_image_width = Some(max_dim);
limits.max_image_height = Some(max_dim);
limits.max_alloc = Some(max_alloc);
reader.limits(limits);
Some(reader.decode().ok()?.to_luma8())
}
pub fn bytes_to_png(data: &[u8]) -> Vec<u8> {
let mut payload = Vec::with_capacity(LEN_PREFIX + data.len());
payload.extend_from_slice(&(data.len() as u32).to_le_bytes());
payload.extend_from_slice(data);
let total = payload.len().max(1);
let width = (total as f64).sqrt().ceil() as u32;
let width = width.max(1);
let height = (total as u32).div_ceil(width);
let mut img = GrayImage::new(width, height);
let mut bytes = payload.iter();
for y in 0..height {
for x in 0..width {
let v = bytes.next().copied().unwrap_or(0);
img.put_pixel(x, y, Luma([v]));
}
}
let mut out = Vec::new();
img.write_to(&mut Cursor::new(&mut out), ImageFormat::Png)
.expect("codificación PNG");
out
}
pub fn png_to_bytes(png: &[u8]) -> Option<Vec<u8>> {
let img = decode_png_luma(png)?;
let payload: Vec<u8> = img.pixels().map(|p| p.0[0]).collect();
if payload.len() < LEN_PREFIX {
return None;
}
let len = u32::from_le_bytes(payload[0..LEN_PREFIX].try_into().ok()?) as usize;
let end = LEN_PREFIX.checked_add(len)?;
if end > payload.len() {
return None;
}
Some(payload[LEN_PREFIX..end].to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_bytes_through_png() {
let data = b"datos arbitrarios para pintar como imagen";
let png = bytes_to_png(data);
assert_eq!(png_to_bytes(&png).unwrap(), data);
}
#[test]
fn round_trips_empty() {
let png = bytes_to_png(b"");
assert_eq!(png_to_bytes(&png).unwrap(), b"");
}
#[test]
fn output_is_a_valid_png() {
let png = bytes_to_png(b"hola");
assert_eq!(&png[0..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]);
}
#[test]
fn rejects_garbage() {
assert!(png_to_bytes(b"no soy un png").is_none());
}
#[test]
fn rejects_image_exceeding_dimension_limit() {
let png = bytes_to_png(&vec![0u8; 5000]);
assert!(
decode_png_luma_limited(&png, 8, 1 << 30).is_none(),
"una imagen mayor que el límite de dimensión debe rechazarse"
);
assert!(
decode_png_luma_limited(&png, 8192, 1 << 30).is_some(),
"dentro del límite debe decodificar"
);
}
#[test]
fn rejects_image_exceeding_alloc_limit() {
let png = bytes_to_png(&vec![0u8; 5000]);
assert!(decode_png_luma_limited(&png, 8192, 16).is_none());
}
}