pebble/src/fw/applib/graphics/gbitmap_png.c
2025-01-27 11:38:16 -08:00

293 lines
10 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "gbitmap_png.h"
#include "applib/app_logging.h"
#include "applib/applib_malloc.auto.h"
#include "system/logging.h"
#include "syscall/syscall.h"
#include "util/net.h"
#define PNG_DECODE_ERROR "PNG decoding failed"
#define PNG_MEMORY_ERROR "PNG memory allocation failed"
#define PNG_FORMAT_ERROR "Unsupported PNG format, only PNG8 is supported!"
#define PNG_LOAD_ERROR "Failed to load PNG"
static GBitmapFormat prv_get_format_for_bpp(uint8_t bits_per_pixel) {
if (bits_per_pixel == 1) return GBitmapFormat1BitPalette;
if (bits_per_pixel == 2) return GBitmapFormat2BitPalette;
if (bits_per_pixel == 4) return GBitmapFormat4BitPalette;
return GBitmapFormat8Bit;
}
bool gbitmap_png_data_is_png(const uint8_t *data, size_t data_size) {
if (data_size >= sizeof(PNG_SIGNATURE)) {
// PNG files start with [137, 'P', 'N', 'G']
return (ntohl(*(uint32_t*)data) == PNG_SIGNATURE);
}
return false;
}
// ! Distance from current resource cursor to next IDAT/fdAT chunk including that chunks data
int32_t png_seek_chunk_in_resource(uint32_t resource_id, uint32_t offset,
bool seek_framedata, bool *found_actl) {
ResAppNum app_num = sys_get_current_resource_num();
return png_seek_chunk_in_resource_system(app_num, resource_id, offset, seek_framedata,
found_actl);
}
int32_t png_seek_chunk_in_resource_system(ResAppNum app_num, uint32_t resource_id, uint32_t offset,
bool seek_framedata, bool *found_actl) {
uint32_t current_offset = offset;
bool actl_chunk_found = false; // ACTL chunk indicates PNG is an APNG
struct png_chunk_marker {
uint32_t length;
uint32_t chunk_type;
} marker;
// we are assuming the current_offset is always left at the start of the next chunk
// for alignment purposes
size_t max_size = sys_resource_size(app_num, resource_id);
while (current_offset + sizeof(marker) < max_size) {
if (sizeof(marker) != sys_resource_load_range(app_num, resource_id, current_offset,
(uint8_t*)&marker, sizeof(marker))) {
return -1;
}
// Need to byte swap it
marker.length = ntohl(marker.length);
marker.chunk_type = ntohl(marker.chunk_type);
if (marker.chunk_type == CHUNK_ACTL) {
actl_chunk_found = true;
}
if (seek_framedata) {
if (marker.chunk_type == CHUNK_FDAT || marker.chunk_type == CHUNK_IDAT) {
if (found_actl) {
*found_actl = actl_chunk_found;
}
// current distance + data_length + chunk_parts
return (current_offset - offset + marker.length + CHUNK_META_SIZE);
}
} else { // Seeking for data up to but not including FCTL or IDAT chunk (ie. image metadata)
if (marker.chunk_type == CHUNK_IDAT || marker.chunk_type == CHUNK_FCTL) {
if (found_actl) {
*found_actl = actl_chunk_found;
}
// current distance to the beginning of this chunk
return (current_offset - offset);
}
}
current_offset += CHUNK_META_SIZE + marker.length;
}
return -1; // Error
}
GBitmap* gbitmap_create_from_png_data(const uint8_t *png_data, size_t png_data_size) {
GBitmap *bitmap = applib_type_malloc(GBitmap);
if (bitmap) {
memset(bitmap, 0, sizeof(GBitmap));
gbitmap_init_with_png_data(bitmap, png_data, png_data_size);
}
return bitmap;
}
bool gbitmap_init_with_png_data(GBitmap *bitmap, const uint8_t *data, size_t data_size) {
GColor8 *palette = NULL;
bool retval = false;
upng_t *upng = upng_create();
if (!upng) {
goto cleanup;
}
upng_load_bytes(upng, data, data_size);
upng_error upng_state = upng_decode_image(upng);
if (upng_state != UPNG_EOK) {
APP_LOG(APP_LOG_LEVEL_ERROR, (upng_state == UPNG_ENOMEM) ? PNG_MEMORY_ERROR : PNG_DECODE_ERROR);
goto cleanup;
}
// Use UPNG to decode image and get data
uint32_t width = upng_get_width(upng);
uint32_t height = upng_get_height(upng);
uint8_t *upng_buffer = (uint8_t*)upng_get_buffer(upng);
uint32_t bpp = upng_get_bpp(upng);
uint16_t palette_size = 0;
if (!gbitmap_png_is_format_supported(upng)) {
APP_LOG(APP_LOG_LEVEL_ERROR, PNG_FORMAT_ERROR);
goto cleanup;
}
// Create a color palette in GColor8 format from RGB24 + ALPHA8 PNG Palettes (or Grayscale)
palette_size = gbitmap_png_load_palette(upng, &palette);
if (palette_size == 0) {
goto cleanup;
}
// Get the GBitmap format based on the bit depth of the raw data
GBitmapFormat format = prv_get_format_for_bpp(bpp);
// Convert 8-bit palettized PNGs to raw ARGB color images in-place
// as we don't support palettized bitdepths above 4
if (format == GBitmapFormat8Bit) {
for (uint32_t i = 0; i < width * height; i++) {
upng_buffer[i] = palette[upng_buffer[i]].argb; // De-palettize the image data
}
applib_free(palette); // Free the palette to avoid storing it as part of GBitmap
palette = NULL;
}
// Set the image or pixel data
gbitmap_set_data(bitmap, upng_buffer, format,
gbitmap_format_get_row_size_bytes(width, format), true);
gbitmap_set_bounds(bitmap, (GRect){.origin = {0, 0}, .size = {width, height}});
bitmap->info.version = GBITMAP_VERSION_CURRENT;
if (palette) {
gbitmap_set_palette(bitmap, palette, true);
}
retval = true;
cleanup:
if (!retval) {
// bitmap init failed, free palette
APP_LOG(APP_LOG_LEVEL_ERROR, PNG_LOAD_ERROR);
applib_free(palette);
}
// we are keeping the image data to avoid copying it
upng_destroy(upng, !retval);
return retval;
}
static uint16_t prv_gbitmap_png_create_palette_for_grayscale(upng_t *upng, GColor8 **palette_out) {
uint16_t palette_entries = 0;
uint32_t bpp = upng_get_bpp(upng);
// Convert Luminance format from Grayscale to palette
// Pebble only has 4 grayscale shades + 1 transparent value, max bpp == 4
if (bpp > 4) {
return 0;
}
int32_t transparent_gray = gbitmap_png_get_transparent_gray_value(upng);
// Palette will be size required to hold count of shades of gray
palette_entries = 0x1 << bpp;
GColor8 *palette = (GColor8*)applib_malloc(palette_entries * sizeof(GColor8));
if (!palette) {
return 0;
}
memset(palette, 0, palette_entries * sizeof(GColor8));
for (uint16_t i = 0; i < palette_entries; i ++) {
// If the color value matches transparent_gray, color is transparent
if (transparent_gray >= 0 && i == transparent_gray) {
palette[i] = GColorClear;
} else {
// Only have 2 bits per channel, but attempt to make grayscale 4-bit work
// which occurs with black, white, gray1, gray2 and a transparent color
uint8_t luminance = 0;
if (bpp > 2) {
luminance = (i >> (bpp - 2));
} else if (bpp == 2) {
// For bitdepth 2, use bits directly
luminance = i;
} else if (bpp == 1) {
// For bitdepth 1, need max and minimal values
luminance = i ? 0x3 : 0x0;
}
palette[i] = (GColor8){.a = 0x3, .r = luminance, .g = luminance, .b = luminance};
}
}
// Return the converted palette and number of entries
*palette_out = palette;
return palette_entries;
}
static uint16_t prv_gbitmap_png_create_palette_for_color(upng_t *upng, GColor8 **palette_out) {
if (!palette_out) {
return 0;
}
rgb *rgb_palette = NULL;
uint16_t palette_entries = upng_get_palette(upng, &rgb_palette);
uint8_t *alpha_palette = NULL;
uint16_t alpha_palette_entries = upng_get_alpha_palette(upng, &alpha_palette);
// To make palette entries consistent with PBI, pad to the bitdepth number of colors
uint32_t padded_palette_size = (1 << upng_get_bpp(upng));
GColor8 *palette = (GColor8*)applib_malloc(padded_palette_size * sizeof(GColor8));
if (palette == NULL) {
return 0;
}
memset(palette, 0, padded_palette_size * sizeof(GColor8));
// Convert rgb + alpha palette to GColor8 palette
for (int i = 0; i < palette_entries; i++) {
(palette)[i] = GColorFromRGBA(
rgb_palette[i].r, rgb_palette[i].g, rgb_palette[i].b, // RGB
(i < alpha_palette_entries) ? alpha_palette[i] : UINT8_MAX); // Conditional A value
}
// Return the converted palette and number of entries
*palette_out = palette;
return palette_entries;
}
uint16_t gbitmap_png_load_palette(upng_t *upng, GColor8 **palette_out) {
if (upng) {
upng_format png_format = upng_get_format(upng);
// Create a color palette in RGBA8 format from RGB24 + ALPHA8 PNG Palettes
if (png_format >= UPNG_INDEXED1 && png_format <= UPNG_INDEXED8) {
return prv_gbitmap_png_create_palette_for_color(upng, palette_out);
} else if (png_format >= UPNG_LUMINANCE1 && png_format <= UPNG_LUMINANCE8) {
return prv_gbitmap_png_create_palette_for_grayscale(upng, palette_out);
}
}
return 0;
}
bool gbitmap_png_is_format_supported(upng_t *upng) {
if (upng) {
upng_format png_format = upng_get_format(upng);
if ((png_format >= UPNG_INDEXED1 && png_format <= UPNG_INDEXED8) ||
(png_format >= UPNG_LUMINANCE1 && png_format <= UPNG_LUMINANCE8)) {
return true;
}
}
return false;
}
int32_t gbitmap_png_get_transparent_gray_value(upng_t *upng) {
int32_t transparent_gray = -1; // default to invalid value
// Handle grayscale transparency value (1 single transparent gray)
uint8_t *alpha_palette = NULL;
uint16_t alpha_palette_entries = upng_get_alpha_palette(upng, &alpha_palette);
if (alpha_palette_entries == 2) {
transparent_gray = ntohs(*(uint16_t*)alpha_palette);
}
return transparent_gray;
}