///| An 8-bit grayscale image, stored row-major with the top-left corner as
///| the origin. This is the internal representation used by the OCR pipeline:
///| every downstream stage (binarization, segmentation, classification) reads
///| a single byte per pixel in `0..=255`.
///| Error raised by the image decoders when the input is malformed or uses an
///|
/// unsupported feature.
pub suberror DecodeError {
DecodeError(String)
}
///|
pub struct Image {
width : Int
height : Int
pixels : Bytes
}
///|
/// Construct an image from raw grayscale bytes (row-major, one byte per pixel).
pub fn Image::new(width : Int, height : Int, pixels : Bytes) -> Image {
{ width, height, pixels, }
}
///|
/// Total number of pixels.
pub fn Image::pixel_count(self : Image) -> Int {
self.width * self.height
}
///|
/// Byte length of the underlying pixel buffer.
pub fn Image::byte_length(self : Image) -> Int {
self.pixels.length()
}
///| Grayscale value of the pixel at `(x, y)`, in the inclusive range `0..=255`.
///|
///|
/// `x` is the column and `y` is the row, both zero-based from the top-left.
pub fn Image::at(self : Image, x : Int, y : Int) -> Int {
self.pixels.at(y * self.width + x).to_int()
}
///| Convert an sRGB byte triple to luminance using the ITU-R BT.601
///|
/// coefficients (0.299 R + 0.587 G + 0.114 B), computed in integer arithmetic.
pub fn rgb_to_gray(r : Int, g : Int, b : Int) -> Int {
(299 * r + 587 * g + 114 * b) / 1000
}
///| Build a grayscale image from packed multi-channel pixels (row-major).
///|
///| `channels` must be either `3` (RGB) or `4` (RGBA); when it is `4` the
///| alpha byte is ignored. The pixel order within each channel group is the
///|
/// conventional RGB(A) order.
pub fn Image::from_packed(
width : Int,
height : Int,
packed : Bytes,
channels : Int,
) -> Image {
let pixels = Bytes::makei(width * height, fn(i) {
let o = i * channels
let r = packed.at(o).to_int()
let g = packed.at(o + 1).to_int()
let b = packed.at(o + 2).to_int()
rgb_to_gray(r, g, b).to_byte()
})
{ width, height, pixels, }
}