mirror of
https://github.com/odin-lang/Odin.git
synced 2026-02-13 23:03:16 +00:00
Added initial Netpbm image format support
This commit is contained in:
@@ -57,6 +57,7 @@ Image :: struct {
|
||||
}
|
||||
|
||||
Image_Metadata :: union {
|
||||
^Netpbm_Info,
|
||||
^PNG_Info,
|
||||
^QOI_Info,
|
||||
}
|
||||
@@ -152,6 +153,7 @@ Options :: distinct bit_set[Option]
|
||||
|
||||
Error :: union #shared_nil {
|
||||
General_Image_Error,
|
||||
Netpbm_Error,
|
||||
PNG_Error,
|
||||
QOI_Error,
|
||||
|
||||
@@ -171,6 +173,50 @@ General_Image_Error :: enum {
|
||||
Invalid_Output,
|
||||
}
|
||||
|
||||
/*
|
||||
Netpbm-specific definitions
|
||||
*/
|
||||
Netpbm_Format :: enum {
|
||||
P1, P2, P3, P4, P5, P6, P7, Pf, PF,
|
||||
}
|
||||
|
||||
Netpbm_Header :: struct {
|
||||
format: Netpbm_Format,
|
||||
width: int,
|
||||
height: int,
|
||||
channels: int,
|
||||
depth: int,
|
||||
maxval: int,
|
||||
tupltype: string,
|
||||
scale: f32,
|
||||
little_endian: bool,
|
||||
}
|
||||
|
||||
Netpbm_Info :: struct {
|
||||
header: Netpbm_Header,
|
||||
}
|
||||
|
||||
Netpbm_Error :: enum {
|
||||
None = 0,
|
||||
|
||||
// reading
|
||||
File_Not_Readable,
|
||||
Invalid_Signature,
|
||||
Invalid_Header_Token_Character,
|
||||
Incomplete_Header,
|
||||
Invalid_Header_Value,
|
||||
Duplicate_Header_Field,
|
||||
Buffer_Too_Small,
|
||||
Invalid_Buffer_ASCII_Token,
|
||||
Invalid_Buffer_Value,
|
||||
|
||||
// writing
|
||||
File_Not_Writable,
|
||||
Invalid_Format,
|
||||
Invalid_Number_Of_Channels,
|
||||
Invalid_Image_Depth,
|
||||
}
|
||||
|
||||
/*
|
||||
PNG-specific definitions
|
||||
*/
|
||||
|
||||
32
core/image/netpbm/doc.odin
Normal file
32
core/image/netpbm/doc.odin
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
Formats:
|
||||
PBM (P1, P4): Portable Bit Map, stores black and white images (1 channel)
|
||||
PGM (P2, P5): Portable Gray Map, stores greyscale images (1 channel, 1 or 2 bytes per value)
|
||||
PPM (P3, P6): Portable Pixel Map, stores colour images (3 channel, 1 or 2 bytes per value)
|
||||
PAM (P7 ): Portable Arbitrary Map, stores arbitrary channel images (1 or 2 bytes per value)
|
||||
PFM (Pf, PF): Portable Float Map, stores floating-point images (Pf: 1 channel, PF: 3 channel)
|
||||
|
||||
Reading
|
||||
All formats fill out header fields `format`, `width`, `height`, `channels`, `depth`
|
||||
Specific formats use more fields
|
||||
PGM, PPM, and PAM set `maxval`
|
||||
PAM also sets `tupltype`, and is able to set `channels` to an arbitrary value
|
||||
PFM sets `scale` and `little_endian`
|
||||
Currently doesn't support reading multiple images from one binary-format file
|
||||
|
||||
Writing
|
||||
All formats require the header field `format` to be specified
|
||||
Additional header fields are required for specific formats
|
||||
PGM, PPM, and PAM require `maxval`
|
||||
PAM also uses `tupltype`, though it may be left as default (empty or nil string)
|
||||
PFM requires `scale` and `little_endian`, though the latter may be left untouched (default is false)
|
||||
|
||||
Some syntax differences from the specifications:
|
||||
`channels` stores what the PAM specification calls `depth`
|
||||
`depth` instead stores how many bytes will fit `maxval` (should only be 1, 2, or 4)
|
||||
`scale` and `little_endian` are separated, so the `header` will always store a positive `scale`
|
||||
`little_endian` will only be true for a negative `scale` PFM, every other format will be false
|
||||
`little_endian` only describes the netpbm data being read/written, the image buffer will be native
|
||||
*/
|
||||
|
||||
package netpbm
|
||||
26
core/image/netpbm/helpers.odin
Normal file
26
core/image/netpbm/helpers.odin
Normal file
@@ -0,0 +1,26 @@
|
||||
package netpbm
|
||||
|
||||
import "core:bytes"
|
||||
import "core:image"
|
||||
|
||||
destroy :: proc(img: ^image.Image) -> bool {
|
||||
if img == nil do return false
|
||||
|
||||
//! TEMP CAST
|
||||
info, ok := img.metadata.(^image.Netpbm_Info)
|
||||
if !ok do return false
|
||||
|
||||
bytes.buffer_destroy(&img.pixels)
|
||||
header_destroy(&info.header)
|
||||
free(info)
|
||||
img.metadata = nil
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
header_destroy :: proc(using header: ^Header) {
|
||||
if format == .P7 && tupltype != "" {
|
||||
delete(tupltype)
|
||||
tupltype = ""
|
||||
}
|
||||
}
|
||||
681
core/image/netpbm/netpbm.odin
Normal file
681
core/image/netpbm/netpbm.odin
Normal file
@@ -0,0 +1,681 @@
|
||||
package netpbm
|
||||
|
||||
import "core:bytes"
|
||||
import "core:fmt"
|
||||
import "core:image"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
import "core:strconv"
|
||||
import "core:strings"
|
||||
import "core:unicode"
|
||||
|
||||
|
||||
|
||||
Image :: image.Image
|
||||
Format :: image.Netpbm_Format
|
||||
Header :: image.Netpbm_Header
|
||||
Info :: image.Netpbm_Info
|
||||
Error :: image.Error
|
||||
Format_Error :: image.Netpbm_Error
|
||||
|
||||
|
||||
|
||||
Formats :: bit_set[Format]
|
||||
PBM :: Formats{.P1, .P4}
|
||||
PGM :: Formats{.P2, .P5}
|
||||
PPM :: Formats{.P3, .P6}
|
||||
PNM :: PBM + PGM + PPM
|
||||
PAM :: Formats{.P7}
|
||||
PFM :: Formats{.Pf, .PF}
|
||||
ASCII :: Formats{.P1, .P2, .P3}
|
||||
BINARY :: Formats{.P4, .P5, .P6} + PAM + PFM
|
||||
|
||||
|
||||
|
||||
read :: proc {
|
||||
read_from_file,
|
||||
read_from_buffer,
|
||||
}
|
||||
|
||||
read_from_file :: proc(filename: string, allocator := context.allocator) -> (img: Image, err: Error) {
|
||||
context.allocator = allocator
|
||||
|
||||
data, ok := os.read_entire_file(filename); defer delete(data)
|
||||
if !ok {
|
||||
err = .File_Not_Readable
|
||||
return
|
||||
}
|
||||
|
||||
return read_from_buffer(data)
|
||||
}
|
||||
|
||||
read_from_buffer :: proc(data: []byte, allocator := context.allocator) -> (img: Image, err: Error) {
|
||||
context.allocator = allocator
|
||||
|
||||
header: Header; defer header_destroy(&header)
|
||||
header_size: int
|
||||
header, header_size = parse_header(data) or_return
|
||||
|
||||
img_data := data[header_size:]
|
||||
img = decode_image(header, img_data) or_return
|
||||
|
||||
info := new(Info)
|
||||
info.header = header
|
||||
if header.format == .P7 && header.tupltype != "" {
|
||||
info.header.tupltype = strings.clone(header.tupltype)
|
||||
}
|
||||
img.metadata = info
|
||||
|
||||
err = Format_Error.None
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
write :: proc {
|
||||
write_to_file,
|
||||
write_to_buffer,
|
||||
}
|
||||
|
||||
write_to_file :: proc(filename: string, img: Image, allocator := context.allocator) -> (err: Error) {
|
||||
context.allocator = allocator
|
||||
|
||||
data: []byte; defer delete(data)
|
||||
data = write_to_buffer(img) or_return
|
||||
|
||||
if ok := os.write_entire_file(filename, data); !ok {
|
||||
return .File_Not_Writable
|
||||
}
|
||||
|
||||
return Format_Error.None
|
||||
}
|
||||
|
||||
write_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: []byte, err: Error) {
|
||||
context.allocator = allocator
|
||||
|
||||
info, ok := img.metadata.(^image.Netpbm_Info)
|
||||
if !ok {
|
||||
err = image.General_Image_Error.Invalid_Input_Image
|
||||
return
|
||||
}
|
||||
// using info so we can just talk about the header
|
||||
using info
|
||||
|
||||
//? validation
|
||||
if header.format in (PBM + PGM + Formats{.Pf}) && img.channels != 1 \
|
||||
|| header.format in (PPM + Formats{.PF}) && img.channels != 3 {
|
||||
err = Format_Error.Invalid_Number_Of_Channels
|
||||
return
|
||||
}
|
||||
|
||||
if header.format in (PNM + PAM) {
|
||||
if header.maxval <= int(max(u8)) && img.depth != 1 \
|
||||
|| header.maxval > int(max(u8)) && header.maxval <= int(max(u16)) && img.depth != 2 {
|
||||
err = Format_Error.Invalid_Image_Depth
|
||||
return
|
||||
}
|
||||
} else if header.format in PFM && img.depth != 4 {
|
||||
err = Format_Error.Invalid_Image_Depth
|
||||
return
|
||||
}
|
||||
|
||||
// we will write to a string builder
|
||||
data: strings.Builder
|
||||
strings.init_builder(&data)
|
||||
|
||||
// all PNM headers start with the format
|
||||
fmt.sbprintf(&data, "%s\n", header.format)
|
||||
if header.format in PNM {
|
||||
fmt.sbprintf(&data, "%i %i\n", img.width, img.height)
|
||||
if header.format not_in PBM {
|
||||
fmt.sbprintf(&data, "%i\n", header.maxval)
|
||||
}
|
||||
} else if header.format in PAM {
|
||||
fmt.sbprintf(&data, "WIDTH %i\nHEIGHT %i\nMAXVAL %i\nDEPTH %i\nTUPLTYPE %s\nENDHDR\n",
|
||||
img.width, img.height, header.maxval, img.channels, header.tupltype)
|
||||
} else if header.format in PFM {
|
||||
scale := -header.scale if header.little_endian else header.scale
|
||||
fmt.sbprintf(&data, "%i %i\n%f\n", img.width, img.height, scale)
|
||||
}
|
||||
|
||||
switch header.format {
|
||||
// Compressed binary
|
||||
case .P4:
|
||||
header_buf := data.buf[:]
|
||||
pixels := img.pixels.buf[:]
|
||||
|
||||
p4_buffer_size := (img.width / 8 + 1) * img.height
|
||||
reserve(&data.buf, len(header_buf) + p4_buffer_size)
|
||||
|
||||
// we build up a byte value until it is completely filled
|
||||
// or we reach the end the row
|
||||
for y in 0 ..< img.height {
|
||||
b: byte
|
||||
|
||||
for x in 0 ..< img.width {
|
||||
i := y * img.width + x
|
||||
bit := byte(7 - (x % 8))
|
||||
v : byte = 0 if pixels[i] == 0 else 1
|
||||
b |= (v << bit)
|
||||
|
||||
if bit == 0 {
|
||||
append(&data.buf, b)
|
||||
b = 0
|
||||
}
|
||||
}
|
||||
|
||||
if b != 0 {
|
||||
append(&data.buf, b)
|
||||
b = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Simple binary
|
||||
case .P5, .P6, .P7, .Pf, .PF:
|
||||
header_buf := data.buf[:]
|
||||
pixels := img.pixels.buf[:]
|
||||
|
||||
resize(&data.buf, len(header_buf) + len(pixels))
|
||||
mem.copy(raw_data(data.buf[len(header_buf):]), raw_data(pixels), len(pixels))
|
||||
|
||||
// convert from native endianness
|
||||
if img.depth == 2 {
|
||||
pixels := mem.slice_data_cast([]u16be, data.buf[len(header_buf):])
|
||||
for p in &pixels {
|
||||
p = u16be(transmute(u16) p)
|
||||
}
|
||||
} else if header.format in PFM {
|
||||
if header.little_endian {
|
||||
pixels := mem.slice_data_cast([]f32le, data.buf[len(header_buf):])
|
||||
for p in &pixels {
|
||||
p = f32le(transmute(f32) p)
|
||||
}
|
||||
} else {
|
||||
pixels := mem.slice_data_cast([]f32be, data.buf[len(header_buf):])
|
||||
for p in &pixels {
|
||||
p = f32be(transmute(f32) p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If-it-looks-like-a-bitmap ASCII
|
||||
case .P1:
|
||||
pixels := img.pixels.buf[:]
|
||||
for y in 0 ..< img.height {
|
||||
for x in 0 ..< img.width {
|
||||
i := y * img.width + x
|
||||
append(&data.buf, '0' if pixels[i] == 0 else '1')
|
||||
}
|
||||
append(&data.buf, '\n')
|
||||
}
|
||||
|
||||
// Token ASCII
|
||||
case .P2, .P3:
|
||||
switch img.depth {
|
||||
case 1:
|
||||
pixels := img.pixels.buf[:]
|
||||
for y in 0 ..< img.height {
|
||||
for x in 0 ..< img.width {
|
||||
i := y * img.width + x
|
||||
for c in 0 ..< img.channels {
|
||||
i := i * img.channels + c
|
||||
fmt.sbprintf(&data, "%i ", pixels[i])
|
||||
}
|
||||
fmt.sbprint(&data, "\n")
|
||||
}
|
||||
fmt.sbprint(&data, "\n")
|
||||
}
|
||||
|
||||
case 2:
|
||||
pixels := mem.slice_data_cast([]u16, img.pixels.buf[:])
|
||||
for y in 0 ..< img.height {
|
||||
for x in 0 ..< img.width {
|
||||
i := y * img.width + x
|
||||
for c in 0 ..< img.channels {
|
||||
i := i * img.channels + c
|
||||
fmt.sbprintf(&data, "%i ", pixels[i])
|
||||
}
|
||||
fmt.sbprint(&data, "\n")
|
||||
}
|
||||
fmt.sbprint(&data, "\n")
|
||||
}
|
||||
|
||||
case:
|
||||
return data.buf[:], Format_Error.Invalid_Image_Depth
|
||||
}
|
||||
|
||||
case:
|
||||
return data.buf[:], Format_Error.Invalid_Format
|
||||
}
|
||||
|
||||
return data.buf[:], Format_Error.None
|
||||
}
|
||||
|
||||
|
||||
|
||||
parse_header :: proc(data: []byte, allocator := context.allocator) -> (header: Header, length: int, err: Error) {
|
||||
context.allocator = allocator
|
||||
|
||||
// we need the signature and a space
|
||||
if len(data) < 3 {
|
||||
err = Format_Error.Incomplete_Header
|
||||
return
|
||||
}
|
||||
|
||||
if data[0] == 'P' {
|
||||
switch data[1] {
|
||||
case '1' ..= '6':
|
||||
return _parse_header_pnm(data)
|
||||
case '7':
|
||||
return _parse_header_pam(data, allocator)
|
||||
case 'F', 'f':
|
||||
return _parse_header_pfm(data)
|
||||
}
|
||||
}
|
||||
|
||||
err = Format_Error.Invalid_Signature
|
||||
return
|
||||
}
|
||||
|
||||
@(private)
|
||||
_parse_header_pnm :: proc(data: []byte) -> (header: Header, length: int, err: Error) {
|
||||
SIG_LENGTH :: 2
|
||||
|
||||
{
|
||||
header_formats := []Format{.P1, .P2, .P3, .P4, .P5, .P6}
|
||||
header.format = header_formats[data[1] - '0' - 1]
|
||||
}
|
||||
|
||||
// have a list of fielda for easy iteration
|
||||
header_fields: []^int
|
||||
if header.format in PBM {
|
||||
header_fields = {&header.width, &header.height}
|
||||
header.maxval = 1 // we know maxval for a bitmap
|
||||
} else {
|
||||
header_fields = {&header.width, &header.height, &header.maxval}
|
||||
}
|
||||
|
||||
// we're keeping track of the header byte length
|
||||
length = SIG_LENGTH
|
||||
|
||||
// loop state
|
||||
in_comment := false
|
||||
already_in_space := true
|
||||
current_field := 0
|
||||
current_value := header_fields[0]
|
||||
|
||||
parse_loop: for d, i in data[SIG_LENGTH:] {
|
||||
length += 1
|
||||
|
||||
// handle comments
|
||||
if in_comment {
|
||||
switch d {
|
||||
// comments only go up to next carriage return or line feed
|
||||
case '\r', '\n':
|
||||
in_comment = false
|
||||
}
|
||||
continue
|
||||
} else if d == '#' {
|
||||
in_comment = true
|
||||
continue
|
||||
}
|
||||
|
||||
// handle whitespace
|
||||
in_space := unicode.is_white_space(rune(d))
|
||||
if in_space {
|
||||
if already_in_space {
|
||||
continue
|
||||
}
|
||||
already_in_space = true
|
||||
|
||||
// switch to next value
|
||||
current_field += 1
|
||||
if current_field == len(header_fields) {
|
||||
// header byte length is 1-index so we'll increment again
|
||||
length += 1
|
||||
break parse_loop
|
||||
}
|
||||
current_value = header_fields[current_field]
|
||||
} else {
|
||||
already_in_space = false
|
||||
|
||||
if !unicode.is_digit(rune(d)) {
|
||||
err = Format_Error.Invalid_Header_Token_Character
|
||||
return
|
||||
}
|
||||
|
||||
val := int(d - '0')
|
||||
current_value^ = current_value^ * 10 + val
|
||||
}
|
||||
}
|
||||
|
||||
// set extra info
|
||||
header.channels = 3 if header.format in PPM else 1
|
||||
header.depth = 2 if header.maxval > int(max(u8)) else 1
|
||||
|
||||
// limit checking
|
||||
if current_field < len(header_fields) {
|
||||
err = Format_Error.Incomplete_Header
|
||||
return
|
||||
}
|
||||
|
||||
if header.width < 1 \
|
||||
|| header.height < 1 \
|
||||
|| header.maxval < 1 || header.maxval > int(max(u16)) {
|
||||
err = Format_Error.Invalid_Header_Value
|
||||
return
|
||||
}
|
||||
|
||||
err = Format_Error.None
|
||||
return
|
||||
}
|
||||
|
||||
@(private)
|
||||
_parse_header_pam :: proc(data: []byte, allocator := context.allocator) -> (header: Header, length: int, err: Error) {
|
||||
context.allocator = allocator
|
||||
|
||||
// the spec needs the newline apparently
|
||||
if string(data[0:3]) != "P7\n" {
|
||||
err = Format_Error.Invalid_Signature
|
||||
return
|
||||
}
|
||||
header.format = .P7
|
||||
|
||||
SIGNATURE_LENGTH :: 3
|
||||
HEADER_END :: "ENDHDR\n"
|
||||
|
||||
// we can already work out the size of the header
|
||||
header_end_index := strings.index(string(data), HEADER_END)
|
||||
if header_end_index == -1 {
|
||||
err = Format_Error.Incomplete_Header
|
||||
return
|
||||
}
|
||||
length = header_end_index + len(HEADER_END)
|
||||
|
||||
// string buffer for the tupltype
|
||||
tupltype: strings.Builder
|
||||
strings.init_builder(&tupltype, context.temp_allocator); defer strings.destroy_builder(&tupltype)
|
||||
fmt.sbprint(&tupltype, "")
|
||||
|
||||
// PAM uses actual lines, so we can iterate easily
|
||||
line_iterator := string(data[SIGNATURE_LENGTH : header_end_index])
|
||||
parse_loop: for line in strings.split_lines_iterator(&line_iterator) {
|
||||
line := line
|
||||
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
|
||||
field, ok := strings.fields_iterator(&line)
|
||||
value := strings.trim_space(line)
|
||||
|
||||
// the field will change, but the logic stays the same
|
||||
current_field: ^int
|
||||
|
||||
switch field {
|
||||
case "WIDTH": current_field = &header.width
|
||||
case "HEIGHT": current_field = &header.height
|
||||
case "DEPTH": current_field = &header.channels
|
||||
case "MAXVAL": current_field = &header.maxval
|
||||
|
||||
case "TUPLTYPE":
|
||||
if len(value) == 0 {
|
||||
err = Format_Error.Invalid_Header_Value
|
||||
return
|
||||
}
|
||||
|
||||
if len(tupltype.buf) == 0 {
|
||||
fmt.sbprint(&tupltype, value)
|
||||
} else {
|
||||
fmt.sbprint(&tupltype, "", value)
|
||||
}
|
||||
|
||||
continue
|
||||
|
||||
case:
|
||||
continue
|
||||
}
|
||||
|
||||
if current_field^ != 0 {
|
||||
err = Format_Error.Duplicate_Header_Field
|
||||
return
|
||||
}
|
||||
current_field^, ok = strconv.parse_int(value)
|
||||
if !ok {
|
||||
err = Format_Error.Invalid_Header_Value
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// extra info
|
||||
header.depth = 2 if header.maxval > int(max(u8)) else 1
|
||||
|
||||
// limit checking
|
||||
if header.width < 1 \
|
||||
|| header.height < 1 \
|
||||
|| header.depth < 1 \
|
||||
|| header.maxval < 1 \
|
||||
|| header.maxval > int(max(u16)) {
|
||||
err = Format_Error.Invalid_Header_Value
|
||||
return
|
||||
}
|
||||
|
||||
header.tupltype = strings.clone(strings.to_string(tupltype))
|
||||
err = Format_Error.None
|
||||
return
|
||||
}
|
||||
|
||||
@(private)
|
||||
_parse_header_pfm :: proc(data: []byte) -> (header: Header, length: int, err: Error) {
|
||||
// we can just cycle through tokens for PFM
|
||||
field_iterator := string(data)
|
||||
field, ok := strings.fields_iterator(&field_iterator)
|
||||
|
||||
switch field {
|
||||
case "Pf":
|
||||
header.format = .Pf
|
||||
header.channels = 1
|
||||
case "PF":
|
||||
header.format = .PF
|
||||
header.channels = 3
|
||||
case:
|
||||
err = Format_Error.Invalid_Signature
|
||||
return
|
||||
}
|
||||
|
||||
// floating point
|
||||
header.depth = 4
|
||||
|
||||
// width
|
||||
field, ok = strings.fields_iterator(&field_iterator)
|
||||
if !ok {
|
||||
err = Format_Error.Incomplete_Header
|
||||
return
|
||||
}
|
||||
header.width, ok = strconv.parse_int(field)
|
||||
if !ok {
|
||||
err = Format_Error.Invalid_Header_Value
|
||||
return
|
||||
}
|
||||
|
||||
// height
|
||||
field, ok = strings.fields_iterator(&field_iterator)
|
||||
if !ok {
|
||||
err = Format_Error.Incomplete_Header
|
||||
return
|
||||
}
|
||||
header.height, ok = strconv.parse_int(field)
|
||||
if !ok {
|
||||
err = Format_Error.Invalid_Header_Value
|
||||
return
|
||||
}
|
||||
|
||||
// scale (sign is endianness)
|
||||
field, ok = strings.fields_iterator(&field_iterator)
|
||||
if !ok {
|
||||
err = Format_Error.Incomplete_Header
|
||||
return
|
||||
}
|
||||
header.scale, ok = strconv.parse_f32(field)
|
||||
if !ok {
|
||||
err = Format_Error.Invalid_Header_Value
|
||||
return
|
||||
}
|
||||
|
||||
if header.scale < 0.0 {
|
||||
header.little_endian = true
|
||||
header.scale = -header.scale
|
||||
}
|
||||
|
||||
// pointer math to get header size
|
||||
length = int((uintptr(raw_data(field_iterator)) + 1) - uintptr(raw_data(data)))
|
||||
|
||||
// limit checking
|
||||
if header.width < 1 \
|
||||
|| header.height < 1 \
|
||||
|| header.scale == 0.0 {
|
||||
err = Format_Error.Invalid_Header_Value
|
||||
return
|
||||
}
|
||||
|
||||
err = Format_Error.None
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
decode_image :: proc(header: Header, data: []byte, allocator := context.allocator) -> (img: Image, err: Error) {
|
||||
context.allocator = allocator
|
||||
|
||||
img = Image {
|
||||
width = header.width,
|
||||
height = header.height,
|
||||
channels = header.channels,
|
||||
depth = header.depth,
|
||||
}
|
||||
|
||||
buffer_size := img.width * img.height * img.channels * img.depth
|
||||
|
||||
// we can check data size for binary formats
|
||||
if header.format in BINARY {
|
||||
if header.format == .P4 {
|
||||
p4_size := (img.width / 8 + 1) * img.height
|
||||
if len(data) < p4_size {
|
||||
err = Format_Error.Buffer_Too_Small
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if len(data) < buffer_size {
|
||||
err = Format_Error.Buffer_Too_Small
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for ASCII and P4, we use length for the termination condition, so start at 0
|
||||
// BINARY will be a simple memcopy so the buffer length should also be initialised
|
||||
if header.format in ASCII || header.format == .P4 {
|
||||
bytes.buffer_init_allocator(&img.pixels, 0, buffer_size)
|
||||
} else {
|
||||
bytes.buffer_init_allocator(&img.pixels, buffer_size, buffer_size)
|
||||
}
|
||||
|
||||
switch header.format {
|
||||
// Compressed binary
|
||||
case .P4:
|
||||
for d in data {
|
||||
for b in 1 ..= 8 {
|
||||
bit := byte(8 - b)
|
||||
pix := (d >> bit) & 1
|
||||
bytes.buffer_write_byte(&img.pixels, pix)
|
||||
if len(img.pixels.buf) % img.width == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(img.pixels.buf) == cap(img.pixels.buf) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Simple binary
|
||||
case .P5, .P6, .P7, .Pf, .PF:
|
||||
mem.copy(raw_data(img.pixels.buf), raw_data(data), buffer_size)
|
||||
|
||||
// convert to native endianness
|
||||
if header.format in PFM {
|
||||
pixels := mem.slice_data_cast([]f32, img.pixels.buf[:])
|
||||
if header.little_endian {
|
||||
for p in &pixels {
|
||||
p = f32(transmute(f32le) p)
|
||||
}
|
||||
} else {
|
||||
for p in &pixels {
|
||||
p = f32(transmute(f32be) p)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if img.depth == 2 {
|
||||
pixels := mem.slice_data_cast([]u16, img.pixels.buf[:])
|
||||
for p in &pixels {
|
||||
p = u16(transmute(u16be) p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If-it-looks-like-a-bitmap ASCII
|
||||
case .P1:
|
||||
for c in data {
|
||||
switch c {
|
||||
case '0', '1':
|
||||
bytes.buffer_write_byte(&img.pixels, c - '0')
|
||||
}
|
||||
|
||||
if len(img.pixels.buf) == cap(img.pixels.buf) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(img.pixels.buf) < cap(img.pixels.buf) {
|
||||
err = Format_Error.Buffer_Too_Small
|
||||
return
|
||||
}
|
||||
|
||||
// Token ASCII
|
||||
case .P2, .P3:
|
||||
field_iterator := string(data)
|
||||
for field in strings.fields_iterator(&field_iterator) {
|
||||
value, ok := strconv.parse_int(field)
|
||||
if !ok {
|
||||
err = Format_Error.Invalid_Buffer_ASCII_Token
|
||||
return
|
||||
}
|
||||
|
||||
//? do we want to enforce the maxval, the limit, or neither
|
||||
if value > int(max(u16)) /*header.maxval*/ {
|
||||
err = Format_Error.Invalid_Buffer_Value
|
||||
return
|
||||
}
|
||||
|
||||
switch img.depth {
|
||||
case 1:
|
||||
bytes.buffer_write_byte(&img.pixels, u8(value))
|
||||
case 2:
|
||||
vb := transmute([2]u8) u16(value)
|
||||
bytes.buffer_write(&img.pixels, vb[:])
|
||||
}
|
||||
|
||||
if len(img.pixels.buf) == cap(img.pixels.buf) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(img.pixels.buf) < cap(img.pixels.buf) {
|
||||
err = Format_Error.Buffer_Too_Small
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = Format_Error.None
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user