Add new PNG post processing options.

This commit is contained in:
Jeroen van Rijn
2021-05-02 20:38:30 +02:00
parent 0a81fcc2af
commit 7d534769d6
3 changed files with 143 additions and 17 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ Error :: union {
This is here because png.load will return a this type of 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. as it may involve an I/O error, a Deflate error, etc.
*/ */
image.PNG_Error, image.Error,
} }
General_Error :: enum { General_Error :: enum {
+115 -14
View File
@@ -1,6 +1,9 @@
package image package image
import "core:bytes" import "core:bytes"
import "core:mem"
import "core:fmt"
Image :: struct { Image :: struct {
width: int, width: int,
@@ -17,49 +20,69 @@ Image :: struct {
sidecar: any, 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: Image_Option:
`.info` `.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. to gather an image's dimensions and color information.
`.return_header` `.return_header`
Fill out img.sidecar.header with the image's format-specific header struct. 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` + 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. `.do_not_decompress_image`, or `.info`, which works as if both of these were set.
`.return_metadata` `.return_metadata`
Returns all chunks not needed to decode the data. 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. 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). If the image has no alpha channel, it'll add one set to max(type).
Turns RGB into RGBA and Gray into Gray+Alpha 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. 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, NOTE: For PNG, this also skips handling of the tRNS chunk, if present,
unless you select `alpha_premultiply`. unless you select `alpha_premultiply`.
In this case it'll premultiply the specified pixels in question only, In this case it'll premultiply the specified pixels in question only,
as the others are implicitly fully opaque. as the others are implicitly fully opaque.
`alpha_premultiply` `.alpha_premultiply`
If the image has an alpha channel, returns image data as follows: If the image has an alpha channel, returns image data as follows:
RGB *= A, Gray = Gray *= A RGB *= A, Gray = Gray *= A
`blend_background` `.blend_background`
If a bKGD chunk is present in a PNG, we normally just set `img.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, with its value and leave it up to the application to decide how to display the image,
as per the PNG specification. 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_ 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. 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_drop_if_present,
alpha_premultiply, alpha_premultiply,
blend_background, blend_background,
// Unimplemented
do_not_expand_grayscale,
do_not_expand_indexed,
do_not_expand_channels,
} }
Options :: distinct bit_set[Option]; Options :: distinct bit_set[Option];
PNG_Error :: enum { Error :: enum {
Invalid_PNG_Signature, Invalid_PNG_Signature,
IHDR_Not_First_Chunk, IHDR_Not_First_Chunk,
IHDR_Corrupt, IHDR_Corrupt,
@@ -93,9 +120,10 @@ PNG_Error :: enum {
Invalid_Color_Bit_Depth_Combo, Invalid_Color_Bit_Depth_Combo,
Unknown_Filter_Method, Unknown_Filter_Method,
Unknown_Interlace_Method, Unknown_Interlace_Method,
Requested_Channel_Not_Present,
Post_Processing_Error,
} }
/* /*
Functions to help with image buffer calculations Functions to help with image buffer calculations
*/ */
@@ -105,3 +133,76 @@ compute_buffer_size :: proc(width, height, channels, depth: int, extra_row_bytes
size = ((((channels * width * depth) + 7) >> 3) + extra_row_bytes) * height; size = ((((channels * width * depth) + 7) >> 3) + extra_row_bytes) * height;
return; 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;
}
+27 -2
View File
@@ -14,7 +14,7 @@ import "core:intrinsics"
Error :: compress.Error; Error :: compress.Error;
E_General :: compress.General_Error; E_General :: compress.General_Error;
E_PNG :: image.PNG_Error; E_PNG :: image.Error;
E_Deflate :: compress.Deflate_Error; E_Deflate :: compress.Deflate_Error;
is_kind :: compress.is_kind; is_kind :: compress.is_kind;
@@ -382,13 +382,17 @@ load_from_stream :: proc(stream: io.Stream, options := Options{}, allocator := c
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 .alpha_drop_if_present in options && .alpha_add_if_missing in options { if .alpha_drop_if_present in options && .alpha_add_if_missing in options {
return {}, E_General.Incompatible_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 { if img == nil {
img = new(Image); img = new(Image);
} }
@@ -723,6 +727,14 @@ load_from_stream :: proc(stream: io.Stream, options := Options{}, allocator := c
will become the default. 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; raw_image_channels := img.channels;
out_image_channels := 3; 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."); 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; return img, E_General.OK;
} }