Procházet zdrojové kódy

Merge pull request #1210 from Kelimion/png_improvements

Lots of PNG improvements
Jeroen van Rijn před 4 roky
rodič
revize
d3a18fbe9a

+ 4 - 9
core/compress/common.odin

@@ -1,5 +1,3 @@
-package compress
-
 /*
 	Copyright 2021 Jeroen van Rijn <[email protected]>.
 	Made available under Odin's BSD-3 license.
@@ -7,10 +5,11 @@ package compress
 	List of contributors:
 		Jeroen van Rijn: Initial implementation, optimization.
 */
+package compress
 
 import "core:io"
-import "core:image"
 import "core:bytes"
+import "core:runtime"
 
 /*
 	These settings bound how much compression algorithms will allocate for their output buffer.
@@ -51,11 +50,8 @@ Error :: union {
 	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 {
@@ -69,7 +65,6 @@ General_Error :: enum {
 	Incompatible_Options,
 	Unimplemented,
 
-
 	/*
 		Memory errors
 	*/

+ 123 - 12
core/image/common.odin

@@ -1,16 +1,17 @@
-package image
-
 /*
 	Copyright 2021 Jeroen van Rijn <[email protected]>.
-	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"
+import "core:compress"
+import "core:runtime"
 
 Image :: struct {
 	width:         int,
@@ -25,8 +26,11 @@ Image :: struct {
 	*/
 	background:    Maybe([3]u16),
 
-	metadata_ptr:  rawptr,
-	metadata_type: typeid,
+	metadata:      Image_Metadata,
+}
+
+Image_Metadata :: union {
+	^PNG_Info,
 }
 
 /*
@@ -112,31 +116,140 @@ 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_Dimensions_Too_Large,
+	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,
+	IDAT_Size_Too_Large,
 	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,
 	Unknown_Interlace_Method,
 	Requested_Channel_Not_Present,
 	Post_Processing_Error,
+	Invalid_Chunk_Length,
 }
 
 /*
-	Functions to help with image buffer calculations
+	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
@@ -145,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,
@@ -207,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
 }

+ 29 - 32
core/image/png/example.odin

@@ -1,9 +1,6 @@
-//+ignore
-package png
-
 /*
 	Copyright 2021 Jeroen van Rijn <[email protected]>.
-	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,8 +8,9 @@ package png
 
 	An example of how to use `load`.
 */
+//+ignore
+package png
 
-import "core:compress"
 import "core:image"
 // import "core:image/png"
 import "core:bytes"
@@ -41,8 +39,8 @@ main :: proc() {
 demo :: proc() {
 	file: string
 
-	options := image.Options{} // {.return_metadata};
-	err:       compress.Error
+	options := image.Options{.return_metadata}
+	err:       image.Error
 	img:      ^image.Image
 
 	file = "../../../misc/logo-slim.png"
@@ -53,32 +51,33 @@ 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)
 
+		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:
-					t, _ := core_time(c)
-					fmt.printf("[tIME]: %v\n", t)
+					if t, t_ok := core_time(c); t_ok {
+						fmt.printf("[tIME]: %v\n", t)
+					}
 				case .gAMA:
-					fmt.printf("[gAMA]: %v\n", gamma(c))
+					if gama, gama_ok := gamma(c); gama_ok {
+						fmt.printf("[gAMA]: %v\n", gama)
+					}
 				case .pHYs:
-					phys := phys(c)
-					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 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)
@@ -93,8 +92,7 @@ demo :: proc() {
 				case .bKGD:
 					fmt.printf("[bKGD] %v\n", img.background)
 				case .eXIf:
-					res, ok_exif := exif(c)
-					if ok_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.
@@ -102,20 +100,17 @@ demo :: proc() {
 						fmt.printf("[eXIf] %v\n", res)
 					}
 				case .PLTE:
-					plte, plte_ok := plte(c)
-					if plte_ok {
+					if plte, plte_ok := plte(c); plte_ok {
 						fmt.printf("[PLTE] %v\n", plte)
 					} else {
 						fmt.printf("[PLTE] Error\n")
 					}
 				case .hIST:
-					res, ok_hist := hist(c)
-					if ok_hist {
+					if res, ok_hist := hist(c); ok_hist {
 						fmt.printf("[hIST] %v\n", res)
 					}
 				case .cHRM:
-					res, ok_chrm := chrm(c)
-					if ok_chrm {
+					if res, ok_chrm := chrm(c); ok_chrm {
 						fmt.printf("[cHRM] %v\n", res)
 					}
 				case .sPLT:
@@ -147,6 +142,8 @@ demo :: proc() {
 		}
 	}
 
+	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.")

+ 47 - 47
core/image/png/helpers.odin

@@ -1,5 +1,3 @@
-package png
-
 /*
 	Copyright 2021 Jeroen van Rijn <[email protected]>.
 	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"
@@ -34,15 +33,14 @@ destroy :: proc(img: ^Image) {
 	}
 
 	bytes.buffer_destroy(&img.pixels)
-	// 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.
-	*/
+	if v, ok := img.metadata.(^image.PNG_Info); ok {
+		for chunk in &v.chunks {
+			delete(chunk.data)
+		}
+		delete(v.chunks)
+		free(v)
+	}
 	free(img)
 }
 
@@ -50,46 +48,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: image.PNG_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: image.PNG_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: image.PNG_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),
-	)
+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(
+			int(year), int(month), int(day),
+			int(hour), int(minute), int(second),
+		)
+	} else {
+		return {}, false
+	}
 }
 
