From 15594706c963b98051a2d6a6f3627052234bf957 Mon Sep 17 00:00:00 2001 From: Josh Engler Date: Tue, 5 Oct 2021 19:09:20 -0400 Subject: [PATCH 1/9] Additional win32 bindings. --- core/sys/win32/general.odin | 69 +++++++++++++++++++++++++++++++++++- core/sys/win32/kernel32.odin | 2 ++ core/sys/win32/user32.odin | 2 ++ core/sys/win32/winmm.odin | 2 ++ 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/core/sys/win32/general.odin b/core/sys/win32/general.odin index 51f8c7d5f..d53bf8a4f 100644 --- a/core/sys/win32/general.odin +++ b/core/sys/win32/general.odin @@ -99,6 +99,57 @@ Rect :: struct { bottom: i32, } +Dev_Mode_A :: struct { + device_name: [32]u8, + spec_version: u16, + driver_version: u16, + size: u16, + driver_extra: u16, + fields: u32, + using _: struct #raw_union { + // Printer only fields. + using _: struct { + orientation: i16, + paper_size: i16, + paper_length: i16, + paper_width: i16, + scale: i16, + copies: i16, + default_source: i16, + print_quality: i16, + }, + // Display only fields. + using _: struct { + position: Point, + display_orientation: u32, + display_fixed_output: u32, + }, + }, + color: i16, + duplex: i16, + y_resolution: i16, + tt_option: i16, + collate: i16, + form_name: [32]u8, + log_pixels: u16, + bits_per_pel: u32, + pels_width: u32, + pels_height: u32, + using _: struct #raw_union { + display_flags: u32, + nup: u32, + }, + display_frequency: u32, + icm_method: u32, + icm_intent: u32, + media_type: u32, + dither_type: u32, + reserved_1: u32, + reserved_2: u32, + panning_width: u32, + panning_height: u32, +} + Filetime :: struct { lo, hi: u32, } @@ -364,6 +415,9 @@ MAPVK_VK_TO_CHAR :: 2 MAPVK_VSC_TO_VK_EX :: 3 //WinUser.h +ENUM_CURRENT_SETTINGS :: u32(4294967295) // (DWORD)-1 +ENUM_REGISTRY_SETTINGS :: u32(4294967294) // (DWORD)-2 + VK_LBUTTON :: 0x01 VK_RBUTTON :: 0x02 VK_CANCEL :: 0x03 @@ -877,7 +931,20 @@ FILE_GENERIC_EXECUTE :: 0x20000000 FILE_GENERIC_WRITE :: 0x40000000 FILE_GENERIC_READ :: 0x80000000 -FILE_APPEND_DATA :: 0x0004 +FILE_READ_DATA :: 0x0001 +FILE_LIST_DIRECTORY :: 0x0001 +FILE_WRITE_DATA :: 0x0002 +FILE_ADD_FILE :: 0x0002 +FILE_APPEND_DATA :: 0x0004 +FILE_ADD_SUBDIRECTORY :: 0x0004 +FILE_CREATE_PIPE_INSTANCE :: 0x0004 +FILE_READ_EA :: 0x0008 +FILE_WRITE_EA :: 0x0010 +FILE_EXECUTE :: 0x0020 +FILE_TRAVERSE :: 0x0020 +FILE_DELETE_CHILD :: 0x0040 +FILE_READ_ATTRIBUTES :: 0x0080 +FILE_WRITE_ATTRIBUTES :: 0x0100 STD_INPUT_HANDLE :: -10 STD_OUTPUT_HANDLE :: -11 diff --git a/core/sys/win32/kernel32.odin b/core/sys/win32/kernel32.odin index 187bcd06f..709964fb4 100644 --- a/core/sys/win32/kernel32.odin +++ b/core/sys/win32/kernel32.odin @@ -106,6 +106,8 @@ foreign kernel32 { bytes_returned: ^u32, overlapped: ^Overlapped, completion: rawptr) -> Bool --- + @(link_name="GetOverlappedResult") get_overlapped_result :: proc(file: Handle, overlapped: ^Overlapped, number_of_bytes_transferred: ^u32, wait: Bool) -> Bool --- + @(link_name="WideCharToMultiByte") wide_char_to_multi_byte :: proc(code_page: u32, flags: u32, wchar_str: Wstring, wchar: i32, multi_str: cstring, multi: i32, diff --git a/core/sys/win32/user32.odin b/core/sys/win32/user32.odin index 57aea88aa..593fdbb8e 100644 --- a/core/sys/win32/user32.odin +++ b/core/sys/win32/user32.odin @@ -201,6 +201,8 @@ foreign user32 { @(link_name="MapVirtualKeyExA") map_virtual_key_ex_a :: proc(code, map_type: u32, hkl: HKL) -> u32 --- @(link_name="EnumDisplayMonitors") enum_display_monitors :: proc(hdc: Hdc, rect: ^Rect, enum_proc: Monitor_Enum_Proc, lparam: Lparam) -> bool --- + + @(link_name="EnumDisplaySettingsA") enum_display_settings_a :: proc(device_name: cstring, mode_number: u32, mode: ^Dev_Mode_A) -> Bool --- } @(default_calling_convention = "std") diff --git a/core/sys/win32/winmm.odin b/core/sys/win32/winmm.odin index 05d099a1a..0f567fbcc 100644 --- a/core/sys/win32/winmm.odin +++ b/core/sys/win32/winmm.odin @@ -6,5 +6,7 @@ foreign import "system:winmm.lib" @(default_calling_convention = "std") foreign winmm { + @(link_name="timeBeginPeriod") time_begin_period :: proc(period: u32) -> u32 --- + @(link_name="timeGetTime") time_get_time :: proc() -> u32 --- } From b345176bde96c9d1ba6db4ae60c728d04b5f50ee Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Wed, 6 Oct 2021 16:33:52 +0200 Subject: [PATCH 2/9] Make `append` builtins return an `Allocator_Error`. --- core/runtime/core_builtin.odin | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/core/runtime/core_builtin.odin b/core/runtime/core_builtin.odin index 44da894c1..84b71a763 100644 --- a/core/runtime/core_builtin.odin +++ b/core/runtime/core_builtin.odin @@ -301,17 +301,17 @@ delete_key :: proc(m: ^$T/map[$K]$V, key: K) -> (deleted_key: K, deleted_value: return } - - @builtin -append_elem :: proc(array: ^$T/[dynamic]$E, arg: E, loc := #caller_location) { +append_elem :: proc(array: ^$T/[dynamic]$E, arg: E, loc := #caller_location) -> Allocator_Error { if array == nil { - return + return .None } if cap(array) < len(array)+1 { cap := 2 * cap(array) + max(8, 1) - _ = reserve(array, cap, loc) + if ok := reserve(array, cap, loc); !ok { + return .Out_Of_Memory + } } if cap(array)-len(array) > 0 { a := (^Raw_Dynamic_Array)(array) @@ -322,23 +322,25 @@ append_elem :: proc(array: ^$T/[dynamic]$E, arg: E, loc := #caller_location) { } a.len += 1 } + return .None } @builtin -append_elems :: proc(array: ^$T/[dynamic]$E, args: ..E, loc := #caller_location) { +append_elems :: proc(array: ^$T/[dynamic]$E, args: ..E, loc := #caller_location) -> Allocator_Error { if array == nil { - return + return .None } arg_len := len(args) if arg_len <= 0 { - return + return .Invalid_Argument } - if cap(array) < len(array)+arg_len { cap := 2 * cap(array) + max(8, arg_len) - _ = reserve(array, cap, loc) + if ok := reserve(array, cap, loc); !ok { + return .Out_Of_Memory + } } arg_len = min(cap(array)-len(array), arg_len) if arg_len > 0 { @@ -350,21 +352,22 @@ append_elems :: proc(array: ^$T/[dynamic]$E, args: ..E, loc := #caller_location) } a.len += arg_len } + return .None } // The append_string built-in procedure appends a string to the end of a [dynamic]u8 like type @builtin -append_elem_string :: proc(array: ^$T/[dynamic]$E/u8, arg: $A/string, loc := #caller_location) { +append_elem_string :: proc(array: ^$T/[dynamic]$E/u8, arg: $A/string, loc := #caller_location) -> Allocator_Error { args := transmute([]E)arg - append_elems(array=array, args=args, loc=loc) + return append_elems(array=array, args=args, loc=loc) } // The append_string built-in procedure appends multiple strings to the end of a [dynamic]u8 like type @builtin -append_string :: proc(array: ^$T/[dynamic]$E/u8, args: ..string, loc := #caller_location) { +append_string :: proc(array: ^$T/[dynamic]$E/u8, args: ..string, loc := #caller_location) -> Allocator_Error { for arg in args { - append(array = array, args = transmute([]E)(arg), loc = loc) + append(array = array, args = transmute([]E)(arg), loc = loc) or_return } } From 2e9eec156c641688d8efe2f4de3d4f24c2601cc4 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Wed, 6 Oct 2021 19:09:28 +0200 Subject: [PATCH 3/9] Revert "Merge pull request #1209 from Kelimion/append_error" This reverts commit c9468adcfdf2f390a2d923a4008ed25776e5d5e5, reversing changes made to 879a4d49ae797ad4a0970b94e0404b206f420f41. --- core/runtime/core_builtin.odin | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/core/runtime/core_builtin.odin b/core/runtime/core_builtin.odin index 84b71a763..44da894c1 100644 --- a/core/runtime/core_builtin.odin +++ b/core/runtime/core_builtin.odin @@ -301,17 +301,17 @@ delete_key :: proc(m: ^$T/map[$K]$V, key: K) -> (deleted_key: K, deleted_value: return } + + @builtin -append_elem :: proc(array: ^$T/[dynamic]$E, arg: E, loc := #caller_location) -> Allocator_Error { +append_elem :: proc(array: ^$T/[dynamic]$E, arg: E, loc := #caller_location) { if array == nil { - return .None + return } if cap(array) < len(array)+1 { cap := 2 * cap(array) + max(8, 1) - if ok := reserve(array, cap, loc); !ok { - return .Out_Of_Memory - } + _ = reserve(array, cap, loc) } if cap(array)-len(array) > 0 { a := (^Raw_Dynamic_Array)(array) @@ -322,25 +322,23 @@ append_elem :: proc(array: ^$T/[dynamic]$E, arg: E, loc := #caller_location) -> } a.len += 1 } - return .None } @builtin -append_elems :: proc(array: ^$T/[dynamic]$E, args: ..E, loc := #caller_location) -> Allocator_Error { +append_elems :: proc(array: ^$T/[dynamic]$E, args: ..E, loc := #caller_location) { if array == nil { - return .None + return } arg_len := len(args) if arg_len <= 0 { - return .Invalid_Argument + return } + if cap(array) < len(array)+arg_len { cap := 2 * cap(array) + max(8, arg_len) - if ok := reserve(array, cap, loc); !ok { - return .Out_Of_Memory - } + _ = reserve(array, cap, loc) } arg_len = min(cap(array)-len(array), arg_len) if arg_len > 0 { @@ -352,22 +350,21 @@ append_elems :: proc(array: ^$T/[dynamic]$E, args: ..E, loc := #caller_location) } a.len += arg_len } - return .None } // The append_string built-in procedure appends a string to the end of a [dynamic]u8 like type @builtin -append_elem_string :: proc(array: ^$T/[dynamic]$E/u8, arg: $A/string, loc := #caller_location) -> Allocator_Error { +append_elem_string :: proc(array: ^$T/[dynamic]$E/u8, arg: $A/string, loc := #caller_location) { args := transmute([]E)arg - return append_elems(array=array, args=args, loc=loc) + append_elems(array=array, args=args, loc=loc) } // The append_string built-in procedure appends multiple strings to the end of a [dynamic]u8 like type @builtin -append_string :: proc(array: ^$T/[dynamic]$E/u8, args: ..string, loc := #caller_location) -> Allocator_Error { +append_string :: proc(array: ^$T/[dynamic]$E/u8, args: ..string, loc := #caller_location) { for arg in args { - append(array = array, args = transmute([]E)(arg), loc = loc) or_return + append(array = array, args = transmute([]E)(arg), loc = loc) } } From 21c6d691d8ee1ae8a5678c79ab7c453a71009e4a Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Wed, 6 Oct 2021 20:10:37 +0200 Subject: [PATCH 4/9] Add additional error checking to helpers. --- core/compress/common.odin | 2 + core/image/common.odin | 1 + core/image/png/example.odin | 155 +++++++++++++------------- core/image/png/helpers.odin | 72 ++++++------ core/image/png/png.odin | 71 ++++++++---- tests/core/image/test_core_image.odin | 18 +-- 6 files changed, 176 insertions(+), 143 deletions(-) diff --git a/core/compress/common.odin b/core/compress/common.odin index 683679566..5951f8bd0 100644 --- a/core/compress/common.odin +++ b/core/compress/common.odin @@ -11,6 +11,7 @@ package compress import "core:io" import "core:image" import "core:bytes" +import "core:mem" /* These settings bound how much compression algorithms will allocate for their output buffer. @@ -47,6 +48,7 @@ when size_of(uintptr) == 8 { Error :: union { General_Error, + mem.Allocator_Error, Deflate_Error, ZLIB_Error, GZIP_Error, diff --git a/core/image/common.odin b/core/image/common.odin index f30febc26..5de4bb84e 100644 --- a/core/image/common.odin +++ b/core/image/common.odin @@ -131,6 +131,7 @@ Error :: enum { Unknown_Interlace_Method, Requested_Channel_Not_Present, Post_Processing_Error, + Invalid_Chunk_Length, } /* diff --git a/core/image/png/example.odin b/core/image/png/example.odin index e27ea9671..b047f5c6a 100644 --- a/core/image/png/example.odin +++ b/core/image/png/example.odin @@ -41,7 +41,7 @@ main :: proc() { demo :: proc() { file: string - options := image.Options{} // {.return_metadata}; + options := image.Options{.return_metadata} err: compress.Error img: ^image.Image @@ -53,23 +53,26 @@ demo :: proc() { if err != nil { fmt.printf("Trying to read PNG file %v returned %v\n", file, err) } else { - v: ^Info - fmt.printf("Image: %vx%vx%v, %v-bit.\n", img.width, img.height, img.channels, img.depth) - if img.metadata_ptr != nil && img.metadata_type == Info { - v = (^Info)(img.metadata_ptr) - // Handle ancillary chunks as you wish. - // We provide helper functions for a few types. - for c in v.chunks { - #partial switch c.header.type { - case .tIME: - t, _ := core_time(c) + assert(img.metadata_ptr != nil && img.metadata_type == Info) + + v := (^Info)(img.metadata_ptr) + + // Handle ancillary chunks as you wish. + // We provide helper functions for a few types. + for c in v.chunks { + #partial switch c.header.type { + case .tIME: + if t, t_ok := core_time(c); t_ok { fmt.printf("[tIME]: %v\n", t) - case .gAMA: - fmt.printf("[gAMA]: %v\n", gamma(c)) - case .pHYs: - phys := phys(c) + } + case .gAMA: + if gama, gama_ok := gamma(c); gama_ok { + fmt.printf("[gAMA]: %v\n", gama) + } + case .pHYs: + if phys, phys_ok := phys(c); phys_ok { if phys.unit == .Meter { xm := f32(img.width) / f32(phys.ppu_x) ym := f32(img.height) / f32(phys.ppu_y) @@ -80,73 +83,71 @@ demo :: proc() { } else { fmt.printf("[pHYs] x: %v, y: %v pixels per unknown unit.\n", phys.ppu_x, phys.ppu_y) } - case .iTXt, .zTXt, .tEXt: - res, ok_text := text(c) - if ok_text { - if c.header.type == .iTXt { - fmt.printf("[iTXt] %v (%v:%v): %v\n", res.keyword, res.language, res.keyword_localized, res.text) - } else { - fmt.printf("[tEXt/zTXt] %v: %v\n", res.keyword, res.text) - } - } - defer text_destroy(res) - case .bKGD: - fmt.printf("[bKGD] %v\n", img.background) - case .eXIf: - res, ok_exif := exif(c) - if ok_exif { - /* - Other than checking the signature and byte order, we don't handle Exif data. - If you wish to interpret it, pass it to an Exif parser. - */ - fmt.printf("[eXIf] %v\n", res) - } - case .PLTE: - plte, plte_ok := plte(c) - if plte_ok { - fmt.printf("[PLTE] %v\n", plte) - } else { - fmt.printf("[PLTE] Error\n") - } - case .hIST: - res, ok_hist := hist(c) - if ok_hist { - fmt.printf("[hIST] %v\n", res) - } - case .cHRM: - res, ok_chrm := chrm(c) - if ok_chrm { - fmt.printf("[cHRM] %v\n", res) - } - case .sPLT: - res, ok_splt := splt(c) - if ok_splt { - fmt.printf("[sPLT] %v\n", res) - } - splt_destroy(res) - case .sBIT: - if res, ok_sbit := sbit(c); ok_sbit { - fmt.printf("[sBIT] %v\n", res) - } - case .iCCP: - res, ok_iccp := iccp(c) - if ok_iccp { - fmt.printf("[iCCP] %v\n", res) - } - iccp_destroy(res) - case .sRGB: - if res, ok_srgb := srgb(c); ok_srgb { - fmt.printf("[sRGB] Rendering intent: %v\n", res) - } - case: - type := c.header.type - name := chunk_type_to_name(&type) - fmt.printf("[%v]: %v\n", name, c.data) } + case .iTXt, .zTXt, .tEXt: + res, ok_text := text(c) + if ok_text { + if c.header.type == .iTXt { + fmt.printf("[iTXt] %v (%v:%v): %v\n", res.keyword, res.language, res.keyword_localized, res.text) + } else { + fmt.printf("[tEXt/zTXt] %v: %v\n", res.keyword, res.text) + } + } + defer text_destroy(res) + case .bKGD: + fmt.printf("[bKGD] %v\n", img.background) + case .eXIf: + if res, ok_exif := exif(c); ok_exif { + /* + Other than checking the signature and byte order, we don't handle Exif data. + If you wish to interpret it, pass it to an Exif parser. + */ + fmt.printf("[eXIf] %v\n", res) + } + case .PLTE: + if plte, plte_ok := plte(c); plte_ok { + fmt.printf("[PLTE] %v\n", plte) + } else { + fmt.printf("[PLTE] Error\n") + } + case .hIST: + if res, ok_hist := hist(c); ok_hist { + fmt.printf("[hIST] %v\n", res) + } + case .cHRM: + if res, ok_chrm := chrm(c); ok_chrm { + fmt.printf("[cHRM] %v\n", res) + } + case .sPLT: + res, ok_splt := splt(c) + if ok_splt { + fmt.printf("[sPLT] %v\n", res) + } + splt_destroy(res) + case .sBIT: + if res, ok_sbit := sbit(c); ok_sbit { + fmt.printf("[sBIT] %v\n", res) + } + case .iCCP: + res, ok_iccp := iccp(c) + if ok_iccp { + fmt.printf("[iCCP] %v\n", res) + } + iccp_destroy(res) + case .sRGB: + if res, ok_srgb := srgb(c); ok_srgb { + fmt.printf("[sRGB] Rendering intent: %v\n", res) + } + case: + type := c.header.type + name := chunk_type_to_name(&type) + fmt.printf("[%v]: %v\n", name, c.data) } } } + fmt.printf("Done parsing metadata.\n") + if err == nil && .do_not_decompress_image not_in options && .info not_in options { if ok := write_image_as_ppm("out.ppm", img); ok { fmt.println("Saved decoded image.") diff --git a/core/image/png/helpers.odin b/core/image/png/helpers.odin index 4d8cebedf..1b3749f76 100644 --- a/core/image/png/helpers.odin +++ b/core/image/png/helpers.odin @@ -34,15 +34,17 @@ destroy :: proc(img: ^Image) { } bytes.buffer_destroy(&img.pixels) + + assert(img.metadata_ptr != nil && img.metadata_type == Info) + v := (^Info)(img.metadata_ptr) + + for chunk in &v.chunks { + delete(chunk.data) + } + delete(v.chunks) + // Clean up Info. free(img.metadata_ptr) - - /* - We don't need to do anything for the individual chunks. - They're allocated on the temp allocator, as is info.chunks - - See read_chunk. - */ free(img) } @@ -50,46 +52,50 @@ destroy :: proc(img: ^Image) { Chunk helpers */ -gamma :: proc(c: Chunk) -> f32 { - assert(c.header.type == .gAMA) - res := (^gAMA)(raw_data(c.data))^ - when true { - // Returns the wrong result on old backend - // Fixed for -llvm-api - return f32(res.gamma_100k) / 100_000.0 - } else { - return f32(u32(res.gamma_100k)) / 100_000.0 +gamma :: proc(c: Chunk) -> (res: f32, ok: bool) { + if c.header.type != .gAMA || len(c.data) != size_of(gAMA) { + return {}, false } + gama := (^gAMA)(raw_data(c.data))^ + return f32(gama.gamma_100k) / 100_000.0, true } INCHES_PER_METER :: 1000.0 / 25.4 -phys :: proc(c: Chunk) -> pHYs { - assert(c.header.type == .pHYs) - res := (^pHYs)(raw_data(c.data))^ - return res +phys :: proc(c: Chunk) -> (res: pHYs, ok: bool) { + if c.header.type != .pHYs || len(c.data) != size_of(pHYs) { + return {}, false + } + + return (^pHYs)(raw_data(c.data))^, true } phys_to_dpi :: proc(p: pHYs) -> (x_dpi, y_dpi: f32) { return f32(p.ppu_x) / INCHES_PER_METER, f32(p.ppu_y) / INCHES_PER_METER } -time :: proc(c: Chunk) -> tIME { - assert(c.header.type == .tIME) - res := (^tIME)(raw_data(c.data))^ - return res +time :: proc(c: Chunk) -> (res: tIME, ok: bool) { + if c.header.type != .tIME || len(c.data) != size_of(tIME) { + return {}, false + } + + return (^tIME)(raw_data(c.data))^, true } core_time :: proc(c: Chunk) -> (t: coretime.Time, ok: bool) { - png_time := time(c) - using png_time - return coretime.datetime_to_time( - int(year), int(month), int(day), - int(hour), int(minute), int(second), - ) + if png_time, png_ok := time(c); png_ok { + using png_time + return coretime.datetime_to_time( + int(year), int(month), int(day), + int(hour), int(minute), int(second), + ) + } else { + return {}, false + } } text :: proc(c: Chunk) -> (res: Text, ok: bool) { + assert(len(c.data) == int(c.header.length)) #partial switch c.header.type { case .tEXt: ok = true @@ -228,9 +234,7 @@ iccp_destroy :: proc(i: iCCP) { } srgb :: proc(c: Chunk) -> (res: sRGB, ok: bool) { - ok = true - - if c.header.type != .sRGB || len(c.data) != 1 { + if c.header.type != .sRGB || len(c.data) != size_of(sRGB_Rendering_Intent) { return {}, false } @@ -238,7 +242,7 @@ srgb :: proc(c: Chunk) -> (res: sRGB, ok: bool) { if res.intent > max(sRGB_Rendering_Intent) { ok = false; return } - return + return res, true } plte :: proc(c: Chunk) -> (res: PLTE, ok: bool) { diff --git a/core/image/png/png.odin b/core/image/png/png.odin index 6e9102aaa..204f98b66 100644 --- a/core/image/png/png.odin +++ b/core/image/png/png.odin @@ -100,13 +100,13 @@ Chunk_Type :: enum u32be { } IHDR :: struct #packed { - width: u32be, - height: u32be, - bit_depth: u8, - color_type: Color_Type, + width: u32be, + height: u32be, + bit_depth: u8, + color_type: Color_Type, compression_method: u8, - filter_method: u8, - interlace_method: Interlace_Method, + filter_method: u8, + interlace_method: Interlace_Method, } IHDR_SIZE :: size_of(IHDR) #assert (IHDR_SIZE == 13) @@ -135,22 +135,22 @@ PLTE_Entry :: [3]u8 PLTE :: struct #packed { entries: [256]PLTE_Entry, - used: u16, + used: u16, } hIST :: struct #packed { entries: [256]u16, - used: u16, + used: u16, } sPLT :: struct #packed { - name: string, - depth: u8, + name: string, + depth: u8, entries: union { [][4]u8, [][4]u16, }, - used: u16, + used: u16, } // Other chunks @@ -223,14 +223,14 @@ Exif :: struct { } iCCP :: struct { - name: string, + name: string, profile: []u8, } sRGB_Rendering_Intent :: enum u8 { - Perceptual = 0, + Perceptual = 0, Relative_colorimetric = 1, - Saturation = 2, + Saturation = 2, Absolute_colorimetric = 3, } @@ -274,6 +274,35 @@ read_chunk :: proc(ctx: ^$C) -> (chunk: Chunk, err: Error) { return chunk, nil } +copy_chunk :: proc(src: Chunk, allocator := context.allocator) -> (dest: Chunk, err: Error) { + if int(src.header.length) != len(src.data) { + return {}, .Invalid_Chunk_Length + } + + dest.header = src.header + dest.crc = src.crc + dest.data = make([]u8, dest.header.length, allocator) or_return + + copy(dest.data[:], src.data[:]) + return +} + +append_chunk :: proc(list: ^[dynamic]Chunk, src: Chunk, allocator := context.allocator) -> (err: Error) { + if int(src.header.length) != len(src.data) { + return .Invalid_Chunk_Length + } + + c := copy_chunk(src, allocator) or_return + length := len(list) + append(list, c) + if len(list) != length + 1 { + // Resize during append failed. + return .Out_Of_Memory + } + + return +} + read_header :: proc(ctx: ^$C) -> (IHDR, Error) { c, e := read_chunk(ctx) if e != nil { @@ -422,8 +451,6 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a header: IHDR - info.chunks.allocator = context.temp_allocator - // State to ensure correct chunk ordering. seen_ihdr := false; first := true seen_plte := false @@ -518,7 +545,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a } if .return_metadata in options { - append(&info.chunks, c) + append_chunk(&info.chunks, c) or_return } case .IDAT: // If we only want image metadata and don't want the pixel data, we can early out. @@ -563,7 +590,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a c = read_chunk(ctx) or_return seen_bkgd = true if .return_metadata in options { - append(&info.chunks, c) + append_chunk(&info.chunks, c) or_return } ct := transmute(u8)info.header.color_type @@ -599,7 +626,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a } if .return_metadata in options { - append(&info.chunks, c) + append_chunk(&info.chunks, c) or_return } /* @@ -626,16 +653,14 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a /* iPhone PNG bastardization that doesn't adhere to spec with broken IDAT chunk. We're not going to add support for it. If you have the misfortunte of coming - across one of these files, use a utility to defry it.s + across one of these files, use a utility to defry it. */ return img, E_PNG.PNG_Does_Not_Adhere_to_Spec case: // Unhandled type c = read_chunk(ctx) or_return - if .return_metadata in options { - // NOTE: Chunk cata is currently allocated on the temp allocator. - append(&info.chunks, c) + append_chunk(&info.chunks, c) or_return } first = false diff --git a/tests/core/image/test_core_image.odin b/tests/core/image/test_core_image.odin index 339faff58..14f7db074 100644 --- a/tests/core/image/test_core_image.odin +++ b/tests/core/image/test_core_image.odin @@ -1514,10 +1514,10 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) { case .gAMA: switch(file.file) { case "pp0n2c16", "pp0n6a08": - gamma := png.gamma(c) + gamma, gamma_ok := png.gamma(c) expected_gamma := f32(1.0) error = fmt.tprintf("%v test %v gAMA is %v, expected %v.", file.file, count, gamma, expected_gamma) - expect(t, gamma == expected_gamma, error) + expect(t, gamma == expected_gamma && gamma_ok, error) } case .PLTE: switch(file.file) { @@ -1557,25 +1557,25 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) { expect(t, expected_chrm == chrm && chrm_ok, error) } case .pHYs: - phys := png.phys(c) + phys, phys_ok := png.phys(c) phys_err := "%v test %v cHRM is %v, expected %v." switch (file.file) { case "cdfn2c08": expected_phys := png.pHYs{ppu_x = 1, ppu_y = 4, unit = .Unknown} error = fmt.tprintf(phys_err, file.file, count, phys, expected_phys) - expect(t, expected_phys == phys, error) + expect(t, expected_phys == phys && phys_ok, error) case "cdhn2c08": expected_phys := png.pHYs{ppu_x = 4, ppu_y = 1, unit = .Unknown} error = fmt.tprintf(phys_err, file.file, count, phys, expected_phys) - expect(t, expected_phys == phys, error) + expect(t, expected_phys == phys && phys_ok, error) case "cdsn2c08": expected_phys := png.pHYs{ppu_x = 1, ppu_y = 1, unit = .Unknown} error = fmt.tprintf(phys_err, file.file, count, phys, expected_phys) - expect(t, expected_phys == phys, error) + expect(t, expected_phys == phys && phys_ok, error) case "cdun2c08": expected_phys := png.pHYs{ppu_x = 1000, ppu_y = 1000, unit = .Meter} error = fmt.tprintf(phys_err, file.file, count, phys, expected_phys) - expect(t, expected_phys == phys, error) + expect(t, expected_phys == phys && phys_ok, error) } case .hIST: hist, hist_ok := png.hist(c) @@ -1589,7 +1589,7 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) { expect(t, hist.used == 256 && hist_ok, error) } case .tIME: - png_time := png.time(c) + png_time, png_time_ok := png.time(c) time_err := "%v test %v tIME was %v, expected %v." expected_time: png.tIME @@ -1610,7 +1610,7 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) { } error = fmt.tprintf(time_err, file.file, count, png_time, expected_time) - expect(t, png_time == expected_time, error) + expect(t, png_time == expected_time && png_time_ok, error) error = fmt.tprintf(time_core_err, file.file, count, core_time, expected_core) expect(t, core_time == expected_core && core_time_ok, error) From 9b5ae9567790d0f340d6ef56ff993e39f0b4873a Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Wed, 6 Oct 2021 20:45:29 +0200 Subject: [PATCH 5/9] png + compress: Rearrange error unions. --- core/compress/common.odin | 12 ++--- core/image/common.odin | 23 ++++++-- core/image/png/example.odin | 3 +- core/image/png/png.odin | 78 +++++++++++++-------------- tests/core/image/build.bat | 4 ++ tests/core/image/test_core_image.odin | 26 ++++----- 6 files changed, 78 insertions(+), 68 deletions(-) create mode 100644 tests/core/image/build.bat diff --git a/core/compress/common.odin b/core/compress/common.odin index 5951f8bd0..819cfb481 100644 --- a/core/compress/common.odin +++ b/core/compress/common.odin @@ -9,9 +9,8 @@ package compress */ import "core:io" -import "core:image" import "core:bytes" -import "core:mem" +import "core:runtime" /* These settings bound how much compression algorithms will allocate for their output buffer. @@ -48,16 +47,12 @@ when size_of(uintptr) == 8 { Error :: union { General_Error, - mem.Allocator_Error, Deflate_Error, ZLIB_Error, GZIP_Error, ZIP_Error, - /* - This is here because png.load will return a this type of error union, - as it may involve an I/O error, a Deflate error, etc. - */ - image.Error, + + runtime.Allocator_Error, } General_Error :: enum { @@ -71,7 +66,6 @@ General_Error :: enum { Incompatible_Options, Unimplemented, - /* Memory errors */ diff --git a/core/image/common.odin b/core/image/common.odin index 5de4bb84e..9bb99a5d4 100644 --- a/core/image/common.odin +++ b/core/image/common.odin @@ -11,6 +11,8 @@ package image import "core:bytes" import "core:mem" +import "core:compress" +import "core:runtime" Image :: struct { width: int, @@ -112,19 +114,34 @@ Option :: enum { } Options :: distinct bit_set[Option] -Error :: enum { +Error :: union { + General_Image_Error, + PNG_Error, + + compress.Error, + compress.General_Error, + compress.Deflate_Error, + compress.ZLIB_Error, + runtime.Allocator_Error, +} + +General_Image_Error :: enum { + None = 0, + Invalid_Image_Dimensions, + Image_Does_Not_Adhere_to_Spec, +} + +PNG_Error :: enum { Invalid_PNG_Signature, IHDR_Not_First_Chunk, IHDR_Corrupt, IDAT_Missing, IDAT_Must_Be_Contiguous, IDAT_Corrupt, - PNG_Does_Not_Adhere_to_Spec, PLTE_Encountered_Unexpectedly, PLTE_Invalid_Length, TRNS_Encountered_Unexpectedly, BKGD_Invalid_Length, - Invalid_Image_Dimensions, Unknown_Color_Type, Invalid_Color_Bit_Depth_Combo, Unknown_Filter_Method, diff --git a/core/image/png/example.odin b/core/image/png/example.odin index b047f5c6a..ad0423d67 100644 --- a/core/image/png/example.odin +++ b/core/image/png/example.odin @@ -12,7 +12,6 @@ package png An example of how to use `load`. */ -import "core:compress" import "core:image" // import "core:image/png" import "core:bytes" @@ -42,7 +41,7 @@ demo :: proc() { file: string options := image.Options{.return_metadata} - err: compress.Error + err: image.Error img: ^image.Image file = "../../../misc/logo-slim.png" diff --git a/core/image/png/png.odin b/core/image/png/png.odin index 204f98b66..21b27fc82 100644 --- a/core/image/png/png.odin +++ b/core/image/png/png.odin @@ -21,11 +21,7 @@ import "core:io" import "core:mem" import "core:intrinsics" -Error :: compress.Error -E_General :: compress.General_Error -E_PNG :: image.Error -E_Deflate :: compress.Deflate_Error - +Error :: image.Error Image :: image.Image Options :: image.Options @@ -248,13 +244,13 @@ ADAM7_Y_SPACING := []int{ 8,8,8,4,4,2,2 } read_chunk :: proc(ctx: ^$C) -> (chunk: Chunk, err: Error) { ch, e := compress.read_data(ctx, Chunk_Header) if e != .None { - return {}, E_General.Stream_Too_Short + return {}, compress.General_Error.Stream_Too_Short } chunk.header = ch chunk.data, e = compress.read_slice(ctx, int(ch.length)) if e != .None { - return {}, E_General.Stream_Too_Short + return {}, compress.General_Error.Stream_Too_Short } // Compute CRC over chunk type + data @@ -264,12 +260,12 @@ read_chunk :: proc(ctx: ^$C) -> (chunk: Chunk, err: Error) { crc, e3 := compress.read_data(ctx, u32be) if e3 != .None { - return {}, E_General.Stream_Too_Short + return {}, compress.General_Error.Stream_Too_Short } chunk.crc = crc if chunk.crc != u32be(computed_crc) { - return {}, E_General.Checksum_Failed + return {}, compress.General_Error.Checksum_Failed } return chunk, nil } @@ -297,7 +293,7 @@ append_chunk :: proc(list: ^[dynamic]Chunk, src: Chunk, allocator := context.all append(list, c) if len(list) != length + 1 { // Resize during append failed. - return .Out_Of_Memory + return mem.Allocator_Error.Out_Of_Memory } return @@ -313,19 +309,19 @@ read_header :: proc(ctx: ^$C) -> (IHDR, Error) { // Validate IHDR using header if width == 0 || height == 0 { - return {}, E_PNG.Invalid_Image_Dimensions + return {}, .Invalid_Image_Dimensions } if compression_method != 0 { - return {}, E_General.Unknown_Compression_Method + return {}, compress.General_Error.Unknown_Compression_Method } if filter_method != 0 { - return {}, E_PNG.Unknown_Filter_Method + return {}, .Unknown_Filter_Method } if interlace_method != .None && interlace_method != .Adam7 { - return {}, E_PNG.Unknown_Interlace_Method + return {}, .Unknown_Interlace_Method } @@ -343,7 +339,7 @@ read_header :: proc(ctx: ^$C) -> (IHDR, Error) { } } if !allowed { - return {}, E_PNG.Invalid_Color_Bit_Depth_Combo + return {}, .Invalid_Color_Bit_Depth_Combo } case 2, 4, 6: /* @@ -351,7 +347,7 @@ read_header :: proc(ctx: ^$C) -> (IHDR, Error) { Allowed bit depths: 8 and 16 */ if bit_depth != 8 && bit_depth != 16 { - return {}, E_PNG.Invalid_Color_Bit_Depth_Combo + return {}, .Invalid_Color_Bit_Depth_Combo } case 3: /* @@ -366,11 +362,11 @@ read_header :: proc(ctx: ^$C) -> (IHDR, Error) { } } if !allowed { - return {}, E_PNG.Invalid_Color_Bit_Depth_Combo + return {}, .Invalid_Color_Bit_Depth_Combo } case: - return {}, E_PNG.Unknown_Color_Type + return {}, .Unknown_Color_Type } return header, nil @@ -406,7 +402,7 @@ load_from_file :: proc(filename: string, options := Options{}, allocator := cont return load_from_slice(data, options) } else { img = new(Image) - return img, E_General.File_Not_Found + return img, compress.General_Error.File_Not_Found } } @@ -420,7 +416,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a } if .alpha_drop_if_present in options && .alpha_add_if_missing in options { - return {}, E_General.Incompatible_Options + return {}, compress.General_Error.Incompatible_Options } if .do_not_expand_channels in options { @@ -437,7 +433,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, E_PNG.Invalid_PNG_Signature + return img, .Invalid_PNG_Signature } idat: []u8 @@ -472,14 +468,14 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a ch, e = compress.peek_data(ctx, Chunk_Header) if e != .None { - return img, E_General.Stream_Too_Short + return img, compress.General_Error.Stream_Too_Short } // name := chunk_type_to_name(&ch.type); // Only used for debug prints during development. #partial switch ch.type { case .IHDR: if seen_ihdr || !first { - return {}, E_PNG.IHDR_Not_First_Chunk + return {}, .IHDR_Not_First_Chunk } seen_ihdr = true @@ -508,7 +504,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a } if img.channels == 0 || img.depth == 0 { - return {}, E_PNG.IHDR_Corrupt + return {}, .IHDR_Corrupt } img.width = int(header.width) @@ -530,18 +526,18 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a // PLTE must appear before IDAT and can't appear for color types 0, 4. ct := transmute(u8)info.header.color_type if seen_idat || ct == 0 || ct == 4 { - return img, E_PNG.PLTE_Encountered_Unexpectedly + return img, .PLTE_Encountered_Unexpectedly } c = read_chunk(ctx) or_return if c.header.length % 3 != 0 || c.header.length > 768 { - return img, E_PNG.PLTE_Invalid_Length + return img, .PLTE_Invalid_Length } plte_ok: bool _plte, plte_ok = plte(c) if !plte_ok { - return img, E_PNG.PLTE_Invalid_Length + return img, .PLTE_Invalid_Length } if .return_metadata in options { @@ -555,11 +551,11 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a } // There must be at least 1 IDAT, contiguous if more. if seen_idat { - return img, E_PNG.IDAT_Must_Be_Contiguous + return img, .IDAT_Must_Be_Contiguous } if idat_length > 0 { - return img, E_PNG.IDAT_Must_Be_Contiguous + return img, .IDAT_Must_Be_Contiguous } next := ch.type @@ -571,13 +567,13 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a ch, e = compress.peek_data(ctx, Chunk_Header) if e != .None { - return img, E_General.Stream_Too_Short + return img, compress.General_Error.Stream_Too_Short } next = ch.type } idat = bytes.buffer_to_bytes(&idat_b) if int(idat_length) != len(idat) { - return {}, E_PNG.IDAT_Corrupt + return {}, .IDAT_Corrupt } seen_idat = true case .IEND: @@ -597,7 +593,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a switch ct { case 3: // Indexed color if c.header.length != 1 { - return {}, E_PNG.BKGD_Invalid_Length + return {}, .BKGD_Invalid_Length } col := _plte.entries[c.data[0]] img.background = [3]u16{ @@ -607,13 +603,13 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a } case 0, 4: // Grayscale, with and without Alpha if c.header.length != 2 { - return {}, E_PNG.BKGD_Invalid_Length + return {}, .BKGD_Invalid_Length } col := u16(mem.slice_data_cast([]u16be, c.data[:])[0]) img.background = [3]u16{col, col, col} case 2, 6: // Color, with and without Alpha if c.header.length != 6 { - return {}, E_PNG.BKGD_Invalid_Length + return {}, .BKGD_Invalid_Length } col := mem.slice_data_cast([]u16be, c.data[:]) img.background = [3]u16{u16(col[0]), u16(col[1]), u16(col[2])} @@ -622,7 +618,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a c = read_chunk(ctx) or_return if .Alpha in info.header.color_type { - return img, E_PNG.TRNS_Encountered_Unexpectedly + return img, .TRNS_Encountered_Unexpectedly } if .return_metadata in options { @@ -655,7 +651,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a We're not going to add support for it. If you have the misfortunte of coming across one of these files, use a utility to defry it. */ - return img, E_PNG.PNG_Does_Not_Adhere_to_Spec + return img, .Image_Does_Not_Adhere_to_Spec case: // Unhandled type c = read_chunk(ctx) or_return @@ -673,7 +669,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a } if !seen_idat { - return img, E_PNG.IDAT_Missing + return img, .IDAT_Missing } /* @@ -710,7 +706,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a buf_len := len(buf.buf) if expected_size != buf_len { - return {}, E_PNG.IDAT_Corrupt + return {}, .IDAT_Corrupt } /* @@ -1549,7 +1545,7 @@ defilter_16 :: proc(params: ^Filter_Params) -> (ok: bool) { return } -defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, options: Options) -> (err: compress.Error) { +defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, options: Options) -> (err: Error) { input := bytes.buffer_to_bytes(filter_bytes) width := int(header.width) height := int(header.height) @@ -1585,7 +1581,7 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, option } if !filter_ok { // Caller will destroy buffer for us. - return E_PNG.Unknown_Filter_Method + return .Unknown_Filter_Method } } else { /* @@ -1623,7 +1619,7 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, option if !filter_ok { // Caller will destroy buffer for us. - return E_PNG.Unknown_Filter_Method + return .Unknown_Filter_Method } t := temp.buf[:] diff --git a/tests/core/image/build.bat b/tests/core/image/build.bat new file mode 100644 index 000000000..03ee6b9a5 --- /dev/null +++ b/tests/core/image/build.bat @@ -0,0 +1,4 @@ +@echo off +pushd .. +odin run image +popd \ 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 14f7db074..d2db9d11d 100644 --- a/tests/core/image/test_core_image.odin +++ b/tests/core/image/test_core_image.odin @@ -64,7 +64,7 @@ PNG_Test :: struct { file: string, tests: []struct { options: image.Options, - expected_error: compress.Error, + expected_error: image.Error, dims: PNG_Dims, hash: u32, }, @@ -1198,37 +1198,37 @@ Corrupt_PNG_Tests := []PNG_Test{ { "xs1n0g01", // signature byte 1 MSBit reset to zero { - {Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000}, + {Default, .Invalid_PNG_Signature, {}, 0x_0000_0000}, }, }, { "xs2n0g01", // signature byte 2 is a 'Q' { - {Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000}, + {Default, .Invalid_PNG_Signature, {}, 0x_0000_0000}, }, }, { "xs4n0g01", // signature byte 4 lowercase { - {Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000}, + {Default, .Invalid_PNG_Signature, {}, 0x_0000_0000}, }, }, { "xs7n0g01", // 7th byte a space instead of control-Z { - {Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000}, + {Default, .Invalid_PNG_Signature, {}, 0x_0000_0000}, }, }, { "xcrn0g04", // added cr bytes { - {Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000}, + {Default, .Invalid_PNG_Signature, {}, 0x_0000_0000}, }, }, { "xlfn0g04", // added lf bytes { - {Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000}, + {Default, .Invalid_PNG_Signature, {}, 0x_0000_0000}, }, }, { @@ -1240,37 +1240,37 @@ Corrupt_PNG_Tests := []PNG_Test{ { "xc1n0g08", // color type 1 { - {Default, I_Error.Unknown_Color_Type, {}, 0x_0000_0000}, + {Default, .Unknown_Color_Type, {}, 0x_0000_0000}, }, }, { "xc9n2c08", // color type 9 { - {Default, I_Error.Unknown_Color_Type, {}, 0x_0000_0000}, + {Default, .Unknown_Color_Type, {}, 0x_0000_0000}, }, }, { "xd0n2c08", // bit-depth 0 { - {Default, I_Error.Invalid_Color_Bit_Depth_Combo, {}, 0x_0000_0000}, + {Default, .Invalid_Color_Bit_Depth_Combo, {}, 0x_0000_0000}, }, }, { "xd3n2c08", // bit-depth 3 { - {Default, I_Error.Invalid_Color_Bit_Depth_Combo, {}, 0x_0000_0000}, + {Default, .Invalid_Color_Bit_Depth_Combo, {}, 0x_0000_0000}, }, }, { "xd9n2c08", // bit-depth 99 { - {Default, I_Error.Invalid_Color_Bit_Depth_Combo, {}, 0x_0000_0000}, + {Default, .Invalid_Color_Bit_Depth_Combo, {}, 0x_0000_0000}, }, }, { "xdtn0g01", // missing IDAT chunk { - {Default, I_Error.IDAT_Missing, {}, 0x_0000_0000}, + {Default, .IDAT_Missing, {}, 0x_0000_0000}, }, }, { From 8fcd1794a676610f838bc00b754f80830d6c6e50 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Wed, 6 Oct 2021 21:48:22 +0200 Subject: [PATCH 6/9] png: Add sane compile-time maximums for dimensions + chunk sizes. --- core/compress/common.odin | 3 +- core/image/common.odin | 7 ++-- core/image/png/example.odin | 7 ++-- core/image/png/helpers.odin | 3 +- core/image/png/png.odin | 60 ++++++++++++++++++++++++--- tests/core/image/test_core_image.odin | 3 +- 6 files changed, 64 insertions(+), 19 deletions(-) diff --git a/core/compress/common.odin b/core/compress/common.odin index 819cfb481..41f292b6f 100644 --- a/core/compress/common.odin +++ b/core/compress/common.odin @@ -1,5 +1,3 @@ -package compress - /* Copyright 2021 Jeroen van Rijn . Made available under Odin's BSD-3 license. @@ -7,6 +5,7 @@ package compress List of contributors: Jeroen van Rijn: Initial implementation, optimization. */ +package compress import "core:io" import "core:bytes" diff --git a/core/image/common.odin b/core/image/common.odin index 9bb99a5d4..2826a65ca 100644 --- a/core/image/common.odin +++ b/core/image/common.odin @@ -1,13 +1,12 @@ -package image - /* Copyright 2021 Jeroen van Rijn . - Made available under Odin's BSD-2 license. + Made available under Odin's BSD-3 license. List of contributors: Jeroen van Rijn: Initial implementation, optimization. Ginger Bill: Cosmetic changes. */ +package image import "core:bytes" import "core:mem" @@ -128,6 +127,7 @@ Error :: union { General_Image_Error :: enum { None = 0, Invalid_Image_Dimensions, + Image_Dimensions_Too_Large, Image_Does_Not_Adhere_to_Spec, } @@ -138,6 +138,7 @@ PNG_Error :: enum { IDAT_Missing, IDAT_Must_Be_Contiguous, IDAT_Corrupt, + IDAT_Size_Too_Large, PLTE_Encountered_Unexpectedly, PLTE_Invalid_Length, TRNS_Encountered_Unexpectedly, diff --git a/core/image/png/example.odin b/core/image/png/example.odin index ad0423d67..5370b0bcf 100644 --- a/core/image/png/example.odin +++ b/core/image/png/example.odin @@ -1,9 +1,6 @@ -//+ignore -package png - /* Copyright 2021 Jeroen van Rijn . - Made available under Odin's BSD-2 license. + Made available under Odin's BSD-3 license. List of contributors: Jeroen van Rijn: Initial implementation. @@ -11,6 +8,8 @@ package png An example of how to use `load`. */ +//+ignore +package png import "core:image" // import "core:image/png" diff --git a/core/image/png/helpers.odin b/core/image/png/helpers.odin index 1b3749f76..59d8fb70b 100644 --- a/core/image/png/helpers.odin +++ b/core/image/png/helpers.odin @@ -1,5 +1,3 @@ -package png - /* Copyright 2021 Jeroen van Rijn . Made available under Odin's BSD-2 license. @@ -10,6 +8,7 @@ package png These are a few useful utility functions to work with PNG images. */ +package png import "core:image" import "core:compress/zlib" diff --git a/core/image/png/png.odin b/core/image/png/png.odin index 21b27fc82..883e51d83 100644 --- a/core/image/png/png.odin +++ b/core/image/png/png.odin @@ -1,13 +1,12 @@ -package png - /* Copyright 2021 Jeroen van Rijn . - Made available under Odin's BSD-2 license. + Made available under Odin's BSD-3 license. List of contributors: Jeroen van Rijn: Initial implementation. Ginger Bill: Cosmetic changes. */ +package png import "core:compress" import "core:compress/zlib" @@ -21,6 +20,28 @@ import "core:io" import "core:mem" import "core:intrinsics" +/* + 67_108_864 pixels max by default. + Maximum allowed dimensions are capped at 65535 * 65535. +*/ +MAX_DIMENSIONS :: min(#config(PNG_MAX_DIMENSIONS, 8192 * 8192), 65535 * 65535) + +/* + Limit chunk sizes. + By default: IDAT = 8k x 8k x 16-bits + 8k filter bytes. +*/ +_MAX_IDAT_DEFAULT :: ( 8192 /* Width */ * 8192 /* Height */ * 2 /* 16-bit */) + 8192 /* Filter bytes */ +_MAX_IDAT :: (65535 /* Width */ * 65535 /* Height */ * 2 /* 16-bit */) + 65535 /* Filter bytes */ + +MAX_IDAT_SIZE :: min(#config(PNG_MAX_IDAT_SIZE, _MAX_IDAT_DEFAULT), _MAX_IDAT) + +/* + For chunks other than IDAT with a variable size like `zTXT` and `eXIf`, + limit their size to 16 MiB each by default. Max of 256 MiB each. +*/ +MAX_CHUNK_SIZE :: min(#config(PNG_MAX_CHUNK_SIZE, 16_777_216), 268_435_456) + + Error :: image.Error Image :: image.Image Options :: image.Options @@ -248,6 +269,20 @@ read_chunk :: proc(ctx: ^$C) -> (chunk: Chunk, err: Error) { } chunk.header = ch + /* + Sanity check chunk size + */ + #partial switch ch.type { + case .IDAT: + if ch.length > MAX_IDAT_SIZE { + return {}, image.PNG_Error.IDAT_Size_Too_Large + } + case: + if ch.length > MAX_CHUNK_SIZE { + return {}, image.PNG_Error.Invalid_Chunk_Length + } + } + chunk.data, e = compress.read_slice(ctx, int(ch.length)) if e != .None { return {}, compress.General_Error.Stream_Too_Short @@ -308,7 +343,7 @@ read_header :: proc(ctx: ^$C) -> (IHDR, Error) { header := (^IHDR)(raw_data(c.data))^ // Validate IHDR using header - if width == 0 || height == 0 { + if width == 0 || height == 0 || u128(width) * u128(height) > MAX_DIMENSIONS { return {}, .Invalid_Image_Dimensions } @@ -438,9 +473,10 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a idat: []u8 idat_b: bytes.Buffer - idat_length := u32be(0) defer bytes.buffer_destroy(&idat_b) + idat_length := u64(0) + c: Chunk ch: Chunk_Header e: io.Error @@ -521,6 +557,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a interlace_method = interlace_method, } info.header = h + case .PLTE: seen_plte = true // PLTE must appear before IDAT and can't appear for color types 0, 4. @@ -543,6 +580,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a if .return_metadata in options { append_chunk(&info.chunks, c) or_return } + case .IDAT: // If we only want image metadata and don't want the pixel data, we can early out. if .return_metadata not_in options && .do_not_decompress_image in options { @@ -563,7 +601,11 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a c = read_chunk(ctx) or_return bytes.buffer_write(&idat_b, c.data) - idat_length += c.header.length + idat_length += u64(c.header.length) + + if idat_length > MAX_IDAT_SIZE { + return {}, image.PNG_Error.IDAT_Size_Too_Large + } ch, e = compress.peek_data(ctx, Chunk_Header) if e != .None { @@ -571,14 +613,17 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a } next = ch.type } + idat = bytes.buffer_to_bytes(&idat_b) if int(idat_length) != len(idat) { return {}, .IDAT_Corrupt } seen_idat = true + case .IEND: c = read_chunk(ctx) or_return seen_iend = true + case .bKGD: // TODO: Make sure that 16-bit bKGD + tRNS chunks return u16 instead of u16be @@ -614,6 +659,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a col := mem.slice_data_cast([]u16be, c.data[:]) img.background = [3]u16{u16(col[0]), u16(col[1]), u16(col[2])} } + case .tRNS: c = read_chunk(ctx) or_return @@ -645,6 +691,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a } } trns = c + case .iDOT, .CbGI: /* iPhone PNG bastardization that doesn't adhere to spec with broken IDAT chunk. @@ -652,6 +699,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a across one of these files, use a utility to defry it. */ return img, .Image_Does_Not_Adhere_to_Spec + case: // Unhandled type c = read_chunk(ctx) or_return diff --git a/tests/core/image/test_core_image.odin b/tests/core/image/test_core_image.odin index d2db9d11d..7752cf7dc 100644 --- a/tests/core/image/test_core_image.odin +++ b/tests/core/image/test_core_image.odin @@ -1,5 +1,3 @@ -package test_core_image - /* Copyright 2021 Jeroen van Rijn . Made available under Odin's BSD-3 license. @@ -9,6 +7,7 @@ package test_core_image A test suite for PNG. */ +package test_core_image import "core:testing" From 263d63aa5678276532776c129829b6a77f56bcad Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Wed, 6 Oct 2021 22:09:32 +0200 Subject: [PATCH 7/9] png: Add more OOM checks. --- core/image/png/png.odin | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/core/image/png/png.odin b/core/image/png/png.odin index 883e51d83..536b82be2 100644 --- a/core/image/png/png.odin +++ b/core/image/png/png.odin @@ -821,7 +821,9 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a // We need to create a new image buffer dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8) t := bytes.Buffer{} - resize(&t.buf, dest_raw_size) + if !resize(&t.buf, dest_raw_size) { + return {}, mem.Allocator_Error.Out_Of_Memory + } i := 0; j := 0 @@ -900,7 +902,9 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a // We need to create a new image buffer dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 16) t := bytes.Buffer{} - resize(&t.buf, dest_raw_size) + if !resize(&t.buf, dest_raw_size) { + return {}, mem.Allocator_Error.Out_Of_Memory + } p16 := mem.slice_data_cast([]u16, temp.buf[:]) o16 := mem.slice_data_cast([]u16, t.buf[:]) @@ -1097,7 +1101,9 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a // We need to create a new image buffer dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8) t := bytes.Buffer{} - resize(&t.buf, dest_raw_size) + if !resize(&t.buf, dest_raw_size) { + return {}, mem.Allocator_Error.Out_Of_Memory + } p := mem.slice_data_cast([]u8, temp.buf[:]) o := mem.slice_data_cast([]u8, t.buf[:]) @@ -1604,7 +1610,9 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, option bytes_per_channel := depth == 16 ? 2 : 1 num_bytes := compute_buffer_size(width, height, channels, depth == 16 ? 16 : 8) - resize(&img.pixels.buf, num_bytes) + if !resize(&img.pixels.buf, num_bytes) { + return mem.Allocator_Error.Out_Of_Memory + } filter_ok: bool @@ -1644,7 +1652,9 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, option if x > 0 && y > 0 { temp: bytes.Buffer temp_len := compute_buffer_size(x, y, channels, depth == 16 ? 16 : 8) - resize(&temp.buf, temp_len) + if !resize(&temp.buf, temp_len) { + return mem.Allocator_Error.Out_Of_Memory + } params := Filter_Params{ src = input, From c4b4a841d68f4bde69f4e1472b7d8fa24f96a376 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Wed, 6 Oct 2021 22:43:33 +0200 Subject: [PATCH 8/9] png: Move metadata. --- core/image/common.odin | 105 +++++++++++++++- core/image/png/example.odin | 166 +++++++++++++------------- core/image/png/helpers.odin | 41 +++---- core/image/png/png.odin | 124 +++---------------- tests/core/image/test_core_image.odin | 4 +- 5 files changed, 218 insertions(+), 222 deletions(-) diff --git a/core/image/common.odin b/core/image/common.odin index 2826a65ca..919670a61 100644 --- a/core/image/common.odin +++ b/core/image/common.odin @@ -26,8 +26,11 @@ Image :: struct { */ background: Maybe([3]u16), - metadata_ptr: rawptr, - metadata_type: typeid, + metadata: Image_Metadata, +} + +Image_Metadata :: union { + ^PNG_Info, } /* @@ -152,10 +155,101 @@ PNG_Error :: enum { Invalid_Chunk_Length, } +/* + PNG-specific structs +*/ +PNG_Info :: struct { + header: PNG_IHDR, + chunks: [dynamic]PNG_Chunk, +} + +PNG_Chunk_Header :: struct #packed { + length: u32be, + type: PNG_Chunk_Type, +} + +PNG_Chunk :: struct #packed { + header: PNG_Chunk_Header, + data: []byte, + crc: u32be, +} + +PNG_Chunk_Type :: enum u32be { + // IHDR must come first in a file + IHDR = 'I' << 24 | 'H' << 16 | 'D' << 8 | 'R', + // PLTE must precede the first IDAT chunk + PLTE = 'P' << 24 | 'L' << 16 | 'T' << 8 | 'E', + bKGD = 'b' << 24 | 'K' << 16 | 'G' << 8 | 'D', + tRNS = 't' << 24 | 'R' << 16 | 'N' << 8 | 'S', + IDAT = 'I' << 24 | 'D' << 16 | 'A' << 8 | 'T', + + iTXt = 'i' << 24 | 'T' << 16 | 'X' << 8 | 't', + tEXt = 't' << 24 | 'E' << 16 | 'X' << 8 | 't', + zTXt = 'z' << 24 | 'T' << 16 | 'X' << 8 | 't', + + iCCP = 'i' << 24 | 'C' << 16 | 'C' << 8 | 'P', + pHYs = 'p' << 24 | 'H' << 16 | 'Y' << 8 | 's', + gAMA = 'g' << 24 | 'A' << 16 | 'M' << 8 | 'A', + tIME = 't' << 24 | 'I' << 16 | 'M' << 8 | 'E', + + sPLT = 's' << 24 | 'P' << 16 | 'L' << 8 | 'T', + sRGB = 's' << 24 | 'R' << 16 | 'G' << 8 | 'B', + hIST = 'h' << 24 | 'I' << 16 | 'S' << 8 | 'T', + cHRM = 'c' << 24 | 'H' << 16 | 'R' << 8 | 'M', + sBIT = 's' << 24 | 'B' << 16 | 'I' << 8 | 'T', + + /* + eXIf tags are not part of the core spec, but have been ratified + in v1.5.0 of the PNG Ext register. + + We will provide unprocessed chunks to the caller if `.return_metadata` is set. + Applications are free to implement an Exif decoder. + */ + eXIf = 'e' << 24 | 'X' << 16 | 'I' << 8 | 'f', + + // PNG files must end with IEND + IEND = 'I' << 24 | 'E' << 16 | 'N' << 8 | 'D', + + /* + XCode sometimes produces "PNG" files that don't adhere to the PNG spec. + We recognize them only in order to avoid doing further work on them. + + Some tools like PNG Defry may be able to repair them, but we're not + going to reward Apple for producing proprietary broken files purporting + to be PNGs by supporting them. + + */ + iDOT = 'i' << 24 | 'D' << 16 | 'O' << 8 | 'T', + CbGI = 'C' << 24 | 'b' << 16 | 'H' << 8 | 'I', +} + +PNG_IHDR :: struct #packed { + width: u32be, + height: u32be, + bit_depth: u8, + color_type: PNG_Color_Type, + compression_method: u8, + filter_method: u8, + interlace_method: PNG_Interlace_Method, +} +PNG_IHDR_SIZE :: size_of(PNG_IHDR) +#assert (PNG_IHDR_SIZE == 13) + +PNG_Color_Value :: enum u8 { + Paletted = 0, // 1 << 0 = 1 + Color = 1, // 1 << 1 = 2 + Alpha = 2, // 1 << 2 = 4 +} +PNG_Color_Type :: distinct bit_set[PNG_Color_Value; u8] + +PNG_Interlace_Method :: enum u8 { + None = 0, + Adam7 = 1, +} + /* Functions to help with image buffer calculations */ - compute_buffer_size :: proc(width, height, channels, depth: int, extra_row_bytes := int(0)) -> (size: int) { size = ((((channels * width * depth) + 7) >> 3) + extra_row_bytes) * height return @@ -164,7 +258,6 @@ compute_buffer_size :: proc(width, height, channels, depth: int, extra_row_bytes /* For when you have an RGB(A) image, but want a particular channel. */ - Channel :: enum u8 { R = 1, G = 2, @@ -226,8 +319,8 @@ return_single_channel :: proc(img: ^Image, channel: Channel) -> (res: ^Image, ok res.depth = img.depth res.pixels = t res.background = img.background - res.metadata_ptr = img.metadata_ptr - res.metadata_type = img.metadata_type + // res.metadata_ptr = img.metadata_ptr + // res.metadata_type = img.metadata_type return res, true } diff --git a/core/image/png/example.odin b/core/image/png/example.odin index 5370b0bcf..5e7dca4c8 100644 --- a/core/image/png/example.odin +++ b/core/image/png/example.odin @@ -53,93 +53,91 @@ demo :: proc() { } else { fmt.printf("Image: %vx%vx%v, %v-bit.\n", img.width, img.height, img.channels, img.depth) - assert(img.metadata_ptr != nil && img.metadata_type == Info) - - v := (^Info)(img.metadata_ptr) - - // Handle ancillary chunks as you wish. - // We provide helper functions for a few types. - for c in v.chunks { - #partial switch c.header.type { - case .tIME: - if t, t_ok := core_time(c); t_ok { - fmt.printf("[tIME]: %v\n", t) - } - case .gAMA: - if gama, gama_ok := gamma(c); gama_ok { - fmt.printf("[gAMA]: %v\n", gama) - } - case .pHYs: - if phys, phys_ok := phys(c); phys_ok { - if phys.unit == .Meter { - xm := f32(img.width) / f32(phys.ppu_x) - ym := f32(img.height) / f32(phys.ppu_y) - dpi_x, dpi_y := phys_to_dpi(phys) - fmt.printf("[pHYs] Image resolution is %v x %v pixels per meter.\n", phys.ppu_x, phys.ppu_y) - fmt.printf("[pHYs] Image resolution is %v x %v DPI.\n", dpi_x, dpi_y) - fmt.printf("[pHYs] Image dimensions are %v x %v meters.\n", xm, ym) - } else { - fmt.printf("[pHYs] x: %v, y: %v pixels per unknown unit.\n", phys.ppu_x, phys.ppu_y) + if v, ok := img.metadata.(^image.PNG_Info); ok { + // Handle ancillary chunks as you wish. + // We provide helper functions for a few types. + for c in v.chunks { + #partial switch c.header.type { + case .tIME: + if t, t_ok := core_time(c); t_ok { + fmt.printf("[tIME]: %v\n", t) } - } - case .iTXt, .zTXt, .tEXt: - res, ok_text := text(c) - if ok_text { - if c.header.type == .iTXt { - fmt.printf("[iTXt] %v (%v:%v): %v\n", res.keyword, res.language, res.keyword_localized, res.text) - } else { - fmt.printf("[tEXt/zTXt] %v: %v\n", res.keyword, res.text) + case .gAMA: + if gama, gama_ok := gamma(c); gama_ok { + fmt.printf("[gAMA]: %v\n", gama) } + case .pHYs: + if phys, phys_ok := phys(c); phys_ok { + if phys.unit == .Meter { + xm := f32(img.width) / f32(phys.ppu_x) + ym := f32(img.height) / f32(phys.ppu_y) + dpi_x, dpi_y := phys_to_dpi(phys) + fmt.printf("[pHYs] Image resolution is %v x %v pixels per meter.\n", phys.ppu_x, phys.ppu_y) + fmt.printf("[pHYs] Image resolution is %v x %v DPI.\n", dpi_x, dpi_y) + fmt.printf("[pHYs] Image dimensions are %v x %v meters.\n", xm, ym) + } else { + fmt.printf("[pHYs] x: %v, y: %v pixels per unknown unit.\n", phys.ppu_x, phys.ppu_y) + } + } + case .iTXt, .zTXt, .tEXt: + res, ok_text := text(c) + if ok_text { + if c.header.type == .iTXt { + fmt.printf("[iTXt] %v (%v:%v): %v\n", res.keyword, res.language, res.keyword_localized, res.text) + } else { + fmt.printf("[tEXt/zTXt] %v: %v\n", res.keyword, res.text) + } + } + defer text_destroy(res) + case .bKGD: + fmt.printf("[bKGD] %v\n", img.background) + case .eXIf: + if res, ok_exif := exif(c); ok_exif { + /* + Other than checking the signature and byte order, we don't handle Exif data. + If you wish to interpret it, pass it to an Exif parser. + */ + fmt.printf("[eXIf] %v\n", res) + } + case .PLTE: + if plte, plte_ok := plte(c); plte_ok { + fmt.printf("[PLTE] %v\n", plte) + } else { + fmt.printf("[PLTE] Error\n") + } + case .hIST: + if res, ok_hist := hist(c); ok_hist { + fmt.printf("[hIST] %v\n", res) + } + case .cHRM: + if res, ok_chrm := chrm(c); ok_chrm { + fmt.printf("[cHRM] %v\n", res) + } + case .sPLT: + res, ok_splt := splt(c) + if ok_splt { + fmt.printf("[sPLT] %v\n", res) + } + splt_destroy(res) + case .sBIT: + if res, ok_sbit := sbit(c); ok_sbit { + fmt.printf("[sBIT] %v\n", res) + } + case .iCCP: + res, ok_iccp := iccp(c) + if ok_iccp { + fmt.printf("[iCCP] %v\n", res) + } + iccp_destroy(res) + case .sRGB: + if res, ok_srgb := srgb(c); ok_srgb { + fmt.printf("[sRGB] Rendering intent: %v\n", res) + } + case: + type := c.header.type + name := chunk_type_to_name(&type) + fmt.printf("[%v]: %v\n", name, c.data) } - defer text_destroy(res) - case .bKGD: - fmt.printf("[bKGD] %v\n", img.background) - case .eXIf: - if res, ok_exif := exif(c); ok_exif { - /* - Other than checking the signature and byte order, we don't handle Exif data. - If you wish to interpret it, pass it to an Exif parser. - */ - fmt.printf("[eXIf] %v\n", res) - } - case .PLTE: - if plte, plte_ok := plte(c); plte_ok { - fmt.printf("[PLTE] %v\n", plte) - } else { - fmt.printf("[PLTE] Error\n") - } - case .hIST: - if res, ok_hist := hist(c); ok_hist { - fmt.printf("[hIST] %v\n", res) - } - case .cHRM: - if res, ok_chrm := chrm(c); ok_chrm { - fmt.printf("[cHRM] %v\n", res) - } - case .sPLT: - res, ok_splt := splt(c) - if ok_splt { - fmt.printf("[sPLT] %v\n", res) - } - splt_destroy(res) - case .sBIT: - if res, ok_sbit := sbit(c); ok_sbit { - fmt.printf("[sBIT] %v\n", res) - } - case .iCCP: - res, ok_iccp := iccp(c) - if ok_iccp { - fmt.printf("[iCCP] %v\n", res) - } - iccp_destroy(res) - case .sRGB: - if res, ok_srgb := srgb(c); ok_srgb { - fmt.printf("[sRGB] Rendering intent: %v\n", res) - } - case: - type := c.header.type - name := chunk_type_to_name(&type) - fmt.printf("[%v]: %v\n", name, c.data) } } } diff --git a/core/image/png/helpers.odin b/core/image/png/helpers.odin index 59d8fb70b..ecc0183bc 100644 --- a/core/image/png/helpers.odin +++ b/core/image/png/helpers.odin @@ -34,16 +34,13 @@ destroy :: proc(img: ^Image) { bytes.buffer_destroy(&img.pixels) - assert(img.metadata_ptr != nil && img.metadata_type == Info) - v := (^Info)(img.metadata_ptr) - - for chunk in &v.chunks { - delete(chunk.data) + if v, ok := img.metadata.(^image.PNG_Info); ok { + for chunk in &v.chunks { + delete(chunk.data) + } + delete(v.chunks) + free(v) } - delete(v.chunks) - - // Clean up Info. - free(img.metadata_ptr) free(img) } @@ -51,7 +48,7 @@ destroy :: proc(img: ^Image) { Chunk helpers */ -gamma :: proc(c: Chunk) -> (res: f32, ok: bool) { +gamma :: proc(c: image.PNG_Chunk) -> (res: f32, ok: bool) { if c.header.type != .gAMA || len(c.data) != size_of(gAMA) { return {}, false } @@ -61,7 +58,7 @@ gamma :: proc(c: Chunk) -> (res: f32, ok: bool) { INCHES_PER_METER :: 1000.0 / 25.4 -phys :: proc(c: Chunk) -> (res: pHYs, ok: bool) { +phys :: proc(c: image.PNG_Chunk) -> (res: pHYs, ok: bool) { if c.header.type != .pHYs || len(c.data) != size_of(pHYs) { return {}, false } @@ -73,7 +70,7 @@ phys_to_dpi :: proc(p: pHYs) -> (x_dpi, y_dpi: f32) { return f32(p.ppu_x) / INCHES_PER_METER, f32(p.ppu_y) / INCHES_PER_METER } -time :: proc(c: Chunk) -> (res: tIME, ok: bool) { +time :: proc(c: image.PNG_Chunk) -> (res: tIME, ok: bool) { if c.header.type != .tIME || len(c.data) != size_of(tIME) { return {}, false } @@ -81,7 +78,7 @@ time :: proc(c: Chunk) -> (res: tIME, ok: bool) { return (^tIME)(raw_data(c.data))^, true } -core_time :: proc(c: Chunk) -> (t: coretime.Time, ok: bool) { +core_time :: proc(c: image.PNG_Chunk) -> (t: coretime.Time, ok: bool) { if png_time, png_ok := time(c); png_ok { using png_time return coretime.datetime_to_time( @@ -93,7 +90,7 @@ core_time :: proc(c: Chunk) -> (t: coretime.Time, ok: bool) { } } -text :: proc(c: Chunk) -> (res: Text, ok: bool) { +text :: proc(c: image.PNG_Chunk) -> (res: Text, ok: bool) { assert(len(c.data) == int(c.header.length)) #partial switch c.header.type { case .tEXt: @@ -196,7 +193,7 @@ text_destroy :: proc(text: Text) { delete(text.text) } -iccp :: proc(c: Chunk) -> (res: iCCP, ok: bool) { +iccp :: proc(c: image.PNG_Chunk) -> (res: iCCP, ok: bool) { ok = true fields := bytes.split_n(s=c.data, sep=[]u8{0}, n=3, allocator=context.temp_allocator) @@ -232,7 +229,7 @@ iccp_destroy :: proc(i: iCCP) { } -srgb :: proc(c: Chunk) -> (res: sRGB, ok: bool) { +srgb :: proc(c: image.PNG_Chunk) -> (res: sRGB, ok: bool) { if c.header.type != .sRGB || len(c.data) != size_of(sRGB_Rendering_Intent) { return {}, false } @@ -244,7 +241,7 @@ srgb :: proc(c: Chunk) -> (res: sRGB, ok: bool) { return res, true } -plte :: proc(c: Chunk) -> (res: PLTE, ok: bool) { +plte :: proc(c: image.PNG_Chunk) -> (res: PLTE, ok: bool) { if c.header.type != .PLTE { return {}, false } @@ -258,7 +255,7 @@ plte :: proc(c: Chunk) -> (res: PLTE, ok: bool) { return } -splt :: proc(c: Chunk) -> (res: sPLT, ok: bool) { +splt :: proc(c: image.PNG_Chunk) -> (res: sPLT, ok: bool) { if c.header.type != .sPLT { return {}, false } @@ -309,7 +306,7 @@ splt_destroy :: proc(s: sPLT) { delete(s.name) } -sbit :: proc(c: Chunk) -> (res: [4]u8, ok: bool) { +sbit :: proc(c: image.PNG_Chunk) -> (res: [4]u8, ok: bool) { /* Returns [4]u8 with the significant bits in each channel. A channel will contain zero if not applicable to the PNG color type. @@ -327,7 +324,7 @@ sbit :: proc(c: Chunk) -> (res: [4]u8, ok: bool) { } -hist :: proc(c: Chunk) -> (res: hIST, ok: bool) { +hist :: proc(c: image.PNG_Chunk) -> (res: hIST, ok: bool) { if c.header.type != .hIST { return {}, false } @@ -349,7 +346,7 @@ hist :: proc(c: Chunk) -> (res: hIST, ok: bool) { return } -chrm :: proc(c: Chunk) -> (res: cHRM, ok: bool) { +chrm :: proc(c: image.PNG_Chunk) -> (res: cHRM, ok: bool) { ok = true if c.header.length != size_of(cHRM_Raw) { return {}, false @@ -367,7 +364,7 @@ chrm :: proc(c: Chunk) -> (res: cHRM, ok: bool) { return } -exif :: proc(c: Chunk) -> (res: Exif, ok: bool) { +exif :: proc(c: image.PNG_Chunk) -> (res: Exif, ok: bool) { ok = true diff --git a/core/image/png/png.odin b/core/image/png/png.odin index 536b82be2..f77bf7519 100644 --- a/core/image/png/png.odin +++ b/core/image/png/png.odin @@ -51,95 +51,6 @@ Signature :: enum u64be { PNG = 0x89 << 56 | 'P' << 48 | 'N' << 40 | 'G' << 32 | '\r' << 24 | '\n' << 16 | 0x1a << 8 | '\n', } -Info :: struct { - header: IHDR, - chunks: [dynamic]Chunk, -} - -Chunk_Header :: struct #packed { - length: u32be, - type: Chunk_Type, -} - -Chunk :: struct #packed { - header: Chunk_Header, - data: []byte, - crc: u32be, -} - -Chunk_Type :: enum u32be { - // IHDR must come first in a file - IHDR = 'I' << 24 | 'H' << 16 | 'D' << 8 | 'R', - // PLTE must precede the first IDAT chunk - PLTE = 'P' << 24 | 'L' << 16 | 'T' << 8 | 'E', - bKGD = 'b' << 24 | 'K' << 16 | 'G' << 8 | 'D', - tRNS = 't' << 24 | 'R' << 16 | 'N' << 8 | 'S', - IDAT = 'I' << 24 | 'D' << 16 | 'A' << 8 | 'T', - - iTXt = 'i' << 24 | 'T' << 16 | 'X' << 8 | 't', - tEXt = 't' << 24 | 'E' << 16 | 'X' << 8 | 't', - zTXt = 'z' << 24 | 'T' << 16 | 'X' << 8 | 't', - - iCCP = 'i' << 24 | 'C' << 16 | 'C' << 8 | 'P', - pHYs = 'p' << 24 | 'H' << 16 | 'Y' << 8 | 's', - gAMA = 'g' << 24 | 'A' << 16 | 'M' << 8 | 'A', - tIME = 't' << 24 | 'I' << 16 | 'M' << 8 | 'E', - - sPLT = 's' << 24 | 'P' << 16 | 'L' << 8 | 'T', - sRGB = 's' << 24 | 'R' << 16 | 'G' << 8 | 'B', - hIST = 'h' << 24 | 'I' << 16 | 'S' << 8 | 'T', - cHRM = 'c' << 24 | 'H' << 16 | 'R' << 8 | 'M', - sBIT = 's' << 24 | 'B' << 16 | 'I' << 8 | 'T', - - /* - eXIf tags are not part of the core spec, but have been ratified - in v1.5.0 of the PNG Ext register. - - We will provide unprocessed chunks to the caller if `.return_metadata` is set. - Applications are free to implement an Exif decoder. - */ - eXIf = 'e' << 24 | 'X' << 16 | 'I' << 8 | 'f', - - // PNG files must end with IEND - IEND = 'I' << 24 | 'E' << 16 | 'N' << 8 | 'D', - - /* - XCode sometimes produces "PNG" files that don't adhere to the PNG spec. - We recognize them only in order to avoid doing further work on them. - - Some tools like PNG Defry may be able to repair them, but we're not - going to reward Apple for producing proprietary broken files purporting - to be PNGs by supporting them. - - */ - iDOT = 'i' << 24 | 'D' << 16 | 'O' << 8 | 'T', - CbGI = 'C' << 24 | 'b' << 16 | 'H' << 8 | 'I', -} - -IHDR :: struct #packed { - width: u32be, - height: u32be, - bit_depth: u8, - color_type: Color_Type, - compression_method: u8, - filter_method: u8, - interlace_method: Interlace_Method, -} -IHDR_SIZE :: size_of(IHDR) -#assert (IHDR_SIZE == 13) - -Color_Value :: enum u8 { - Paletted = 0, // 1 << 0 = 1 - Color = 1, // 1 << 1 = 2 - Alpha = 2, // 1 << 2 = 4 -} -Color_Type :: distinct bit_set[Color_Value; u8] - -Interlace_Method :: enum u8 { - None = 0, - Adam7 = 1, -} - Row_Filter :: enum u8 { None = 0, Sub = 1, @@ -262,8 +173,8 @@ ADAM7_Y_SPACING := []int{ 8,8,8,4,4,2,2 } // Implementation starts here -read_chunk :: proc(ctx: ^$C) -> (chunk: Chunk, err: Error) { - ch, e := compress.read_data(ctx, Chunk_Header) +read_chunk :: proc(ctx: ^$C) -> (chunk: image.PNG_Chunk, err: Error) { + ch, e := compress.read_data(ctx, image.PNG_Chunk_Header) if e != .None { return {}, compress.General_Error.Stream_Too_Short } @@ -305,7 +216,7 @@ read_chunk :: proc(ctx: ^$C) -> (chunk: Chunk, err: Error) { return chunk, nil } -copy_chunk :: proc(src: Chunk, allocator := context.allocator) -> (dest: Chunk, err: Error) { +copy_chunk :: proc(src: image.PNG_Chunk, allocator := context.allocator) -> (dest: image.PNG_Chunk, err: Error) { if int(src.header.length) != len(src.data) { return {}, .Invalid_Chunk_Length } @@ -318,7 +229,7 @@ copy_chunk :: proc(src: Chunk, allocator := context.allocator) -> (dest: Chunk, return } -append_chunk :: proc(list: ^[dynamic]Chunk, src: Chunk, allocator := context.allocator) -> (err: Error) { +append_chunk :: proc(list: ^[dynamic]image.PNG_Chunk, src: image.PNG_Chunk, allocator := context.allocator) -> (err: Error) { if int(src.header.length) != len(src.data) { return .Invalid_Chunk_Length } @@ -334,13 +245,13 @@ append_chunk :: proc(list: ^[dynamic]Chunk, src: Chunk, allocator := context.all return } -read_header :: proc(ctx: ^$C) -> (IHDR, Error) { +read_header :: proc(ctx: ^$C) -> (image.PNG_IHDR, Error) { c, e := read_chunk(ctx) if e != nil { return {}, e } - header := (^IHDR)(raw_data(c.data))^ + header := (^image.PNG_IHDR)(raw_data(c.data))^ // Validate IHDR using header if width == 0 || height == 0 || u128(width) * u128(height) > MAX_DIMENSIONS { @@ -407,7 +318,7 @@ read_header :: proc(ctx: ^$C) -> (IHDR, Error) { return header, nil } -chunk_type_to_name :: proc(type: ^Chunk_Type) -> string { +chunk_type_to_name :: proc(type: ^image.PNG_Chunk_Type) -> string { t := transmute(^u8)type return strings.string_from_ptr(t, 4) } @@ -462,9 +373,8 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a img = new(Image) } - info := new(Info) - img.metadata_ptr = info - img.metadata_type = typeid_of(Info) + info := new(image.PNG_Info) + img.metadata = info signature, io_error := compress.read_data(ctx, Signature) if io_error != .None || signature != .PNG { @@ -477,11 +387,11 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a idat_length := u64(0) - c: Chunk - ch: Chunk_Header + c: image.PNG_Chunk + ch: image.PNG_Chunk_Header e: io.Error - header: IHDR + header: image.PNG_IHDR // State to ensure correct chunk ordering. seen_ihdr := false; first := true @@ -492,7 +402,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a seen_iend := false _plte := PLTE{} - trns := Chunk{} + trns := image.PNG_Chunk{} final_image_channels := 0 @@ -502,7 +412,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a // Peek at next chunk's length and type. // TODO: Some streams may not provide seek/read_at - ch, e = compress.peek_data(ctx, Chunk_Header) + ch, e = compress.peek_data(ctx, image.PNG_Chunk_Header) if e != .None { return img, compress.General_Error.Stream_Too_Short } @@ -547,7 +457,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a img.height = int(header.height) using header - h := IHDR{ + h := image.PNG_IHDR{ width = width, height = height, bit_depth = bit_depth, @@ -607,7 +517,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a return {}, image.PNG_Error.IDAT_Size_Too_Large } - ch, e = compress.peek_data(ctx, Chunk_Header) + ch, e = compress.peek_data(ctx, image.PNG_Chunk_Header) if e != .None { return img, compress.General_Error.Stream_Too_Short } @@ -1599,7 +1509,7 @@ defilter_16 :: proc(params: ^Filter_Params) -> (ok: bool) { return } -defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, options: Options) -> (err: Error) { +defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^image.PNG_IHDR, options: Options) -> (err: Error) { input := bytes.buffer_to_bytes(filter_bytes) width := int(header.width) height := int(header.height) diff --git a/tests/core/image/test_core_image.odin b/tests/core/image/test_core_image.odin index 7752cf7dc..155b69298 100644 --- a/tests/core/image/test_core_image.odin +++ b/tests/core/image/test_core_image.odin @@ -1504,10 +1504,8 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) { passed &= test.hash == hash if .return_metadata in test.options { - v: ^png.Info - if img.metadata_ptr != nil && img.metadata_type == png.Info { - v = (^png.Info)(img.metadata_ptr) + if v, ok := img.metadata.(^image.PNG_Info); ok { for c in v.chunks { #partial switch(c.header.type) { case .gAMA: From b4b53aeb717a6012c4a1c3c77753842e8c41aea4 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Wed, 6 Oct 2021 22:47:40 +0200 Subject: [PATCH 9/9] png: Channel helper metadata. --- core/image/common.odin | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/image/common.odin b/core/image/common.odin index 919670a61..3ec8e15be 100644 --- a/core/image/common.odin +++ b/core/image/common.odin @@ -319,8 +319,7 @@ return_single_channel :: proc(img: ^Image, channel: Channel) -> (res: ^Image, ok res.depth = img.depth res.pixels = t res.background = img.background - // res.metadata_ptr = img.metadata_ptr - // res.metadata_type = img.metadata_type + res.metadata = img.metadata return res, true }