///|
/// Portable Netpbm encodings supported by the CLI and test fixtures.
pub(all) enum NetpbmFormat {
PpmRgb
PpmRgba
PgmGray
PamRgba
} derive(Eq, Debug)
///|
pub(all) struct NetpbmOptions {
format : NetpbmFormat
max_value : Int
include_comments : Bool
} derive(Debug)
///|
pub fn NetpbmOptions::default() -> NetpbmOptions {
{ format: PamRgba, max_value: 255, include_comments: false }
}
///|
pub fn NetpbmOptions::format(
self : NetpbmOptions,
format : NetpbmFormat,
) -> NetpbmOptions {
{ ..self, format, }
}
///|
pub fn NetpbmOptions::max_value(
self : NetpbmOptions,
value : Int,
) -> NetpbmOptions {
{
..self,
max_value: if value < 1 {
1
} else if value > 65535 {
65535
} else {
value
},
}
}
///|
pub fn NetpbmOptions::comments(
self : NetpbmOptions,
enabled : Bool,
) -> NetpbmOptions {
{ ..self, include_comments: enabled }
}
///|
struct NetpbmBuffer {
bytes : Array[Byte]
} derive(Debug)
///|
fn NetpbmBuffer::new() -> NetpbmBuffer {
{ bytes: [] }
}
///|
fn NetpbmBuffer::text(self : NetpbmBuffer, value : String) -> Unit {
let encoded = @utf8.encode(value)
for byte in encoded {
self.bytes.push(byte)
}
}
///|
fn NetpbmBuffer::byte(self : NetpbmBuffer, value : Int) -> Unit {
self.bytes.push(value.to_byte())
}
///|
fn NetpbmBuffer::u16(self : NetpbmBuffer, value : Int) -> Unit {
self.byte(value >> 8)
self.byte(value)
}
///|
fn NetpbmBuffer::finish(self : NetpbmBuffer) -> Bytes {
Bytes::from_array(self.bytes[:])
}
///|
fn append_ascii_int(buffer : NetpbmBuffer, value : Int) -> Unit {
buffer.text("\{value}")
}
///|
fn netpbm_channel(value : Byte, max_value : Int) -> Int {
value.to_int() * max_value / 255
}
///|
fn byte_from_netpbm(value : Int, max_value : Int) -> Byte {
if max_value <= 0 {
0
} else {
clamp_color_int(value * 255 / max_value).to_byte()
}
}
///|
/// Encode a canvas as binary PPM, PGM, or PAM.
pub fn encode_netpbm(canvas : Canvas, options : NetpbmOptions) -> Bytes {
let max_value = if options.max_value < 1 {
1
} else if options.max_value > 65535 {
65535
} else {
options.max_value
}
let buffer = NetpbmBuffer::new()
match options.format {
PpmRgb => {
buffer.text("P6\n")
append_ascii_int(buffer, canvas.width)
buffer.text(" ")
append_ascii_int(buffer, canvas.height)
buffer.text("\n")
append_ascii_int(buffer, max_value)
buffer.text("\n")
for color in canvas.pixels {
append_netpbm_sample(buffer, color.red(), max_value)
append_netpbm_sample(buffer, color.green(), max_value)
append_netpbm_sample(buffer, color.blue(), max_value)
}
}
PpmRgba => {
buffer.text("P7\nWIDTH ")
append_ascii_int(buffer, canvas.width)
buffer.text("\nHEIGHT ")
append_ascii_int(buffer, canvas.height)
buffer.text("\nDEPTH 4\nMAXVAL ")
append_ascii_int(buffer, max_value)
buffer.text("\nTUPLTYPE RGB_ALPHA\nENDHDR\n")
for color in canvas.pixels {
append_netpbm_sample(buffer, color.red(), max_value)
append_netpbm_sample(buffer, color.green(), max_value)
append_netpbm_sample(buffer, color.blue(), max_value)
append_netpbm_sample(buffer, color.alpha(), max_value)
}
}
PgmGray => {
buffer.text("P5\n")
append_ascii_int(buffer, canvas.width)
buffer.text(" ")
append_ascii_int(buffer, canvas.height)
buffer.text("\n")
append_ascii_int(buffer, max_value)
buffer.text("\n")
for color in canvas.pixels {
append_netpbm_sample(buffer, color.luma(), max_value)
}
}
PamRgba => {
buffer.text("P7\nWIDTH ")
append_ascii_int(buffer, canvas.width)
buffer.text("\nHEIGHT ")
append_ascii_int(buffer, canvas.height)
buffer.text("\nDEPTH 4\nMAXVAL ")
append_ascii_int(buffer, max_value)
buffer.text("\nTUPLTYPE RGB_ALPHA\nENDHDR\n")
for color in canvas.pixels {
append_netpbm_sample(buffer, color.red(), max_value)
append_netpbm_sample(buffer, color.green(), max_value)
append_netpbm_sample(buffer, color.blue(), max_value)
append_netpbm_sample(buffer, color.alpha(), max_value)
}
}
}
buffer.finish()
}
///|
fn append_netpbm_sample(
buffer : NetpbmBuffer,
value : Byte,
max_value : Int,
) -> Unit {
if max_value <= 255 {
buffer.byte(netpbm_channel(value, max_value))
} else {
buffer.u16(netpbm_channel(value, max_value))
}
}
///|
struct NetpbmCursor {
data : Bytes
mut position : Int
} derive(Debug)
///|
fn NetpbmCursor::new(data : Bytes) -> NetpbmCursor {
{ data, position: 0 }
}
///|
fn NetpbmCursor::byte(self : NetpbmCursor) -> Byte raise GifError {
if self.position >= self.data.length() {
raise UnexpectedEnd(context="Netpbm data")
}
let result = self.data[self.position]
self.position += 1
result
}
///|
fn NetpbmCursor::skip_space(self : NetpbmCursor) -> Unit {
let mut continue_scanning = true
while continue_scanning && self.position < self.data.length() {
let current = self.data[self.position]
if current == b'#' {
while self.position < self.data.length() &&
self.data[self.position] != b'\n' {
self.position += 1
}
} else if current == b' ' ||
current == b'\n' ||
current == b'\r' ||
current == b'\t' {
self.position += 1
} else {
continue_scanning = false
}
}
}
///|
fn NetpbmCursor::token(self : NetpbmCursor) -> String raise GifError {
self.skip_space()
let start = self.position
while self.position < self.data.length() {
let current = self.data[self.position]
if current == b' ' ||
current == b'\n' ||
current == b'\r' ||
current == b'\t' ||
current == b'#' {
break
}
self.position += 1
}
if self.position == start {
raise UnexpectedEnd(context="Netpbm token")
}
let token_bytes : Array[Byte] = []
for i in start.. raise InvalidBlock(context="invalid UTF-8 Netpbm token")
}
}
///|
fn parse_netpbm_int(value : String) -> Int raise GifError {
let mut result = 0
let mut seen = false
for byte in @utf8.encode(value) {
if byte >= b'0' && byte <= b'9' {
result = result * 10 + (byte - b'0').to_int()
seen = true
} else if byte == b'-' && !seen && result == 0 {
result = -1
} else {
raise InvalidBlock(context="invalid Netpbm integer")
}
}
if !seen || result < 0 {
raise InvalidBlock(context="invalid Netpbm integer")
}
result
}
///|
fn read_netpbm_sample(
cursor : NetpbmCursor,
max_value : Int,
) -> Int raise GifError {
if max_value <= 255 {
cursor.byte().to_int()
} else {
let high = cursor.byte().to_int()
let low = cursor.byte().to_int()
high * 256 + low
}
}
///|
/// Decode binary PPM/PAM/PGM data into an RGBA canvas.
pub fn decode_netpbm(data : Bytes) -> Canvas raise GifError {
let cursor = NetpbmCursor::new(data)
let magic = cursor.token()
if magic == "P6" || magic == "P5" {
let width = parse_netpbm_int(cursor.token())
let height = parse_netpbm_int(cursor.token())
let max_value = parse_netpbm_int(cursor.token())
if width <= 0 || height <= 0 || max_value <= 0 || max_value > 65535 {
raise InvalidDimensions
}
cursor.skip_space()
let channel_count = if magic == "P6" { 3 } else { 1 }
let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 255))
for i in 0..<(width * height) {
let red = read_netpbm_sample(cursor, max_value)
let green = if channel_count == 3 {
read_netpbm_sample(cursor, max_value)
} else {
red
}
let blue = if channel_count == 3 {
read_netpbm_sample(cursor, max_value)
} else {
red
}
result.pixels[i] = rgba(
byte_from_netpbm(red, max_value),
byte_from_netpbm(green, max_value),
byte_from_netpbm(blue, max_value),
255,
)
}
result
} else if magic == "P7" {
decode_pam(cursor)
} else {
raise UnsupportedFeature(
feature="only binary P5, P6, and P7 Netpbm are supported",
)
}
}
///|
fn decode_pam(cursor : NetpbmCursor) -> Canvas raise GifError {
let mut width = 0
let mut height = 0
let mut depth = 0
let mut max_value = 255
let mut header_done = false
while !header_done {
let key = cursor.token()
if key == "ENDHDR" {
header_done = true
} else if key == "WIDTH" {
width = parse_netpbm_int(cursor.token())
} else if key == "HEIGHT" {
height = parse_netpbm_int(cursor.token())
} else if key == "DEPTH" {
depth = parse_netpbm_int(cursor.token())
} else if key == "MAXVAL" {
max_value = parse_netpbm_int(cursor.token())
} else {
ignore(cursor.token())
}
}
cursor.skip_space()
if width <= 0 ||
height <= 0 ||
depth != 4 ||
max_value <= 0 ||
max_value > 65535 {
raise InvalidDimensions
}
let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
for i in 0..<(width * height) {
let red = read_netpbm_sample(cursor, max_value)
let green = read_netpbm_sample(cursor, max_value)
let blue = read_netpbm_sample(cursor, max_value)
let alpha = read_netpbm_sample(cursor, max_value)
result.pixels[i] = rgba(
byte_from_netpbm(red, max_value),
byte_from_netpbm(green, max_value),
byte_from_netpbm(blue, max_value),
byte_from_netpbm(alpha, max_value),
)
}
result
}
///|
/// Return the format marker without decoding pixel data.
pub fn netpbm_format(data : Bytes) -> NetpbmFormat raise GifError {
let cursor = NetpbmCursor::new(data)
match cursor.token() {
"P5" => PgmGray
"P6" => PpmRgb
"P7" => PamRgba
_ => raise UnsupportedFeature(feature="unknown Netpbm marker")
}
}
///|
/// Crop a canvas to a rectangular PPM-friendly RGB view.
pub fn Canvas::to_rgb(self : Canvas) -> Canvas {
let result = self.copy()
for i in 0.. Canvas {
let result = self.copy()
for i in 0..