Browse Source

[image] Add QOI load/save.

Additionally:
- Firm up PNG loader with some additional checks.
- Add helper functions to `core:image` to expand grayscale to RGB(A), and so on.

TODO: Possibly replace PNG's post-processing steps with calls to the new helper functions.
Jeroen van Rijn 3 years ago
parent
commit
15b440c4f1

+ 0 - 4
core/compress/common.odin

@@ -128,7 +128,6 @@ Deflate_Error :: enum {
 	BType_3,
 	BType_3,
 }
 }
 
 
-
 // General I/O context for ZLIB, LZW, etc.
 // General I/O context for ZLIB, LZW, etc.
 Context_Memory_Input :: struct #packed {
 Context_Memory_Input :: struct #packed {
 	input_data:        []u8,
 	input_data:        []u8,
@@ -151,7 +150,6 @@ when size_of(rawptr) == 8 {
 	#assert(size_of(Context_Memory_Input) == 52)
 	#assert(size_of(Context_Memory_Input) == 52)
 }
 }
 
 
-
 Context_Stream_Input :: struct #packed {
 Context_Stream_Input :: struct #packed {
 	input_data:        []u8,
 	input_data:        []u8,
 	input:             io.Stream,
 	input:             io.Stream,
@@ -185,8 +183,6 @@ Context_Stream_Input :: struct #packed {
 	This simplifies end-of-stream handling where bits may be left in the bit buffer.
 	This simplifies end-of-stream handling where bits may be left in the bit buffer.
 */
 */
 
 
-// TODO: Make these return compress.Error errors.
-
 input_size_from_memory :: proc(z: ^Context_Memory_Input) -> (res: i64, err: Error) {
 input_size_from_memory :: proc(z: ^Context_Memory_Input) -> (res: i64, err: Error) {
 	return i64(len(z.input_data)), nil
 	return i64(len(z.input_data)), nil
 }
 }

+ 811 - 19
core/image/common.odin

@@ -15,6 +15,32 @@ import "core:mem"
 import "core:compress"
 import "core:compress"
 import "core:runtime"
 import "core:runtime"
 
 
+/*
+	67_108_864 pixels max by default.
+
+	For QOI, the Worst case scenario means all pixels will be encoded as RGBA literals, costing 5 bytes each.
+	This caps memory usage at 320 MiB.
+
+	The tunable is limited to 4_294_836_225 pixels maximum, or 4 GiB per 8-bit channel.
+	It is not advised to tune it this large.
+
+	The 64 Megapixel default is considered to be a decent upper bound you won't run into in practice,
+	except in very specific circumstances.
+
+*/
+MAX_DIMENSIONS :: min(#config(MAX_DIMENSIONS, 8192 * 8192), 65535 * 65535)
+
+// Color
+RGB_Pixel     :: [3]u8
+RGBA_Pixel    :: [4]u8
+RGB_Pixel_16  :: [3]u16
+RGBA_Pixel_16 :: [4]u16
+// Grayscale
+G_Pixel       :: [1]u8
+GA_Pixel      :: [2]u8
+G_Pixel_16    :: [1]u16
+GA_Pixel_16   :: [2]u16
+
 Image :: struct {
 Image :: struct {
 	width:         int,
 	width:         int,
 	height:        int,
 	height:        int,
@@ -26,15 +52,17 @@ Image :: struct {
 		For convenience, we return them as u16 so we don't need to switch on the type
 		For convenience, we return them as u16 so we don't need to switch on the type
 		in our viewer, and can just test against nil.
 		in our viewer, and can just test against nil.
 	*/
 	*/
-	background:    Maybe([3]u16),
-
+	background:    Maybe(RGB_Pixel_16),
 	metadata:      Image_Metadata,
 	metadata:      Image_Metadata,
 }
 }
 
 
 Image_Metadata :: union {
 Image_Metadata :: union {
 	^PNG_Info,
 	^PNG_Info,
+	^QOI_Info,
 }
 }
 
 
+
+
 /*
 /*
 	IMPORTANT: `.do_not_expand_*` options currently skip handling of the `alpha_*` options,
 	IMPORTANT: `.do_not_expand_*` options currently skip handling of the `alpha_*` options,
 		therefore Gray+Alpha will be returned as such even if you add `.alpha_drop_if_present`,
 		therefore Gray+Alpha will be returned as such even if you add `.alpha_drop_if_present`,
@@ -46,13 +74,13 @@ Image_Metadata :: union {
 /*
 /*
 Image_Option:
 Image_Option:
 	`.info`
 	`.info`
-		This option behaves as `.return_ihdr` and `.do_not_decompress_image` and can be used
+		This option behaves as `.return_metadata` and `.do_not_decompress_image` and can be used
 		to gather an image's dimensions and color information.
 		to gather an image's dimensions and color information.
 
 
 	`.return_header`
 	`.return_header`
-		Fill out img.sidecar.header with the image's format-specific header struct.
+		Fill out img.metadata.header with the image's format-specific header struct.
 		If we only care about the image specs, we can set `.return_header` +
 		If we only care about the image specs, we can set `.return_header` +
-		`.do_not_decompress_image`, or `.info`, which works as if both of these were set.
+		`.do_not_decompress_image`, or `.info`.
 
 
 	`.return_metadata`
 	`.return_metadata`
 		Returns all chunks not needed to decode the data.
 		Returns all chunks not needed to decode the data.
@@ -88,7 +116,7 @@ Image_Option:
 
 
 	`.alpha_premultiply`
 	`.alpha_premultiply`
 		If the image has an alpha channel, returns image data as follows:
 		If the image has an alpha channel, returns image data as follows:
-			RGB  *= A, Gray = Gray *= A
+			RGB *= A, Gray = Gray *= A
 
 
 	`.blend_background`
 	`.blend_background`
 		If a bKGD chunk is present in a PNG, we normally just set `img.background`
 		If a bKGD chunk is present in a PNG, we normally just set `img.background`
@@ -103,24 +131,29 @@ Image_Option:
 */
 */
 
 
 Option :: enum {
 Option :: enum {
+	// LOAD OPTIONS
 	info = 0,
 	info = 0,
 	do_not_decompress_image,
 	do_not_decompress_image,
 	return_header,
 	return_header,
 	return_metadata,
 	return_metadata,
-	alpha_add_if_missing,
-	alpha_drop_if_present,
-	alpha_premultiply,
-	blend_background,
+	alpha_add_if_missing,          // Ignored for QOI. Always returns RGBA8.
+	alpha_drop_if_present,         // Unimplemented for QOI. Returns error.
+	alpha_premultiply,             // Unimplemented for QOI. Returns error.
+	blend_background,              // Ignored for non-PNG formats
 	// Unimplemented
 	// Unimplemented
 	do_not_expand_grayscale,
 	do_not_expand_grayscale,
 	do_not_expand_indexed,
 	do_not_expand_indexed,
 	do_not_expand_channels,
 	do_not_expand_channels,
+
+	// SAVE OPTIONS
+	qoi_all_channels_linear,       // QOI, informative info. If not set, defaults to sRGB with linear alpha.
 }
 }
 Options :: distinct bit_set[Option]
 Options :: distinct bit_set[Option]
 
 
 Error :: union #shared_nil {
 Error :: union #shared_nil {
 	General_Image_Error,
 	General_Image_Error,
 	PNG_Error,
 	PNG_Error,
+	QOI_Error,
 
 
 	compress.Error,
 	compress.Error,
 	compress.General_Error,
 	compress.General_Error,
@@ -134,8 +167,13 @@ General_Image_Error :: enum {
 	Invalid_Image_Dimensions,
 	Invalid_Image_Dimensions,
 	Image_Dimensions_Too_Large,
 	Image_Dimensions_Too_Large,
 	Image_Does_Not_Adhere_to_Spec,
 	Image_Does_Not_Adhere_to_Spec,
+	Invalid_Input_Image,
+	Invalid_Output,
 }
 }
 
 
+/*
+	PNG-specific definitions
+*/
 PNG_Error :: enum {
 PNG_Error :: enum {
 	None = 0,
 	None = 0,
 	Invalid_PNG_Signature,
 	Invalid_PNG_Signature,
@@ -147,7 +185,9 @@ PNG_Error :: enum {
 	IDAT_Size_Too_Large,
 	IDAT_Size_Too_Large,
 	PLTE_Encountered_Unexpectedly,
 	PLTE_Encountered_Unexpectedly,
 	PLTE_Invalid_Length,
 	PLTE_Invalid_Length,
+	PLTE_Missing,
 	TRNS_Encountered_Unexpectedly,
 	TRNS_Encountered_Unexpectedly,
+	TNRS_Invalid_Length,
 	BKGD_Invalid_Length,
 	BKGD_Invalid_Length,
 	Unknown_Color_Type,
 	Unknown_Color_Type,
 	Invalid_Color_Bit_Depth_Combo,
 	Invalid_Color_Bit_Depth_Combo,
@@ -158,9 +198,6 @@ PNG_Error :: enum {
 	Invalid_Chunk_Length,
 	Invalid_Chunk_Length,
 }
 }
 
 
-/*
-	PNG-specific structs
-*/
 PNG_Info :: struct {
 PNG_Info :: struct {
 	header: PNG_IHDR,
 	header: PNG_IHDR,
 	chunks: [dynamic]PNG_Chunk,
 	chunks: [dynamic]PNG_Chunk,
@@ -223,7 +260,7 @@ PNG_Chunk_Type :: enum u32be {
 
 
 	*/
 	*/
 	iDOT = 'i' << 24 | 'D' << 16 | 'O' << 8 | 'T',
 	iDOT = 'i' << 24 | 'D' << 16 | 'O' << 8 | 'T',
-	CbGI = 'C' << 24 | 'b' << 16 | 'H' << 8 | 'I',
+	CgBI = 'C' << 24 | 'g' << 16 | 'B' << 8 | 'I',
 }
 }
 
 
 PNG_IHDR :: struct #packed {
 PNG_IHDR :: struct #packed {
@@ -251,16 +288,44 @@ PNG_Interlace_Method :: enum u8 {
 }
 }
 
 
 /*
 /*
-	Functions to help with image buffer calculations
+	QOI-specific definitions
 */
 */
+QOI_Error :: enum {
+	None = 0,
+	Invalid_QOI_Signature,
+	Invalid_Number_Of_Channels, // QOI allows 3 or 4 channel data.
+	Invalid_Bit_Depth,          // QOI supports only 8-bit images, error only returned from writer.
+	Invalid_Color_Space,        // QOI allows 0 = sRGB or 1 = linear.
+	Corrupt,                    // More data than pixels to decode into, for example.
+	Missing_Or_Corrupt_Trailer, // Image seemed to have decoded okay, but trailer is missing or corrupt.
+}
+
+QOI_Magic :: u32be(0x716f6966)      // "qoif"
+
+QOI_Color_Space :: enum u8 {
+	sRGB   = 0,
+	Linear = 1,
+}
+
+QOI_Header :: struct #packed {
+	magic:       u32be,
+	width:       u32be,
+	height:      u32be,
+	channels:    u8,
+	color_space: QOI_Color_Space,
+}
+#assert(size_of(QOI_Header) == 14)
+
+QOI_Info :: struct {
+	header: QOI_Header,
+}
+
+// Function 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
 }
 }
 
 
