mirror of
https://github.com/Ed94/Odin.git
synced 2026-08-06 15:48:51 +00:00
[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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+810
-18
@@ -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.
|
||||||
@@ -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_add_if_missing, // Ignored for QOI. Always returns RGBA8.
|
||||||
alpha_drop_if_present,
|
alpha_drop_if_present, // Unimplemented for QOI. Returns error.
|
||||||
alpha_premultiply,
|
alpha_premultiply, // Unimplemented for QOI. Returns error.
|
||||||
blend_background,
|
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
|
||||||
|
}
|
||||||
@@ -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
|
plte := mem.slice_data_cast([]image.RGB_Pixel, c.data[:])
|
||||||
for j < int(c.header.length) {
|
for color, i in plte {
|
||||||
res.entries[i] = {c.data[j], c.data[j+1], c.data[j+2]}
|
res.entries[i] = color
|
||||||
i += 1; j += 3
|
|
||||||
}
|
}
|
||||||
res.used = u16(i)
|
res.used = u16(len(plte))
|
||||||
return
|
return res, true
|
||||||
}
|
}
|
||||||
|
|
||||||
splt :: proc(c: image.PNG_Chunk) -> (res: sPLT, ok: bool) {
|
splt :: proc(c: image.PNG_Chunk) -> (res: sPLT, ok: bool) {
|
||||||
|
|||||||
+43
-28
@@ -25,16 +25,13 @@ import "core:io"
|
|||||||
import "core:mem"
|
import "core:mem"
|
||||||
import "core:intrinsics"
|
import "core:intrinsics"
|
||||||
|
|
||||||
/*
|
import "core:fmt"
|
||||||
67_108_864 pixels max by default.
|
|
||||||
Maximum allowed dimensions are capped at 65535 * 65535.
|
|
||||||
*/
|
// Limit chunk sizes.
|
||||||
MAX_DIMENSIONS :: min(#config(PNG_MAX_DIMENSIONS, 8192 * 8192), 65535 * 65535)
|
// 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
|
||||||
}
|
}
|
||||||
@@ -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,8 +596,23 @@ 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
|
||||||
@@ -605,12 +622,13 @@ load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.a
|
|||||||
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user