mirror of
https://github.com/odin-lang/Odin.git
synced 2026-04-23 23:05:18 +00:00
Merge pull request #1726 from WalterPlinge/image-netpbm
Add Netpbm image format support
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
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
|
||||
28
core/image/netpbm/helpers.odin
Normal file
28
core/image/netpbm/helpers.odin
Normal 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 = ""
|
||||
}
|
||||
}
|
||||
751
core/image/netpbm/netpbm.odin
Normal file
751
core/image/netpbm/netpbm.odin
Normal 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
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user