Apply fix to QOI decoder as well.

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