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 . - 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.. (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 . + 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.. (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