Browse Source

Merge pull request #1210 from Kelimion/png_improvements

Lots of PNG improvements
Jeroen van Rijn 4 years ago
parent
commit
d3a18fbe9a

+ 4 - 9
core/compress/common.odin

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

+ 123 - 12
core/image/common.odin

@@ -1,16 +1,17 @@
-package image
-
 /*
 /*
 	Copyright 2021 Jeroen van Rijn <[email protected]>.
 	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:
 	List of contributors:
 		Jeroen van Rijn: Initial implementation, optimization.
 		Jeroen van Rijn: Initial implementation, optimization.
 		Ginger Bill:     Cosmetic changes.
 		Ginger Bill:     Cosmetic changes.
 */
 */
+package image
 
 
 import "core:bytes"
 import "core:bytes"
 import "core:mem"
 import "core:mem"
+import "core:compress"
+import "core:runtime"
 
 
 Image :: struct {
 Image :: struct {
 	width:         int,
 	width:         int,
@@ -25,8 +26,11 @@ Image :: struct {
 	*/
 	*/
 	background:    Maybe([3]u16),
 	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]
 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,
 	Invalid_PNG_Signature,
 	IHDR_Not_First_Chunk,
 	IHDR_Not_First_Chunk,
 	IHDR_Corrupt,
 	IHDR_Corrupt,
 	IDAT_Missing,
 	IDAT_Missing,
 	IDAT_Must_Be_Contiguous,
 	IDAT_Must_Be_Contiguous,
 	IDAT_Corrupt,
 	IDAT_Corrupt,
-	PNG_Does_Not_Adhere_to_Spec,
+	IDAT_Size_Too_Large,
 	PLTE_Encountered_Unexpectedly,
 	PLTE_Encountered_Unexpectedly,
 	PLTE_Invalid_Length,
 	PLTE_Invalid_Length,
 	TRNS_Encountered_Unexpectedly,
 	TRNS_Encountered_Unexpectedly,
 	BKGD_Invalid_Length,
 	BKGD_Invalid_Length,
-	Invalid_Image_Dimensions,
 	Unknown_Color_Type,
 	Unknown_Color_Type,
 	Invalid_Color_Bit_Depth_Combo,
 	Invalid_Color_Bit_Depth_Combo,
 	Unknown_Filter_Method,
 	Unknown_Filter_Method,
 	Unknown_Interlace_Method,
 	Unknown_Interlace_Method,
 	Requested_Channel_Not_Present,
 	Requested_Channel_Not_Present,
 	Post_Processing_Error,
 	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) {
 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
 	size = ((((channels * width * depth) + 7) >> 3) + extra_row_bytes) * height
 	return
 	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.
 	For when you have an RGB(A) image, but want a particular channel.
 */
 */
-
 Channel :: enum u8 {
 Channel :: enum u8 {
 	R = 1,
 	R = 1,
 	G = 2,
 	G = 2,
@@ -207,8 +319,7 @@ return_single_channel :: proc(img: ^Image, channel: Channel) -> (res: ^Image, ok
 	res.depth         = img.depth
 	res.depth         = img.depth
 	res.pixels        = t
 	res.pixels        = t
 	res.background    = img.background
 	res.background    = img.background
-	res.metadata_ptr  = img.metadata_ptr
-	res.metadata_type = img.metadata_type
+	res.metadata      = img.metadata
 
 
 	return res, true
 	return res, true
 }
 }

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

@@ -1,9 +1,6 @@
-//+ignore
-package png
-
 /*
 /*
 	Copyright 2021 Jeroen van Rijn <[email protected]>.
 	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:
 	List of contributors:
 		Jeroen van Rijn: Initial implementation.
 		Jeroen van Rijn: Initial implementation.
@@ -11,8 +8,9 @@ package png
 
 
 	An example of how to use `load`.
 	An example of how to use `load`.
 */
 */
+//+ignore
+package png
 
 
-import "core:compress"
 import "core:image"
 import "core:image"
 // import "core:image/png"
 // import "core:image/png"
 import "core:bytes"
 import "core:bytes"
@@ -41,8 +39,8 @@ main :: proc() {
 demo :: proc() {
 demo :: proc() {
 	file: string
 	file: string
 
 
-	options := image.Options{} // {.return_metadata};
-	err:       compress.Error
+	options := image.Options{.return_metadata}
+	err:       image.Error
 	img:      ^image.Image
 	img:      ^image.Image
 
 
 	file = "../../../misc/logo-slim.png"
 	file = "../../../misc/logo-slim.png"
@@ -53,32 +51,33 @@ demo :: proc() {
 	if err != nil {
 	if err != nil {
 		fmt.printf("Trying to read PNG file %v returned %v\n", file, err)
 		fmt.printf("Trying to read PNG file %v returned %v\n", file, err)
 	} else {
 	} else {
-		v: ^Info
-
 		fmt.printf("Image: %vx%vx%v, %v-bit.\n", img.width, img.height, img.channels, img.depth)
 		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.
 			// Handle ancillary chunks as you wish.
 			// We provide helper functions for a few types.
 			// We provide helper functions for a few types.
 			for c in v.chunks {
 			for c in v.chunks {
 				#partial switch c.header.type {
 				#partial switch c.header.type {
 				case .tIME:
 				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:
 				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:
 				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:
 				case .iTXt, .zTXt, .tEXt:
 					res, ok_text := text(c)
 					res, ok_text := text(c)
@@ -93,8 +92,7 @@ demo :: proc() {
 				case .bKGD:
 				case .bKGD:
 					fmt.printf("[bKGD] %v\n", img.background)
 					fmt.printf("[bKGD] %v\n", img.background)
 				case .eXIf:
 				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.
 							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.
 							If you wish to interpret it, pass it to an Exif parser.
@@ -102,20 +100,17 @@ demo :: proc() {
 						fmt.printf("[eXIf] %v\n", res)
 						fmt.printf("[eXIf] %v\n", res)
 					}
 					}
 				case .PLTE:
 				case .PLTE:
-					plte, plte_ok := plte(c)
-					if plte_ok {
+					if plte, plte_ok := plte(c); plte_ok {
 						fmt.printf("[PLTE] %v\n", plte)
 						fmt.printf("[PLTE] %v\n", plte)
 					} else {
 					} else {
 						fmt.printf("[PLTE] Error\n")
 						fmt.printf("[PLTE] Error\n")
 					}
 					}
 				case .hIST:
 				case .hIST:
-					res, ok_hist := hist(c)
-					if ok_hist {
+					if res, ok_hist := hist(c); ok_hist {
 						fmt.printf("[hIST] %v\n", res)
 						fmt.printf("[hIST] %v\n", res)
 					}
 					}
 				case .cHRM:
 				case .cHRM:
-					res, ok_chrm := chrm(c)
-					if ok_chrm {
+					if res, ok_chrm := chrm(c); ok_chrm {
 						fmt.printf("[cHRM] %v\n", res)
 						fmt.printf("[cHRM] %v\n", res)
 					}
 					}
 				case .sPLT:
 				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 err == nil && .do_not_decompress_image not_in options && .info not_in options {
 		if ok := write_image_as_ppm("out.ppm", img); ok {
 		if ok := write_image_as_ppm("out.ppm", img); ok {
 			fmt.println("Saved decoded image.")
 			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]>.
 	Copyright 2021 Jeroen van Rijn <[email protected]>.
 	Made available under Odin's BSD-2 license.
 	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.
 	These are a few useful utility functions to work with PNG images.
 */
 */
+package png
 
 
 import "core:image"
 import "core:image"
 import "core:compress/zlib"
 import "core:compress/zlib"
@@ -34,15 +33,14 @@ destroy :: proc(img: ^Image) {
 	}
 	}
 
 
 	bytes.buffer_destroy(&img.pixels)
 	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)
 	free(img)
 }
 }
 
 
@@ -50,46 +48,50 @@ destroy :: proc(img: ^Image) {
 	Chunk helpers
 	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
 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) {
 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
 	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 {
 	#partial switch c.header.type {
 	case .tEXt:
 	case .tEXt:
 		ok = true
 		ok = true
@@ -191,7 +193,7 @@ text_destroy :: proc(text: Text) {
 	delete(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
 	ok = true
 
 
 	fields := bytes.split_n(s=c.data, sep=[]u8{0}, n=3, allocator=context.temp_allocator)
 	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
 		return {}, false
 	}
 	}
 
 
@@ -238,10 +238,10 @@ srgb :: proc(c: Chunk) -> (res: sRGB, ok: bool) {
 	if res.intent > max(sRGB_Rendering_Intent) {
 	if res.intent > max(sRGB_Rendering_Intent) {
 		ok = false; return
 		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 {
 	if c.header.type != .PLTE {
 		return {}, false
 		return {}, false
 	}
 	}
@@ -255,7 +255,7 @@ plte :: proc(c: Chunk) -> (res: PLTE, ok: bool) {
 	return
 	return
 }
 }
 
 
-splt :: proc(c: Chunk) -> (res: sPLT, ok: bool) {
+splt :: proc(c: image.PNG_Chunk) -> (res: sPLT, ok: bool) {
 	if c.header.type != .sPLT {
 	if c.header.type != .sPLT {
 		return {}, false
 		return {}, false
 	}
 	}
@@ -306,7 +306,7 @@ splt_destroy :: proc(s: sPLT) {
 	delete(s.name)
 	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.
 		Returns [4]u8 with the significant bits in each channel.
 		A channel will contain zero if not applicable to the PNG color type.
 		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 {
 	if c.header.type != .hIST {
 		return {}, false
 		return {}, false
 	}
 	}
@@ -346,7 +346,7 @@ hist :: proc(c: Chunk) -> (res: hIST, ok: bool) {
 	return
 	return
 }
 }
 
 
-chrm :: proc(c: Chunk) -> (res: cHRM, ok: bool) {
+chrm :: proc(c: image.PNG_Chunk) -> (res: cHRM, ok: bool) {
 	ok = true
 	ok = true
 	if c.header.length != size_of(cHRM_Raw) {
 	if c.header.length != size_of(cHRM_Raw) {
 		return {}, false
 		return {}, false
@@ -364,7 +364,7 @@ chrm :: proc(c: Chunk) -> (res: cHRM, ok: bool) {
 	return
 	return
 }
 }
 
 
-exif :: proc(c: Chunk) -> (res: Exif, ok: bool) {
+exif :: proc(c: image.PNG_Chunk) -> (res: Exif, ok: bool) {
 
 
 	ok = true
 	ok = true
 
 

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

@@ -1,13 +1,12 @@
-package png
-
 /*
 /*
 	Copyright 2021 Jeroen van Rijn <[email protected]>.
 	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:
 	List of contributors:
 		Jeroen van Rijn: Initial implementation.
 		Jeroen van Rijn: Initial implementation.
 		Ginger Bill:     Cosmetic changes.
 		Ginger Bill:     Cosmetic changes.
 */
 */
+package png
 
 
 import "core:compress"
 import "core:compress"
 import "core:compress/zlib"
 import "core:compress/zlib"
@@ -21,11 +20,29 @@ import "core:io"
 import "core:mem"
 import "core:mem"
 import "core:intrinsics"
 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
 Image     :: image.Image
 Options   :: image.Options
 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',
 	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 {
 Row_Filter :: enum u8 {
 	None    = 0,
 	None    = 0,
 	Sub     = 1,
 	Sub     = 1,
@@ -135,22 +63,22 @@ PLTE_Entry    :: [3]u8
 
 
 PLTE :: struct #packed {
 PLTE :: struct #packed {
 	entries: [256]PLTE_Entry,
 	entries: [256]PLTE_Entry,
-	used: u16,
+	used:    u16,
 }
 }
 
 
 hIST :: struct #packed {
 hIST :: struct #packed {
 	entries: [256]u16,
 	entries: [256]u16,
-	used: u16,
+	used:    u16,
 }
 }
 
 
 sPLT :: struct #packed {
 sPLT :: struct #packed {
-	name: string,
-	depth: u8,
+	name:    string,
+	depth:   u8,
 	entries: union {
 	entries: union {
 		[][4]u8,
 		[][4]u8,
 		[][4]u16,
 		[][4]u16,
 	},
 	},
-	used: u16,
+	used:    u16,
 }
 }
 
 
 // Other chunks
 // Other chunks
@@ -223,14 +151,14 @@ Exif :: struct {
 }
 }
 
 
 iCCP :: struct {
 iCCP :: struct {
-	name: string,
+	name:    string,
 	profile: []u8,
 	profile: []u8,
 }
 }
 
 
 sRGB_Rendering_Intent :: enum u8 {
 sRGB_Rendering_Intent :: enum u8 {
-	Perceptual = 0,
+	Perceptual            = 0,
 	Relative_colorimetric = 1,
 	Relative_colorimetric = 1,
-	Saturation = 2,
+	Saturation            = 2,
 	Absolute_colorimetric = 3,
 	Absolute_colorimetric = 3,
 }
 }
 
 
@@ -245,16 +173,30 @@ ADAM7_Y_SPACING := []int{ 8,8,8,4,4,2,2 }
 
 
 // Implementation starts here
 // 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 {
 	if e != .None {
-		return {}, E_General.Stream_Too_Short
+		return {}, compress.General_Error.Stream_Too_Short
 	}
 	}
 	chunk.header = ch
 	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))
 	chunk.data, e = compress.read_slice(ctx, int(ch.length))
 	if e != .None {
 	if e != .None {
-		return {}, E_General.Stream_Too_Short
+		return {}, compress.General_Error.Stream_Too_Short
 	}
 	}
 
 
 	// Compute CRC over chunk type + data
 	// 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)
 	crc, e3 := compress.read_data(ctx, u32be)
 	if e3 != .None {
 	if e3 != .None {
-		return {}, E_General.Stream_Too_Short
+		return {}, compress.General_Error.Stream_Too_Short
 	}
 	}
 	chunk.crc = crc
 	chunk.crc = crc
 
 
 	if chunk.crc != u32be(computed_crc) {
 	if chunk.crc != u32be(computed_crc) {
-		return {}, E_General.Checksum_Failed
+		return {}, compress.General_Error.Checksum_Failed
 	}
 	}
 	return chunk, nil
 	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)
 	c, e := read_chunk(ctx)
 	if e != nil {
 	if e != nil {
 		return {}, e
 		return {}, e
 	}
 	}
 
 
-	header := (^IHDR)(raw_data(c.data))^
+	header := (^image.PNG_IHDR)(raw_data(c.data))^
 	// Validate IHDR
 	// Validate IHDR
 	using header
 	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 {
 	if compression_method != 0 {
-		return {}, E_General.Unknown_Compression_Method
+		return {}, compress.General_Error.Unknown_Compression_Method
 	}
 	}
 
 
 	if filter_method != 0 {
 	if filter_method != 0 {
-		return {}, E_PNG.Unknown_Filter_Method
+		return {}, .Unknown_Filter_Method
 	}
 	}
 
 
 	if interlace_method != .None && interlace_method != .Adam7 {
 	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 {
 		if !allowed {
-			return {}, E_PNG.Invalid_Color_Bit_Depth_Combo
+			return {}, .Invalid_Color_Bit_Depth_Combo
 		}
 		}
 	case 2, 4, 6:
 	case 2, 4, 6:
 		/*
 		/*
@@ -322,7 +293,7 @@ read_header :: proc(ctx: ^$C) -> (IHDR, Error) {
 			Allowed bit depths: 8 and 16
 			Allowed bit depths: 8 and 16
 		*/
 		*/
 		if bit_depth != 8 && bit_depth != 16 {
 		if bit_depth != 8 && bit_depth != 16 {
-			return {}, E_PNG.Invalid_Color_Bit_Depth_Combo
+			return {}, .Invalid_Color_Bit_Depth_Combo
 		}
 		}
 	case 3:
 	case 3:
 		/*
 		/*
@@ -337,17 +308,17 @@ read_header :: proc(ctx: ^$C) -> (IHDR, Error) {
 			}
 			}
 		}
 		}
 		if !allowed {
 		if !allowed {
-			return {}, E_PNG.Invalid_Color_Bit_Depth_Combo
+			return {}, .Invalid_Color_Bit_Depth_Combo
 		}
 		}
 
 
 	case:
 	case:
-		return {}, E_PNG.Unknown_Color_Type
+		return {}, .Unknown_Color_Type
 	}
 	}
 
 
 	return header, nil
 	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
 	t := transmute(^u8)type
 	return strings.string_from_ptr(t, 4)
 	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)
 		return load_from_slice(data, options)
 	} else {
 	} else {
 		img = new(Image)
 		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 {
 	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 {
 	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)
 		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)
 	signature, io_error := compress.read_data(ctx, Signature)
 	if io_error != .None || signature != .PNG {
 	if io_error != .None || signature != .PNG {
-		return img, E_PNG.Invalid_PNG_Signature
+		return img, .Invalid_PNG_Signature
 	}
 	}
 
 
 	idat: []u8
 	idat: []u8
 	idat_b: bytes.Buffer
 	idat_b: bytes.Buffer
-	idat_length := u32be(0)
 	defer bytes.buffer_destroy(&idat_b)
 	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.
 	// State to ensure correct chunk ordering.
 	seen_ihdr := false; first := true
 	seen_ihdr := false; first := true
@@ -433,7 +402,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 	seen_iend := false
 	seen_iend := false
 
 
 	_plte := PLTE{}
 	_plte := PLTE{}
-	trns := Chunk{}
+	trns := image.PNG_Chunk{}
 
 
 	final_image_channels := 0
 	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.
 		// Peek at next chunk's length and type.
 		// TODO: Some streams may not provide seek/read_at
 		// 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 {
 		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.
 		// name := chunk_type_to_name(&ch.type); // Only used for debug prints during development.
 
 
 		#partial switch ch.type {
 		#partial switch ch.type {
 		case .IHDR:
 		case .IHDR:
 			if seen_ihdr || !first {
 			if seen_ihdr || !first {
-				return {}, E_PNG.IHDR_Not_First_Chunk
+				return {}, .IHDR_Not_First_Chunk
 			}
 			}
 			seen_ihdr = true
 			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 {
 			if img.channels == 0 || img.depth == 0 {
-				return {}, E_PNG.IHDR_Corrupt
+				return {}, .IHDR_Corrupt
 			}
 			}
 
 
 			img.width  = int(header.width)
 			img.width  = int(header.width)
 			img.height = int(header.height)
 			img.height = int(header.height)
 
 
 			using header
 			using header
-			h := IHDR{
+			h := image.PNG_IHDR{
 				width              = width,
 				width              = width,
 				height             = height,
 				height             = height,
 				bit_depth          = bit_depth,
 				bit_depth          = bit_depth,
@@ -498,28 +467,30 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 				interlace_method   = interlace_method,
 				interlace_method   = interlace_method,
 			}
 			}
 			info.header = h
 			info.header = h
+
 		case .PLTE:
 		case .PLTE:
 			seen_plte = true
 			seen_plte = true
 			// PLTE must appear before IDAT and can't appear for color types 0, 4.
 			// PLTE must appear before IDAT and can't appear for color types 0, 4.
 			ct := transmute(u8)info.header.color_type
 			ct := transmute(u8)info.header.color_type
 			if seen_idat || ct == 0 || ct == 4 {
 			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
 			c = read_chunk(ctx) or_return
 
 
 			if c.header.length % 3 != 0 || c.header.length > 768 {
 			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_ok: bool
 			_plte, plte_ok = plte(c)
 			_plte, plte_ok = plte(c)
 			if !plte_ok {
 			if !plte_ok {
-				return img, E_PNG.PLTE_Invalid_Length
+				return img, .PLTE_Invalid_Length
 			}
 			}
 
 
 			if .return_metadata in options {
 			if .return_metadata in options {
-				append(&info.chunks, c)
+				append_chunk(&info.chunks, c) or_return
 			}
 			}
+
 		case .IDAT:
 		case .IDAT:
 			// If we only want image metadata and don't want the pixel data, we can early out.
 			// 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 {
 			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.
 			// There must be at least 1 IDAT, contiguous if more.
 			if seen_idat {
 			if seen_idat {
-				return img, E_PNG.IDAT_Must_Be_Contiguous
+				return img, .IDAT_Must_Be_Contiguous
 			}
 			}
 
 
 			if idat_length > 0 {
 			if idat_length > 0 {
-				return img, E_PNG.IDAT_Must_Be_Contiguous
+				return img, .IDAT_Must_Be_Contiguous
 			}
 			}
 
 
 			next := ch.type
 			next := ch.type
@@ -540,22 +511,29 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 				c = read_chunk(ctx) or_return
 				c = read_chunk(ctx) or_return
 
 
 				bytes.buffer_write(&idat_b, c.data)
 				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 {
 				if e != .None {
-					return img, E_General.Stream_Too_Short
+					return img, compress.General_Error.Stream_Too_Short
 				}
 				}
 				next = ch.type
 				next = ch.type
 			}
 			}
+
 			idat = bytes.buffer_to_bytes(&idat_b)
 			idat = bytes.buffer_to_bytes(&idat_b)
 			if int(idat_length) != len(idat) {
 			if int(idat_length) != len(idat) {
-				return {}, E_PNG.IDAT_Corrupt
+				return {}, .IDAT_Corrupt
 			}
 			}
 			seen_idat = true
 			seen_idat = true
+
 		case .IEND:
 		case .IEND:
 			c = read_chunk(ctx) or_return
 			c = read_chunk(ctx) or_return
 			seen_iend = true
 			seen_iend = true
+
 		case .bKGD:
 		case .bKGD:
 
 
 			// TODO: Make sure that 16-bit bKGD + tRNS chunks return u16 instead of u16be
 			// 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
 			c = read_chunk(ctx) or_return
 			seen_bkgd = true
 			seen_bkgd = true
 			if .return_metadata in options {
 			if .return_metadata in options {
-				append(&info.chunks, c)
+				append_chunk(&info.chunks, c) or_return
 			}
 			}
 
 
 			ct := transmute(u8)info.header.color_type
 			ct := transmute(u8)info.header.color_type
 			switch ct {
 			switch ct {
 				case 3: // Indexed color
 				case 3: // Indexed color
 					if c.header.length != 1 {
 					if c.header.length != 1 {
-						return {}, E_PNG.BKGD_Invalid_Length
+						return {}, .BKGD_Invalid_Length
 					}
 					}
 					col := _plte.entries[c.data[0]]
 					col := _plte.entries[c.data[0]]
 					img.background = [3]u16{
 					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
 				case 0, 4: // Grayscale, with and without Alpha
 					if c.header.length != 2 {
 					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])
 					col := u16(mem.slice_data_cast([]u16be, c.data[:])[0])
 					img.background = [3]u16{col, col, col}
 					img.background = [3]u16{col, col, col}
 				case 2, 6: // Color, with and without Alpha
 				case 2, 6: // Color, with and without Alpha
 					if c.header.length != 6 {
 					if c.header.length != 6 {
-						return {}, E_PNG.BKGD_Invalid_Length
+						return {}, .BKGD_Invalid_Length
 					}
 					}
 					col := mem.slice_data_cast([]u16be, c.data[:])
 					col := mem.slice_data_cast([]u16be, c.data[:])
 					img.background = [3]u16{u16(col[0]), u16(col[1]), u16(col[2])}
 					img.background = [3]u16{u16(col[0]), u16(col[1]), u16(col[2])}
 			}
 			}
+
 		case .tRNS:
 		case .tRNS:
 			c = read_chunk(ctx) or_return
 			c = read_chunk(ctx) or_return
 
 
 			if .Alpha in info.header.color_type {
 			if .Alpha in info.header.color_type {
-				return img, E_PNG.TRNS_Encountered_Unexpectedly
+				return img, .TRNS_Encountered_Unexpectedly
 			}
 			}
 
 
 			if .return_metadata in options {
 			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
 			trns = c
+
 		case .iDOT, .CbGI:
 		case .iDOT, .CbGI:
 			/*
 			/*
 				iPhone PNG bastardization that doesn't adhere to spec with broken IDAT chunk.
 				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
 				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:
 		case:
 			// Unhandled type
 			// Unhandled type
 			c = read_chunk(ctx) or_return
 			c = read_chunk(ctx) or_return
-
 			if .return_metadata in options {
 			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
 			first = false
@@ -648,7 +627,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 	}
 	}
 
 
 	if !seen_idat {
 	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)
 	buf_len := len(buf.buf)
 	if expected_size != buf_len {
 	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
 		// We need to create a new image buffer
 		dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8)
 		dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8)
 		t := bytes.Buffer{}
 		t := bytes.Buffer{}
-		resize(&t.buf, dest_raw_size)
+		if !resize(&t.buf, dest_raw_size) {
+			return {}, mem.Allocator_Error.Out_Of_Memory
+		}
 
 
 		i := 0; j := 0
 		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
 		// We need to create a new image buffer
 		dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 16)
 		dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 16)
 		t := bytes.Buffer{}
 		t := bytes.Buffer{}
-		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[:])
 		p16 := mem.slice_data_cast([]u16, temp.buf[:])
 		o16 := mem.slice_data_cast([]u16, t.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
 		// We need to create a new image buffer
 		dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8)
 		dest_raw_size := compute_buffer_size(int(header.width), int(header.height), out_image_channels, 8)
 		t := bytes.Buffer{}
 		t := bytes.Buffer{}
-		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[:])
 		p := mem.slice_data_cast([]u8, temp.buf[:])
 		o := mem.slice_data_cast([]u8, t.buf[:])
 		o := mem.slice_data_cast([]u8, t.buf[:])
@@ -1524,7 +1509,7 @@ defilter_16 :: proc(params: ^Filter_Params) -> (ok: bool) {
 	return
 	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)
 	input    := bytes.buffer_to_bytes(filter_bytes)
 	width    := int(header.width)
 	width    := int(header.width)
 	height   := int(header.height)
 	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
 	bytes_per_channel := depth == 16 ? 2 : 1
 
 
 	num_bytes := compute_buffer_size(width, height, channels, depth == 16 ? 16 : 8)
 	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
 	filter_ok: bool
 
 
@@ -1560,7 +1547,7 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, option
 		}
 		}
 		if !filter_ok {
 		if !filter_ok {
 			// Caller will destroy buffer for us.
 			// Caller will destroy buffer for us.
-			return E_PNG.Unknown_Filter_Method
+			return .Unknown_Filter_Method
 		}
 		}
 	} else {
 	} else {
 		/*
 		/*
@@ -1575,7 +1562,9 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, option
 			if x > 0 && y > 0 {
 			if x > 0 && y > 0 {
 				temp: bytes.Buffer
 				temp: bytes.Buffer
 				temp_len := compute_buffer_size(x, y, channels, depth == 16 ? 16 : 8)
 				temp_len := compute_buffer_size(x, y, channels, depth == 16 ? 16 : 8)
-				resize(&temp.buf, temp_len)
+				if !resize(&temp.buf, temp_len) {
+					return mem.Allocator_Error.Out_Of_Memory
+				}
 
 
 				params := Filter_Params{
 				params := Filter_Params{
 					src      = input,
 					src      = input,
@@ -1598,7 +1587,7 @@ defilter :: proc(img: ^Image, filter_bytes: ^bytes.Buffer, header: ^IHDR, option
 
 
 				if !filter_ok {
 				if !filter_ok {
 					// Caller will destroy buffer for us.
 					// Caller will destroy buffer for us.
-					return E_PNG.Unknown_Filter_Method
+					return .Unknown_Filter_Method
 				}
 				}
 
 
 				t := temp.buf[:]
 				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]>.
 	Copyright 2021 Jeroen van Rijn <[email protected]>.
 	Made available under Odin's BSD-3 license.
 	Made available under Odin's BSD-3 license.
@@ -9,6 +7,7 @@ package test_core_image
 
 
 	A test suite for PNG.
 	A test suite for PNG.
 */
 */
+package test_core_image
 
 
 import "core:testing"
 import "core:testing"
 
 
@@ -64,7 +63,7 @@ PNG_Test :: struct {
 	file:   string,
 	file:   string,
 	tests:  []struct {
 	tests:  []struct {
 		options:        image.Options,
 		options:        image.Options,
-		expected_error: compress.Error,
+		expected_error: image.Error,
 		dims:           PNG_Dims,
 		dims:           PNG_Dims,
 		hash:           u32,
 		hash:           u32,
 	},
 	},
@@ -1198,37 +1197,37 @@ Corrupt_PNG_Tests   := []PNG_Test{
 	{
 	{
 		"xs1n0g01", // signature byte 1 MSBit reset to zero
 		"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'
 		"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
 		"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
 		"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
 		"xcrn0g04", // added cr bytes
 		{
 		{
-			{Default, I_Error.Invalid_PNG_Signature, {}, 0x_0000_0000},
+			{Default, .Invalid_PNG_Signature, {}, 0x_0000_0000},
 		},
 		},
 	},
 	},
 	{
 	{
 		"xlfn0g04", // added lf bytes
 		"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
 		"xc1n0g08", // color type 1
 		{
 		{
-			{Default, I_Error.Unknown_Color_Type, {}, 0x_0000_0000},
+			{Default, .Unknown_Color_Type, {}, 0x_0000_0000},
 		},
 		},
 	},
 	},
 	{
 	{
 		"xc9n2c08", // color type 9
 		"xc9n2c08", // color type 9
 		{
 		{
-			{Default, I_Error.Unknown_Color_Type, {}, 0x_0000_0000},
+			{Default, .Unknown_Color_Type, {}, 0x_0000_0000},
 		},
 		},
 	},
 	},
 	{
 	{
 		"xd0n2c08", // bit-depth 0
 		"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
 		"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
 		"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
 		"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
 				passed &= test.hash == hash
 				if .return_metadata in test.options {
 				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 {
 						for c in v.chunks {
 							#partial switch(c.header.type) {
 							#partial switch(c.header.type) {
 							case .gAMA:
 							case .gAMA:
 								switch(file.file) {
 								switch(file.file) {
 								case "pp0n2c16", "pp0n6a08":
 								case "pp0n2c16", "pp0n6a08":
-									gamma := png.gamma(c)
+									gamma, gamma_ok := png.gamma(c)
 									expected_gamma := f32(1.0)
 									expected_gamma := f32(1.0)
 									error  = fmt.tprintf("%v test %v gAMA is %v, expected %v.", file.file, count, gamma, expected_gamma)
 									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:
 							case .PLTE:
 								switch(file.file) {
 								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)
 									expect(t, expected_chrm == chrm && chrm_ok, error)
 								}
 								}
 							case .pHYs:
 							case .pHYs:
-								phys     := png.phys(c)
+								phys, phys_ok := png.phys(c)
 								phys_err := "%v test %v cHRM is %v, expected %v."
 								phys_err := "%v test %v cHRM is %v, expected %v."
 								switch (file.file) {
 								switch (file.file) {
 								case "cdfn2c08":
 								case "cdfn2c08":
 									expected_phys := png.pHYs{ppu_x =    1, ppu_y =    4, unit = .Unknown}
 									expected_phys := png.pHYs{ppu_x =    1, ppu_y =    4, unit = .Unknown}
 									error  = fmt.tprintf(phys_err, file.file, count, phys, expected_phys)
 									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":
 								case "cdhn2c08":
 									expected_phys := png.pHYs{ppu_x =    4, ppu_y =    1, unit = .Unknown}
 									expected_phys := png.pHYs{ppu_x =    4, ppu_y =    1, unit = .Unknown}
 									error  = fmt.tprintf(phys_err, file.file, count, phys, expected_phys)
 									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":
 								case "cdsn2c08":
 									expected_phys := png.pHYs{ppu_x =    1, ppu_y =    1, unit = .Unknown}
 									expected_phys := png.pHYs{ppu_x =    1, ppu_y =    1, unit = .Unknown}
 									error  = fmt.tprintf(phys_err, file.file, count, phys, expected_phys)
 									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":
 								case "cdun2c08":
 									expected_phys := png.pHYs{ppu_x = 1000, ppu_y = 1000, unit = .Meter}
 									expected_phys := png.pHYs{ppu_x = 1000, ppu_y = 1000, unit = .Meter}
 									error  = fmt.tprintf(phys_err, file.file, count, phys, expected_phys)
 									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:
 							case .hIST:
 								hist, hist_ok := png.hist(c)
 								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)
 									expect(t, hist.used == 256 && hist_ok, error)
 								}
 								}
 							case .tIME:
 							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."
 								time_err := "%v test %v tIME was %v, expected %v."
 								expected_time: png.tIME
 								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)
 								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)
 								error  = fmt.tprintf(time_core_err, file.file, count, core_time, expected_core)
 								expect(t, core_time == expected_core && core_time_ok, error)
 								expect(t, core_time == expected_core && core_time_ok, error)