///|
/// Parsed Netpbm PGM image. Pixel values stay in the original 0..maxval scale.
pub struct PgmImage {
grid : Array[Array[Double]]
max_value : Int
encoding : String
} derive(@debug.Debug)
///|
pub extend PgmImage with @debug.Debug::{to_repr}
///|
fn pgm_space(value : Int) -> Bool {
value == 32 ||
value == 9 ||
value == 10 ||
value == 13 ||
value == 11 ||
value == 12
}
///|
fn pgm_token(
data : Array[Int],
start : Int,
) -> (String, Int) raise TopologyError {
let mut i = start
while i < data.length() {
if pgm_space(data[i]) {
i += 1
continue
}
if data[i] == 35 {
while i < data.length() && data[i] != 10 && data[i] != 13 {
i += 1
}
continue
}
break
}
let mut token = ""
while i < data.length() && !pgm_space(data[i]) && data[i] != 35 {
if data[i] < 33 || data[i] > 126 || token.length() >= 32 {
raise TopologyError("PGM token must be bounded ASCII")
}
token += data[i].unsafe_to_char().to_string()
i += 1
}
if token == "" {
raise TopologyError("PGM file ends before all required values")
}
(token, i)
}
///|
fn pgm_number(
token : String,
label : String,
upper : Int,
) -> Int raise TopologyError {
let mut value = 0
for digit in token {
let code = digit.to_int()
if code < 48 || code > 57 {
raise TopologyError(label + " must be unsigned decimal")
}
value = value * 10 + code - 48
if value > upper {
raise TopologyError(label + " exceeds supported range")
}
}
value
}
///|
/// Read exactly one plain P2 or raw P5 grayscale image, including 16-bit P5.
/// Reject trailing images, malformed rasters and dimensions beyond 64x64.
pub fn parse_pgm(data : Array[Int]) -> PgmImage raise TopologyError {
if data.length() < 7 || data.length() > 1000000 {
raise TopologyError("PGM input must contain 7..1000000 bytes")
}
for byte in data {
if byte < 0 || byte > 255 {
raise TopologyError("PGM bytes must be in 0..255")
}
}
let (magic, after_magic) = pgm_token(data, 0)
if magic != "P2" && magic != "P5" {
raise TopologyError("expected P2 or P5 grayscale PGM")
}
let (width_text, after_width) = pgm_token(data, after_magic)
let (height_text, after_height) = pgm_token(data, after_width)
let (max_text, after_max) = pgm_token(data, after_height)
let width = pgm_number(width_text, "PGM width", 64)
let height = pgm_number(height_text, "PGM height", 64)
let max_value = pgm_number(max_text, "PGM max value", 65535)
if width == 0 || height == 0 || max_value == 0 {
raise TopologyError("PGM width, height and max value must be positive")
}
let grid = Array::makei(height, fn(_) { Array::make(width, 0.0) })
if magic == "P2" {
let mut cursor = after_max
for row = 0; row < height; row = row + 1 {
for col = 0; col < width; col = col + 1 {
let (token, next) = pgm_token(data, cursor)
grid[row][col] = pgm_number(token, "PGM pixel", max_value).to_double()
cursor = next
}
}
let mut i = cursor
while i < data.length() {
if pgm_space(data[i]) {
i += 1
continue
}
if data[i] == 35 {
while i < data.length() && data[i] != 10 && data[i] != 13 {
i += 1
}
continue
}
raise TopologyError("PGM file contains extra pixels or another image")
}
} else {
if after_max >= data.length() || !pgm_space(data[after_max]) {
raise TopologyError(
"P5 header requires one whitespace separator before raster",
)
}
let mut offset = after_max + 1
if data[after_max] == 13 && offset < data.length() && data[offset] == 10 {
offset += 1
}
let bytes_per_pixel = if max_value < 256 { 1 } else { 2 }
if data.length() - offset != width * height * bytes_per_pixel {
raise TopologyError("P5 raster length does not match image dimensions")
}
for row = 0; row < height; row = row + 1 {
for col = 0; col < width; col = col + 1 {
let value = if bytes_per_pixel == 1 {
data[offset]
} else {
data[offset] * 256 + data[offset + 1]
}
if value > max_value {
raise TopologyError("P5 pixel exceeds max value")
}
grid[row][col] = value.to_double()
offset += bytes_per_pixel
}
}
}
{ grid, max_value, encoding: magic, }
}