-/*
-	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,
@@ -268,7 +333,13 @@ Channel :: enum u8 {
 	A = 4,
 	A = 4,
 }
 }
 
 
+// When you have an RGB(A) image, but want a particular channel.
 return_single_channel :: proc(img: ^Image, channel: Channel) -> (res: ^Image, ok: bool) {
 return_single_channel :: proc(img: ^Image, channel: Channel) -> (res: ^Image, ok: bool) {
+	// Were we actually given a valid image?
+	if img == nil {
+		return nil, false
+	}
+
 	ok = false
 	ok = false
 	t: bytes.Buffer
 	t: bytes.Buffer
 
 
@@ -298,7 +369,7 @@ return_single_channel :: proc(img: ^Image, channel: Channel) -> (res: ^Image, ok
 			o = o[1:]
 			o = o[1:]
 		}
 		}
 	case 16:
 	case 16:
-		buffer_size := compute_buffer_size(img.width, img.height, 2, 8)
+		buffer_size := compute_buffer_size(img.width, img.height, 1, 16)
 		t = bytes.Buffer{}
 		t = bytes.Buffer{}
 		resize(&t.buf, buffer_size)
 		resize(&t.buf, buffer_size)
 
 
@@ -326,3 +397,724 @@ return_single_channel :: proc(img: ^Image, channel: Channel) -> (res: ^Image, ok
 
 
 	return res, true
 	return res, true
 }
 }
+
+// Does the image have 1 or 2 channels, a valid bit depth (8 or 16),
+// Is the pointer valid, are the dimenions valid?
+is_valid_grayscale_image :: proc(img: ^Image) -> (ok: bool) {
+	// Were we actually given a valid image?
+	if img == nil {
+		return false
+	}
+
+	// Are we a Gray or Gray + Alpha image?
+	if img.channels != 1 && img.channels != 2 {
+		return false
+	}
+
+	// Do we have an acceptable bit depth?
+	if img.depth != 8 && img.depth != 16 {
+		return false
+	}
+
+	// This returns 0 if any of the inputs is zero.
+	bytes_expected := compute_buffer_size(img.width, img.height, img.channels, img.depth)
+
+	// If the dimenions are invalid or the buffer size doesn't match the image characteristics, bail.
+	if bytes_expected == 0 || bytes_expected != len(img.pixels.buf) || img.width * img.height > MAX_DIMENSIONS {
+		return false
+	}
+
+	return true
+}
+
+// Does the image have 3 or 4 channels, a valid bit depth (8 or 16),
+// Is the pointer valid, are the dimenions valid?
+is_valid_color_image :: proc(img: ^Image) -> (ok: bool) {
+	// Were we actually given a valid image?
+	if img == nil {
+		return false
+	}
+
+	// Are we an RGB or RGBA image?
+	if img.channels != 3 && img.channels != 4 {
+		return false
+	}
+
+	// Do we have an acceptable bit depth?
+	if img.depth != 8 && img.depth != 16 {
+		return false
+	}
+
+	// This returns 0 if any of the inputs is zero.
+	bytes_expected := compute_buffer_size(img.width, img.height, img.channels, img.depth)
+
+	// If the dimenions are invalid or the buffer size doesn't match the image characteristics, bail.
+	if bytes_expected == 0 || bytes_expected != len(img.pixels.buf) || img.width * img.height > MAX_DIMENSIONS {
+		return false
+	}
+
+	return true
+}
+
+// Does the image have 1..4 channels, a valid bit depth (8 or 16),
+// Is the pointer valid, are the dimenions valid?
+is_valid_image :: proc(img: ^Image) -> (ok: bool) {
+	// Were we actually given a valid image?
+	if img == nil {
+		return false
+	}
+
+	return is_valid_color_image(img) || is_valid_grayscale_image(img)
+}
+
+Alpha_Key :: union {
+	GA_Pixel,
+	RGBA_Pixel,
+	GA_Pixel_16,
+	RGBA_Pixel_16,
+}
+
+/*
+	Add alpha channel if missing, in-place.
+
+	Expects 1..4 channels (Gray, Gray + Alpha, RGB, RGBA).
+	Any other number of channels will be considered an error, returning `false` without modifying the image.
+	If the input image already has an alpha channel, it'll return `true` early (without considering optional keyed alpha).
+
+	If an image doesn't already have an alpha channel:
+	If the optional `alpha_key` is provided, it will be resolved as follows:
+		- For RGB,  if pix = key.rgb -> pix = {0, 0, 0, key.a}
+		- For Gray, if pix = key.r  -> pix = {0, key.g}
+	Otherwise, an opaque alpha channel will be added.
+*/
+alpha_add_if_missing :: proc(img: ^Image, alpha_key := Alpha_Key{}, allocator := context.allocator) -> (ok: bool) {
+	context.allocator = allocator
+
+	if !is_valid_image(img) {
+		return false
+	}
+
+	// We should now have a valid Image with 1..4 channels. Do we already have alpha?
+	if img.channels == 2 || img.channels == 4 {
+		// We're done.
+		return true
+	}
+
+	channels     := img.channels + 1
+	bytes_wanted := compute_buffer_size(img.width, img.height, channels, img.depth)
+
+	buf := bytes.Buffer{}
+
+	// Can we allocate the return buffer?
+	if !resize(&buf.buf, bytes_wanted) {
+		delete(buf.buf)
+		return false
+	}
+
+	switch img.depth {
+	case 8:
+		switch channels {
+		case 2:
+			// Turn Gray into Gray + Alpha
+			inp := mem.slice_data_cast([]G_Pixel,  img.pixels.buf[:])
+			out := mem.slice_data_cast([]GA_Pixel, buf.buf[:])
+
+			if key, key_ok := alpha_key.(GA_Pixel); key_ok {
+				// We have keyed alpha.
+				o: GA_Pixel
+				for p in inp {
+					if p == key.r {
+						o = GA_Pixel{0, key.g}
+					} else {
+						o = GA_Pixel{p.r, 255}
+					}
+					out[0] = o
+					out = out[1:]
+				}
+			} else {
+				// No keyed alpha, just make all pixels opaque.
+				o := GA_Pixel{0, 255}
+				for p in inp {
+					o.r    = p.r
+					out[0] = o
+					out = out[1:]
+				}
+			}
+
+		case 4:
+			// Turn RGB into RGBA
+			inp := mem.slice_data_cast([]RGB_Pixel,  img.pixels.buf[:])
+			out := mem.slice_data_cast([]RGBA_Pixel, buf.buf[:])
+
+			if key, key_ok := alpha_key.(RGBA_Pixel); key_ok {
+				// We have keyed alpha.
+				o: RGBA_Pixel
+				for p in inp {
+					if p == key.rgb {
+						o = RGBA_Pixel{0, 0, 0, key.a}
+					} else {
+						o = RGBA_Pixel{p.r, p.g, p.b, 255}
+					}
+					out[0] = o
+					out = out[1:]
+				}
+			} else {
+				// No keyed alpha, just make all pixels opaque.
+				o := RGBA_Pixel{0, 0, 0, 255}
+				for p in inp {
+					o.rgb  = p
+					out[0] = o
+					out = out[1:]
+				}
+			}
+		case:
+			// We shouldn't get here.
+			unreachable()
+		}
+	case 16:
+		switch channels {
+		case 2:
+			// Turn Gray into Gray + Alpha
+			inp := mem.slice_data_cast([]G_Pixel_16,  img.pixels.buf[:])
+			out := mem.slice_data_cast([]GA_Pixel_16, buf.buf[:])
+
+			if key, key_ok := alpha_key.(GA_Pixel_16); key_ok {
+				// We have keyed alpha.
+				o: GA_Pixel_16
+				for p in inp {
+					if p == key.r {
+						o = GA_Pixel_16{0, key.g}
+					} else {
+						o = GA_Pixel_16{p.r, 65535}
+					}
+					out[0] = o
+					out = out[1:]
+				}
+			} else {
+				// No keyed alpha, just make all pixels opaque.
+				o := GA_Pixel_16{0, 65535}
+				for p in inp {
+					o.r    = p.r
+					out[0] = o
+					out = out[1:]
+				}
+			}
+
+		case 4:
+			// Turn RGB into RGBA
+			inp := mem.slice_data_cast([]RGB_Pixel_16,  img.pixels.buf[:])
+			out := mem.slice_data_cast([]RGBA_Pixel_16, buf.buf[:])
+
+			if key, key_ok := alpha_key.(RGBA_Pixel_16); key_ok {
+				// We have keyed alpha.
+				o: RGBA_Pixel_16
+				for p in inp {
+					if p == key.rgb {
+						o = RGBA_Pixel_16{0, 0, 0, key.a}
+					} else {
+						o = RGBA_Pixel_16{p.r, p.g, p.b, 65535}
+					}
+					out[0] = o
+					out = out[1:]
+				}
+			} else {
+				// No keyed alpha, just make all pixels opaque.
+				o := RGBA_Pixel_16{0, 0, 0, 65535}
+				for p in inp {
+					o.rgb  = p
+					out[0] = o
+					out = out[1:]
+				}
+			}
+		case:
+			// We shouldn't get here.
+			unreachable()
+		}
+	}
+
+	// If we got here, that means we've now got a buffer with the alpha channel added.
+	// Destroy the old pixel buffer and replace it with the new one, and update the channel count.
+	bytes.buffer_destroy(&img.pixels)
+	img.pixels   = buf
+	img.channels = channels
+	return true
+}
+alpha_apply_keyed_alpha :: alpha_add_if_missing
+
+/*
+	Drop alpha channel if present, in-place.
+
+	Expects 1..4 channels (Gray, Gray + Alpha, RGB, RGBA).
+	Any other number of channels will be considered an error, returning `false` without modifying the image.
+
+	Of the `options`, the following are considered:
+	`.alpha_premultiply`
+		If the image has an alpha channel, returns image data as follows:
+			RGB *= A, Gray = Gray *= A
+
+	`.blend_background`
+		If `img.background` is set, it'll be blended in like this:
+			RGB = (1 - A) * Background + A * RGB
+
+	If an image has 1 (Gray) or 3 (RGB) channels, it'll return early without modifying the image,
+	with one exception: `alpha_key` and `img.background` are present, and `.blend_background` is set.
+
+	In this case a keyed alpha pixel will be replaced with the background color.
+*/
+alpha_drop_if_present :: proc(img: ^Image, options := Options{}, alpha_key := Alpha_Key{}, allocator := context.allocator) -> (ok: bool) {
+	context.allocator = allocator
+
+	if !is_valid_image(img) {
+		return false
+	}
+
+	// Do we have a background to blend?
+	will_it_blend := false
+	switch v in img.background {
+	case RGB_Pixel_16: will_it_blend = true if .blend_background in options else false
+	}
+
+	// Do we have keyed alpha?
+	keyed := false
+	switch v in alpha_key {
+	case GA_Pixel:      keyed = true if img.channels == 1 && img.depth ==  8 else false
+	case RGBA_Pixel:    keyed = true if img.channels == 3 && img.depth ==  8 else false
+	case GA_Pixel_16:   keyed = true if img.channels == 1 && img.depth == 16 else false
+	case RGBA_Pixel_16: keyed = true if img.channels == 3 && img.depth == 16 else false
+	}
+
+	// We should now have a valid Image with 1..4 channels. Do we have alpha?
+	if img.channels == 1 || img.channels == 3 {
+		if !(will_it_blend && keyed) {
+			// We're done
+			return true
+		}
+	}
+
+	// # of destination channels
+	channels := 1 if img.channels < 3 else 3
+
+	bytes_wanted := compute_buffer_size(img.width, img.height, channels, img.depth)
+	buf := bytes.Buffer{}
+
+	// Can we allocate the return buffer?
+	if !resize(&buf.buf, bytes_wanted) {
+		delete(buf.buf)
+		return false
+	}
+
+	switch img.depth {
+	case 8:
+		switch img.channels {
+		case 1: // Gray to Gray, but we should have keyed alpha + background.
+			inp := mem.slice_data_cast([]G_Pixel, img.pixels.buf[:])
+			out := mem.slice_data_cast([]G_Pixel, buf.buf[:])
+
+			key := alpha_key.(GA_Pixel).r
+			bg  := G_Pixel{}
+			if temp_bg, temp_bg_ok := img.background.(RGB_Pixel_16); temp_bg_ok {
+				// Background is RGB 16-bit, take just the red channel's topmost byte.
+				bg = u8(temp_bg.r >> 8)
+			}
+
+			for p in inp {
+				out[0] = bg if p == key else p
+				out    = out[1:]
+			}
+
+		case 2: // Gray + Alpha to Gray, no keyed alpha but we can have a background.
+			inp := mem.slice_data_cast([]GA_Pixel, img.pixels.buf[:])
+			out := mem.slice_data_cast([]G_Pixel,  buf.buf[:])
+
+			if will_it_blend {
+				// Blend with background "color", then drop alpha.
+				bg  := f32(0.0)
+				if temp_bg, temp_bg_ok := img.background.(RGB_Pixel_16); temp_bg_ok {
+					// Background is RGB 16-bit, take just the red channel's topmost byte.
+					bg = f32(temp_bg.r >> 8)
+				}
+
+				for p in inp {
+					a := f32(p.g) / 255.0
+					c := ((1.0 - a) * bg + a * f32(p.r))
+					out[0] = u8(c)
+					out    = out[1:]
+				}
+
+			} else if .alpha_premultiply in options {
+				// Premultiply component with alpha, then drop alpha.
+				for p in inp {
+					a := f32(p.g) / 255.0
+					c := f32(p.r) * a
+					out[0] = u8(c)
+					out    = out[1:]
+				}
+			} else {
+				// Just drop alpha on the floor.
+				for p in inp {
+					out[0] = p.r
+					out    = out[1:]
+				}
+			}
+
+		case 3: // RGB to RGB, but we should have keyed alpha + background.
+			inp := mem.slice_data_cast([]RGB_Pixel, img.pixels.buf[:])
+			out := mem.slice_data_cast([]RGB_Pixel, buf.buf[:])
+
+			key := alpha_key.(RGBA_Pixel)
+			bg  := RGB_Pixel{}
+			if temp_bg, temp_bg_ok := img.background.(RGB_Pixel_16); temp_bg_ok {
+				// Background is RGB 16-bit, squash down to 8 bits.
+				bg = {u8(temp_bg.r >> 8), u8(temp_bg.g >> 8), u8(temp_bg.b >> 8)}
+			}
+
+			for p in inp {
+				out[0] = bg if p == key.rgb else p
+				out    = out[1:]
+			}
+
+		case 4: // RGBA to RGB, no keyed alpha but we can have a background or need to premultiply.
+			inp := mem.slice_data_cast([]RGBA_Pixel, img.pixels.buf[:])
+			out := mem.slice_data_cast([]RGB_Pixel,  buf.buf[:])
+
+			if will_it_blend {
+				// Blend with background "color", then drop alpha.
+				bg := [3]f32{}
+				if temp_bg, temp_bg_ok := img.background.(RGB_Pixel_16); temp_bg_ok {
+					// Background is RGB 16-bit, take just the red channel's topmost byte.
+					bg = {f32(temp_bg.r >> 8), f32(temp_bg.g >> 8), f32(temp_bg.b >> 8)}
+				}
+
+				for p in inp {
+					a   := f32(p.a) / 255.0
+					rgb := [3]f32{f32(p.r), f32(p.g), f32(p.b)}
+					c   := ((1.0 - a) * bg + a * rgb)
+
+					out[0] = {u8(c.r), u8(c.g), u8(c.b)}
+					out    = out[1:]
+				}
+
+			} else if .alpha_premultiply in options {
+				// Premultiply component with alpha, then drop alpha.
+				for p in inp {
+					a   := f32(p.a) / 255.0
+					rgb := [3]f32{f32(p.r), f32(p.g), f32(p.b)}
+					c   := rgb * a
+
+					out[0] = {u8(c.r), u8(c.g), u8(c.b)}
+					out    = out[1:]
+				}
+			} else {
+				// Just drop alpha on the floor.
+				for p in inp {
+					out[0] = p.rgb
+					out    = out[1:]
+				}
+			}
+		}
+
+	case 16:
+		switch img.channels {
+		case 1: // Gray to Gray, but we should have keyed alpha + background.
+			inp := mem.slice_data_cast([]G_Pixel_16, img.pixels.buf[:])
+			out := mem.slice_data_cast([]G_Pixel_16, buf.buf[:])
+
+			key := alpha_key.(GA_Pixel_16).r
+			bg  := G_Pixel_16{}
+			if temp_bg, temp_bg_ok := img.background.(RGB_Pixel_16); temp_bg_ok {
+				// Background is RGB 16-bit, take just the red channel.
+				bg = temp_bg.r
+			}
+
+			for p in inp {
+				out[0] = bg if p == key else p
+				out    = out[1:]
+			}
+
+		case 2: // Gray + Alpha to Gray, no keyed alpha but we can have a background.
+			inp := mem.slice_data_cast([]GA_Pixel_16, img.pixels.buf[:])
+			out := mem.slice_data_cast([]G_Pixel_16,  buf.buf[:])
+
+			if will_it_blend {
+				// Blend with background "color", then drop alpha.
+				bg  := f32(0.0)
+				if temp_bg, temp_bg_ok := img.background.(RGB_Pixel_16); temp_bg_ok {
+					// Background is RGB 16-bit, take just the red channel.
+					bg = f32(temp_bg.r)
+				}
+
+				for p in inp {
+					a := f32(p.g) / 65535.0
+					c := ((1.0 - a) * bg + a * f32(p.r))
+					out[0] = u16(c)
+					out    = out[1:]
+				}
+
+			} else if .alpha_premultiply in options {
+				// Premultiply component with alpha, then drop alpha.
+				for p in inp {
+					a := f32(p.g) / 65535.0
+					c := f32(p.r) * a
+					out[0] = u16(c)
+					out    = out[1:]
+				}
+			} else {
+				// Just drop alpha on the floor.
+				for p in inp {
+					out[0] = p.r
+					out    = out[1:]
+				}
+			}
+
+		case 3: // RGB to RGB, but we should have keyed alpha + background.
+			inp := mem.slice_data_cast([]RGB_Pixel_16, img.pixels.buf[:])
+			out := mem.slice_data_cast([]RGB_Pixel_16, buf.buf[:])
+
+			key := alpha_key.(RGBA_Pixel_16)
+			bg  := img.background.(RGB_Pixel_16)
+
+			for p in inp {
+				out[0] = bg if p == key.rgb else p
+				out    = out[1:]
+			}
+
+		case 4: // RGBA to RGB, no keyed alpha but we can have a background or need to premultiply.
+			inp := mem.slice_data_cast([]RGBA_Pixel_16, img.pixels.buf[:])
+			out := mem.slice_data_cast([]RGB_Pixel_16,  buf.buf[:])
+
+			if will_it_blend {
+				// Blend with background "color", then drop alpha.
+				bg := [3]f32{}
+				if temp_bg, temp_bg_ok := img.background.(RGB_Pixel_16); temp_bg_ok {
+					// Background is RGB 16-bit, convert to [3]f32 to blend.
+					bg = {f32(temp_bg.r), f32(temp_bg.g), f32(temp_bg.b)}
+				}
+
+				for p in inp {
+					a   := f32(p.a) / 65535.0
+					rgb := [3]f32{f32(p.r), f32(p.g), f32(p.b)}
+					c   := ((1.0 - a) * bg + a * rgb)
+
+					out[0] = {u16(c.r), u16(c.g), u16(c.b)}
+					out    = out[1:]
+				}
+
+			} else if .alpha_premultiply in options {
+				// Premultiply component with alpha, then drop alpha.
+				for p in inp {
+					a   := f32(p.a) / 65535.0
+					rgb := [3]f32{f32(p.r), f32(p.g), f32(p.b)}
+					c   := rgb * a
+
+					out[0] = {u16(c.r), u16(c.g), u16(c.b)}
+					out    = out[1:]
+				}
+			} else {
+				// Just drop alpha on the floor.
+				for p in inp {
+					out[0] = p.rgb
+					out    = out[1:]
+				}
+			}
+		}
+
+	case:
+		unreachable()
+	}
+
+	// If we got here, that means we've now got a buffer with the alpha channel dropped.
+	// Destroy the old pixel buffer and replace it with the new one, and update the channel count.
+	bytes.buffer_destroy(&img.pixels)
+	img.pixels   = buf
+	img.channels = channels
+	return true
+}
+
+// Apply palette to 8-bit single-channel image and return an 8-bit RGB image, in-place.
+// If the image given is not a valid 8-bit single channel image, the procedure will return `false` early.
+apply_palette_rgb :: proc(img: ^Image, palette: [256]RGB_Pixel, allocator := context.allocator) -> (ok: bool) {
+	context.allocator = allocator
+
+	if img == nil || img.channels != 1 || img.depth != 8 {
+		return false
+	}
+
+	bytes_expected := compute_buffer_size(img.width, img.height, 1, 8)
+	if bytes_expected == 0 || bytes_expected != len(img.pixels.buf) || img.width * img.height > MAX_DIMENSIONS {
+		return false
+	}
+
+	// Can we allocate the return buffer?
+	buf := bytes.Buffer{}
+	bytes_wanted := compute_buffer_size(img.width, img.height, 3, 8)
+	if !resize(&buf.buf, bytes_wanted) {
+		delete(buf.buf)
+		return false
+	}
+
+	out := mem.slice_data_cast([]RGB_Pixel, buf.buf[:])
+
+	// Apply the palette
+	for p, i in img.pixels.buf {
+		out[i] = palette[p]
+	}
+
+	// If we got here, that means we've now got a buffer with the alpha channel dropped.
+	// Destroy the old pixel buffer and replace it with the new one, and update the channel count.
+	bytes.buffer_destroy(&img.pixels)
+	img.pixels   = buf
+	img.channels = 3
+	return true
+}
+
+// Apply palette to 8-bit single-channel image and return an 8-bit RGBA image, in-place.
+// If the image given is not a valid 8-bit single channel image, the procedure will return `false` early.
+apply_palette_rgba :: proc(img: ^Image, palette: [256]RGBA_Pixel, allocator := context.allocator) -> (ok: bool) {
+	context.allocator = allocator
+
+	if img == nil || img.channels != 1 || img.depth != 8 {
+		return false
+	}
+
+	bytes_expected := compute_buffer_size(img.width, img.height, 1, 8)
+	if bytes_expected == 0 || bytes_expected != len(img.pixels.buf) || img.width * img.height > MAX_DIMENSIONS {
+		return false
+	}
+
+	// Can we allocate the return buffer?
+	buf := bytes.Buffer{}
+	bytes_wanted := compute_buffer_size(img.width, img.height, 4, 8)
+	if !resize(&buf.buf, bytes_wanted) {
+		delete(buf.buf)
+		return false
+	}
+
+	out := mem.slice_data_cast([]RGBA_Pixel, buf.buf[:])
+
+	// Apply the palette
+	for p, i in img.pixels.buf {
+		out[i] = palette[p]
+	}
+
+	// If we got here, that means we've now got a buffer with the alpha channel dropped.
+	// Destroy the old pixel buffer and replace it with the new one, and update the channel count.
+	bytes.buffer_destroy(&img.pixels)
+	img.pixels   = buf
+	img.channels = 4
+	return true
+}
+apply_palette :: proc{apply_palette_rgb, apply_palette_rgba}
+
+
+// Replicates grayscale values into RGB(A) 8- or 16-bit images as appropriate.
+// Returns early with `false` if already an RGB(A) image.
+expand_grayscale :: proc(img: ^Image, allocator := context.allocator) -> (ok: bool) {
+	context.allocator = allocator
+
+	if !is_valid_grayscale_image(img) {
+		return false
+	}
+
+	// We should have 1 or 2 channels of 8- or 16 bits now. We need to turn that into 3 or 4.
+	// Can we allocate the return buffer?
+	buf := bytes.Buffer{}
+	bytes_wanted := compute_buffer_size(img.width, img.height, img.channels + 2, img.depth)
+	if !resize(&buf.buf, bytes_wanted) {
+		delete(buf.buf)
+		return false
+	}
+
+	switch img.depth {
+		case 8:
+			switch img.channels {
+			case 1: // Turn Gray into RGB
+				out := mem.slice_data_cast([]RGB_Pixel, buf.buf[:])
+
+				for p in img.pixels.buf {
+					out[0] = p // Broadcast gray value into RGB components.
+					out    = out[1:]
+				}
+
+			case 2: // Turn Gray + Alpha into RGBA
+				inp := mem.slice_data_cast([]GA_Pixel,   img.pixels.buf[:])
+				out := mem.slice_data_cast([]RGBA_Pixel, buf.buf[:])
+
+				for p in inp {
+					out[0].rgb = p.r // Gray component.
+					out[0].a   = p.g // Alpha component.
+				}
+
+			case:
+				unreachable()
+			}
+
+		case 16:
+			switch img.channels {
+			case 1: // Turn Gray into RGB
+				inp := mem.slice_data_cast([]u16, img.pixels.buf[:])
+				out := mem.slice_data_cast([]RGB_Pixel_16, buf.buf[:])
+
+				for p in inp {
+					out[0] = p // Broadcast gray value into RGB components.
+					out    = out[1:]
+				}
+
+			case 2: // Turn Gray + Alpha into RGBA
+				inp := mem.slice_data_cast([]GA_Pixel_16,   img.pixels.buf[:])
+				out := mem.slice_data_cast([]RGBA_Pixel_16, buf.buf[:])
+
+				for p in inp {
+					out[0].rgb = p.r // Gray component.
+					out[0].a   = p.g // Alpha component.
+				}
+
+			case:
+				unreachable()
+			}
+
+		case:
+			unreachable()
+	}
+
+
+	// If we got here, that means we've now got a buffer with the extra alpha channel.
+	// Destroy the old pixel buffer and replace it with the new one, and update the channel count.
+	bytes.buffer_destroy(&img.pixels)
+	img.pixels   = buf
+	img.channels += 2
+	return true
+}
+
+/*
+	Helper functions to read and write data from/to a Context, etc.
+*/
+@(optimization_mode="speed")
+read_data :: proc(z: $C, $T: typeid) -> (res: T, err: compress.General_Error) {
+	if r, e := compress.read_data(z, T); e != .None {
+		return {}, .Stream_Too_Short
+	} else {
+		return r, nil
+	}
+}
+
+@(optimization_mode="speed")
+read_u8 :: proc(z: $C) -> (res: u8, err: compress.General_Error) {
+	if r, e := compress.read_u8(z); e != .None {
+		return {}, .Stream_Too_Short
+	} else {
+		return r, nil
+	}
+}
+
+write_bytes :: proc(buf: ^bytes.Buffer, data: []u8) -> (err: compress.General_Error) {
+	if len(data) == 0 {
+		return nil
+	} else if len(data) == 1 {
+		if bytes.buffer_write_byte(buf, data[0]) != nil {
+			return compress.General_Error.Resize_Failed
+		}
+	} else if n, _ := bytes.buffer_write(buf, data); n != len(data) {
+		return compress.General_Error.Resize_Failed
+	}
+	return nil
+}