-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:
 		ok = true
@@ -191,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)
@@ -227,10 +229,8 @@ iccp_destroy :: proc(i: iCCP) {
 
 }
 
-srgb :: proc(c: Chunk) -> (res: sRGB, ok: bool) {
-	ok = true
-
-	if c.header.type != .sRGB || len(c.data) != 1 {
+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
 	}
 
@@ -238,10 +238,10 @@ 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) {
+plte :: proc(c: image.PNG_Chunk) -> (res: PLTE, ok: bool) {
 	if c.header.type != .PLTE {
 		return {}, false
 	}
@@ -255,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
 	}
@@ -306,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.
@@ -324,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
 	}
@@ -346,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
@@ -364,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
 

+ 160 - 171
core/image/png/png.odin

@@ -1,13 +1,12 @@
-package png
-
 /*
 	Copyright 2021 Jeroen van Rijn <[email protected]>.
-	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,11 +20,29 @@ 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
+/*
+	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
 
@@ -34,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,
@@ -135,22 +63,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 +151,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,
 }
 
@@ -245,16 +173,30 @@ 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 {}, E_General.Stream_Too_Short
+		return {}, compress.General_Error.Stream_Too_Short
 	}
 	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 {}, E_General.Stream_Too_Short
+		return {}, compress.General_Error.Stream_Too_Short
 	}
 
 	// Compute CRC over chunk type + data
@@ -264,39 +206,68 @@ 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
 }
 
-read_header :: proc(ctx: ^$C) -> (IHDR, 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
+	}
+
+	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]image.PNG_Chunk, src: image.PNG_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 mem.Allocator_Error.Out_Of_Memory
+	}
+
+	return
+}
+
+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 {
-		return {}, E_PNG.Invalid_Image_Dimensions
+	if width == 0 || height == 0 || u128(width) * u128(height) > MAX_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
 
 	}
 
@@ -314,7 +285,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:
 		/*
@@ -322,7 +293,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:
 		/*
@@ -337,17 +308,17 @@ 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
 }
 
-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)
 }
@@ -377,7 +348,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
 	}
 }
 
@@ -391,7 +362,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 {
@@ -402,27 +373,25 @@ 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 {
-		return img, E_PNG.Invalid_PNG_Signature
+		return img, .Invalid_PNG_Signature
 	}
 
 	idat: []u8
 	idat_b: bytes.Buffer
-	idat_length := u32be(0)
 	defer bytes.buffer_destroy(&idat_b)
 
-	c:		Chunk
-	ch:     Chunk_Header
-	e:      io.Error
+	idat_length := u64(0)
 
-	header:	IHDR
+	c:		image.PNG_Chunk
+	ch:     image.PNG_Chunk_Header
+	e:      io.Error
 
-	info.chunks.allocator = context.temp_allocator
+	header:	image.PNG_IHDR
 
 	// State to ensure correct chunk ordering.
 	seen_ihdr := false; first := true
@@ -433,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
 
@@ -443,16 +412,16 @@ 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, 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
 
@@ -481,14 +450,14 @@ 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)
 			img.height = int(header.height)
 
 			using header
-			h := IHDR{
+			h := image.PNG_IHDR{
 				width              = width,
 				height             = height,
 				bit_depth          = bit_depth,
@@ -498,28 +467,30 @@ 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.
 			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 {
-				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.
 			if .return_metadata not_in options && .do_not_decompress_image in options {
@@ -528,11 +499,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
@@ -540,22 +511,29 @@ 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)
 
-				ch, e = compress.peek_data(ctx, Chunk_Header)
+				if idat_length > MAX_IDAT_SIZE {
+					return {}, image.PNG_Error.IDAT_Size_Too_Large
+				}
+
+				ch, e = compress.peek_data(ctx, image.PNG_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:
 			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
@@ -563,14 +541,14 @@ 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
 			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{
@@ -580,26 +558,27 @@ 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])}
 			}
+
 		case .tRNS:
 			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 {
-				append(&info.chunks, c)
+				append_chunk(&info.chunks, c) or_return
 			}
 
 			/*
@@ -622,20 +601,20 @@ 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.
 				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
+			return img, .Image_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
@@ -648,7 +627,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
 	}
 
 	/*
@@ -685,7 +664,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
 	}
 
 	/*
@@ -752,7 +731,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
 
@@ -831,7 +812,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[:])
@@ -1028,7 +1011,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[:])
@@ -1524,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: compress.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)
@@ -1535,7 +1520,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
 
@@ -1560,7 +1547,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 {
 		/*
@@ -1575,7 +1562,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,
@@ -1598,7 +1587,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[:]

+ 4 - 0
tests/core/image/build.bat

@@ -0,0 +1,4 @@
+@echo off
+pushd ..
+odin run image
+popd

+ 24 - 27
tests/core/image/test_core_image.odin

@@ -1,5 +1,3 @@
-package test_core_image
-
 /*
 	Copyright 2021 Jeroen van Rijn <[email protected]>.
 	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"
 
@@ -64,7 +63,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 +1197,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 +1239,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},
 		},
 	},
 	{
@@ -1505,19 +1504,17 @@ 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:
 								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 +1554,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 +1586,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 +1607,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)