/* Copyright 2021 Jeroen van Rijn . Made available under Odin's BSD-3 license. List of contributors: Jeroen van Rijn: Initial implementation, optimization. Ginger Bill: Cosmetic changes. */ // package image implements a general 2D image library to be used with other image related packages package image import "core:bytes" import "core:mem" import "core:compress" import "core:runtime" /* 67_108_864 pixels max by default. For QOI, the Worst case scenario means all pixels will be encoded as RGBA literals, costing 5 bytes each. This caps memory usage at 320 MiB. The tunable is limited to 4_294_836_225 pixels maximum, or 4 GiB per 8-bit channel. It is not advised to tune it this large. The 64 Megapixel default is considered to be a decent upper bound you won't run into in practice, except in very specific circumstances. */ MAX_DIMENSIONS :: min(#config(MAX_DIMENSIONS, 8192 * 8192), 65535 * 65535) // Color RGB_Pixel :: [3]u8 RGBA_Pixel :: [4]u8 RGB_Pixel_16 :: [3]u16 RGBA_Pixel_16 :: [4]u16 // Grayscale G_Pixel :: [1]u8 GA_Pixel :: [2]u8 G_Pixel_16 :: [1]u16 GA_Pixel_16 :: [2]u16 Image :: struct { width: int, height: int, channels: int, depth: int, // Channel depth in bits, typically 8 or 16 pixels: bytes.Buffer `fmt:"-"`, /* Some image loaders/writers can return/take an optional background color. For convenience, we return them as u16 so we don't need to switch on the type in our viewer, and can just test against nil. */ background: Maybe(RGB_Pixel_16), metadata: Image_Metadata, which: Which_File_Type, } Image_Metadata :: union #shared_nil { ^Netpbm_Info, ^PNG_Info, ^QOI_Info, ^TGA_Info, } /* IMPORTANT: `.do_not_expand_*` options currently skip handling of the `alpha_*` options, therefore Gray+Alpha will be returned as such even if you add `.alpha_drop_if_present`, and `.alpha_add_if_missing` and keyed transparency will likewise be ignored. The same goes for indexed images. This will be remedied in a near future update. */ /* Image_Option: `.info` This option behaves as `.return_metadata` and `.do_not_decompress_image` and can be used to gather an image's dimensions and color information. `.return_header` Fill out img.metadata.header with the image's format-specific header struct. If we only care about the image specs, we can set `.return_header` + `.do_not_decompress_image`, or `.info`. `.return_metadata` Returns all chunks not needed to decode the data. It also returns the header as if `.return_header` was set. `.do_not_decompress_image` Skip decompressing IDAT chunk, defiltering and the rest. `.do_not_expand_grayscale` Do not turn grayscale (+ Alpha) images into RGB(A). Returns just the 1 or 2 channels present, although 1, 2 and 4 bit are still scaled to 8-bit. `.do_not_expand_indexed` Do not turn indexed (+ Alpha) images into RGB(A). Returns just the 1 or 2 (with `tRNS`) channels present. Make sure to use `return_metadata` to also return the palette chunk so you can recolor it yourself. `.do_not_expand_channels` Applies both `.do_not_expand_grayscale` and `.do_not_expand_indexed`. `.alpha_add_if_missing` If the image has no alpha channel, it'll add one set to max(type). Turns RGB into RGBA and Gray into Gray+Alpha `.alpha_drop_if_present` If the image has an alpha channel, drop it. You may want to use `.alpha_premultiply` in this case. NOTE: For PNG, this also skips handling of the tRNS chunk, if present, unless you select `alpha_premultiply`. In this case it'll premultiply the specified pixels in question only, as the others are implicitly fully opaque. `.alpha_premultiply` If the image has an alpha channel, returns image data as follows: RGB *= A, Gray = Gray *= A `.blend_background` If a bKGD chunk is present in a PNG, we normally just set `img.background` with its value and leave it up to the application to decide how to display the image, as per the PNG specification. With `.blend_background` selected, we blend the image against the background color. As this negates the use for an alpha channel, we'll drop it _unless_ you also specify `.alpha_add_if_missing`. Options that don't apply to an image format will be ignored by their loader. */ Option :: enum { // LOAD OPTIONS info = 0, do_not_decompress_image, return_header, return_metadata, alpha_add_if_missing, // Ignored for QOI. Always returns RGBA8. alpha_drop_if_present, // Unimplemented for QOI. Returns error. alpha_premultiply, // Unimplemented for QOI. Returns error. blend_background, // Ignored for non-PNG formats // Unimplemented do_not_expand_grayscale, do_not_expand_indexed, do_not_expand_channels, // SAVE OPTIONS qoi_all_channels_linear, // QOI, informative only. If not set, defaults to sRGB with linear alpha. } Options :: distinct bit_set[Option] Error :: union #shared_nil { General_Image_Error, Netpbm_Error, PNG_Error, QOI_Error, compress.Error, compress.General_Error, compress.Deflate_Error, compress.ZLIB_Error, runtime.Allocator_Error, } General_Image_Error :: enum { None = 0, Unsupported_Option, // File I/O Unable_To_Read_File, Unable_To_Write_File, // Invalid Unsupported_Format, Invalid_Signature, Invalid_Input_Image, Image_Dimensions_Too_Large, Invalid_Image_Dimensions, Invalid_Number_Of_Channels, Image_Does_Not_Adhere_to_Spec, Invalid_Image_Depth, Invalid_Bit_Depth, Invalid_Color_Space, // More data than pixels to decode into, for example. Corrupt, // Output buffer is the wrong size Invalid_Output, // Allocation Unable_To_Allocate_Or_Resize, } /* Netpbm-specific definitions */ Netpbm_Format :: enum { P1, P2, P3, P4, P5, P6, P7, Pf, PF, } Netpbm_Header :: struct { format: Netpbm_Format, width: int, height: int, channels: int, depth: int, maxval: int, tupltype: string, scale: f32, little_endian: bool, } Netpbm_Info :: struct { header: Netpbm_Header, } Netpbm_Error :: enum { None = 0, // reading Invalid_Header_Token_Character, Incomplete_Header, Invalid_Header_Value, Duplicate_Header_Field, Buffer_Too_Small, Invalid_Buffer_ASCII_Token, Invalid_Buffer_Value, // writing Invalid_Format, } /* PNG-specific definitions */ PNG_Error :: enum { None = 0, IHDR_Not_First_Chunk, IHDR_Corrupt, IDAT_Missing, IDAT_Must_Be_Contiguous, IDAT_Corrupt, IDAT_Size_Too_Large, PLTE_Encountered_Unexpectedly, PLTE_Invalid_Length, PLTE_Missing, TRNS_Encountered_Unexpectedly, TNRS_Invalid_Length, BKGD_Invalid_Length, Unknown_Color_Type, Invalid_Color_Bit_Depth_Combo, Unknown_Filter_Method, Unknown_Interlace_Method, Requested_Channel_Not_Present, Post_Processing_Error, Invalid_Chunk_Length, } PNG_Info :: struct { header: PNG_IHDR, chunks: [dynamic]PNG_Chunk, } PNG_Chunk_Header :: struct #packed { length: u32be, type: PNG_Chunk_Type, } PNG_Chunk :: struct #packed { header: PNG_Chunk_Header, data: []byte, crc: u32be, } PNG_Chunk_Type :: enum u32be { // IHDR must come first in a file IHDR = 'I' << 24 | 'H' << 16 | 'D' << 8 | 'R', // PLTE must precede the first IDAT chunk PLTE = 'P' << 24 | 'L' << 16 | 'T' << 8 | 'E', bKGD = 'b' << 24 | 'K' << 16 | 'G' << 8 | 'D', tRNS = 't' << 24 | 'R' << 16 | 'N' << 8 | 'S', IDAT = 'I' << 24 | 'D' << 16 | 'A' << 8 | 'T', iTXt = 'i' << 24 | 'T' << 16 | 'X' << 8 | 't', tEXt = 't' << 24 | 'E' << 16 | 'X' << 8 | 't', zTXt = 'z' << 24 | 'T' << 16 | 'X' << 8 | 't', iCCP = 'i' << 24 | 'C' << 16 | 'C' << 8 | 'P', pHYs = 'p' << 24 | 'H' << 16 | 'Y' << 8 | 's', gAMA = 'g' << 24 | 'A' << 16 | 'M' << 8 | 'A', tIME = 't' << 24 | 'I' << 16 | 'M' << 8 | 'E', sPLT = 's' << 24 | 'P' << 16 | 'L' << 8 | 'T', sRGB = 's' << 24 | 'R' << 16 | 'G' << 8 | 'B', hIST = 'h' << 24 | 'I' << 16 | 'S' << 8 | 'T', cHRM = 'c' << 24 | 'H' << 16 | 'R' << 8 | 'M', sBIT = 's' << 24 | 'B' << 16 | 'I' << 8 | 'T', /* eXIf tags are not part of the core spec, but have been ratified in v1.5.0 of the PNG Ext register. We will provide unprocessed chunks to the caller if `.return_metadata` is set. Applications are free to implement an Exif decoder. */ eXIf = 'e' << 24 | 'X' << 16 | 'I' << 8 | 'f', // PNG files must end with IEND IEND = 'I' << 24 | 'E' << 16 | 'N' << 8 | 'D', /* XCode sometimes produces "PNG" files that don't adhere to the PNG spec. We recognize them only in order to avoid doing further work on them. Some tools like PNG Defry may be able to repair them, but we're not going to reward Apple for producing proprietary broken files purporting to be PNGs by supporting them. */ iDOT = 'i' << 24 | 'D' << 16 | 'O' << 8 | 'T', CgBI = 'C' << 24 | 'g' << 16 | 'B' << 8 | 'I', } PNG_IHDR :: struct #packed { width: u32be, height: u32be, bit_depth: u8, color_type: PNG_Color_Type, compression_method: u8, filter_method: u8, interlace_method: PNG_Interlace_Method, } PNG_IHDR_SIZE :: size_of(PNG_IHDR) #assert (PNG_IHDR_SIZE == 13) PNG_Color_Value :: enum u8 { Paletted = 0, // 1 << 0 = 1 Color = 1, // 1 << 1 = 2 Alpha = 2, // 1 << 2 = 4 } PNG_Color_Type :: distinct bit_set[PNG_Color_Value; u8] PNG_Interlace_Method :: enum u8 { None = 0, Adam7 = 1, } /* QOI-specific definitions */ QOI_Error :: enum { None = 0, 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, } TGA_Data_Type :: enum u8 { No_Image_Data = 0, Uncompressed_Color_Mapped = 1, Uncompressed_RGB = 2, Uncompressed_Black_White = 3, Compressed_Color_Mapped = 9, Compressed_RGB = 10, Compressed_Black_White = 11, } TGA_Header :: struct #packed { id_length: u8, color_map_type: u8, data_type_code: TGA_Data_Type, color_map_origin: u16le, color_map_length: u16le, color_map_depth: u8, origin: [2]u16le, dimensions: [2]u16le, bits_per_pixel: u8, image_descriptor: u8, } #assert(size_of(TGA_Header) == 18) New_TGA_Signature :: "TRUEVISION-XFILE.\x00" TGA_Footer :: struct #packed { extension_area_offset: u32le, developer_directory_offset: u32le, signature: [18]u8 `fmt:"s,0"`, // Should match signature if New TGA. } #assert(size_of(TGA_Footer) == 26) TGA_Extension :: struct #packed { extension_size: u16le, // Size of this struct. If not 495 bytes it means it's an unsupported version. author_name: [41]u8 `fmt:"s,0"`, // Author name, ASCII. Zero-terminated author_comments: [324]u8 `fmt:"s,0"`, // Author comments, formatted as 4 lines of 80 character lines, each zero terminated. datetime: struct {month, day, year, hour, minute, second: u16le}, job_name: [41]u8 `fmt:"s,0"`, // Author name, ASCII. Zero-terminated job_time: struct {hour, minute, second: u16le}, software_id: [41]u8 `fmt:"s,0"`, // Software ID name, ASCII. Zero-terminated software_version: struct #packed { number: u16le, // Version number * 100 letter: u8 `fmt:"r"`, // " " if not used }, key_color: [4]u8, // ARGB key color used at time of production aspect_ratio: [2]u16le, // Numerator / Denominator gamma: [2]u16le, // Numerator / Denominator, range should be 0.0..10.0 color_correction_offset: u32le, // 0 if no color correction information postage_stamp_offset: u32le, // 0 if no thumbnail scanline_offset: u32le, // 0 if no scanline table attributes: TGA_Alpha_Kind, } #assert(size_of(TGA_Extension) == 495) TGA_Alpha_Kind :: enum u8 { None, Undefined_Ignore, Undefined_Retain, Useful, Premultiplied, } TGA_Info :: struct { header: TGA_Header, image_id: string, footer: Maybe(TGA_Footer), extension: Maybe(TGA_Extension), } // Function to help with image buffer calculations compute_buffer_size :: proc(width, height, channels, depth: int, extra_row_bytes := int(0)) -> (size: int) { size = ((((channels * width * depth) + 7) >> 3) + extra_row_bytes) * height return } Channel :: enum u8 { R = 1, G = 2, B = 3, A = 4, } // When you have an RGB(A) image, but want a particular channel. return_single_channel :: proc(img: ^Image, channel: Channel) -> (res: ^Image, ok: bool) { // Were we actually given a valid image? if img == nil { return nil, false } ok = false t: bytes.Buffer idx := int(channel) if img.channels == 2 && idx == 4 { // Alpha requested, which in a two channel image is index 2: G. idx = 2 } if idx > img.channels { return {}, false } switch img.depth { case 8: buffer_size := compute_buffer_size(img.width, img.height, 1, 8) t = bytes.Buffer{} resize(&t.buf, buffer_size) i := bytes.buffer_to_bytes(&img.pixels) o := bytes.buffer_to_bytes(&t) for len(i) > 0 { o[0] = i[idx] i = i[img.channels:] o = o[1:] } case 16: buffer_size := compute_buffer_size(img.width, img.height, 1, 16) t = bytes.Buffer{} resize(&t.buf, buffer_size) i := mem.slice_data_cast([]u16, img.pixels.buf[:]) o := mem.slice_data_cast([]u16, t.buf[:]) for len(i) > 0 { o[0] = i[idx] i = i[img.channels:] o = o[1:] } case 1, 2, 4: // We shouldn't see this case, as the loader already turns these into 8-bit. return {}, false } res = new(Image) res.width = img.width res.height = img.height res.channels = 1 res.depth = img.depth res.pixels = t res.background = img.background res.metadata = img.metadata return res, true } // Does the image have 1 or 2 channels, a valid bit depth (8 or 16), // Is the pointer valid, are the dimensions 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 dimensions 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 dimensions 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 dimensions 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 dimensions 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) != nil { 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) != nil { 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) != nil { 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) != nil { 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) != nil { 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 .Resize_Failed } } else if n, _ := bytes.buffer_write(buf, data); n != len(data) { return .Resize_Failed } return nil }