+ 6 - 7
core/image/png/helpers.odin

@@ -242,17 +242,16 @@ srgb :: proc(c: image.PNG_Chunk) -> (res: sRGB, ok: bool) {
 }
 }
 
 
 plte :: proc(c: image.PNG_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 || c.header.length % 3 != 0 || c.header.length > 768 {
 		return {}, false
 		return {}, false
 	}
 	}
 
 
-	i := 0; j := 0; ok = true
-	for j < int(c.header.length) {
-		res.entries[i] = {c.data[j], c.data[j+1], c.data[j+2]}
-		i += 1; j += 3
+	plte := mem.slice_data_cast([]image.RGB_Pixel, c.data[:])
+	for color, i in plte {
+		res.entries[i] = color
 	}
 	}
-	res.used = u16(i)
-	return
+	res.used = u16(len(plte))
+	return res, true
 }
 }
 
 
 splt :: proc(c: image.PNG_Chunk) -> (res: sPLT, ok: bool) {
 splt :: proc(c: image.PNG_Chunk) -> (res: sPLT, ok: bool) {

+ 45 - 30
core/image/png/png.odin

@@ -25,16 +25,13 @@ import "core:io"
 import "core:mem"
 import "core:mem"
 import "core:intrinsics"
 import "core:intrinsics"
 
 
-/*
-	67_108_864 pixels max by default.
-	Maximum allowed dimensions are capped at 65535 * 65535.
-*/
-MAX_DIMENSIONS    :: min(#config(PNG_MAX_DIMENSIONS, 8192 * 8192), 65535 * 65535)
+import "core:fmt"
+
+
+// Limit chunk sizes.
+// By default: IDAT = 8k x 8k x 16-bits + 8k filter bytes.
+// The total number of pixels defaults to 64 Megapixel and can be tuned in image/common.odin.
 
 
-/*
-	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_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         :: (65535 /* Width */ * 65535 /* Height */ * 2 /* 16-bit */) + 65535 /* Filter bytes */
 
 
@@ -64,7 +61,7 @@ Row_Filter :: enum u8 {
 	Paeth   = 4,
 	Paeth   = 4,
 }
 }
 
 
-PLTE_Entry    :: [3]u8
+PLTE_Entry :: image.RGB_Pixel
 
 
 PLTE :: struct #packed {
 PLTE :: struct #packed {
 	entries: [256]PLTE_Entry,
 	entries: [256]PLTE_Entry,
@@ -259,7 +256,7 @@ read_header :: proc(ctx: ^$C) -> (image.PNG_IHDR, Error) {
 	header := (^image.PNG_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 || u128(width) * u128(height) > MAX_DIMENSIONS {
+	if width == 0 || height == 0 || u128(width) * u128(height) > image.MAX_DIMENSIONS {
 		return {}, .Invalid_Image_Dimensions
 		return {}, .Invalid_Image_Dimensions
 	}
 	}
 
 
@@ -366,6 +363,10 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 		options -= {.info}
 		options -= {.info}
 	}
 	}
 
 
+	if .return_header in options && .return_metadata in options {
+		options -= {.return_header}
+	}
+
 	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 {}, compress.General_Error.Incompatible_Options
 		return {}, compress.General_Error.Incompatible_Options
 	}
 	}
