mirror of
https://github.com/Ed94/Odin.git
synced 2026-08-06 15:48:51 +00:00
Merge pull request #1726 from WalterPlinge/image-netpbm
Add Netpbm image format support
This commit is contained in:
+68
-14
@@ -45,7 +45,7 @@ Image :: struct {
|
|||||||
width: int,
|
width: int,
|
||||||
height: int,
|
height: int,
|
||||||
channels: int,
|
channels: int,
|
||||||
depth: int,
|
depth: int, // Channel depth in bits, typically 8 or 16
|
||||||
pixels: bytes.Buffer,
|
pixels: bytes.Buffer,
|
||||||
/*
|
/*
|
||||||
Some image loaders/writers can return/take an optional background color.
|
Some image loaders/writers can return/take an optional background color.
|
||||||
@@ -57,6 +57,7 @@ Image :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Image_Metadata :: union {
|
Image_Metadata :: union {
|
||||||
|
^Netpbm_Info,
|
||||||
^PNG_Info,
|
^PNG_Info,
|
||||||
^QOI_Info,
|
^QOI_Info,
|
||||||
}
|
}
|
||||||
@@ -140,18 +141,20 @@ Option :: enum {
|
|||||||
alpha_drop_if_present, // Unimplemented for QOI. Returns error.
|
alpha_drop_if_present, // Unimplemented for QOI. Returns error.
|
||||||
alpha_premultiply, // Unimplemented for QOI. Returns error.
|
alpha_premultiply, // Unimplemented for QOI. Returns error.
|
||||||
blend_background, // Ignored for non-PNG formats
|
blend_background, // Ignored for non-PNG formats
|
||||||
|
|
||||||
// Unimplemented
|
// Unimplemented
|
||||||
do_not_expand_grayscale,
|
do_not_expand_grayscale,
|
||||||
do_not_expand_indexed,
|
do_not_expand_indexed,
|
||||||
do_not_expand_channels,
|
do_not_expand_channels,
|
||||||
|
|
||||||
// SAVE OPTIONS
|
// 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]
|
Options :: distinct bit_set[Option]
|
||||||
|
|
||||||
Error :: union #shared_nil {
|
Error :: union #shared_nil {
|
||||||
General_Image_Error,
|
General_Image_Error,
|
||||||
|
Netpbm_Error,
|
||||||
PNG_Error,
|
PNG_Error,
|
||||||
QOI_Error,
|
QOI_Error,
|
||||||
|
|
||||||
@@ -164,11 +167,68 @@ Error :: union #shared_nil {
|
|||||||
|
|
||||||
General_Image_Error :: enum {
|
General_Image_Error :: enum {
|
||||||
None = 0,
|
None = 0,
|
||||||
Invalid_Image_Dimensions,
|
// File I/O
|
||||||
Image_Dimensions_Too_Large,
|
Unable_To_Read_File,
|
||||||
Image_Does_Not_Adhere_to_Spec,
|
Unable_To_Write_File,
|
||||||
|
|
||||||
|
// Invalid
|
||||||
|
Invalid_Signature,
|
||||||
Invalid_Input_Image,
|
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,
|
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 {
|
PNG_Error :: enum {
|
||||||
None = 0,
|
None = 0,
|
||||||
Invalid_PNG_Signature,
|
|
||||||
IHDR_Not_First_Chunk,
|
IHDR_Not_First_Chunk,
|
||||||
IHDR_Corrupt,
|
IHDR_Corrupt,
|
||||||
IDAT_Missing,
|
IDAT_Missing,
|
||||||
@@ -292,15 +351,10 @@ PNG_Interlace_Method :: enum u8 {
|
|||||||
*/
|
*/
|
||||||
QOI_Error :: enum {
|
QOI_Error :: enum {
|
||||||
None = 0,
|
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.
|
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 {
|
QOI_Color_Space :: enum u8 {
|
||||||
sRGB = 0,
|
sRGB = 0,
|
||||||
@@ -1125,10 +1179,10 @@ write_bytes :: proc(buf: ^bytes.Buffer, data: []u8) -> (err: compress.General_Er
|
|||||||
return nil
|
return nil
|
||||||
} else if len(data) == 1 {
|
} else if len(data) == 1 {
|
||||||
if bytes.buffer_write_byte(buf, data[0]) != nil {
|
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) {
|
} else if n, _ := bytes.buffer_write(buf, data); n != len(data) {
|
||||||
return compress.General_Error.Resize_Failed
|
return .Resize_Failed
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
append(list, c)
|
||||||
if len(list) != length + 1 {
|
if len(list) != length + 1 {
|
||||||
// Resize during append failed.
|
// Resize during append failed.
|
||||||
return mem.Allocator_Error.Out_Of_Memory
|
return .Unable_To_Allocate_Or_Resize
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -347,7 +347,7 @@ load_from_file :: proc(filename: string, options := Options{}, allocator := cont
|
|||||||
return load_from_slice(data, options)
|
return load_from_slice(data, options)
|
||||||
} else {
|
} else {
|
||||||
img = new(Image)
|
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)
|
signature, io_error := compress.read_data(ctx, Signature)
|
||||||
if io_error != .None || signature != .PNG {
|
if io_error != .None || signature != .PNG {
|
||||||
return img, .Invalid_PNG_Signature
|
return img, .Invalid_Signature
|
||||||
}
|
}
|
||||||
|
|
||||||
idat: []u8
|
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)
|
dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8)
|
||||||
t := bytes.Buffer{}
|
t := bytes.Buffer{}
|
||||||
if !resize(&t.buf, dest_raw_size) {
|
if !resize(&t.buf, dest_raw_size) {
|
||||||
return {}, mem.Allocator_Error.Out_Of_Memory
|
return {}, .Unable_To_Allocate_Or_Resize
|
||||||
}
|
}
|
||||||
|
|
||||||
i := 0; j := 0
|
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)
|
dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 16)
|
||||||
t := bytes.Buffer{}
|
t := bytes.Buffer{}
|
||||||
if !resize(&t.buf, dest_raw_size) {
|
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[:])
|
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)
|
dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8)
|
||||||
t := bytes.Buffer{}
|
t := bytes.Buffer{}
|
||||||
if !resize(&t.buf, dest_raw_size) {
|
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[:])
|
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)
|
num_bytes := compute_buffer_size(width, height, channels, depth == 16 ? 16 : 8)
|
||||||
if !resize(&img.pixels.buf, num_bytes) {
|
if !resize(&img.pixels.buf, num_bytes) {
|
||||||
return mem.Allocator_Error.Out_Of_Memory
|
return .Unable_To_Allocate_Or_Resize
|
||||||
}
|
}
|
||||||
|
|
||||||
filter_ok: bool
|
filter_ok: bool
|
||||||
@@ -1577,7 +1577,7 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^image.PNG_IH
|
|||||||
temp: bytes.Buffer
|
temp: bytes.Buffer
|
||||||
temp_len := compute_buffer_size(x, y, channels, depth == 16 ? 16 : 8)
|
temp_len := compute_buffer_size(x, y, channels, depth == 16 ? 16 : 8)
|
||||||
if !resize(&temp.buf, temp_len) {
|
if !resize(&temp.buf, temp_len) {
|
||||||
return mem.Allocator_Error.Out_Of_Memory
|
return .Unable_To_Allocate_Or_Resize
|
||||||
}
|
}
|
||||||
|
|
||||||
params := Filter_Params{
|
params := Filter_Params{
|
||||||
|
|||||||
@@ -12,14 +12,12 @@
|
|||||||
// The QOI specification is at https://qoiformat.org.
|
// The QOI specification is at https://qoiformat.org.
|
||||||
package qoi
|
package qoi
|
||||||
|
|
||||||
import "core:mem"
|
|
||||||
import "core:image"
|
import "core:image"
|
||||||
import "core:compress"
|
import "core:compress"
|
||||||
import "core:bytes"
|
import "core:bytes"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
|
|
||||||
Error :: image.Error
|
Error :: image.Error
|
||||||
General :: compress.General_Error
|
|
||||||
Image :: image.Image
|
Image :: image.Image
|
||||||
Options :: image.Options
|
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)
|
max_size := pixels * (img.channels + 1) + size_of(image.QOI_Header) + size_of(u64be)
|
||||||
|
|
||||||
if !resize(&output.buf, max_size) {
|
if !resize(&output.buf, max_size) {
|
||||||
return General.Resize_Failed
|
return .Unable_To_Allocate_Or_Resize
|
||||||
}
|
}
|
||||||
|
|
||||||
header := image.QOI_Header{
|
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
|
save_to_memory(out, img, options) or_return
|
||||||
write_ok := os.write_entire_file(output, out.buf[:])
|
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}
|
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)
|
return load_from_slice(data, options)
|
||||||
} else {
|
} else {
|
||||||
img = new(Image)
|
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
|
header := image.read_data(ctx, image.QOI_Header) or_return
|
||||||
if header.magic != image.QOI_Magic {
|
if header.magic != image.QOI_Magic {
|
||||||
return img, .Invalid_QOI_Signature
|
return img, .Invalid_Signature
|
||||||
}
|
}
|
||||||
|
|
||||||
if img == nil {
|
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)
|
bytes_needed := image.compute_buffer_size(int(header.width), int(header.height), img.channels, 8)
|
||||||
|
|
||||||
if !resize(&img.pixels.buf, bytes_needed) {
|
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"
|
import "core:os"
|
||||||
|
|
||||||
Error :: image.Error
|
Error :: image.Error
|
||||||
General :: compress.General_Error
|
|
||||||
Image :: image.Image
|
Image :: image.Image
|
||||||
Options :: image.Options
|
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)
|
necessary := pixels * img.channels + size_of(image.TGA_Header)
|
||||||
|
|
||||||
if !resize(&output.buf, necessary) {
|
if !resize(&output.buf, necessary) {
|
||||||
return General.Resize_Failed
|
return .Unable_To_Allocate_Or_Resize
|
||||||
}
|
}
|
||||||
|
|
||||||
header := image.TGA_Header{
|
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
|
save_to_memory(out, img, options) or_return
|
||||||
write_ok := os.write_entire_file(output, out.buf[:])
|
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}
|
save :: proc{save_to_memory, save_to_file}
|
||||||
@@ -13,6 +13,7 @@ import "core:testing"
|
|||||||
|
|
||||||
import "core:compress"
|
import "core:compress"
|
||||||
import "core:image"
|
import "core:image"
|
||||||
|
import pbm "core:image/netpbm"
|
||||||
import "core:image/png"
|
import "core:image/png"
|
||||||
import "core:image/qoi"
|
import "core:image/qoi"
|
||||||
|
|
||||||
@@ -1199,37 +1200,37 @@ Corrupt_PNG_Tests := []PNG_Test{
|
|||||||
{
|
{
|
||||||
"xs1n0g01", // signature byte 1 MSBit reset to zero
|
"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'
|
"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
|
"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
|
"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
|
"xcrn0g04", // added cr bytes
|
||||||
{
|
{
|
||||||
{Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
|
{Default, .Invalid_Signature, {}, 0x_0000_0000},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"xlfn0g04", // added lf bytes
|
"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
|
passed &= test.hash == png_hash
|
||||||
|
|
||||||
// Roundtrip through QOI to test the QOI encoder and decoder.
|
if passed {
|
||||||
if passed && img.depth == 8 && (img.channels == 3 || img.channels == 4) {
|
// Roundtrip through QOI to test the QOI encoder and decoder.
|
||||||
qoi_buffer: bytes.Buffer
|
if img.depth == 8 && (img.channels == 3 || img.channels == 4) {
|
||||||
defer bytes.buffer_destroy(&qoi_buffer)
|
qoi_buffer: bytes.Buffer
|
||||||
qoi_save_err := qoi.save(&qoi_buffer, img)
|
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)
|
error = fmt.tprintf("%v test %v QOI save failed with %v.", file.file, count, qoi_save_err)
|
||||||
expect(t, qoi_save_err == nil, error)
|
expect(t, qoi_save_err == nil, error)
|
||||||
|
|
||||||
if qoi_save_err == nil {
|
if qoi_save_err == nil {
|
||||||
qoi_img, qoi_load_err := qoi.load(qoi_buffer.buf[:])
|
qoi_img, qoi_load_err := qoi.load(qoi_buffer.buf[:])
|
||||||
defer qoi.destroy(qoi_img)
|
defer qoi.destroy(qoi_img)
|
||||||
|
|
||||||
error = fmt.tprintf("%v test %v QOI load failed with %v.", file.file, count, qoi_load_err)
|
error = fmt.tprintf("%v test %v QOI load failed with %v.", file.file, count, qoi_load_err)
|
||||||
expect(t, qoi_load_err == nil, error)
|
expect(t, qoi_load_err == nil, error)
|
||||||
|
|
||||||
qoi_hash := hash.crc32(qoi_img.pixels.buf[:])
|
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)
|
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)
|
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