Merge pull request #1726 from WalterPlinge/image-netpbm

Add Netpbm image format support
This commit is contained in:
Jeroen van Rijn
2022-04-30 22:01:22 +02:00
committed by GitHub
8 changed files with 1050 additions and 53 deletions

View File

@@ -45,7 +45,7 @@ Image :: struct {
width: int,
height: int,
channels: int,
depth: int,
depth: int, // Channel depth in bits, typically 8 or 16
pixels: bytes.Buffer,
/*
Some image loaders/writers can return/take an optional background color.
@@ -57,6 +57,7 @@ Image :: struct {
}
Image_Metadata :: union {
^Netpbm_Info,
^PNG_Info,
^QOI_Info,
}
@@ -140,18 +141,20 @@ Option :: enum {
alpha_drop_if_present, // Unimplemented for QOI. Returns error.
alpha_premultiply, // Unimplemented for QOI. Returns error.
blend_background, // Ignored for non-PNG formats
// Unimplemented
do_not_expand_grayscale,
do_not_expand_indexed,
do_not_expand_channels,
// SAVE OPTIONS
qoi_all_channels_linear, // QOI, informative info. If not set, defaults to sRGB with linear alpha.
qoi_all_channels_linear, // QOI, informative only. If not set, defaults to sRGB with linear alpha.
}
Options :: distinct bit_set[Option]
Error :: union #shared_nil {
General_Image_Error,
Netpbm_Error,
PNG_Error,
QOI_Error,
@@ -164,11 +167,68 @@ Error :: union #shared_nil {
General_Image_Error :: enum {
None = 0,
Invalid_Image_Dimensions,
Image_Dimensions_Too_Large,
Image_Does_Not_Adhere_to_Spec,
// File I/O
Unable_To_Read_File,
Unable_To_Write_File,
// Invalid
Invalid_Signature,
Invalid_Input_Image,
Image_Dimensions_Too_Large,
Invalid_Image_Dimensions,
Invalid_Number_Of_Channels,
Image_Does_Not_Adhere_to_Spec,
Invalid_Image_Depth,
Invalid_Bit_Depth,
Invalid_Color_Space,
// More data than pixels to decode into, for example.
Corrupt,
// Output buffer is the wrong size
Invalid_Output,
// Allocation
Unable_To_Allocate_Or_Resize,
}
/*
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
Invalid_Header_Token_Character,
Incomplete_Header,
Invalid_Header_Value,
Duplicate_Header_Field,
Buffer_Too_Small,
Invalid_Buffer_ASCII_Token,
Invalid_Buffer_Value,
// writing
Invalid_Format,
}
/*
@@ -176,7 +236,6 @@ General_Image_Error :: enum {
*/
PNG_Error :: enum {
None = 0,
Invalid_PNG_Signature,
IHDR_Not_First_Chunk,
IHDR_Corrupt,
IDAT_Missing,
@@ -292,15 +351,10 @@ PNG_Interlace_Method :: enum u8 {
*/
QOI_Error :: enum {
None = 0,
Invalid_QOI_Signature,
Invalid_Number_Of_Channels, // QOI allows 3 or 4 channel data.
Invalid_Bit_Depth, // QOI supports only 8-bit images, error only returned from writer.
Invalid_Color_Space, // QOI allows 0 = sRGB or 1 = linear.
Corrupt, // More data than pixels to decode into, for example.
Missing_Or_Corrupt_Trailer, // Image seemed to have decoded okay, but trailer is missing or corrupt.
}
QOI_Magic :: u32be(0x716f6966) // "qoif"
QOI_Magic :: u32be(0x716f6966) // "qoif"
QOI_Color_Space :: enum u8 {
sRGB = 0,
@@ -1125,10 +1179,10 @@ write_bytes :: proc(buf: ^bytes.Buffer, data: []u8) -> (err: compress.General_Er
return nil
} else if len(data) == 1 {
if bytes.buffer_write_byte(buf, data[0]) != nil {
return compress.General_Error.Resize_Failed
return .Resize_Failed
}
} else if n, _ := bytes.buffer_write(buf, data); n != len(data) {
return compress.General_Error.Resize_Failed
return .Resize_Failed
}
return nil
}

View 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

View File

@@ -0,0 +1,28 @@
package netpbm
import "core:bytes"
import "core:image"
destroy :: proc(img: ^image.Image) -> bool {
if img == nil do return false
defer free(img)
bytes.buffer_destroy(&img.pixels)
//! TEMP CAST
info, ok := img.metadata.(^image.Netpbm_Info)
if !ok do return false
header_destroy(&info.header)
free(info)
img.metadata = nil
return true
}
header_destroy :: proc(using header: ^Header) {
if format == .P7 && tupltype != "" {
delete(tupltype)
tupltype = ""
}
}

View File

@@ -0,0 +1,751 @@
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
load :: proc {
load_from_file,
load_from_buffer,
}
load_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 = .Unable_To_Read_File
return
}
return load_from_buffer(data)
}
load_from_buffer :: proc(data: []byte, allocator := context.allocator) -> (img: ^Image, err: Error) {
context.allocator = allocator
img = new(Image)
header: Header; defer header_destroy(&header)
header_size: int
header, header_size = parse_header(data) or_return
img_data := data[header_size:]
decode_image(img, 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
return img, nil
}
save :: proc {
save_to_file,
save_to_buffer,
}
save_to_file :: proc(filename: string, img: ^Image, custom_info: Info = {}, allocator := context.allocator) -> (err: Error) {
context.allocator = allocator
data: []byte; defer delete(data)
data = save_to_buffer(img, custom_info) or_return
if ok := os.write_entire_file(filename, data); !ok {
return .Unable_To_Write_File
}
return Format_Error.None
}
save_to_buffer :: proc(img: ^Image, custom_info: Info = {}, allocator := context.allocator) -> (buffer: []byte, err: Error) {
context.allocator = allocator
info: Info = {}
if custom_info.header.width > 0 {
// Custom info has been set, use it.
info = custom_info
} else {
img_info, ok := img.metadata.(^image.Netpbm_Info)
if !ok {
// image doesn't have .Netpbm info, guess it
auto_info, auto_info_found := autoselect_pbm_format_from_image(img)
if auto_info_found {
info = auto_info
} else {
return {}, .Invalid_Input_Image
}
} else {
// use info as stored on image
info = img_info^
}
}
// 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 = .Invalid_Number_Of_Channels
return
}
if header.format in (PNM + PAM) {
if header.maxval <= int(max(u8)) && img.depth != 8 \
|| header.maxval > int(max(u8)) && header.maxval <= int(max(u16)) && img.depth != 16 {
err = .Invalid_Image_Depth
return
}
} else if header.format in PFM && img.depth != 32 {
err = .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 {
if len(header.tupltype) > 0 {
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 {
fmt.sbprintf(&data, "WIDTH %i\nHEIGHT %i\nMAXVAL %i\nDEPTH %i\nENDHDR\n",
img.width, img.height, header.maxval, img.channels)
}
} 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 == 16 {
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 8:
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 16:
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[:], .Invalid_Image_Depth
}
case:
return data.buf[:], .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 = .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 = 16 if header.maxval > int(max(u8)) else 8
// 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)) {
fmt.printf("[pnm] Header: {{width = %v, height = %v, maxval: %v}}\n", header.width, header.height, header.maxval)
err = .Invalid_Header_Value
return
}
length -= 1
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 = .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 = .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 = 16 if header.maxval > int(max(u8)) else 8
// limit checking
if header.width < 1 \
|| header.height < 1 \
|| header.maxval < 1 \
|| header.maxval > int(max(u16)) {
fmt.printf("[pam] Header: {{width = %v, height = %v, maxval: %v}}\n", header.width, header.height, header.maxval)
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 = .Invalid_Signature
return
}
// floating point
header.depth = 32
// 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 {
fmt.printf("[pfm] Header: {{width = %v, height = %v, scale: %v}}\n", header.width, header.height, header.scale)
err = .Invalid_Header_Value
return
}
err = Format_Error.None
return
}
decode_image :: proc(img: ^Image, header: Header, data: []byte, allocator := context.allocator) -> (err: Error) {
assert(img != nil)
context.allocator = allocator
img.width = header.width
img.height = header.height
img.channels = header.channels
img.depth = header.depth
buffer_size := image.compute_buffer_size(img.width, img.height, img.channels, img.depth)
// we can check data size for binary formats
if header.format in BINARY {
if len(data) < buffer_size {
fmt.printf("len(data): %v, buffer size: %v\n", len(data), buffer_size)
return .Buffer_Too_Small
}
}
// 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:
copy(img.pixels.buf[:], data[:])
// 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 == 16 {
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 8:
bytes.buffer_write_byte(&img.pixels, u8(value))
case 16:
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
}
// Automatically try to select an appropriate format to save to based on `img.channel` and `img.depth`
autoselect_pbm_format_from_image :: proc(img: ^Image, prefer_binary := true, force_black_and_white := false, pfm_scale := f32(1.0)) -> (res: Info, ok: bool) {
/*
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)
ASCII :: Formats{.P1, .P2, .P3}
*/
using res.header
width = img.width
height = img.height
channels = img.channels
depth = img.depth
maxval = 255 if img.depth == 8 else 65535
little_endian = true if ODIN_ENDIAN == .Little else false
// Assume we'll find a suitable format
ok = true
switch img.channels {
case 1:
// Must be Portable Float Map
if img.depth == 32 {
format = .Pf
return
}
if force_black_and_white {
// Portable Bit Map
format = .P4 if prefer_binary else .P1
maxval = 1
return
} else {
// Portable Gray Map
format = .P5 if prefer_binary else .P2
return
}
case 3:
// Must be Portable Float Map
if img.depth == 32 {
format = .PF
return
}
// Portable Pixel Map
format = .P6 if prefer_binary else .P3
return
case:
// Portable Arbitrary Map
if img.depth == 8 || img.depth == 16 {
format = .P7
scale = pfm_scale
return
}
}
// We couldn't find a suitable format
return {}, false
}

View File

@@ -238,7 +238,7 @@ append_chunk :: proc(list: ^[dynamic]image.PNG_Chunk, src: image.PNG_Chunk, allo
append(list, c)
if len(list) != length + 1 {
// Resize during append failed.
return mem.Allocator_Error.Out_Of_Memory
return .Unable_To_Allocate_Or_Resize
}
return
@@ -347,7 +347,7 @@ load_from_file :: proc(filename: string, options := Options{}, allocator := cont
return load_from_slice(data, options)
} else {
img = new(Image)
return img, compress.General_Error.File_Not_Found
return img, .Unable_To_Read_File
}
}
@@ -381,7 +381,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
signature, io_error := compress.read_data(ctx, Signature)
if io_error != .None || signature != .PNG {
return img, .Invalid_PNG_Signature
return img, .Invalid_Signature
}
idat: []u8
@@ -747,7 +747,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8)
t := bytes.Buffer{}
if !resize(&t.buf, dest_raw_size) {
return {}, mem.Allocator_Error.Out_Of_Memory
return {}, .Unable_To_Allocate_Or_Resize
}
i := 0; j := 0
@@ -828,7 +828,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 16)
t := bytes.Buffer{}
if !resize(&t.buf, dest_raw_size) {
return {}, mem.Allocator_Error.Out_Of_Memory
return {}, .Unable_To_Allocate_Or_Resize
}
p16 := mem.slice_data_cast([]u16, temp.buf[:])
@@ -1027,7 +1027,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8)
t := bytes.Buffer{}
if !resize(&t.buf, dest_raw_size) {
return {}, mem.Allocator_Error.Out_Of_Memory
return {}, .Unable_To_Allocate_Or_Resize
}
p := mem.slice_data_cast([]u8, temp.buf[:])
@@ -1535,7 +1535,7 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^image.PNG_IH
num_bytes := compute_buffer_size(width, height, channels, depth == 16 ? 16 : 8)
if !resize(&img.pixels.buf, num_bytes) {
return mem.Allocator_Error.Out_Of_Memory
return .Unable_To_Allocate_Or_Resize
}
filter_ok: bool
@@ -1577,7 +1577,7 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^image.PNG_IH
temp: bytes.Buffer
temp_len := compute_buffer_size(x, y, channels, depth == 16 ? 16 : 8)
if !resize(&temp.buf, temp_len) {
return mem.Allocator_Error.Out_Of_Memory
return .Unable_To_Allocate_Or_Resize
}
params := Filter_Params{

View File

@@ -12,14 +12,12 @@
// The QOI specification is at https://qoiformat.org.
package qoi
import "core:mem"
import "core:image"
import "core:compress"
import "core:bytes"
import "core:os"
Error :: image.Error
General :: compress.General_Error
Image :: image.Image
Options :: image.Options
@@ -57,7 +55,7 @@ save_to_memory :: proc(output: ^bytes.Buffer, img: ^Image, options := Options{}
max_size := pixels * (img.channels + 1) + size_of(image.QOI_Header) + size_of(u64be)
if !resize(&output.buf, max_size) {
return General.Resize_Failed
return .Unable_To_Allocate_Or_Resize
}
header := image.QOI_Header{
@@ -177,7 +175,7 @@ save_to_file :: proc(output: string, img: ^Image, options := Options{}, allocato
save_to_memory(out, img, options) or_return
write_ok := os.write_entire_file(output, out.buf[:])
return nil if write_ok else General.Cannot_Open_File
return nil if write_ok else .Unable_To_Write_File
}
save :: proc{save_to_memory, save_to_file}
@@ -201,7 +199,7 @@ load_from_file :: proc(filename: string, options := Options{}, allocator := cont
return load_from_slice(data, options)
} else {
img = new(Image)
return img, compress.General_Error.File_Not_Found
return img, .Unable_To_Read_File
}
}
@@ -221,7 +219,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
header := image.read_data(ctx, image.QOI_Header) or_return
if header.magic != image.QOI_Magic {
return img, .Invalid_QOI_Signature
return img, .Invalid_Signature
}
if img == nil {
@@ -264,7 +262,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
bytes_needed := image.compute_buffer_size(int(header.width), int(header.height), img.channels, 8)
if !resize(&img.pixels.buf, bytes_needed) {
return img, mem.Allocator_Error.Out_Of_Memory
return img, .Unable_To_Allocate_Or_Resize
}
/*

View File

@@ -17,7 +17,6 @@ import "core:bytes"
import "core:os"
Error :: image.Error
General :: compress.General_Error
Image :: image.Image
Options :: image.Options
@@ -55,7 +54,7 @@ save_to_memory :: proc(output: ^bytes.Buffer, img: ^Image, options := Options{}
necessary := pixels * img.channels + size_of(image.TGA_Header)
if !resize(&output.buf, necessary) {
return General.Resize_Failed
return .Unable_To_Allocate_Or_Resize
}
header := image.TGA_Header{
@@ -97,7 +96,7 @@ save_to_file :: proc(output: string, img: ^Image, options := Options{}, allocato
save_to_memory(out, img, options) or_return
write_ok := os.write_entire_file(output, out.buf[:])
return nil if write_ok else General.Cannot_Open_File
return nil if write_ok else .Unable_To_Write_File
}
save :: proc{save_to_memory, save_to_file}

View File

@@ -13,6 +13,7 @@ import "core:testing"
import "core:compress"
import "core:image"
import pbm "core:image/netpbm"
import "core:image/png"
import "core:image/qoi"
@@ -1199,37 +1200,37 @@ Corrupt_PNG_Tests := []PNG_Test{
{
"xs1n0g01", // signature byte 1 MSBit reset to zero
{
{Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
{Default, .Invalid_Signature, {}, 0x_0000_0000},
},
},
{
"xs2n0g01", // signature byte 2 is a 'Q'
{
{Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
{Default, .Invalid_Signature, {}, 0x_0000_0000},
},
},
{
"xs4n0g01", // signature byte 4 lowercase
{
{Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
{Default, .Invalid_Signature, {}, 0x_0000_0000},
},
},
{
"xs7n0g01", // 7th byte a space instead of control-Z
{
{Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
{Default, .Invalid_Signature, {}, 0x_0000_0000},
},
},
{
"xcrn0g04", // added cr bytes
{
{Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
{Default, .Invalid_Signature, {}, 0x_0000_0000},
},
},
{
"xlfn0g04", // added lf bytes
{
{Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
{Default, .Invalid_Signature, {}, 0x_0000_0000},
},
},
{
@@ -1506,25 +1507,159 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) {
passed &= test.hash == png_hash
// Roundtrip through QOI to test the QOI encoder and decoder.
if passed && img.depth == 8 && (img.channels == 3 || img.channels == 4) {
qoi_buffer: bytes.Buffer
defer bytes.buffer_destroy(&qoi_buffer)
qoi_save_err := qoi.save(&qoi_buffer, img)
if passed {
// Roundtrip through QOI to test the QOI encoder and decoder.
if img.depth == 8 && (img.channels == 3 || img.channels == 4) {
qoi_buffer: bytes.Buffer
defer bytes.buffer_destroy(&qoi_buffer)
qoi_save_err := qoi.save(&qoi_buffer, img)
error = fmt.tprintf("%v test %v QOI save failed with %v.", file.file, count, qoi_save_err)
expect(t, qoi_save_err == nil, error)
error = fmt.tprintf("%v test %v QOI save failed with %v.", file.file, count, qoi_save_err)
expect(t, qoi_save_err == nil, error)
if qoi_save_err == nil {
qoi_img, qoi_load_err := qoi.load(qoi_buffer.buf[:])
defer qoi.destroy(qoi_img)
if qoi_save_err == nil {
qoi_img, qoi_load_err := qoi.load(qoi_buffer.buf[:])
defer qoi.destroy(qoi_img)
error = fmt.tprintf("%v test %v QOI load failed with %v.", file.file, count, qoi_load_err)
expect(t, qoi_load_err == nil, error)
error = fmt.tprintf("%v test %v QOI load failed with %v.", file.file, count, qoi_load_err)
expect(t, qoi_load_err == nil, error)
qoi_hash := hash.crc32(qoi_img.pixels.buf[:])
error = fmt.tprintf("%v test %v QOI load hash is %08x, expected it match PNG's %08x with %v.", file.file, count, qoi_hash, png_hash, test.options)
expect(t, qoi_hash == png_hash, error)
qoi_hash := hash.crc32(qoi_img.pixels.buf[:])
error = fmt.tprintf("%v test %v QOI load hash is %08x, expected it match PNG's %08x with %v.", file.file, count, qoi_hash, png_hash, test.options)
expect(t, qoi_hash == png_hash, error)
}
}
{
// Roundtrip through PBM to test the PBM encoders and decoders - prefer binary
pbm_buf, pbm_save_err := pbm.save_to_buffer(img)
defer delete(pbm_buf)
error = fmt.tprintf("%v test %v PBM save failed with %v.", file.file, count, pbm_save_err)
expect(t, pbm_save_err == nil, error)
if pbm_save_err == nil {
// Try to load it again.
pbm_img, pbm_load_err := pbm.load(pbm_buf)
defer pbm.destroy(pbm_img)
error = fmt.tprintf("%v test %v PBM load failed with %v.", file.file, count, pbm_load_err)
expect(t, pbm_load_err == nil, error)
if pbm_load_err == nil {
pbm_hash := hash.crc32(pbm_img.pixels.buf[:])
error = fmt.tprintf("%v test %v PBM load hash is %08x, expected it match PNG's %08x with %v.", file.file, count, pbm_hash, png_hash, test.options)
expect(t, pbm_hash == png_hash, error)
}
}
}
{
// Roundtrip through PBM to test the PBM encoders and decoders - prefer ASCII
pbm_info, pbm_format_selected := pbm.autoselect_pbm_format_from_image(img, false)
// We already tested the binary formats above.
if pbm_info.header.format in pbm.ASCII {
pbm_buf, pbm_save_err := pbm.save_to_buffer(img, pbm_info)
defer delete(pbm_buf)
error = fmt.tprintf("%v test %v PBM save failed with %v.", file.file, count, pbm_save_err)
expect(t, pbm_save_err == nil, error)
if pbm_save_err == nil {
// Try to load it again.
pbm_img, pbm_load_err := pbm.load(pbm_buf)
defer pbm.destroy(pbm_img)
error = fmt.tprintf("%v test %v PBM load failed with %v.", file.file, count, pbm_load_err)
expect(t, pbm_load_err == nil, error)
if pbm_load_err == nil {
pbm_hash := hash.crc32(pbm_img.pixels.buf[:])
error = fmt.tprintf("%v test %v PBM load hash is %08x, expected it match PNG's %08x with %v.", file.file, count, pbm_hash, png_hash, test.options)
expect(t, pbm_hash == png_hash, error)
}
}
}
}
{
// We still need to test Portable Float Maps
if (img.channels == 1 || img.channels == 3) && (img.depth == 8 || img.depth == 16) {
// Make temporary float image
float_img := new(image.Image)
defer png.destroy(float_img)
float_img.width = img.width
float_img.height = img.height
float_img.channels = img.channels
float_img.depth = 32
buffer_size := image.compute_buffer_size(img.width, img.height, img.channels, 32)
resize(&float_img.pixels.buf, buffer_size)
pbm_info := pbm.Info {
header = {
width = img.width,
height = img.height,
channels = img.channels,
depth = img.depth,
maxval = 255 if img.depth == 8 else 65535,
little_endian = true if ODIN_ENDIAN == .Little else false,
scale = 1.0,
format = .Pf if img.channels == 1 else .PF,
},
}
// Transform data...
orig_float := mem.slice_data_cast([]f32, float_img.pixels.buf[:])
switch img.depth {
case 8:
for v, i in img.pixels.buf {
orig_float[i] = f32(v) / f32(256)
}
case 16:
wide := mem.slice_data_cast([]u16, img.pixels.buf[:])
for v, i in wide {
orig_float[i] = f32(v) / f32(65536)
}
}
float_pbm_buf, float_pbm_save_err := pbm.save_to_buffer(float_img, pbm_info)
defer delete(float_pbm_buf)
error = fmt.tprintf("%v test %v save as PFM failed with %v", file.file, count, float_pbm_save_err)
expect(t, float_pbm_save_err == nil, error)
if float_pbm_save_err == nil {
// Load float image and compare.
float_pbm_img, float_pbm_load_err := pbm.load(float_pbm_buf)
defer pbm.destroy(float_pbm_img)
error = fmt.tprintf("%v test %v PFM load failed with %v", file.file, count, float_pbm_load_err)
expect(t, float_pbm_load_err == nil, error)
load_float := mem.slice_data_cast([]f32, float_pbm_img.pixels.buf[:])
error = fmt.tprintf("%v test %v PFM load returned %v floats, expected %v", file.file, count, len(load_float), len(orig_float))
expect(t, len(load_float) == len(orig_float), error)
// Compare floats
equal := true
for orig, i in orig_float {
if orig != load_float[i] {
equal = false
break
}
}
error = fmt.tprintf("%v test %v PFM loaded floats to match", file.file, count)
expect(t, equal, error)
}
}
}
}