Browse Source

png: First pass at a PNG decoder

(only 8bpp rgba, non-interlaced for now)
K. Lange 7 months ago
parent
commit
8e74bee6d0
6 changed files with 381 additions and 0 deletions
  1. 3 0
      apps/imgviewer.c
  2. 9 0
      apps/png-test.c
  3. BIN
      base/opt/logo_login.png
  4. 11 0
      base/usr/include/toaru/png.h
  5. 357 0
      lib/png.c
  6. 1 0
      util/auto-dep.py

+ 3 - 0
apps/imgviewer.c

@@ -26,6 +26,7 @@
 #include <toaru/decorations.h>
 #include <toaru/menu.h>
 #include <toaru/jpeg.h>
+#include <toaru/png.h>
 
 /* Pointer to graphics memory */
 static yutani_t * yctx;
@@ -153,6 +154,8 @@ int main(int argc, char * argv[]) {
 	int status;
 	if (strstr(argv[optind],".jpg")) {
 		status = load_sprite_jpg(&img, argv[optind]);
+	} else if (strstr(argv[optind],".png")) {
+		status = load_sprite_png(&img, argv[optind]);
 	} else {
 		status = load_sprite(&img, argv[optind]);
 	}

+ 9 - 0
apps/png-test.c

@@ -0,0 +1,9 @@
+#include <stdio.h>
+
+#include <toaru/graphics.h>
+#include <toaru/png.h>
+
+int main(int argc, char * argv[]) {
+	sprite_t sprite;
+	return load_sprite_png(&sprite,"/opt/logo_login.png");
+}

BIN
base/opt/logo_login.png


+ 11 - 0
base/usr/include/toaru/png.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#include <_cheader.h>
+#include <toaru/graphics.h>
+
+_Begin_C_Header
+
+extern int load_sprite_png(sprite_t * sprite, char * filename);
+
+_End_C_Header
+

+ 357 - 0
lib/png.c

@@ -0,0 +1,357 @@
+/* vim: tabstop=4 shiftwidth=4 noexpandtab
+ * This file is part of ToaruOS and is released under the terms
+ * of the NCSA / University of Illinois License - see LICENSE.md
+ * Copyright (C) 2020 K. Lange
+ *
+ * libtoaru_png: PNG decoder
+ */
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <toaru/graphics.h>
+#include <toaru/inflate.h>
+
+/**
+ * Read 32-bit big-endian value from file.
+ */
+unsigned int read_32(FILE * f) {
+	unsigned char a = fgetc(f);
+	unsigned char b = fgetc(f);
+	unsigned char c = fgetc(f);
+	unsigned char d = fgetc(f);
+	return (a << 24) | (b << 16) | (c << 8) | d;
+}
+
+/**
+ * Read 16-bit big-endian value from file.
+ */
+unsigned int read_16(FILE * f) {
+	unsigned char a = fgetc(f);
+	unsigned char b = fgetc(f);
+	return (a << 8) | b;
+}
+
+/**
+ * (Debug) Return a chunk type as a string.
+ */
+__attribute__((unused))
+static char*  reorder_type(unsigned int type) {
+	static char out[4];
+	out[0] = (type >> 24) & 0xFF;
+	out[1] = (type >> 16) & 0xFF;
+	out[2] = (type >> 8)  & 0xFF;
+	out[3] = (type >> 0)  & 0xFF;
+	return out;
+}
+
+/**
+ * Internal PNG decoder state for use with inflate.
+ */
+struct png_ctx {
+	FILE * f;          /* File being decoded. */
+	sprite_t * sprite; /* Sprite being generated. */
+	int y;             /* Cursor pointers for writing out bitmap data */
+	int x;
+	char buffer[4];   /* A buffer to hold a pixel's worth of data until it can
+	                     be written out to the image with the right filter. */
+	int  buf_off;     /* How much data is in the above buffer */
+	int seen_ihdr;    /* Whether the IHDR was seen; for error handling */
+
+	unsigned int width;   /* Image width (dup from sprite) */
+	unsigned int height;  /* Image height (dup from sprite) */
+	int bit_depth;        /* Bit depth of the image */
+	int color_type;       /* PNG color type */
+	int compression;      /* Compression method (must be 0) */
+	int filter;           /* Filter method (must be 0) */
+	int interlace;        /* Interlace method (we only support 0) */
+
+	unsigned int size;    /* Remaining IDAT chunk size */
+	int sf;               /* Current scanline filter type */
+};
+
+/* PNG chunk types */
+#define PNG_IHDR 0x49484452
+#define PNG_IDAT 0x49444154
+#define PNG_IEND 0x49454e44
+
+/* PNG filter types */
+#define PNG_FILTER_NONE  0
+#define PNG_FILTER_SUB   1
+#define PNG_FILTER_UP    2
+#define PNG_FILTER_AVG   3
+#define PNG_FILTER_PAETH 4
+
+/**
+ * Read a byte from the IDAT chunk.
+ * Tracks when an IDAT has been read to completion and
+ * can load the next IDAT (or bail of this was the last one)
+ */
+static uint8_t _get(struct inflate_context * ctx) {
+	struct png_ctx * c = (ctx->input_priv);
+	if (c->size == 0) {
+
+		/* Read the CRC32 from the end of this IDAT */
+		unsigned int check = read_32(c->f);
+		(void)check; /* ... and in theory check it... */
+
+		/* Read the next IDAT chunk header */
+		unsigned int size = read_32(c->f);
+		unsigned int type = read_32(c->f);
+
+		if (type != PNG_IDAT) {
+			/* This isn't an IDAT? That's wrong! */
+			fprintf(stderr, "And this is the wrong type (0x%x), I'm just bailing.\n", type);
+			fprintf(stderr, "size read was 0x%x\n", size);
+			exit(0);
+		}
+	}
+
+	/* Read one byte from the input */
+	c->size--;
+	int i = fgetc(c->f);
+
+	/* If this was EOF, we should handle that error case... probably... */
+	if (i < 0) fprintf(stderr, "This is probably not good.\n");
+
+	return i;
+}
+
+/**
+ * Paeth predictor
+ * Described in section 6.6 of the RFC
+ */
+#define ABS(a) ((a >= 0) ? (a) : -(a))
+static int paeth(int a, int b, int c) {
+	int p = a + b - c;
+	int pa = ABS(p - a);
+	int pb = ABS(p - b);
+	int pc = ABS(p - c);
+	if (pa <= pb && pa <= pc) return a;
+	else if (pb <= pc) return b;
+	return c;
+}
+
+/**
+ * Handle decompressed output from the inflater
+ *
+ * Writes pixel data to the image, and applies relevant filters.
+ */
+static void _write(struct inflate_context * ctx, unsigned int sym) {
+	struct png_ctx * c = (ctx->input_priv);
+
+	/* Put this byte into the short buffer */
+	c->buffer[c->buf_off] = sym;
+	c->buf_off++;
+
+	/* If this is the beginning of a scanline... */
+	if (c->x == -1 && c->buf_off == 1) {
+		/* Then this is the scanline filter type */
+		c->sf = sym;
+
+		/* Reset the buffer, advance to the beginning of the actual scanline */
+		c->x = 0;
+		c->buf_off = 0;
+	} else if (c->buf_off == 4) {
+		/*
+		 * Obtain pixel data from short buffer;
+		 * For color type 6, this is always in R G B A order in the
+		 * bytestream, so we don't have to worry about subpixel ordering
+		 * or weird color masks.
+		 */
+		unsigned int r = c->buffer[0];
+		unsigned int g = c->buffer[1];
+		unsigned int b = c->buffer[2];
+		unsigned int a = c->buffer[3];
+
+		/* Apply filters */
+		if (c->sf == PNG_FILTER_SUB) {
+			/* Add raw value to the pixel on the left */
+			if (c->x > 0) {
+				uint32_t left = SPRITE((c->sprite), (c->x - 1), (c->y));
+				r += _RED(left);
+				g += _GRE(left);
+				b += _BLU(left);
+				a += _ALP(left);
+			}
+		} else if (c->sf == PNG_FILTER_UP) {
+			/* Add raw value to the pixel above */
+			if (c->y > 0) {
+				uint32_t up = SPRITE((c->sprite), (c->x), (c->y - 1));
+				r += _RED(up);
+				g += _GRE(up);
+				b += _BLU(up);
+				a += _ALP(up);
+			}
+		} else if (c->sf == PNG_FILTER_AVG) {
+			/* Add raw value to the average of the pixel above and left */
+			uint32_t left = (c->x > 0) ? SPRITE((c->sprite), (c->x - 1), (c->y)) : 0;
+			uint32_t up = (c->x > 0) ? SPRITE((c->sprite), (c->x), (c->y - 1)) : 0;
+
+			r += ((int)_RED(left) + (int)_RED(up)) / 2;
+			g += ((int)_GRE(left) + (int)_GRE(up)) / 2;
+			b += ((int)_BLU(left) + (int)_BLU(up)) / 2;
+			a += ((int)_ALP(left) + (int)_ALP(up)) / 2;
+		} else if (c->sf == PNG_FILTER_PAETH) {
+			/* Use the Paeth predictor */
+			uint32_t left = (c->x > 0) ? SPRITE((c->sprite), (c->x - 1), (c->y)) : 0;
+			uint32_t up = (c->y > 0) ? SPRITE((c->sprite), (c->x), (c->y - 1)) : 0;
+			uint32_t upleft = (c->x > 0 && c->y > 0) ? SPRITE((c->sprite), (c->x - 1), (c->y - 1)) : 0;
+
+			r = ((int)r + paeth((int)_RED(left),(int)_RED(up),(int)_RED(upleft))) % 256;
+			g = ((int)g + paeth((int)_GRE(left),(int)_GRE(up),(int)_GRE(upleft))) % 256;
+			b = ((int)b + paeth((int)_BLU(left),(int)_BLU(up),(int)_BLU(upleft))) % 256;
+			a = ((int)a + paeth((int)_ALP(left),(int)_ALP(up),(int)_ALP(upleft))) % 256;
+		}
+
+		/* Write new pixel to the image */
+		SPRITE((c->sprite), (c->x), (c->y)) = rgba(r,g,b,a);
+
+		/* Reset the short buffer */
+		c->buf_off = 0;
+
+		/* Advance to next pixel */
+		c->x++;
+		if (c->x == (int)c->width) {
+			/* Advance to next line; next read is scanline filter type */
+			c->x = -1;
+			c->y++;
+		}
+	}
+}
+
+int load_sprite_png(sprite_t * sprite, char * filename) {
+	FILE * f = fopen(filename,"r");
+	if (!f) {
+		fprintf(stderr, "Failed to open file %s\n", filename);
+		return 1;
+	}
+
+	/* Read the PNG signature */
+	unsigned char sig[] = {137, 80, 78, 71, 13, 10, 26, 10};
+	for (int i = 0; i < 8; ++i) {
+		unsigned char c = fgetc(f);
+		if (c != sig[i]) {
+			fprintf(stderr, "byte %d (%d) does not match expected (%d)\n", i, c, sig[i]);
+			goto _error;
+		}
+	}
+
+	/* Set up context for future calls to inflate */
+	struct png_ctx c;
+	c.sprite = sprite;
+	c.x = -1;
+	c.y = 0;
+	c.f = f;
+	c.buf_off = 0;
+	c.seen_ihdr = 0;
+
+	while (1) {
+		/* read chunks */
+		unsigned int size = read_32(f);
+		unsigned int type = read_32(f);
+
+		if (feof(f)) break;
+
+		switch (type) {
+			case PNG_IHDR:
+				{
+					/* Image should only have one IHDR */
+					if (c.seen_ihdr) return 1;
+
+					c.seen_ihdr = 1;
+					c.width = read_32(f); /* 4 */
+					c.height = read_32(f); /* 8 */
+					c.bit_depth = fgetc(f); /* 9 */
+					c.color_type = fgetc(f); /* 10 */
+					c.compression = fgetc(f); /* 11 */
+					c.filter = fgetc(f); /* 12 */
+					c.interlace = fgetc(f); /* 13 */
+
+					/* Invalid / non-standard compression and filter types */
+					if (c.compression != 0) return 1;
+					if (c.filter != 0) return 1;
+
+					/* 0 for none, 1 for Adam7 */
+					if (c.interlace != 0 && c.interlace != 1) return 1;
+
+					if (c.bit_depth != 8) return 1; /* Sorry */
+					if (c.color_type != 6) return 1; /* Sorry */
+
+					/* Allocate space */
+					sprite->width  = c.width;
+					sprite->height = c.height;
+					sprite->bitmap = malloc(sizeof(uint32_t) * sprite->width * sprite->height);
+					sprite->masks = NULL;
+					sprite->alpha = (c.color_type == 4 || c.color_type == 6) ? ALPHA_EMBEDDED : 0;
+					sprite->blank = 0;
+
+
+					/* Skip */
+					for (unsigned int i = 13; i < size; ++i) fgetc(f);
+				}
+				break;
+
+			case PNG_IDAT:
+				{
+					/* First two bytes of IDAT data are ZLIB header */
+					unsigned int cflags = fgetc(f);
+					if ((cflags & 0xF) != 8) {
+						/* Compression type must be 8 */
+						fprintf(stderr, "Expected flags to be 8 but it's 0x%x\n", cflags);
+						return 1;
+					}
+					unsigned int aflags = fgetc(f);
+					if (aflags & (1 << 5)) {
+						fprintf(stderr, "There are preset bytes and I don't know what to do.\n");
+						return 1;
+					}
+
+					struct inflate_context ctx;
+					ctx.input_priv = &c;
+					ctx.output_priv = &c;
+					ctx.get_input = _get;
+					ctx.write_output = _write;
+					ctx.ring = NULL; /* use builtin */
+
+					c.size = size - 2; /* 2 for the bytes we already read */
+
+					deflate_decompress(&ctx);
+
+					/* The IDATs contain a ZLIB stream, so they end with an
+					 * adler32 checksum. Skip that. */
+					unsigned int adler = read_32(f);
+					(void)adler;
+				}
+				break;
+			case PNG_IEND:
+				/* We don't actually have anything to do here. */
+				break;
+			default:
+				/* IHDR must be first */
+				if (!c.seen_ihdr) return 1;
+				//fprintf(stderr, "I don't know what this is! %4s 0x%x\n", reorder_type(type), type);
+				/* Skip */
+				for (unsigned int i = 0; i < size; ++i) fgetc(f);
+				break;
+		}
+
+		unsigned int crc32 = read_32(f);
+		(void)crc32;
+	}
+
+	/*
+	 * Data in PNGs is unpremultiplied, but our sprites expect
+	 * premultiplied alpha, so convert the image data
+	 */
+	for (int y = 0; y < sprite->height; ++y) {
+		for (int x = 0; x < sprite->width; ++x) {
+			SPRITE(sprite,x,y) = premultiply(SPRITE(sprite,x,y));
+		}
+	}
+
+	return 0;
+
+_error:
+	fclose(f);
+	return 1;
+}

+ 1 - 0
util/auto-dep.py

@@ -22,6 +22,7 @@ class Classifier(object):
         '<toaru/inflate.h>':     (None, '-ltoaru_inflate',     []),
         '<toaru/drawstring.h>':  (None, '-ltoaru_drawstring',  ['<toaru/graphics.h>']),
         '<toaru/jpeg.h>':        (None, '-ltoaru_jpeg',        ['<toaru/graphics.h>']),
+        '<toaru/png.h>':         (None, '-ltoaru_png',         ['<toaru/graphics.h>','<toaru/inflate.h>']),
         '<toaru/rline.h>':       (None, '-ltoaru_rline',       ['<toaru/kbd.h>']),
         '<toaru/rline_exp.h>':   (None, '-ltoaru_rline_exp',   ['<toaru/rline.h>']),
         '<toaru/confreader.h>':  (None, '-ltoaru_confreader',  ['<toaru/hashmap.h>']),