@@ -392,7 +393,7 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 
 
 	idat_length := u64(0)
 	idat_length := u64(0)
 
 
-	c:		image.PNG_Chunk
+	c:	image.PNG_Chunk
 	ch:     image.PNG_Chunk_Header
 	ch:     image.PNG_Chunk_Header
 	e:      io.Error
 	e:      io.Error
 
 
@@ -473,6 +474,10 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 			}
 			}
 			info.header = h
 			info.header = h
 
 
+			if .return_header in options && .return_metadata not_in options && .do_not_decompress_image not_in options {
+				return img, nil
+			}
+
 		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.
@@ -540,9 +545,6 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 			seen_iend = true
 			seen_iend = true
 
 
 		case .bKGD:
 		case .bKGD:
-
-			// TODO: Make sure that 16-bit bKGD + tRNS chunks return u16 instead of u16be
-
 			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 {
@@ -594,23 +596,39 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 			*/
 			*/
 
 
 			final_image_channels += 1
 			final_image_channels += 1
-
 			seen_trns = true
 			seen_trns = true
+
+			if .Paletted in header.color_type {
+				if len(c.data) > 256 {
+					fmt.printf("[PLTE] tRNS length: %v\n", len(c.data))
+					return img, .TNRS_Invalid_Length
+				}
+			} else if .Color in header.color_type {
+				if len(c.data) != 6 {
+					fmt.printf("[COLOR] tRNS length: %v\n", len(c.data))
+					return img, .TNRS_Invalid_Length
+				}
+			} else if len(c.data) != 2 {
+				fmt.printf("[GRAY] tRNS length: %v\n", len(c.data))
+				return img, .TNRS_Invalid_Length
+			}
+
 			if info.header.bit_depth < 8 && .Paletted not_in info.header.color_type {
 			if info.header.bit_depth < 8 && .Paletted not_in info.header.color_type {
 				// Rescale tRNS data so key matches intensity
 				// Rescale tRNS data so key matches intensity
-				dsc := depth_scale_table
+				dsc   := depth_scale_table
 				scale := dsc[info.header.bit_depth]
 				scale := dsc[info.header.bit_depth]
 				if scale != 1 {
 				if scale != 1 {
 					key := mem.slice_data_cast([]u16be, c.data)[0] * u16be(scale)
 					key := mem.slice_data_cast([]u16be, c.data)[0] * u16be(scale)
 					c.data = []u8{0, u8(key & 255)}
 					c.data = []u8{0, u8(key & 255)}
 				}
 				}
 			}
 			}
+
 			trns = c
 			trns = c
 
 
-		case .iDOT, .CbGI:
+		case .iDOT, .CgBI:
 			/*
 			/*
 				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 misfortune of coming
 				across one of these files, use a utility to defry it.
 				across one of these files, use a utility to defry it.
 			*/
 			*/
 			return img, .Image_Does_Not_Adhere_to_Spec
 			return img, .Image_Does_Not_Adhere_to_Spec
@@ -635,6 +653,10 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 		return img, .IDAT_Missing
 		return img, .IDAT_Missing
 	}
 	}
 
 
