[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.
This commit is contained in:
Jeroen van Rijn
2022-04-04 15:39:42 +02:00
parent f2f1330238
commit 15b440c4f1
6 changed files with 1270 additions and 61 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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
core/image/qoi/qoi.odin Normal file
View File

@@ -0,0 +1,407 @@
/*
Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
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)
}

View File

@@ -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