summaryrefslogtreecommitdiffstats
path: root/lisiblepng/src/lisiblepng.c
diff options
context:
space:
mode:
authorClement Sibille <clements+git@lisible.xyz>2024-03-05 16:02:17 +0900
committerClement Sibille <clements+git@lisible.xyz>2024-03-05 16:45:45 +0900
commitbb6f08f3c79efc7bb7877aca97cebd3dab8b7838 (patch)
tree9b3405e607c1f746a7061730adcf831c50a9f70b /lisiblepng/src/lisiblepng.c
parente1e5b4e92bcd460b43ce1b852560751b6525593e (diff)
Implement PNG decompression
This patch adds PNG decompression for images without interlacing and without alpha channel. Only basic image data is supported. Background, transparency, gamma, paletted images are not supported.
Diffstat (limited to 'lisiblepng/src/lisiblepng.c')
-rw-r--r--lisiblepng/src/lisiblepng.c290
1 files changed, 262 insertions, 28 deletions
diff --git a/lisiblepng/src/lisiblepng.c b/lisiblepng/src/lisiblepng.c
index 5ccd300..7a3818b 100644
--- a/lisiblepng/src/lisiblepng.c
+++ b/lisiblepng/src/lisiblepng.c
@@ -1,7 +1,7 @@
#include "lisiblepng.h"
-#include "assert.h"
-#include "deflate.h"
-#include "log.h"
+#include "lisiblepng/assert.h"
+#include "lisiblepng/deflate.h"
+#include "lisiblepng/log.h"
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
@@ -61,8 +61,39 @@ const uint32_t CRC32_TABLE[256] = {
0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D};
+typedef enum {
+ ColourType_Greyscale = 0,
+ ColourType_Truecolour = 2,
+ ColourType_IndexedColour = 3,
+ ColourType_GreyscaleWithAlpha = 4,
+ ColourType_TruecolourWithAlpha = 6,
+ ColourType_Unknown
+} ColourType;
+
+uint8_t ColourType_sample_count(const ColourType colour_type) {
+ switch (colour_type) {
+ case ColourType_Greyscale:
+ return 1;
+ case ColourType_Truecolour:
+ return 3;
+ case ColourType_IndexedColour:
+ return 1;
+ case ColourType_GreyscaleWithAlpha:
+ return 2;
+ case ColourType_TruecolourWithAlpha:
+ return 4;
+ default:
+ LPNG_LOG_ERR0("Unknown colour type");
+ abort();
+ }
+}
+
struct Png {
- const uint8_t *data;
+ char *data;
+ size_t width;
+ size_t height;
+ ColourType colour_type;
+ uint8_t bits_per_sample;
};
typedef struct {
@@ -103,7 +134,7 @@ long ParsingContext_cursor_position(DeflateDecompressor *ctx) {
bool ParsingContext_skip_bytes(DeflateDecompressor *ctx, size_t byte_count) {
ASSERT(ctx != NULL);
if (fseek(ctx->stream, byte_count, SEEK_CUR) != 0) {
- LOGN("Couldn't skip bytes: %s", strerror(errno));
+ LPNG_LOG_ERR("Couldn't skip bytes: %s", strerror(errno));
return false;
}
@@ -115,7 +146,7 @@ bool ParsingContext_parse_bytes(DeflateDecompressor *ctx, size_t byte_count,
ASSERT(ctx != NULL);
ASSERT(output_buffer != NULL);
if (fread(output_buffer, 1, byte_count, ctx->stream) < byte_count) {
- LOG0("Couldn't parse bytes, EOF reached");
+ LPNG_LOG_ERR0("Couldn't parse bytes, EOF reached");
return false;
}
@@ -180,16 +211,17 @@ typedef struct {
void ImageHeader_print_image_header(const ImageHeader *image_header) {
ASSERT(image_header != NULL);
- LOGN("Image header:\n"
- "- dimensions: %dx%d\n"
- "- bit depth: %d\n"
- "- colour type: %d\n"
- "- compression method: %d\n"
- "- filter method: %d\n"
- "- interlace method: %d",
- image_header->width, image_header->height, image_header->bit_depth,
- image_header->colour_type, image_header->compression_method,
- image_header->filter_method, image_header->interlace_method);
+ LPNG_LOG_DBG("Image header:\n"
+ "- dimensions: %dx%d\n"
+ "- bit depth: %d\n"
+ "- colour type: %d\n"
+ "- compression method: %d\n"
+ "- filter method: %d\n"
+ "- interlace method: %d",
+ image_header->width, image_header->height,
+ image_header->bit_depth, image_header->colour_type,
+ image_header->compression_method, image_header->filter_method,
+ image_header->interlace_method);
}
bool ParsingContext_validate_crc_if_required(DeflateDecompressor *ctx) {
@@ -198,7 +230,7 @@ bool ParsingContext_validate_crc_if_required(DeflateDecompressor *ctx) {
uint32_t crc;
PARSE_FIELD(uint32_t, crc);
if (computed_crc != crc) {
- LOG0("Invalid CRC checksum");
+ LPNG_LOG_ERR0("Invalid CRC checksum");
return false;
}
#else
@@ -208,6 +240,11 @@ bool ParsingContext_validate_crc_if_required(DeflateDecompressor *ctx) {
return true;
}
+bool is_valid_bit_depth(uint8_t bit_depth) {
+ return bit_depth == 1 || bit_depth == 2 || bit_depth == 4 || bit_depth == 8 ||
+ bit_depth == 16;
+}
+
bool parse_IHDR_chunk(DeflateDecompressor *ctx, ImageHeader *image_header) {
ASSERT(ctx != NULL);
ASSERT(image_header != NULL);
@@ -220,7 +257,7 @@ bool parse_IHDR_chunk(DeflateDecompressor *ctx, ImageHeader *image_header) {
uint32_t type;
PARSE_FIELD(uint32_t, type);
if (type != IHDR_CHUNK_TYPE) {
- LOG0("Expected IHDR chunk");
+ LPNG_LOG_ERR0("Expected IHDR chunk");
return false;
}
@@ -228,6 +265,10 @@ bool parse_IHDR_chunk(DeflateDecompressor *ctx, ImageHeader *image_header) {
PARSE_FIELD(uint32_t, image_header->width);
PARSE_FIELD(uint32_t, image_header->height);
PARSE_FIELD(uint8_t, image_header->bit_depth);
+ if (!is_valid_bit_depth(image_header->bit_depth)) {
+ LPNG_LOG_ERR("Invalid bitdepth: %d", image_header->bit_depth);
+ return false;
+ }
PARSE_FIELD(uint8_t, image_header->colour_type);
PARSE_FIELD(uint8_t, image_header->compression_method);
PARSE_FIELD(uint8_t, image_header->filter_method);
@@ -265,6 +306,119 @@ uint32_t uint32_t_to_le(uint32_t value) {
(value_bytes[2] << 8) + value_bytes[3];
}
+uint8_t none_reconstruction_function(uint8_t recon_a, uint8_t recon_b,
+ uint8_t recon_c, uint8_t x) {
+ (void)recon_a;
+ (void)recon_b;
+ (void)recon_c;
+ return x;
+}
+uint8_t sub_reconstruction_function(uint8_t recon_a, uint8_t recon_b,
+ uint8_t recon_c, uint8_t x) {
+ (void)recon_b;
+ (void)recon_c;
+ return x + recon_a;
+}
+uint8_t up_reconstruction_function(uint8_t recon_a, uint8_t recon_b,
+ uint8_t recon_c, uint8_t x) {
+ (void)recon_a;
+ (void)recon_c;
+ return x + recon_b;
+}
+uint8_t average_reconstruction_function(uint8_t recon_a, uint8_t recon_b,
+ uint8_t recon_c, uint8_t x) {
+ (void)recon_c;
+ uint16_t sum = (recon_a + recon_b) / 2u;
+ return x + sum;
+}
+uint8_t paeth_predictor(uint8_t a, uint8_t b, uint8_t c) {
+ int16_t p = a + b - c;
+ int16_t pa = abs(p - a);
+ int16_t pb = abs(p - b);
+ int16_t pc = abs(p - c);
+ uint8_t pr;
+ if (pa <= pb && pa <= pc) {
+ pr = a;
+ } else if (pb <= pc) {
+ pr = b;
+ } else {
+ pr = c;
+ }
+
+ return pr;
+}
+uint8_t paeth_reconstruction_function(uint8_t recon_a, uint8_t recon_b,
+ uint8_t recon_c, uint8_t x) {
+ return x + paeth_predictor(recon_a, recon_b, recon_c);
+}
+typedef uint8_t (*ReconstructionFn)(uint8_t, uint8_t, uint8_t, uint8_t);
+static const ReconstructionFn reconstruction_functions[] = {
+ none_reconstruction_function, sub_reconstruction_function,
+ up_reconstruction_function, average_reconstruction_function,
+ paeth_reconstruction_function};
+
+void apply_reconstruction_functions_to_scanline(
+ char *output_scanline, const char *input_scanline,
+ const char *previous_output_scanline, size_t filter_type,
+ size_t scanline_size, size_t bytes_per_pixel) {
+ ASSERT(output_scanline != NULL);
+ ASSERT(input_scanline != NULL);
+ ASSERT(filter_type <
+ sizeof(reconstruction_functions) / sizeof(ReconstructionFn));
+
+ for (size_t i = 0; i < scanline_size / bytes_per_pixel; i++) {
+ for (size_t p = 0; p < bytes_per_pixel; p++) {
+ uint8_t a = 0;
+ if (i > 0) {
+ a = output_scanline[i * bytes_per_pixel - bytes_per_pixel + p];
+ }
+
+ uint8_t b = 0;
+ uint8_t c = 0;
+ if (previous_output_scanline != NULL) {
+ b = previous_output_scanline[i * bytes_per_pixel + p];
+ if (i > 0) {
+ c = previous_output_scanline[i * bytes_per_pixel - bytes_per_pixel +
+ p];
+ }
+ }
+
+ uint8_t x = input_scanline[i * bytes_per_pixel + p];
+ output_scanline[i * bytes_per_pixel + p] =
+ reconstruction_functions[filter_type](a, b, c, x);
+ }
+ }
+}
+
+void apply_reconstruction_functions(Png *image,
+ const char *decompressed_data_buffer) {
+ ASSERT(image != NULL);
+ ASSERT(decompressed_data_buffer != NULL);
+ size_t sample_count = ColourType_sample_count(image->colour_type);
+ size_t bits_per_pixel = (image->bits_per_sample * sample_count);
+ size_t bytes_per_pixel = bits_per_pixel / 8;
+ if (image->bits_per_sample < 8) {
+ bytes_per_pixel = 1;
+ }
+ size_t scanline_size = image->width * bits_per_pixel / 8;
+
+ for (size_t scanline = 0; scanline < image->height; scanline++) {
+ size_t output_scanline_start = scanline_size * scanline;
+ size_t input_scanline_start = (scanline_size + 1) * scanline;
+ uint8_t filter_type = decompressed_data_buffer[input_scanline_start];
+ const char *previous_output_scanline = NULL;
+ if (scanline > 0) {
+ size_t previous_output_scanline_start = scanline_size * (scanline - 1);
+ previous_output_scanline = &image->data[previous_output_scanline_start];
+ }
+
+ apply_reconstruction_functions_to_scanline(
+ &image->data[output_scanline_start],
+ &decompressed_data_buffer[input_scanline_start + 1],
+ previous_output_scanline, filter_type, scanline_size, bytes_per_pixel);
+ }
+}
+
Png *lis_Png_parse(FILE *stream) {
Png *png = malloc(sizeof(Png));
DeflateDecompressor ctx;
@@ -273,12 +427,12 @@ Png *lis_Png_parse(FILE *stream) {
uint8_t parsed_png_signature[PNG_SIGNATURE_LENGTH];
if (!ParsingContext_parse_bytes(&ctx, PNG_SIGNATURE_LENGTH,
parsed_png_signature)) {
- LOG0("Couldn't parse signature");
+ LPNG_LOG_ERR0("Couldn't parse signature");
goto err;
}
if (!matches_png_signature(parsed_png_signature)) {
- LOG0("Invalid signature");
+ LPNG_LOG_ERR0("Invalid signature");
goto err;
}
@@ -289,40 +443,64 @@ Png *lis_Png_parse(FILE *stream) {
ImageHeader_print_image_header(&header);
ImageData image_data = {0};
+ size_t parsed_data_chunk_count = 0;
bool end_reached = false;
while (!end_reached) {
uint32_t length;
if (!ParsingContext_parse_uint32_t(&ctx, &length)) {
- LOG0("Couldn't parse chunk length");
+ LPNG_LOG_ERR0("Couldn't parse chunk length");
goto cleanup_data;
}
ParsingContext_crc_reset(&ctx);
uint32_t type;
if (!ParsingContext_parse_uint32_t(&ctx, &type)) {
- LOG0("Couldn't parse chunk type");
+ LPNG_LOG_ERR0("Couldn't parse chunk type");
goto cleanup_data;
}
+#ifdef LPNG_DEBUG_LOG
uint32_t type_le = uint32_t_to_le(type);
- LOGN("Parsing %.4s chunk", (char *)&type_le);
+ LPNG_LOG_DBG("Parsing %.4s chunk", (char *)&type_le);
+#endif
+
switch (type) {
case IDAT_CHUNK_TYPE:
- parse_IDAT_chunk(&ctx, length, &image_data);
+ if (!parse_IDAT_chunk(&ctx, length, &image_data)) {
+ LPNG_LOG_ERR0("Couldn't parse IDAT chunk");
+ goto cleanup_data;
+ }
+ parsed_data_chunk_count++;
break;
case IEND_CHUNK_TYPE:
end_reached = true;
ParsingContext_skip_bytes(&ctx, sizeof(uint32_t));
break;
default:
- LOG0("Unknown chunk type, skipping chunk...");
+ LPNG_LOG_DBG0("Unknown chunk type, skipping chunk...");
ParsingContext_skip_bytes(&ctx, length + sizeof(uint32_t));
break;
}
}
- LOGN("Data length: %zul", image_data.length);
- zlib_decompress(image_data.data, image_data.length);
+ if (parsed_data_chunk_count == 0) {
+ LPNG_LOG_ERR0("No IDAT chunk found, at least one is required");
+ goto cleanup_data;
+ }
+
+ LPNG_LOG_DBG("Data length: %zu", image_data.length);
+ png->width = header.width;
+ png->height = header.height;
+ png->colour_type = header.colour_type;
+ png->bits_per_sample = header.bit_depth;
+ size_t output_length;
+ char *output_buffer =
+ zlib_decompress(image_data.data, image_data.length, &output_length);
+ png->data = output_buffer;
+
+ apply_reconstruction_functions(png, output_buffer);
+
+ free(image_data.data);
return png;
@@ -334,4 +512,60 @@ err:
#undef PARSE_FIELD
void lis_Png_destroy(Png *png) { free(png); }
-void lis_Png_dump_ppm(const Png *png) { ASSERT(png != NULL); }
+void lis_Png_dump_ppm(const Png *png) {
+ ASSERT(png != NULL);
+ printf("P3\n");
+ printf("%zu %zu\n", png->width, png->height);
+ printf("%d\n", (1 << png->bits_per_sample) - 1);
+ size_t sample_count = ColourType_sample_count(png->colour_type);
+ size_t bits_per_pixel = png->bits_per_sample * sample_count;
+ size_t bytes_per_pixel = bits_per_pixel / 8;
+ if (png->bits_per_sample < 8) {
+ bytes_per_pixel = 1;
+ }
+ for (size_t pixel_index = 0; pixel_index < png->height * png->width;
+ pixel_index++) {
+
+ if (png->colour_type == ColourType_Greyscale) {
+ if (bits_per_pixel == 16) {
+ uint8_t hi = png->data[pixel_index * bytes_per_pixel];
+ uint8_t lo = png->data[pixel_index * bytes_per_pixel + 1];
+ uint16_t grey = ((hi & 0xFFu) << 8) | (lo & 0xFFu);
+ printf("%u %u %u\n", grey, grey, grey);
+ } else {
+ size_t absolute_bit_offset = pixel_index * bits_per_pixel;
+ size_t byte_offset = absolute_bit_offset / 8;
+ size_t relative_bit_offset = absolute_bit_offset % 8;
+ unsigned char grey =
+ (png->data[byte_offset] >>
+ (7 - relative_bit_offset - (bits_per_pixel - 1))) &
+ ((1 << bits_per_pixel) - 1);
+ printf("%u %u %u\n", grey, grey, grey);
+ }
+ } else if (png->colour_type == ColourType_Truecolour) {
+ if (png->bits_per_sample == 16) {
+ uint8_t hi_r = png->data[pixel_index * bytes_per_pixel];
+ uint8_t lo_r = png->data[pixel_index * bytes_per_pixel + 1];
+ uint16_t r = ((hi_r & 0xFFu) << 8) | (lo_r & 0xFFu);
+
+ uint8_t hi_g = png->data[pixel_index * bytes_per_pixel + 2];
+ uint8_t lo_g = png->data[pixel_index * bytes_per_pixel + 3];
+ uint16_t g = ((hi_g & 0xFFu) << 8) | (lo_g & 0xFFu);
+
+ uint8_t hi_b = png->data[pixel_index * bytes_per_pixel + 4];
+ uint8_t lo_b = png->data[pixel_index * bytes_per_pixel + 5];
+ uint16_t b = ((hi_b & 0xFFu) << 8) | (lo_b & 0xFFu);
+
+ printf("%u %u %u\n", r, g, b);
+ } else {
+ uint8_t r = png->data[pixel_index * bytes_per_pixel];
+ uint8_t g = png->data[pixel_index * bytes_per_pixel + 1];
+ uint8_t b = png->data[pixel_index * bytes_per_pixel + 2];
+ printf("%u %u %u\n", r, g, b);
+ }
+ } else {
+ LPNG_LOG_ERR0("Unsupported colour type");
+ exit(1);
+ }
+ }
+}
Go back to lisible.xyz