Browse Source

Add new PNG post processing options.

Jeroen van Rijn 4 years ago
parent
commit
7d534769d6
3 changed files with 143 additions and 17 deletions
  1. 1 1
      core/compress/common.odin
  2. 115 14
      core/image/common.odin
  3. 27 2
      core/image/png/png.odin

+ 1 - 1
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 {

+ 115 - 14
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;
 }

+ 27 - 2
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;
 }