///| A dependency-free Netpbm color loader for P6 (binary RGB) and P3 (ASCII
///| RGB). Both decode into a grayscale `Image` using BT.601 luminance, so a
///| scanned colour photograph can feed the same pipeline as a PGM file.
///| Decode a P6 (binary RGB) raster, rescaling `maxval` to 255 and converting
///|
/// each triple to grayscale.
fn decode_p6(
data : Bytes,
width : Int,
height : Int,
data_start : Int,
maxval : Int,
) -> Image {
let pixels = Bytes::makei(width * height, fn(i) {
let o = data_start + i * 3
let r = data.at(o).to_int() * 255 / maxval
let g = data.at(o + 1).to_int() * 255 / maxval
let b = data.at(o + 2).to_int() * 255 / maxval
rgb_to_gray(r, g, b).to_byte()
})
Image::new(width, height, pixels)
}
///| Decode a P3 (ASCII RGB) raster, rescaling `maxval` to 255 and converting
///|
/// each triple to grayscale.
fn decode_p3(
data : Bytes,
width : Int,
height : Int,
data_start : Int,
maxval : Int,
) -> Image {
let pixels = Array::make(width * height, b'\x00')
let mut p = data_start
for i = 0; i < width * height; i = i + 1 {
let (r, p1) = read_token_int(data, p)
let (g, p2) = read_token_int(data, p1)
let (b, p3) = read_token_int(data, p2)
pixels[i] = rgb_to_gray(
r * 255 / maxval,
g * 255 / maxval,
b * 255 / maxval,
).to_byte()
p = p3
}
Image::new(width, height, Bytes::from_array(pixels))
}
///| Parse a Netpbm colour image (`P3` or `P6`) into a grayscale `Image`.
///
///| Raises `DecodeError` describing the problem when the input is malformed or
///|
/// uses a feature outside the supported subset.
pub fn parse_ppm(data : Bytes) -> Image raise DecodeError {
if data.length() < 2 {
raise DecodeError("PPM: buffer too short")
}
if data.at(0) != b'P' {
raise DecodeError("PPM: not a Netpbm file")
}
let magic = data.at(1).to_int()
let (width, pos1) = read_token_int(data, 2)
let (height, pos2) = read_token_int(data, pos1)
if width <= 0 || height <= 0 {
raise DecodeError("PPM: invalid dimensions")
}
if magic == 0x36 {
// P6: binary RGB. A single whitespace byte follows `maxval`.
let (maxval, pos3) = read_token_int(data, pos2)
if maxval <= 0 || maxval > 255 {
raise DecodeError("PPM: maxval outside 1..255 is not supported")
}
decode_p6(data, width, height, pos3 + 1, maxval)
} else if magic == 0x33 {
// P3: ASCII RGB.
let (maxval, pos3) = read_token_int(data, pos2)
if maxval <= 0 || maxval > 255 {
raise DecodeError("PPM: maxval outside 1..255 is not supported")
}
decode_p3(data, width, height, pos3, maxval)
} else {
raise DecodeError("PPM: unsupported format (only P3 and P6)")
}
}
///| Encode a grayscale image as a P6 (binary RGB) PPM with `maxval` 255,
///| writing each gray value into all three channels. The result round-trips
///|
/// through `parse_ppm` byte-for-byte in grayscale.
pub fn write_ppm_p6(img : Image) -> Bytes {
let header = "P6\n\{img.width} \{img.height}\n255\n"
let hb = @utf8.encode(header)
let n = img.pixel_count()
let out = Array::make(hb.length() + n * 3, b'\x00')
for i = 0; i < hb.length(); i = i + 1 {
out[i] = hb.at(i)
}
for i = 0; i < n; i = i + 1 {
let g = img.pixels.at(i)
out[hb.length() + i * 3] = g
out[hb.length() + i * 3 + 1] = g
out[hb.length() + i * 3 + 2] = g
}
Bytes::from_array(out)
}