From 7d534769d6437cb63eeb4718bc8ed36ffef937f9 Mon Sep 17 00:00:00 2001 From: Jeroen van Rijn Date: Sun, 2 May 2021 20:38:30 +0200 Subject: [PATCH] Add new PNG post processing options. --- core/compress/common.odin | 2 +- core/image/common.odin | 129 +++++++++++++++++++++++++++++++++----- core/image/png/png.odin | 29 ++++++++- 3 files changed, 143 insertions(+), 17 deletions(-) diff --git a/core/compress/common.odin b/core/compress/common.odin index 11b5b74b3..ffdc2d208 100644 --- a/core/compress/common.odin +++ b/core/compress/common.odin @@ -19,7 +19,7 @@ Error :: union { This is here because png.load will return a this type of error union, as it may involve an I/O error, a Deflate error, etc. */ - image.PNG_Error, + image.Error, } General_Error :: enum { diff --git a/core/image/common.odin b/core/image/common.odin index d05feabaa..6630dfd5b 100644 --- a/core/image/common.odin +++ b/core/image/common.odin @@ -1,6 +1,9 @@ package image import "core:bytes" +import "core:mem" + +import "core:fmt" Image :: struct { width: int, @@ -17,49 +20,69 @@ Image :: struct { sidecar: any, } +/* + 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_ihdr` and `do_not_decompress_image` and can be used + This option behaves as `.return_ihdr` and `.do_not_decompress_image` and can be used to gather an image's dimensions and color information. `.return_header` Fill out img.sidecar.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`, which works as if both of these were set. + 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. `.return_metadata` Returns all chunks not needed to decode the data. - It also returns the header as if `.return_header` is set. + It also returns the header as if `.return_header` was set. - `do_not_decompress_image` + `.do_not_decompress_image` Skip decompressing IDAT chunk, defiltering and the rest. - `alpha_add_if_missing` + `.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` + `.alpha_drop_if_present` If the image has an alpha channel, drop it. - You may want to use `alpha_premultiply` in this case. + 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` + `.alpha_premultiply` If the image has an alpha channel, returns image data as follows: RGB *= A, Gray = Gray *= A - `blend_background` + `.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 + 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`. + you also specify `.alpha_add_if_missing`. Options that don't apply to an image format will be ignored by their loader. */ @@ -73,10 +96,14 @@ Option :: enum { alpha_drop_if_present, alpha_premultiply, blend_background, + // Unimplemented + do_not_expand_grayscale, + do_not_expand_indexed, + do_not_expand_channels, } Options :: distinct bit_set[Option]; -PNG_Error :: enum { +Error :: enum { Invalid_PNG_Signature, IHDR_Not_First_Chunk, IHDR_Corrupt, @@ -93,9 +120,10 @@ PNG_Error :: enum { Invalid_Color_Bit_Depth_Combo, Unknown_Filter_Method, Unknown_Interlace_Method, + Requested_Channel_Not_Present, + Post_Processing_Error, } - /* Functions to help with image buffer calculations */ @@ -104,4 +132,77 @@ compute_buffer_size :: proc(width, height, channels, depth: int, extra_row_bytes size = ((((channels * width * depth) + 7) >> 3) + extra_row_bytes) * height; return; +} + +/* + For when you have an RGB(A) image, but want a particular channel. +*/ + +Channel :: enum u8 { + R = 1, + G = 2, + B = 3, + A = 4, +} + +return_single_channel :: proc(img: ^Image, channel: Channel) -> (res: ^Image, ok: bool) { + + ok = false; + t: bytes.Buffer; + + idx := int(channel); + + if idx > img.channels { + return {}, false; + } + + if img.channels == 2 && idx == 4 { + // Alpha requested, which in a two channel image is index 2: G. + idx = 2; + } + + 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, 2, 8); + 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.sidecar = img.sidecar; + + fmt.println(t); + + return res, true; } \ No newline at end of file diff --git a/core/image/png/png.odin b/core/image/png/png.odin index 41ea8e625..912b31300 100644 --- a/core/image/png/png.odin +++ b/core/image/png/png.odin @@ -14,7 +14,7 @@ import "core:intrinsics" Error :: compress.Error; E_General :: compress.General_Error; -E_PNG :: image.PNG_Error; +E_PNG :: image.Error; E_Deflate :: compress.Deflate_Error; is_kind :: compress.is_kind; @@ -382,13 +382,17 @@ load_from_stream :: proc(stream: io.Stream, options := Options{}, allocator := c options := options; if .info in options { options |= {.return_metadata, .do_not_decompress_image}; - options ~= {.info}; + options -= {.info}; } if .alpha_drop_if_present in options && .alpha_add_if_missing in options { return {}, E_General.Incompatible_Options; } + if .do_not_expand_channels in options { + options |= {.do_not_expand_grayscale, .do_not_expand_indexed}; + } + if img == nil { img = new(Image); } @@ -723,6 +727,14 @@ load_from_stream :: proc(stream: io.Stream, options := Options{}, allocator := c will become the default. */ + if .Paletted in header.color_type && .do_not_expand_indexed in options { + return img, E_General.OK; + } + if .Color not_in header.color_type && .do_not_expand_grayscale in options { + return img, E_General.OK; + } + + raw_image_channels := img.channels; out_image_channels := 3; @@ -1218,6 +1230,19 @@ load_from_stream :: proc(stream: io.Stream, options := Options{}, allocator := c unreachable("We should never see bit depths other than 8, 16 and 'Paletted' here."); } + // TODO: Rather than first expanding to RGB(A) and then dropping channels, give these their own path. + if .do_not_expand_grayscale in options && .Color not_in info.header.color_type { + + single, single_ok := image.return_single_channel(img, .R); + if single_ok { + destroy(img); + img = single; + } else { + destroy(single); + return img, E_PNG.Post_Processing_Error; + } + } + return img, E_General.OK; }