+	if .Paletted in header.color_type && !seen_plte {
+		return img, .PLTE_Missing
+	}
+
 	/*
 	/*
 		Calculate the expected output size, to help `inflate` make better decisions about the output buffer.
 		Calculate the expected output size, to help `inflate` make better decisions about the output buffer.
 		We'll also use it to check the returned buffer size is what we expected it to be.
 		We'll also use it to check the returned buffer size is what we expected it to be.
@@ -683,15 +705,6 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 		return {}, defilter_error
 		return {}, defilter_error
 	}
 	}
 
 
-	/*
-		Now we'll handle the relocoring of paletted images, handling of tRNS chunks,
-		and we'll expand grayscale images to RGB(A).
-
-		For the sake of convenience we return only RGB(A) images. In the future we
-		may supply an option to return Gray/Gray+Alpha as-is, in which case RGB(A)
-		will become the default.
-	*/
-
 	if .Paletted in header.color_type && .do_not_expand_indexed in options {
 	if .Paletted in header.color_type && .do_not_expand_indexed in options {
 		return img, nil
 		return img, nil
 	}
 	}
@@ -699,7 +712,10 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 		return img, nil
 		return img, nil
 	}
 	}
 
 
-
+	/*
+		Now we're going to optionally apply various post-processing stages,
+		to for example expand grayscale, apply a palette, premultiply alpha, etc.
+	*/
 	raw_image_channels := img.channels
 	raw_image_channels := img.channels
 	out_image_channels := 3
 	out_image_channels := 3
 
 
