From 8d370fabdd7925f1856a998063ef5de49f4efa2a Mon Sep 17 00:00:00 2001 From: WalterPlinge <22519813+WalterPlinge@users.noreply.github.com> Date: Mon, 18 Apr 2022 01:00:19 +0100 Subject: [PATCH 1/7] Added initial Netpbm image format support --- core/image/common.odin | 46 +++ core/image/netpbm/doc.odin | 32 ++ core/image/netpbm/helpers.odin | 26 ++ core/image/netpbm/netpbm.odin | 681 +++++++++++++++++++++++++++++++++ 4 files changed, 785 insertions(+) create mode 100644 core/image/netpbm/doc.odin create mode 100644 core/image/netpbm/helpers.odin create mode 100644 core/image/netpbm/netpbm.odin diff --git a/core/image/common.odin b/core/image/common.odin index 2e7bca17e..bf93e9313 100644 --- a/core/image/common.odin +++ b/core/image/common.odin @@ -57,6 +57,7 @@ Image :: struct { } Image_Metadata :: union { + ^Netpbm_Info, ^PNG_Info, ^QOI_Info, } @@ -152,6 +153,7 @@ Options :: distinct bit_set[Option] Error :: union #shared_nil { General_Image_Error, + Netpbm_Error, PNG_Error, QOI_Error, @@ -171,6 +173,50 @@ General_Image_Error :: enum { Invalid_Output, } +/* + Netpbm-specific definitions +*/ +Netpbm_Format :: enum { + P1, P2, P3, P4, P5, P6, P7, Pf, PF, +} + +Netpbm_Header :: struct { + format: Netpbm_Format, + width: int, + height: int, + channels: int, + depth: int, + maxval: int, + tupltype: string, + scale: f32, + little_endian: bool, +} + +Netpbm_Info :: struct { + header: Netpbm_Header, +} + +Netpbm_Error :: enum { + None = 0, + + // reading + File_Not_Readable, + Invalid_Signature, + Invalid_Header_Token_Character, + Incomplete_Header, + Invalid_Header_Value, + Duplicate_Header_Field, + Buffer_Too_Small, + Invalid_Buffer_ASCII_Token, + Invalid_Buffer_Value, + + // writing + File_Not_Writable, + Invalid_Format, + Invalid_Number_Of_Channels, + Invalid_Image_Depth, +} + /* PNG-specific definitions */ diff --git a/core/image/netpbm/doc.odin b/core/image/netpbm/doc.odin new file mode 100644 index 000000000..baeb99968 --- /dev/null +++ b/core/image/netpbm/doc.odin @@ -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 diff --git a/core/image/netpbm/helpers.odin b/core/image/netpbm/helpers.odin new file mode 100644 index 000000000..5a3000a87 --- /dev/null +++ b/core/image/netpbm/helpers.odin @@ -0,0 +1,26 @@ +package netpbm + +import "core:bytes" +import "core:image" + +destroy :: proc(img: ^image.Image) -> bool { + if img == nil do return false + + //! TEMP CAST + info, ok := img.metadata.(^image.Netpbm_Info) + if !ok do return false + + bytes.buffer_destroy(&img.pixels) + header_destroy(&info.header) + free(info) + img.metadata = nil + + return true +} + +header_destroy :: proc(using header: ^Header) { + if format == .P7 && tupltype != "" { + delete(tupltype) + tupltype = "" + } +} diff --git a/core/image/netpbm/netpbm.odin b/core/image/netpbm/netpbm.odin new file mode 100644 index 000000000..fa8ffc1db --- /dev/null +++ b/core/image/netpbm/netpbm.odin @@ -0,0 +1,681 @@ +package netpbm + +import "core:bytes" +import "core:fmt" +import "core:image" +import "core:mem" +import "core:os" +import "core:strconv" +import "core:strings" +import "core:unicode" + + + +Image :: image.Image +Format :: image.Netpbm_Format +Header :: image.Netpbm_Header +Info :: image.Netpbm_Info +Error :: image.Error +Format_Error :: image.Netpbm_Error + + + +Formats :: bit_set[Format] +PBM :: Formats{.P1, .P4} +PGM :: Formats{.P2, .P5} +PPM :: Formats{.P3, .P6} +PNM :: PBM + PGM + PPM +PAM :: Formats{.P7} +PFM :: Formats{.Pf, .PF} +ASCII :: Formats{.P1, .P2, .P3} +BINARY :: Formats{.P4, .P5, .P6} + PAM + PFM + + + +read :: proc { + read_from_file, + read_from_buffer, +} + +read_from_file :: proc(filename: string, allocator := context.allocator) -> (img: Image, err: Error) { + context.allocator = allocator + + data, ok := os.read_entire_file(filename); defer delete(data) + if !ok { + err = .File_Not_Readable + return + } + + return read_from_buffer(data) +} + +read_from_buffer :: proc(data: []byte, allocator := context.allocator) -> (img: Image, err: Error) { + context.allocator = allocator + + header: Header; defer header_destroy(&header) + header_size: int + header, header_size = parse_header(data) or_return + + img_data := data[header_size:] + img = decode_image(header, img_data) or_return + + info := new(Info) + info.header = header + if header.format == .P7 && header.tupltype != "" { + info.header.tupltype = strings.clone(header.tupltype) + } + img.metadata = info + + err = Format_Error.None + return +} + + + +write :: proc { + write_to_file, + write_to_buffer, +} + +write_to_file :: proc(filename: string, img: Image, allocator := context.allocator) -> (err: Error) { + context.allocator = allocator + + data: []byte; defer delete(data) + data = write_to_buffer(img) or_return + + if ok := os.write_entire_file(filename, data); !ok { + return .File_Not_Writable + } + + return Format_Error.None +} + +write_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: []byte, err: Error) { + context.allocator = allocator + + info, ok := img.metadata.(^image.Netpbm_Info) + if !ok { + err = image.General_Image_Error.Invalid_Input_Image + return + } + // using info so we can just talk about the header + using info + + //? validation + if header.format in (PBM + PGM + Formats{.Pf}) && img.channels != 1 \ + || header.format in (PPM + Formats{.PF}) && img.channels != 3 { + err = Format_Error.Invalid_Number_Of_Channels + return + } + + if header.format in (PNM + PAM) { + if header.maxval <= int(max(u8)) && img.depth != 1 \ + || header.maxval > int(max(u8)) && header.maxval <= int(max(u16)) && img.depth != 2 { + err = Format_Error.Invalid_Image_Depth + return + } + } else if header.format in PFM && img.depth != 4 { + err = Format_Error.Invalid_Image_Depth + return + } + + // we will write to a string builder + data: strings.Builder + strings.init_builder(&data) + + // all PNM headers start with the format + fmt.sbprintf(&data, "%s\n", header.format) + if header.format in PNM { + fmt.sbprintf(&data, "%i %i\n", img.width, img.height) + if header.format not_in PBM { + fmt.sbprintf(&data, "%i\n", header.maxval) + } + } else if header.format in PAM { + fmt.sbprintf(&data, "WIDTH %i\nHEIGHT %i\nMAXVAL %i\nDEPTH %i\nTUPLTYPE %s\nENDHDR\n", + img.width, img.height, header.maxval, img.channels, header.tupltype) + } else if header.format in PFM { + scale := -header.scale if header.little_endian else header.scale + fmt.sbprintf(&data, "%i %i\n%f\n", img.width, img.height, scale) + } + + switch header.format { + // Compressed binary + case .P4: + header_buf := data.buf[:] + pixels := img.pixels.buf[:] + + p4_buffer_size := (img.width / 8 + 1) * img.height + reserve(&data.buf, len(header_buf) + p4_buffer_size) + + // we build up a byte value until it is completely filled + // or we reach the end the row + for y in 0 ..< img.height { + b: byte + + for x in 0 ..< img.width { + i := y * img.width + x + bit := byte(7 - (x % 8)) + v : byte = 0 if pixels[i] == 0 else 1 + b |= (v << bit) + + if bit == 0 { + append(&data.buf, b) + b = 0 + } + } + + if b != 0 { + append(&data.buf, b) + b = 0 + } + } + + // Simple binary + case .P5, .P6, .P7, .Pf, .PF: + header_buf := data.buf[:] + pixels := img.pixels.buf[:] + + resize(&data.buf, len(header_buf) + len(pixels)) + mem.copy(raw_data(data.buf[len(header_buf):]), raw_data(pixels), len(pixels)) + + // convert from native endianness + if img.depth == 2 { + pixels := mem.slice_data_cast([]u16be, data.buf[len(header_buf):]) + for p in &pixels { + p = u16be(transmute(u16) p) + } + } else if header.format in PFM { + if header.little_endian { + pixels := mem.slice_data_cast([]f32le, data.buf[len(header_buf):]) + for p in &pixels { + p = f32le(transmute(f32) p) + } + } else { + pixels := mem.slice_data_cast([]f32be, data.buf[len(header_buf):]) + for p in &pixels { + p = f32be(transmute(f32) p) + } + } + } + + // If-it-looks-like-a-bitmap ASCII + case .P1: + pixels := img.pixels.buf[:] + for y in 0 ..< img.height { + for x in 0 ..< img.width { + i := y * img.width + x + append(&data.buf, '0' if pixels[i] == 0 else '1') + } + append(&data.buf, '\n') + } + + // Token ASCII + case .P2, .P3: + switch img.depth { + case 1: + pixels := img.pixels.buf[:] + for y in 0 ..< img.height { + for x in 0 ..< img.width { + i := y * img.width + x + for c in 0 ..< img.channels { + i := i * img.channels + c + fmt.sbprintf(&data, "%i ", pixels[i]) + } + fmt.sbprint(&data, "\n") + } + fmt.sbprint(&data, "\n") + } + + case 2: + pixels := mem.slice_data_cast([]u16, img.pixels.buf[:]) + for y in 0 ..< img.height { + for x in 0 ..< img.width { + i := y * img.width + x + for c in 0 ..< img.channels { + i := i * img.channels + c + fmt.sbprintf(&data, "%i ", pixels[i]) + } + fmt.sbprint(&data, "\n") + } + fmt.sbprint(&data, "\n") + } + + case: + return data.buf[:], Format_Error.Invalid_Image_Depth + } + + case: + return data.buf[:], Format_Error.Invalid_Format + } + + return data.buf[:], Format_Error.None +} + + + +parse_header :: proc(data: []byte, allocator := context.allocator) -> (header: Header, length: int, err: Error) { + context.allocator = allocator + + // we need the signature and a space + if len(data) < 3 { + err = Format_Error.Incomplete_Header + return + } + + if data[0] == 'P' { + switch data[1] { + case '1' ..= '6': + return _parse_header_pnm(data) + case '7': + return _parse_header_pam(data, allocator) + case 'F', 'f': + return _parse_header_pfm(data) + } + } + + err = Format_Error.Invalid_Signature + return +} + +@(private) +_parse_header_pnm :: proc(data: []byte) -> (header: Header, length: int, err: Error) { + SIG_LENGTH :: 2 + + { + header_formats := []Format{.P1, .P2, .P3, .P4, .P5, .P6} + header.format = header_formats[data[1] - '0' - 1] + } + + // have a list of fielda for easy iteration + header_fields: []^int + if header.format in PBM { + header_fields = {&header.width, &header.height} + header.maxval = 1 // we know maxval for a bitmap + } else { + header_fields = {&header.width, &header.height, &header.maxval} + } + + // we're keeping track of the header byte length + length = SIG_LENGTH + + // loop state + in_comment := false + already_in_space := true + current_field := 0 + current_value := header_fields[0] + + parse_loop: for d, i in data[SIG_LENGTH:] { + length += 1 + + // handle comments + if in_comment { + switch d { + // comments only go up to next carriage return or line feed + case '\r', '\n': + in_comment = false + } + continue + } else if d == '#' { + in_comment = true + continue + } + + // handle whitespace + in_space := unicode.is_white_space(rune(d)) + if in_space { + if already_in_space { + continue + } + already_in_space = true + + // switch to next value + current_field += 1 + if current_field == len(header_fields) { + // header byte length is 1-index so we'll increment again + length += 1 + break parse_loop + } + current_value = header_fields[current_field] + } else { + already_in_space = false + + if !unicode.is_digit(rune(d)) { + err = Format_Error.Invalid_Header_Token_Character + return + } + + val := int(d - '0') + current_value^ = current_value^ * 10 + val + } + } + + // set extra info + header.channels = 3 if header.format in PPM else 1 + header.depth = 2 if header.maxval > int(max(u8)) else 1 + + // limit checking + if current_field < len(header_fields) { + err = Format_Error.Incomplete_Header + return + } + + if header.width < 1 \ + || header.height < 1 \ + || header.maxval < 1 || header.maxval > int(max(u16)) { + err = Format_Error.Invalid_Header_Value + return + } + + err = Format_Error.None + return +} + +@(private) +_parse_header_pam :: proc(data: []byte, allocator := context.allocator) -> (header: Header, length: int, err: Error) { + context.allocator = allocator + + // the spec needs the newline apparently + if string(data[0:3]) != "P7\n" { + err = Format_Error.Invalid_Signature + return + } + header.format = .P7 + + SIGNATURE_LENGTH :: 3 + HEADER_END :: "ENDHDR\n" + + // we can already work out the size of the header + header_end_index := strings.index(string(data), HEADER_END) + if header_end_index == -1 { + err = Format_Error.Incomplete_Header + return + } + length = header_end_index + len(HEADER_END) + + // string buffer for the tupltype + tupltype: strings.Builder + strings.init_builder(&tupltype, context.temp_allocator); defer strings.destroy_builder(&tupltype) + fmt.sbprint(&tupltype, "") + + // PAM uses actual lines, so we can iterate easily + line_iterator := string(data[SIGNATURE_LENGTH : header_end_index]) + parse_loop: for line in strings.split_lines_iterator(&line_iterator) { + line := line + + if len(line) == 0 || line[0] == '#' { + continue + } + + field, ok := strings.fields_iterator(&line) + value := strings.trim_space(line) + + // the field will change, but the logic stays the same + current_field: ^int + + switch field { + case "WIDTH": current_field = &header.width + case "HEIGHT": current_field = &header.height + case "DEPTH": current_field = &header.channels + case "MAXVAL": current_field = &header.maxval + + case "TUPLTYPE": + if len(value) == 0 { + err = Format_Error.Invalid_Header_Value + return + } + + if len(tupltype.buf) == 0 { + fmt.sbprint(&tupltype, value) + } else { + fmt.sbprint(&tupltype, "", value) + } + + continue + + case: + continue + } + + if current_field^ != 0 { + err = Format_Error.Duplicate_Header_Field + return + } + current_field^, ok = strconv.parse_int(value) + if !ok { + err = Format_Error.Invalid_Header_Value + return + } + } + + // extra info + header.depth = 2 if header.maxval > int(max(u8)) else 1 + + // limit checking + if header.width < 1 \ + || header.height < 1 \ + || header.depth < 1 \ + || header.maxval < 1 \ + || header.maxval > int(max(u16)) { + err = Format_Error.Invalid_Header_Value + return + } + + header.tupltype = strings.clone(strings.to_string(tupltype)) + err = Format_Error.None + return +} + +@(private) +_parse_header_pfm :: proc(data: []byte) -> (header: Header, length: int, err: Error) { + // we can just cycle through tokens for PFM + field_iterator := string(data) + field, ok := strings.fields_iterator(&field_iterator) + + switch field { + case "Pf": + header.format = .Pf + header.channels = 1 + case "PF": + header.format = .PF + header.channels = 3 + case: + err = Format_Error.Invalid_Signature + return + } + + // floating point + header.depth = 4 + + // width + field, ok = strings.fields_iterator(&field_iterator) + if !ok { + err = Format_Error.Incomplete_Header + return + } + header.width, ok = strconv.parse_int(field) + if !ok { + err = Format_Error.Invalid_Header_Value + return + } + + // height + field, ok = strings.fields_iterator(&field_iterator) + if !ok { + err = Format_Error.Incomplete_Header + return + } + header.height, ok = strconv.parse_int(field) + if !ok { + err = Format_Error.Invalid_Header_Value + return + } + + // scale (sign is endianness) + field, ok = strings.fields_iterator(&field_iterator) + if !ok { + err = Format_Error.Incomplete_Header + return + } + header.scale, ok = strconv.parse_f32(field) + if !ok { + err = Format_Error.Invalid_Header_Value + return + } + + if header.scale < 0.0 { + header.little_endian = true + header.scale = -header.scale + } + + // pointer math to get header size + length = int((uintptr(raw_data(field_iterator)) + 1) - uintptr(raw_data(data))) + + // limit checking + if header.width < 1 \ + || header.height < 1 \ + || header.scale == 0.0 { + err = Format_Error.Invalid_Header_Value + return + } + + err = Format_Error.None + return +} + + + +decode_image :: proc(header: Header, data: []byte, allocator := context.allocator) -> (img: Image, err: Error) { + context.allocator = allocator + + img = Image { + width = header.width, + height = header.height, + channels = header.channels, + depth = header.depth, + } + + buffer_size := img.width * img.height * img.channels * img.depth + + // we can check data size for binary formats + if header.format in BINARY { + if header.format == .P4 { + p4_size := (img.width / 8 + 1) * img.height + if len(data) < p4_size { + err = Format_Error.Buffer_Too_Small + return + } + } else { + if len(data) < buffer_size { + err = Format_Error.Buffer_Too_Small + return + } + } + } + + // for ASCII and P4, we use length for the termination condition, so start at 0 + // BINARY will be a simple memcopy so the buffer length should also be initialised + if header.format in ASCII || header.format == .P4 { + bytes.buffer_init_allocator(&img.pixels, 0, buffer_size) + } else { + bytes.buffer_init_allocator(&img.pixels, buffer_size, buffer_size) + } + + switch header.format { + // Compressed binary + case .P4: + for d in data { + for b in 1 ..= 8 { + bit := byte(8 - b) + pix := (d >> bit) & 1 + bytes.buffer_write_byte(&img.pixels, pix) + if len(img.pixels.buf) % img.width == 0 { + break + } + } + + if len(img.pixels.buf) == cap(img.pixels.buf) { + break + } + } + + // Simple binary + case .P5, .P6, .P7, .Pf, .PF: + mem.copy(raw_data(img.pixels.buf), raw_data(data), buffer_size) + + // convert to native endianness + if header.format in PFM { + pixels := mem.slice_data_cast([]f32, img.pixels.buf[:]) + if header.little_endian { + for p in &pixels { + p = f32(transmute(f32le) p) + } + } else { + for p in &pixels { + p = f32(transmute(f32be) p) + } + } + } else { + if img.depth == 2 { + pixels := mem.slice_data_cast([]u16, img.pixels.buf[:]) + for p in &pixels { + p = u16(transmute(u16be) p) + } + } + } + + // If-it-looks-like-a-bitmap ASCII + case .P1: + for c in data { + switch c { + case '0', '1': + bytes.buffer_write_byte(&img.pixels, c - '0') + } + + if len(img.pixels.buf) == cap(img.pixels.buf) { + break + } + } + + if len(img.pixels.buf) < cap(img.pixels.buf) { + err = Format_Error.Buffer_Too_Small + return + } + + // Token ASCII + case .P2, .P3: + field_iterator := string(data) + for field in strings.fields_iterator(&field_iterator) { + value, ok := strconv.parse_int(field) + if !ok { + err = Format_Error.Invalid_Buffer_ASCII_Token + return + } + + //? do we want to enforce the maxval, the limit, or neither + if value > int(max(u16)) /*header.maxval*/ { + err = Format_Error.Invalid_Buffer_Value + return + } + + switch img.depth { + case 1: + bytes.buffer_write_byte(&img.pixels, u8(value)) + case 2: + vb := transmute([2]u8) u16(value) + bytes.buffer_write(&img.pixels, vb[:]) + } + + if len(img.pixels.buf) == cap(img.pixels.buf) { + break + } + } + + if len(img.pixels.buf) < cap(img.pixels.buf) { + err = Format_Error.Buffer_Too_Small + return + } + } + + err = Format_Error.None + return +} From b6abd691f487d4583df65e7127d05b3feaf9f181 Mon Sep 17 00:00:00 2001 From: WalterPlinge <22519813+WalterPlinge@users.noreply.github.com> Date: Mon, 18 Apr 2022 20:42:50 +0100 Subject: [PATCH 2/7] Image: Fix implicit enum error --- core/image/common.odin | 3 +-- core/image/netpbm/netpbm.odin | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/image/common.odin b/core/image/common.odin index bf93e9313..0ebeb5b72 100644 --- a/core/image/common.odin +++ b/core/image/common.odin @@ -167,6 +167,7 @@ Error :: union #shared_nil { General_Image_Error :: enum { None = 0, Invalid_Image_Dimensions, + Invalid_Number_Of_Channels, Image_Dimensions_Too_Large, Image_Does_Not_Adhere_to_Spec, Invalid_Input_Image, @@ -213,7 +214,6 @@ Netpbm_Error :: enum { // writing File_Not_Writable, Invalid_Format, - Invalid_Number_Of_Channels, Invalid_Image_Depth, } @@ -339,7 +339,6 @@ 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. diff --git a/core/image/netpbm/netpbm.odin b/core/image/netpbm/netpbm.odin index fa8ffc1db..5c082e384 100644 --- a/core/image/netpbm/netpbm.odin +++ b/core/image/netpbm/netpbm.odin @@ -104,7 +104,7 @@ write_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: //? validation if header.format in (PBM + PGM + Formats{.Pf}) && img.channels != 1 \ || header.format in (PPM + Formats{.PF}) && img.channels != 3 { - err = Format_Error.Invalid_Number_Of_Channels + err = .Invalid_Number_Of_Channels return } From d6a8216ce48eb9478f307ccf70e5295424bd78ed Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Sat, 30 Apr 2022 14:34:07 +0200 Subject: [PATCH 3/7] [pbm] Normalize some errors, correct .depth --- core/image/common.odin | 41 +++++++++++------- core/image/netpbm/netpbm.odin | 61 +++++++++++---------------- core/image/png/png.odin | 16 +++---- core/image/qoi/qoi.odin | 12 +++--- core/image/tga/tga.odin | 5 +-- tests/core/image/test_core_image.odin | 12 +++--- 6 files changed, 70 insertions(+), 77 deletions(-) diff --git a/core/image/common.odin b/core/image/common.odin index 2196565bd..6722036ff 100644 --- a/core/image/common.odin +++ b/core/image/common.odin @@ -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. @@ -141,13 +141,14 @@ 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] @@ -166,12 +167,29 @@ Error :: union #shared_nil { General_Image_Error :: enum { None = 0, + // 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_Dimensions_Too_Large, Image_Does_Not_Adhere_to_Spec, - Invalid_Input_Image, + 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, } /* @@ -201,8 +219,6 @@ Netpbm_Error :: enum { None = 0, // reading - File_Not_Readable, - Invalid_Signature, Invalid_Header_Token_Character, Incomplete_Header, Invalid_Header_Value, @@ -212,9 +228,7 @@ Netpbm_Error :: enum { Invalid_Buffer_Value, // writing - File_Not_Writable, Invalid_Format, - Invalid_Image_Depth, } /* @@ -222,7 +236,6 @@ Netpbm_Error :: enum { */ PNG_Error :: enum { None = 0, - Invalid_PNG_Signature, IHDR_Not_First_Chunk, IHDR_Corrupt, IDAT_Missing, @@ -338,14 +351,10 @@ PNG_Interlace_Method :: enum u8 { */ QOI_Error :: enum { None = 0, - Invalid_QOI_Signature, - 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, @@ -1170,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 } \ No newline at end of file diff --git a/core/image/netpbm/netpbm.odin b/core/image/netpbm/netpbm.odin index 5c082e384..54935d6c6 100644 --- a/core/image/netpbm/netpbm.odin +++ b/core/image/netpbm/netpbm.odin @@ -9,8 +9,6 @@ import "core:strconv" import "core:strings" import "core:unicode" - - Image :: image.Image Format :: image.Netpbm_Format Header :: image.Netpbm_Header @@ -18,8 +16,6 @@ Info :: image.Netpbm_Info Error :: image.Error Format_Error :: image.Netpbm_Error - - Formats :: bit_set[Format] PBM :: Formats{.P1, .P4} PGM :: Formats{.P2, .P5} @@ -30,14 +26,12 @@ PFM :: Formats{.Pf, .PF} ASCII :: Formats{.P1, .P2, .P3} BINARY :: Formats{.P4, .P5, .P6} + PAM + PFM - - -read :: proc { - read_from_file, - read_from_buffer, +load :: proc { + load_from_file, + load_from_buffer, } -read_from_file :: proc(filename: string, allocator := context.allocator) -> (img: Image, err: Error) { +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) @@ -49,7 +43,7 @@ read_from_file :: proc(filename: string, allocator := context.allocator) -> (img return read_from_buffer(data) } -read_from_buffer :: proc(data: []byte, allocator := context.allocator) -> (img: Image, err: Error) { +load_from_buffer :: proc(data: []byte, allocator := context.allocator) -> (img: Image, err: Error) { context.allocator = allocator header: Header; defer header_destroy(&header) @@ -70,14 +64,12 @@ read_from_buffer :: proc(data: []byte, allocator := context.allocator) -> (img: return } - - -write :: proc { - write_to_file, - write_to_buffer, +save :: proc { + save_to_file, + save_to_buffer, } -write_to_file :: proc(filename: string, img: Image, allocator := context.allocator) -> (err: Error) { +save_to_file :: proc(filename: string, img: Image, allocator := context.allocator) -> (err: Error) { context.allocator = allocator data: []byte; defer delete(data) @@ -90,7 +82,7 @@ write_to_file :: proc(filename: string, img: Image, allocator := context.allocat return Format_Error.None } -write_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: []byte, err: Error) { +save_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: []byte, err: Error) { context.allocator = allocator info, ok := img.metadata.(^image.Netpbm_Info) @@ -109,12 +101,12 @@ write_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: } if header.format in (PNM + PAM) { - if header.maxval <= int(max(u8)) && img.depth != 1 \ - || header.maxval > int(max(u8)) && header.maxval <= int(max(u16)) && img.depth != 2 { + if header.maxval <= int(max(u8)) && img.depth != 8 \ + || header.maxval > int(max(u8)) && header.maxval <= int(max(u16)) && img.depth != 16 { err = Format_Error.Invalid_Image_Depth return } - } else if header.format in PFM && img.depth != 4 { + } else if header.format in PFM && img.depth != 32 { err = Format_Error.Invalid_Image_Depth return } @@ -179,7 +171,7 @@ write_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: mem.copy(raw_data(data.buf[len(header_buf):]), raw_data(pixels), len(pixels)) // convert from native endianness - if img.depth == 2 { + if img.depth == 16 { pixels := mem.slice_data_cast([]u16be, data.buf[len(header_buf):]) for p in &pixels { p = u16be(transmute(u16) p) @@ -212,7 +204,7 @@ write_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: // Token ASCII case .P2, .P3: switch img.depth { - case 1: + case 8: pixels := img.pixels.buf[:] for y in 0 ..< img.height { for x in 0 ..< img.width { @@ -226,7 +218,7 @@ write_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: fmt.sbprint(&data, "\n") } - case 2: + case 16: pixels := mem.slice_data_cast([]u16, img.pixels.buf[:]) for y in 0 ..< img.height { for x in 0 ..< img.width { @@ -251,8 +243,6 @@ write_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: return data.buf[:], Format_Error.None } - - parse_header :: proc(data: []byte, allocator := context.allocator) -> (header: Header, length: int, err: Error) { context.allocator = allocator @@ -351,7 +341,7 @@ _parse_header_pnm :: proc(data: []byte) -> (header: Header, length: int, err: Er // set extra info header.channels = 3 if header.format in PPM else 1 - header.depth = 2 if header.maxval > int(max(u8)) else 1 + header.depth = 16 if header.maxval > int(max(u8)) else 8 // limit checking if current_field < len(header_fields) { @@ -448,12 +438,11 @@ _parse_header_pam :: proc(data: []byte, allocator := context.allocator) -> (head } // extra info - header.depth = 2 if header.maxval > int(max(u8)) else 1 + header.depth = 16 if header.maxval > int(max(u8)) else 8 // limit checking if header.width < 1 \ || header.height < 1 \ - || header.depth < 1 \ || header.maxval < 1 \ || header.maxval > int(max(u16)) { err = Format_Error.Invalid_Header_Value @@ -484,7 +473,7 @@ _parse_header_pfm :: proc(data: []byte) -> (header: Header, length: int, err: Er } // floating point - header.depth = 4 + header.depth = 32 // width field, ok = strings.fields_iterator(&field_iterator) @@ -542,8 +531,6 @@ _parse_header_pfm :: proc(data: []byte) -> (header: Header, length: int, err: Er return } - - decode_image :: proc(header: Header, data: []byte, allocator := context.allocator) -> (img: Image, err: Error) { context.allocator = allocator @@ -554,7 +541,7 @@ decode_image :: proc(header: Header, data: []byte, allocator := context.allocato depth = header.depth, } - buffer_size := img.width * img.height * img.channels * img.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 { @@ -615,7 +602,7 @@ decode_image :: proc(header: Header, data: []byte, allocator := context.allocato } } } else { - if img.depth == 2 { + if img.depth == 16 { pixels := mem.slice_data_cast([]u16, img.pixels.buf[:]) for p in &pixels { p = u16(transmute(u16be) p) @@ -658,9 +645,9 @@ decode_image :: proc(header: Header, data: []byte, allocator := context.allocato } switch img.depth { - case 1: + case 8: bytes.buffer_write_byte(&img.pixels, u8(value)) - case 2: + case 16: vb := transmute([2]u8) u16(value) bytes.buffer_write(&img.pixels, vb[:]) } @@ -678,4 +665,4 @@ decode_image :: proc(header: Header, data: []byte, allocator := context.allocato err = Format_Error.None return -} +} \ No newline at end of file diff --git a/core/image/png/png.odin b/core/image/png/png.odin index ba888cb78..d526dfb27 100644 --- a/core/image/png/png.odin +++ b/core/image/png/png.odin @@ -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{ diff --git a/core/image/qoi/qoi.odin b/core/image/qoi/qoi.odin index fdbaab686..83b212be8 100644 --- a/core/image/qoi/qoi.odin +++ b/core/image/qoi/qoi.odin @@ -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 } /* diff --git a/core/image/tga/tga.odin b/core/image/tga/tga.odin index 3c860cb62..0539706b3 100644 --- a/core/image/tga/tga.odin +++ b/core/image/tga/tga.odin @@ -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} \ No newline at end of file diff --git a/tests/core/image/test_core_image.odin b/tests/core/image/test_core_image.odin index 0c11ca5ae..c328757e4 100644 --- a/tests/core/image/test_core_image.odin +++ b/tests/core/image/test_core_image.odin @@ -1199,37 +1199,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}, }, }, { From dd8b71e353bc72eecf95ca2ae45c437dc01e89bf Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Sat, 30 Apr 2022 17:52:23 +0200 Subject: [PATCH 4/7] [pbm] WIP unit tests. --- core/image/netpbm/helpers.odin | 1 + core/image/netpbm/netpbm.odin | 138 +++++++++++++++++++++----- tests/core/image/test_core_image.odin | 58 ++++++++--- 3 files changed, 155 insertions(+), 42 deletions(-) diff --git a/core/image/netpbm/helpers.odin b/core/image/netpbm/helpers.odin index 5a3000a87..8c5cdd622 100644 --- a/core/image/netpbm/helpers.odin +++ b/core/image/netpbm/helpers.odin @@ -14,6 +14,7 @@ destroy :: proc(img: ^image.Image) -> bool { header_destroy(&info.header) free(info) img.metadata = nil + free(img) return true } diff --git a/core/image/netpbm/netpbm.odin b/core/image/netpbm/netpbm.odin index 54935d6c6..cccf7e865 100644 --- a/core/image/netpbm/netpbm.odin +++ b/core/image/netpbm/netpbm.odin @@ -31,19 +31,19 @@ load :: proc { load_from_buffer, } -load_from_file :: proc(filename: string, allocator := context.allocator) -> (img: Image, err: Error) { +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 = .File_Not_Readable + err = .Unable_To_Read_File return } - return read_from_buffer(data) + return load_from_buffer(data) } -load_from_buffer :: proc(data: []byte, allocator := context.allocator) -> (img: Image, err: Error) { +load_from_buffer :: proc(data: []byte, allocator := context.allocator) -> (img: ^Image, err: Error) { context.allocator = allocator header: Header; defer header_destroy(&header) @@ -51,7 +51,9 @@ load_from_buffer :: proc(data: []byte, allocator := context.allocator) -> (img: header, header_size = parse_header(data) or_return img_data := data[header_size:] - img = decode_image(header, img_data) or_return + + img = new(Image) + decode_image(img, header, img_data) or_return info := new(Info) info.header = header @@ -69,27 +71,42 @@ save :: proc { save_to_buffer, } -save_to_file :: proc(filename: string, img: Image, allocator := context.allocator) -> (err: Error) { +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 = write_to_buffer(img) or_return + data = save_to_buffer(img, custom_info) or_return if ok := os.write_entire_file(filename, data); !ok { - return .File_Not_Writable + return .Unable_To_Write_File } return Format_Error.None } -save_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: []byte, err: Error) { +save_to_buffer :: proc(img: ^Image, custom_info: Info = {}, allocator := context.allocator) -> (buffer: []byte, err: Error) { context.allocator = allocator - info, ok := img.metadata.(^image.Netpbm_Info) - if !ok { - err = image.General_Image_Error.Invalid_Input_Image - return + 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 @@ -103,11 +120,11 @@ save_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: [ 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 = Format_Error.Invalid_Image_Depth + err = .Invalid_Image_Depth return } } else if header.format in PFM && img.depth != 32 { - err = Format_Error.Invalid_Image_Depth + err = .Invalid_Image_Depth return } @@ -233,11 +250,11 @@ save_to_buffer :: proc(img: Image, allocator := context.allocator) -> (buffer: [ } case: - return data.buf[:], Format_Error.Invalid_Image_Depth + return data.buf[:], .Invalid_Image_Depth } case: - return data.buf[:], Format_Error.Invalid_Format + return data.buf[:], .Invalid_Format } return data.buf[:], Format_Error.None @@ -263,7 +280,7 @@ parse_header :: proc(data: []byte, allocator := context.allocator) -> (header: H } } - err = Format_Error.Invalid_Signature + err = .Invalid_Signature return } @@ -366,7 +383,7 @@ _parse_header_pam :: proc(data: []byte, allocator := context.allocator) -> (head // the spec needs the newline apparently if string(data[0:3]) != "P7\n" { - err = Format_Error.Invalid_Signature + err = .Invalid_Signature return } header.format = .P7 @@ -468,7 +485,7 @@ _parse_header_pfm :: proc(data: []byte) -> (header: Header, length: int, err: Er header.format = .PF header.channels = 3 case: - err = Format_Error.Invalid_Signature + err = .Invalid_Signature return } @@ -531,18 +548,18 @@ _parse_header_pfm :: proc(data: []byte) -> (header: Header, length: int, err: Er return } -decode_image :: proc(header: Header, data: []byte, allocator := context.allocator) -> (img: Image, err: Error) { +decode_image :: proc(img: ^Image, header: Header, data: []byte, allocator := context.allocator) -> (err: Error) { + assert(img != nil) context.allocator = allocator - img = Image { - width = header.width, - height = header.height, - channels = header.channels, - depth = header.depth, - } + 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) + when false { // we can check data size for binary formats if header.format in BINARY { if header.format == .P4 { @@ -558,6 +575,7 @@ decode_image :: proc(header: Header, data: []byte, allocator := context.allocato } } } + } // 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 @@ -665,4 +683,70 @@ decode_image :: proc(header: Header, data: []byte, allocator := context.allocato 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 } \ No newline at end of file diff --git a/tests/core/image/test_core_image.odin b/tests/core/image/test_core_image.odin index c328757e4..1ffd3b93d 100644 --- a/tests/core/image/test_core_image.odin +++ b/tests/core/image/test_core_image.odin @@ -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" @@ -1506,26 +1507,53 @@ 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) + + if pbm_load_err == nil { + fmt.printf("%v test %v PBM load worked with %v.\n", file.file, count, pbm_load_err) + } + error = fmt.tprintf("%v test %v PBM load failed with %v.", file.file, count, pbm_load_err) + expect(t, pbm_load_err == nil, 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) + // fmt.printf("Autoselect PBM: %v (%v)\n", pbm_info, pbm_format_selected) + + } if .return_metadata in test.options { From 234d52986776812641a2766de40e63798b55c857 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Sat, 30 Apr 2022 19:25:16 +0200 Subject: [PATCH 5/7] [pbm] WIP unit tests. part deux. --- tests/core/image/test_core_image.odin | 28 +++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/core/image/test_core_image.odin b/tests/core/image/test_core_image.odin index 1ffd3b93d..93baa76ec 100644 --- a/tests/core/image/test_core_image.odin +++ b/tests/core/image/test_core_image.odin @@ -1534,6 +1534,9 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) { pbm_buf, pbm_save_err := pbm.save_to_buffer(img) defer delete(pbm_buf) + filename := fmt.tprintf("%v-%v.ppm", file.file, count) + pbm.save_to_file(filename, img) + error = fmt.tprintf("%v test %v PBM save failed with %v.", file.file, count, pbm_save_err) expect(t, pbm_save_err == nil, error) @@ -1544,9 +1547,30 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) { if pbm_load_err == nil { fmt.printf("%v test %v PBM load worked with %v.\n", file.file, count, pbm_load_err) + + pbm_hash := hash.crc32(pbm_img.pixels.buf[:]) + if pbm_hash == png_hash { + fmt.printf("\t%v test %v PBM load hash %08x matched PNG's\n", file.file, count, png_hash) + } else { + if img.width != pbm_img.width || img.height != pbm_img.height || img.channels != pbm_img.channels || img.depth != pbm_img.depth { + fmt.printf("\tHash failed. IMG: %v, %v, %v, %v PBM: %v, %v, %v, %v\n", img.width, img.height, img.channels, img.depth, pbm_img.width, pbm_img.height, pbm_img.channels, pbm_img.depth) + } else if len(img.pixels.buf) != len(pbm_img.pixels.buf) { + fmt.printf("\tLengths differ. IMG: %v PBM: %v\n", len(img.pixels.buf), len(pbm_img.pixels.buf)) + } else if file.file[:3] == "bas" { + for v, i in img.pixels.buf { + if v != pbm_img.pixels.buf[i] { + fmt.printf("\tChannels: %v, Depth: %v, Pixel %v differs. PNG: %v, PBM: %v\n", img.channels, img.depth, i, img.pixels.buf[i:][:4], pbm_img.pixels.buf[i:][:4]) + break + } + } + } + // 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) + } + } else { + // error = fmt.tprintf("%v test %v PBM load failed with %v.", file.file, count, pbm_load_err) + // expect(t, pbm_load_err == nil, error) } - error = fmt.tprintf("%v test %v PBM load failed with %v.", file.file, count, pbm_load_err) - expect(t, pbm_load_err == nil, error) } // Roundtrip through PBM to test the PBM encoders and decoders - prefer ASCII From 8bd16c32f3bbe724321127fcaaf798a1928cf0fe Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Sat, 30 Apr 2022 21:00:32 +0200 Subject: [PATCH 6/7] [pbm] Fixes. --- core/image/netpbm/helpers.odin | 5 +- core/image/netpbm/netpbm.odin | 45 ++++++++-------- tests/core/image/test_core_image.odin | 76 ++++++++++++++------------- 3 files changed, 64 insertions(+), 62 deletions(-) diff --git a/core/image/netpbm/helpers.odin b/core/image/netpbm/helpers.odin index 8c5cdd622..2fbd32ecc 100644 --- a/core/image/netpbm/helpers.odin +++ b/core/image/netpbm/helpers.odin @@ -6,15 +6,16 @@ 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 - bytes.buffer_destroy(&img.pixels) header_destroy(&info.header) free(info) img.metadata = nil - free(img) return true } diff --git a/core/image/netpbm/netpbm.odin b/core/image/netpbm/netpbm.odin index cccf7e865..768c06110 100644 --- a/core/image/netpbm/netpbm.odin +++ b/core/image/netpbm/netpbm.odin @@ -46,13 +46,13 @@ load_from_file :: proc(filename: string, allocator := context.allocator) -> (img 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:] - - img = new(Image) decode_image(img, header, img_data) or_return info := new(Info) @@ -62,8 +62,7 @@ load_from_buffer :: proc(data: []byte, allocator := context.allocator) -> (img: } img.metadata = info - err = Format_Error.None - return + return img, nil } save :: proc { @@ -140,8 +139,14 @@ save_to_buffer :: proc(img: ^Image, custom_info: Info = {}, allocator := context fmt.sbprintf(&data, "%i\n", header.maxval) } } else if header.format in PAM { - fmt.sbprintf(&data, "WIDTH %i\nHEIGHT %i\nMAXVAL %i\nDEPTH %i\nTUPLTYPE %s\nENDHDR\n", - img.width, img.height, header.maxval, img.channels, header.tupltype) + 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) @@ -369,10 +374,12 @@ _parse_header_pnm :: proc(data: []byte) -> (header: Header, length: int, err: Er if header.width < 1 \ || header.height < 1 \ || header.maxval < 1 || header.maxval > int(max(u16)) { - err = Format_Error.Invalid_Header_Value + 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 } @@ -427,7 +434,7 @@ _parse_header_pam :: proc(data: []byte, allocator := context.allocator) -> (head case "TUPLTYPE": if len(value) == 0 { - err = Format_Error.Invalid_Header_Value + err = .Invalid_Header_Value return } @@ -462,6 +469,7 @@ _parse_header_pam :: proc(data: []byte, allocator := context.allocator) -> (head || 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 } @@ -540,7 +548,8 @@ _parse_header_pfm :: proc(data: []byte) -> (header: Header, length: int, err: Er if header.width < 1 \ || header.height < 1 \ || header.scale == 0.0 { - err = Format_Error.Invalid_Header_Value + fmt.printf("[pfm] Header: {{width = %v, height = %v, scale: %v}}\n", header.width, header.height, header.scale) + err = .Invalid_Header_Value return } @@ -559,23 +568,13 @@ decode_image :: proc(img: ^Image, header: Header, data: []byte, allocator := con buffer_size := image.compute_buffer_size(img.width, img.height, img.channels, img.depth) - when false { // we can check data size for binary formats if header.format in BINARY { - if header.format == .P4 { - p4_size := (img.width / 8 + 1) * img.height - if len(data) < p4_size { - err = Format_Error.Buffer_Too_Small - return - } - } else { - if len(data) < buffer_size { - err = Format_Error.Buffer_Too_Small - return - } + 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 @@ -605,7 +604,7 @@ decode_image :: proc(img: ^Image, header: Header, data: []byte, allocator := con // Simple binary case .P5, .P6, .P7, .Pf, .PF: - mem.copy(raw_data(img.pixels.buf), raw_data(data), buffer_size) + copy(img.pixels.buf[:], data[:]) // convert to native endianness if header.format in PFM { diff --git a/tests/core/image/test_core_image.odin b/tests/core/image/test_core_image.odin index 93baa76ec..f1f1e1244 100644 --- a/tests/core/image/test_core_image.odin +++ b/tests/core/image/test_core_image.odin @@ -1530,54 +1530,56 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) { } } - // 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) + { + // 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) - filename := fmt.tprintf("%v-%v.ppm", file.file, count) - pbm.save_to_file(filename, img) + error = fmt.tprintf("%v test %v PBM save failed with %v.", file.file, count, pbm_save_err) + expect(t, pbm_save_err == nil, error) - 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) - 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 { - fmt.printf("%v test %v PBM load worked with %v.\n", file.file, count, pbm_load_err) + if pbm_load_err == nil { + pbm_hash := hash.crc32(pbm_img.pixels.buf[:]) - pbm_hash := hash.crc32(pbm_img.pixels.buf[:]) - if pbm_hash == png_hash { - fmt.printf("\t%v test %v PBM load hash %08x matched PNG's\n", file.file, count, png_hash) - } else { - if img.width != pbm_img.width || img.height != pbm_img.height || img.channels != pbm_img.channels || img.depth != pbm_img.depth { - fmt.printf("\tHash failed. IMG: %v, %v, %v, %v PBM: %v, %v, %v, %v\n", img.width, img.height, img.channels, img.depth, pbm_img.width, pbm_img.height, pbm_img.channels, pbm_img.depth) - } else if len(img.pixels.buf) != len(pbm_img.pixels.buf) { - fmt.printf("\tLengths differ. IMG: %v PBM: %v\n", len(img.pixels.buf), len(pbm_img.pixels.buf)) - } else if file.file[:3] == "bas" { - for v, i in img.pixels.buf { - if v != pbm_img.pixels.buf[i] { - fmt.printf("\tChannels: %v, Depth: %v, Pixel %v differs. PNG: %v, PBM: %v\n", img.channels, img.depth, i, img.pixels.buf[i:][:4], pbm_img.pixels.buf[i:][:4]) - break - } - } - } - // 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) + 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) } - } else { - // error = fmt.tprintf("%v test %v PBM load failed with %v.", file.file, count, pbm_load_err) - // expect(t, pbm_load_err == nil, 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) - // fmt.printf("Autoselect PBM: %v (%v)\n", pbm_info, pbm_format_selected) + { + // 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) + 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) + } + } + } } if .return_metadata in test.options { From 7a032cf9f9818cfb566e91b7f5926bd7278daf07 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Sat, 30 Apr 2022 21:57:14 +0200 Subject: [PATCH 7/7] [pbm] Also test PFM formats. --- tests/core/image/test_core_image.odin | 109 ++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 14 deletions(-) diff --git a/tests/core/image/test_core_image.odin b/tests/core/image/test_core_image.odin index f1f1e1244..2ecc67151 100644 --- a/tests/core/image/test_core_image.odin +++ b/tests/core/image/test_core_image.odin @@ -1558,25 +1558,106 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) { { // 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) - 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) + // 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) - 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 save failed with %v.", file.file, count, pbm_save_err) + expect(t, pbm_save_err == nil, error) - 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_save_err == nil { + // Try to load it again. + pbm_img, pbm_load_err := pbm.load(pbm_buf) + defer pbm.destroy(pbm_img) - if pbm_load_err == nil { - pbm_hash := hash.crc32(pbm_img.pixels.buf[:]) + error = fmt.tprintf("%v test %v PBM load failed with %v.", file.file, count, pbm_load_err) + expect(t, pbm_load_err == nil, error) - 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) + 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) } } }