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,
 }
 
-
 // General I/O context for ZLIB, LZW, etc.
 Context_Memory_Input :: struct #packed {
 	input_data:        []u8,
@@ -151,7 +150,6 @@ when size_of(rawptr) == 8 {
 	#assert(size_of(Context_Memory_Input) == 52)
 }
 
-
 Context_Stream_Input :: struct #packed {
 	input_data:        []u8,
 	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.
 */
 
-// TODO: Make these return compress.Error errors.
-
 input_size_from_memory :: proc(z: ^Context_Memory_Input) -> (res: i64, err: Error) {
 	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: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 {
 	width:         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
 		in our viewer, and can just test against nil.
 	*/
-	background:    Maybe([3]u16),
-
+	background:    Maybe(RGB_Pixel_16),
 	metadata:      Image_Metadata,
 }
 
 Image_Metadata :: union {
 	^PNG_Info,
+	^QOI_Info,
 }
 
+
+
 /*
 	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`,
@@ -46,13 +74,13 @@ Image_Metadata :: union {
 /*
 Image_Option:
 	`.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.
 
 	`.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` +
-		`.do_not_decompress_image`, or `.info`, which works as if both of these were set.
+		`.do_not_decompress_image`, or `.info`.
 
 	`.return_metadata`
 		Returns all chunks not needed to decode the data.
@@ -88,7 +116,7 @@ Image_Option:
 
 	`.alpha_premultiply`
 		If the image has an alpha channel, returns image data as follows:
-			RGB  *= A, Gray = Gray *= A
+			RGB *= A, Gray = Gray *= A
 
 	`.blend_background`
 		If a bKGD chunk is present in a PNG, we normally just set `img.background`
@@ -103,24 +131,29 @@ Image_Option:
 */
 
 Option :: enum {
+	// LOAD OPTIONS
 	info = 0,
 	do_not_decompress_image,
 	return_header,
 	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
 	do_not_expand_grayscale,
 	do_not_expand_indexed,
 	do_not_expand_channels,
+
+	// SAVE OPTIONS
+	qoi_all_channels_linear,       // QOI, informative info. If not set, defaults to sRGB with linear alpha.
 }
 Options :: distinct bit_set[Option]
 
 Error :: union #shared_nil {
 	General_Image_Error,
 	PNG_Error,
+	QOI_Error,
 
 	compress.Error,
 	compress.General_Error,
@@ -134,8 +167,13 @@ General_Image_Error :: enum {
 	Invalid_Image_Dimensions,
 	Image_Dimensions_Too_Large,
 	Image_Does_Not_Adhere_to_Spec,
+	Invalid_Input_Image,
+	Invalid_Output,
 }
 
+/*
+	PNG-specific definitions
+*/
 PNG_Error :: enum {
 	None = 0,
 	Invalid_PNG_Signature,
@@ -147,7 +185,9 @@ PNG_Error :: enum {
 	IDAT_Size_Too_Large,
 	PLTE_Encountered_Unexpectedly,
 	PLTE_Invalid_Length,
+	PLTE_Missing,
 	TRNS_Encountered_Unexpectedly,
+	TNRS_Invalid_Length,
 	BKGD_Invalid_Length,
 	Unknown_Color_Type,
 	Invalid_Color_Bit_Depth_Combo,
@@ -158,9 +198,6 @@ PNG_Error :: enum {
 	Invalid_Chunk_Length,
 }
 
-/*
-	PNG-specific structs
-*/
 PNG_Info :: struct {
 	header: PNG_IHDR,
 	chunks: [dynamic]PNG_Chunk,
@@ -223,7 +260,7 @@ PNG_Chunk_Type :: enum u32be {
 
 	*/
 	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 {
@@ -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) {
 	size = ((((channels * width * depth) + 7) >> 3) + extra_row_bytes) * height
 	return
 }
 
-/*
-	For when you have an RGB(A) image, but want a particular channel.
-*/
 Channel :: enum u8 {
 	R = 1,
 	G = 2,
@@ -268,7 +333,13 @@ Channel :: enum u8 {
 	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) {
+	// Were we actually given a valid image?
+	if img == nil {
+		return nil, false
+	}
+
 	ok = false
 	t: bytes.Buffer
 
@@ -298,7 +369,7 @@ return_single_channel :: proc(img: ^Image, channel: Channel) -> (res: ^Image, ok
 			o = o[1:]
 		}
 	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{}
 		resize(&t.buf, buffer_size)
 
@@ -326,3 +397,724 @@ return_single_channel :: proc(img: ^Image, channel: Channel) -> (res: ^Image, ok
 
 	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) {
-	if c.header.type != .PLTE {
+	if c.header.type != .PLTE || c.header.length % 3 != 0 || c.header.length > 768 {
 		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) {

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

@@ -25,16 +25,13 @@ import "core:io"
 import "core:mem"
 import "core:intrinsics"
 
-/*
-	67_108_864 pixels max by default.
-	Maximum allowed dimensions are capped at 65535 * 65535.
-*/
-MAX_DIMENSIONS    :: min(#config(PNG_MAX_DIMENSIONS, 8192 * 8192), 65535 * 65535)
+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         :: (65535 /* Width */ * 65535 /* Height */ * 2 /* 16-bit */) + 65535 /* Filter bytes */
 
@@ -64,7 +61,7 @@ Row_Filter :: enum u8 {
 	Paeth   = 4,
 }
 
-PLTE_Entry    :: [3]u8
+PLTE_Entry :: image.RGB_Pixel
 
 PLTE :: struct #packed {
 	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))^
 	// Validate IHDR
 	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
 	}
 
@@ -366,6 +363,10 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 		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 {
 		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)
 
-	c:		image.PNG_Chunk
+	c:	image.PNG_Chunk
 	ch:     image.PNG_Chunk_Header
 	e:      io.Error
 
@@ -473,6 +474,10 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 			}
 			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:
 			seen_plte = true
 			// 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
 
 		case .bKGD:
-
-			// TODO: Make sure that 16-bit bKGD + tRNS chunks return u16 instead of u16be
-
 			c = read_chunk(ctx) or_return
 			seen_bkgd = true
 			if .return_metadata in options {
@@ -594,23 +596,39 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 			*/
 
 			final_image_channels += 1
-
 			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 {
 				// Rescale tRNS data so key matches intensity
-				dsc := depth_scale_table
+				dsc   := depth_scale_table
 				scale := dsc[info.header.bit_depth]
 				if scale != 1 {
 					key := mem.slice_data_cast([]u16be, c.data)[0] * u16be(scale)
 					c.data = []u8{0, u8(key & 255)}
 				}
 			}
+
 			trns = c
 
-		case .iDOT, .CbGI:
+		case .iDOT, .CgBI:
 			/*
 				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.
 			*/
 			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
 	}
 
+	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.
 		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
 	}
 
-	/*
-		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 {
 		return img, nil
 	}
@@ -699,7 +712,10 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 		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
 	out_image_channels := 3
 
@@ -1204,7 +1220,6 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
 	return img, nil
 }
 
-
 filter_paeth :: #force_inline proc(left, up, up_left: u8) -> u8 {
 	aa, bb, cc := i16(left), i16(up), i16(up_left)
 	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
 
 				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)
 
 				passed &= test.hash == hash