@@ -1204,7 +1220,6 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 	return img, nil
 	return img, nil
 }
 }
 
 
-
 filter_paeth :: #force_inline proc(left, up, up_left: u8) -> u8 {
 filter_paeth :: #force_inline proc(left, up, up_left: u8) -> u8 {
 	aa, bb, cc := i16(left), i16(up), i16(up_left)
 	aa, bb, cc := i16(left), i16(up), i16(up_left)
 	p  := aa + bb - cc
 	p  := aa + bb - cc

+ 407 - 0
core/image/qoi/qoi.odin

@@ -0,0 +1,407 @@
+/*
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	Made available under Odin's BSD-3 license.
+
+	List of contributors:
+		Jeroen van Rijn: Initial implementation.
+*/
+
+
+// package qoi implements a QOI image reader
+//
+// The QOI specification is at https://qoiformat.org.
+package qoi
+
+import "core:mem"
+import "core:image"
+import "core:compress"
+import "core:bytes"
+import "core:os"
+
+Error   :: image.Error
+General :: compress.General_Error
+Image   :: image.Image
+Options :: image.Options
+
+RGB_Pixel  :: image.RGB_Pixel
+RGBA_Pixel :: image.RGBA_Pixel
+
+save_to_memory  :: proc(output: ^bytes.Buffer, img: ^Image, options := Options{}, allocator := context.allocator) -> (err: Error) {
+	context.allocator = allocator
+
+	if img == nil {
+		return .Invalid_Input_Image
+	}
+
+	if output == nil {
+		return .Invalid_Output
+	}
+
+	pixels := img.width * img.height
+	if pixels == 0 || pixels > image.MAX_DIMENSIONS {
+		return .Invalid_Input_Image
+	}
+
+	// QOI supports only 8-bit images with 3 or 4 channels.
+	if img.depth != 8 || img.channels < 3 || img.channels > 4 {
+		return .Invalid_Input_Image
+	}
+
+	if img.channels * pixels != len(img.pixels.buf) {
+		return .Invalid_Input_Image
+	}
+
+	written := 0
+
+	// Calculate and allocate maximum size. We'll reclaim space to actually written output at the end.
+	max_size := pixels * (img.channels + 1) + size_of(image.QOI_Header) + size_of(u64be)
+
+	if !resize(&output.buf, max_size) {
+		return General.Resize_Failed
+	}
+
+	header := image.QOI_Header{
+		magic       = image.QOI_Magic,
+		width       = u32be(img.width),
+		height      = u32be(img.height),
+		channels    = u8(img.channels),
+		color_space = .Linear if .qoi_all_channels_linear in options else .sRGB,
+	}
+	header_bytes := transmute([size_of(image.QOI_Header)]u8)header
+
+	copy(output.buf[written:], header_bytes[:])
+	written += size_of(image.QOI_Header)
+
+	/*
+		Encode loop starts here.
+	*/
+	seen: [64]RGBA_Pixel
+	pix  := RGBA_Pixel{0, 0, 0, 255}
+	prev := pix
+
+	seen[qoi_hash(pix)] = pix
+
+	input := img.pixels.buf[:]
+	run   := u8(0)
+
+	for len(input) > 0 {
+		if img.channels == 4 {
+			pix     = (^RGBA_Pixel)(raw_data(input))^
+		} else {
+			pix.rgb = (^RGB_Pixel)(raw_data(input))^
+		}
+		input = input[img.channels:]
+
+		if pix == prev {
+			run += 1
+			// As long as the pixel matches the last one, accumulate the run total.
+			// If we reach the max run length or the end of the image, write the run.
+			if run == 62 || len(input) == 0 {
+				// Encode and write run
+				output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
+				written += 1
+				run = 0
+			}
+		} else {
+			if run > 0 {
+				// The pixel differs from the previous one, but we still need to write the pending run.
+				// Encode and write run
+				output.buf[written] = u8(QOI_Opcode_Tag.RUN) | (run - 1)
+				written += 1
+				run = 0
+			}
+
+			index := qoi_hash(pix)
+
+			if seen[index] == pix {
+				// Write indexed pixel
+				output.buf[written] = u8(QOI_Opcode_Tag.INDEX) | index
+				written += 1
+			} else {
+				// Add pixel to index
+				seen[index] = pix
+
+				// If the alpha matches the previous pixel's alpha, we don't need to write a full RGBA literal.
+				if pix.a == prev.a {
+					// Delta
+					d  := pix.rgb - prev.rgb
+
+					// DIFF, biased and modulo 256
+					_d := d + 2
+
+					// LUMA, biased and modulo 256
+					_l := RGB_Pixel{ d.r - d.g + 8, d.g + 32, d.b - d.g + 8 }
+
+					if _d.r < 4 && _d.g < 4 && _d.b < 4 {
+						// Delta is between -2 and 1 inclusive
+						output.buf[written] = u8(QOI_Opcode_Tag.DIFF) | _d.r << 4 | _d.g << 2 | _d.b
+						written += 1
+					} else if _l.r < 16 && _l.g < 64 && _l.b < 16 {
+						// Biased luma is between {-8..7, -32..31, -8..7}
+						output.buf[written    ] = u8(QOI_Opcode_Tag.LUMA) | _l.g
+						output.buf[written + 1] = _l.r << 4 | _l.b
+						written += 2
+					} else {
+						// Write RGB literal
+						output.buf[written] = u8(QOI_Opcode_Tag.RGB)
+						pix_bytes := transmute([4]u8)pix
+						copy(output.buf[written + 1:], pix_bytes[:3])
+						written += 4
+					}
+				} else {
+					// Write RGBA literal
+					output.buf[written] = u8(QOI_Opcode_Tag.RGBA)
+					pix_bytes := transmute([4]u8)pix
+					copy(output.buf[written + 1:], pix_bytes[:])
+					written += 5
+				}
+			}
+		}
+		prev = pix
+	}
+
+	trailer := []u8{0, 0, 0, 0, 0, 0, 0, 1}
+	copy(output.buf[written:], trailer[:])
+	written += len(trailer)
+
+	resize(&output.buf, written)
+	return nil
+}
+
+save_to_file :: proc(output: string, img: ^Image, options := Options{}, allocator := context.allocator) -> (err: Error) {
+	context.allocator = allocator
+
+	out := &bytes.Buffer{}
+	defer bytes.buffer_destroy(out)
+
+	save_to_memory(out, img, options) or_return
+	write_ok := os.write_entire_file(output, out.buf[:])
+
+	return nil if write_ok else General.Cannot_Open_File
+}
+
+save :: proc{save_to_memory, save_to_file}
+
+load_from_slice :: proc(slice: []u8, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
+	ctx := &compress.Context_Memory_Input{
+		input_data = slice,
+	}
+
+	img, err = load_from_context(ctx, options, allocator)
+	return img, err
+}
+
+load_from_file :: proc(filename: string, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
+	context.allocator = allocator
+
+	data, ok := os.read_entire_file(filename)
+	defer delete(data)
+
+	if ok {
+		return load_from_slice(data, options)
+	} else {
+		img = new(Image)
+		return img, compress.General_Error.File_Not_Found
+	}
+}
+
+@(optimization_mode="speed")
+load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
+	context.allocator = allocator
+	options := options
+
+	if .alpha_drop_if_present in options || .alpha_premultiply in options {
+		// TODO: Implement.
+		// As stated in image/common, unimplemented options are ignored.
+	}
+
+	if .info in options {
+		options |= {.return_metadata, .do_not_decompress_image}
+		options -= {.info}
+	}
+
+	if .return_header in options && .return_metadata in options {
+		options -= {.return_header}
+	}
+
+	header := image.read_data(ctx, image.QOI_Header) or_return
+	if header.magic != image.QOI_Magic {
+		return img, .Invalid_QOI_Signature
+	}
+
+	if img == nil {
+		img = new(Image)
+	}
+
+	if .return_metadata in options {
+		info := new(image.QOI_Info)
+		info.header  = header
+		img.metadata = info		
+	}
+
+	if header.channels != 3 && header.channels != 4 {
+		return img, .Invalid_Number_Of_Channels
+	}
+
+	if header.color_space != .sRGB && header.color_space != .Linear {
+		return img, .Invalid_Color_Space
+	}
+
+	if header.width == 0 || header.height == 0 {
+		return img, .Invalid_Image_Dimensions
+	}
+
+	total_pixels := header.width * header.height
+	if total_pixels > image.MAX_DIMENSIONS {
+		return img, .Image_Dimensions_Too_Large
+	}
+
+	img.width    = int(header.width)
+	img.height   = int(header.height)
+	img.channels = 4
+	img.depth    = 8
+
+	if .do_not_decompress_image in options {
+		return
+	}
+
+	bytes_needed := image.compute_buffer_size(int(header.width), int(header.height), 4, 8)
+
+	if !resize(&img.pixels.buf, bytes_needed) {
+	 	return img, mem.Allocator_Error.Out_Of_Memory
+	}
+	pixels := mem.slice_data_cast([]RGBA_Pixel, img.pixels.buf[:])
+
+	/*
+		Decode loop starts here.
+	*/
+	seen: [64]RGBA_Pixel
+	pix := RGBA_Pixel{0, 0, 0, 255}
+	seen[qoi_hash(pix)] = pix
+
+	decode: for len(pixels) > 0 {
+		data := image.read_u8(ctx) or_return
+
+		tag := QOI_Opcode_Tag(data)
+		#partial switch tag {
+		case .RGB:
+			pix.rgb = image.read_data(ctx, RGB_Pixel) or_return
+
+			#no_bounds_check {
+				seen[qoi_hash(pix)] = pix	
+			}
+
+		case .RGBA:
+			pix = image.read_data(ctx, RGBA_Pixel) or_return
+
+			#no_bounds_check {
+				seen[qoi_hash(pix)] = pix	
+			}
+
+		case:
+			// 2-bit tag
+			tag = QOI_Opcode_Tag(data & QOI_Opcode_Mask)
+			#partial switch tag {
+				case .INDEX:
+					pix = seen[data & 63]
+
+				case .DIFF:
+					diff_r := ((data >> 4) & 3) - 2
+					diff_g := ((data >> 2) & 3) - 2
+					diff_b := ((data >> 0) & 3) - 2
+
+					pix += {diff_r, diff_g, diff_b, 0}
+
+					#no_bounds_check {
+						seen[qoi_hash(pix)] = pix	
+					}
+
+				case .LUMA:
+					data2 := image.read_u8(ctx) or_return
+
+					diff_g := (data & 63) - 32
+					diff_r := diff_g - 8 + ((data2 >> 4) & 15)
+					diff_b := diff_g - 8 + (data2 & 15)
+
+					pix += {diff_r, diff_g, diff_b, 0}
+
+					#no_bounds_check {
+						seen[qoi_hash(pix)] = pix	
+					}
+
+				case .RUN:
+					if length := int(data & 63) + 1; length > len(pixels) {
+						return img, .Corrupt
+					} else {
+						#no_bounds_check for i in 0..<length {
+							pixels[i] = pix
+						}
+						pixels = pixels[length:]
+					}
+
+					continue decode
+
+				case:
+					unreachable()
+			}
+		}
+
+		#no_bounds_check {
+			pixels[0] = pix
+			pixels = pixels[1:]
+		}
+	}
+
+	// The byte stream's end is marked with 7 0x00 bytes followed by a single 0x01 byte.
+	trailer, trailer_err := compress.read_data(ctx, u64be)
+	if trailer_err != nil || trailer != 0x1 {
+		return img, .Missing_Or_Corrupt_Trailer
+	}
+	return
+}
+
+load :: proc{load_from_file, load_from_slice, load_from_context}
+
+/*
+	Cleanup of image-specific data.
+*/
+destroy :: proc(img: ^Image) {
+	if img == nil {
+		/*
+			Nothing to do.
+			Load must've returned with an error.
+		*/
+		return
+	}
+
+	bytes.buffer_destroy(&img.pixels)
+
+	if v, ok := img.metadata.(^image.QOI_Info); ok {
+	 	free(v)
+	}
+	free(img)
+}
+
+QOI_Opcode_Tag :: enum u8 {
+	// 2-bit tags
+	INDEX = 0b0000_0000, // 6-bit index into color array follows
+	DIFF  = 0b0100_0000, // 3x (RGB) 2-bit difference follows (-2..1), bias of 2.
+	LUMA  = 0b1000_0000, // Luma difference
+	RUN   = 0b1100_0000, // Run length encoding, bias -1
+
+	// 8-bit tags
+	RGB   = 0b1111_1110, // Raw RGB  pixel follows
+	RGBA  = 0b1111_1111, // Raw RGBA pixel follows
+}
+
+QOI_Opcode_Mask :: 0b1100_0000
+QOI_Data_Mask   :: 0b0011_1111
+
+qoi_hash :: #force_inline proc(pixel: RGBA_Pixel) -> (index: u8) {
+	i1 := u16(pixel.r) * 3
+	i2 := u16(pixel.g) * 5
+	i3 := u16(pixel.b) * 7
+	i4 := u16(pixel.a) * 11
+
+	return u8((i1 + i2 + i3 + i4) & 63)
+}

+ 1 - 1
tests/core/image/test_core_image.odin

@@ -1500,7 +1500,7 @@ run_png_suite :: proc(t: ^testing.T, suite: []PNG_Test) -> (subtotal: int) {
 				passed &= dims_pass
 				passed &= dims_pass
 
 
 				hash   := hash.crc32(pixels)
 				hash   := hash.crc32(pixels)
-				error  = fmt.tprintf("%v test %v hash is %08x, expected %08x.", file.file, count, hash, test.hash)
+				error  = fmt.tprintf("%v test %v hash is %08x, expected %08x with %v.", file.file, count, hash, test.hash, test.options)
 				expect(t, test.hash == hash, error)
 				expect(t, test.hash == hash, error)
 
 
 				passed &= test.hash == hash
 				passed &= test.hash == hash