diff options
| author | Jeroen van Rijn <Kelimion@users.noreply.github.com> | 2024-05-18 19:41:07 +0200 |
|---|---|---|
| committer | Jeroen van Rijn <Kelimion@users.noreply.github.com> | 2024-05-18 19:41:07 +0200 |
| commit | 58a1bb32e5808ec0b92bfa7dc989801eeacc3137 (patch) | |
| tree | eb86a5798d601cdffa5e6e4ae845e1bebf4dde76 /core/image/qoi | |
| parent | 5b92425e93b198f029d5bcf2e7e8dc9f2563c21c (diff) | |
Apply fix to QOI decoder as well.
Diffstat (limited to 'core/image/qoi')
| -rw-r--r-- | core/image/qoi/qoi.odin | 755 |
1 files changed, 377 insertions, 378 deletions
diff --git a/core/image/qoi/qoi.odin b/core/image/qoi/qoi.odin index 987a48e5b..061943f68 100644 --- a/core/image/qoi/qoi.odin +++ b/core/image/qoi/qoi.odin @@ -1,379 +1,378 @@ -/*
- 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:image"
-import "core:compress"
-import "core:bytes"
-
-Error :: image.Error
-Image :: image.Image
-Options :: image.Options
-
-RGB_Pixel :: image.RGB_Pixel
-RGBA_Pixel :: image.RGBA_Pixel
-
-save_to_buffer :: 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) != nil {
- return .Unable_To_Allocate_Or_Resize
- }
-
- 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
-
- 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
-}
-
-load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
- ctx := &compress.Context_Memory_Input{
- input_data = data,
- }
-
- img, err = load_from_context(ctx, options, allocator)
- return img, err
-}
-
-@(optimization_mode="speed")
-load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) {
- context.allocator = allocator
- options := options
-
- 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_Signature
- }
-
- if img == nil {
- img = new(Image)
- }
- img.which = .QOI
-
- 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 if .alpha_add_if_missing in options else int(header.channels)
- img.depth = 8
-
- if .do_not_decompress_image in options {
- img.channels = int(header.channels)
- return
- }
-
- bytes_needed := image.compute_buffer_size(int(header.width), int(header.height), img.channels, 8)
-
- if resize(&img.pixels.buf, bytes_needed) != nil {
- return img, .Unable_To_Allocate_Or_Resize
- }
-
- /*
- Decode loop starts here.
- */
- seen: [64]RGBA_Pixel
- pix := RGBA_Pixel{0, 0, 0, 255}
- seen[qoi_hash(pix)] = pix
- pixels := img.pixels.buf[:]
-
- 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 * img.channels) > len(pixels) {
- return img, .Corrupt
- } else {
- #no_bounds_check for _ in 0..<length {
- copy(pixels, pix[:img.channels])
- pixels = pixels[img.channels:]
- }
- }
-
- continue decode
-
- case:
- unreachable()
- }
- }
-
- #no_bounds_check {
- copy(pixels, pix[:img.channels])
- pixels = pixels[img.channels:]
- }
- }
-
- // 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
- }
-
- if .alpha_premultiply in options && !image.alpha_drop_if_present(img, options) {
- return img, .Post_Processing_Error
- }
-
- return
-}
-
-/*
- 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)
-}
-
-@(init, private)
-_register :: proc() {
- image.register(.QOI, load_from_bytes, destroy)
+/* + 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:image" +import "core:compress" +import "core:bytes" + +Error :: image.Error +Image :: image.Image +Options :: image.Options + +RGB_Pixel :: image.RGB_Pixel +RGBA_Pixel :: image.RGBA_Pixel + +save_to_buffer :: 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) != nil { + return .Unable_To_Allocate_Or_Resize + } + + 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 + + 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 +} + +load_from_bytes :: proc(data: []byte, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) { + ctx := &compress.Context_Memory_Input{ + input_data = data, + } + + img, err = load_from_context(ctx, options, allocator) + return img, err +} + +@(optimization_mode="speed") +load_from_context :: proc(ctx: ^$C, options := Options{}, allocator := context.allocator) -> (img: ^Image, err: Error) { + context.allocator = allocator + options := options + + 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_Signature + } + + if img == nil { + img = new(Image) + } + img.which = .QOI + + 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 if .alpha_add_if_missing in options else int(header.channels) + img.depth = 8 + + if .do_not_decompress_image in options { + img.channels = int(header.channels) + return + } + + bytes_needed := image.compute_buffer_size(int(header.width), int(header.height), img.channels, 8) + + if resize(&img.pixels.buf, bytes_needed) != nil { + return img, .Unable_To_Allocate_Or_Resize + } + + /* + Decode loop starts here. + */ + seen: [64]RGBA_Pixel + pix := RGBA_Pixel{0, 0, 0, 255} + pixels := img.pixels.buf[:] + + 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 * img.channels) > len(pixels) { + return img, .Corrupt + } else { + #no_bounds_check for _ in 0..<length { + copy(pixels, pix[:img.channels]) + pixels = pixels[img.channels:] + } + } + + continue decode + + case: + unreachable() + } + } + + #no_bounds_check { + copy(pixels, pix[:img.channels]) + pixels = pixels[img.channels:] + } + } + + // 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 + } + + if .alpha_premultiply in options && !image.alpha_drop_if_present(img, options) { + return img, .Post_Processing_Error + } + + return +} + +/* + 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) +} + +@(init, private) +_register :: proc() { + image.register(.QOI, load_from_bytes, destroy) }
\ No newline at end of file |