mirror of https://github.com/google/pebble
493 lines
17 KiB
C
493 lines
17 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_sequence.h"
|
|
|
|
#include "gbitmap_png.h"
|
|
#include "util/graphics.h"
|
|
#include "util/net.h"
|
|
#include "util/time/time.h"
|
|
#include "applib/app_logging.h"
|
|
#include "applib/applib_malloc.auto.h"
|
|
#include "syscall/syscall.h"
|
|
#include "system/passert.h"
|
|
#include "util/bitset.h"
|
|
#include "util/math.h"
|
|
|
|
#define APNG_DECODE_ERROR "APNG decoding failed"
|
|
#define APNG_MEMORY_ERROR "APNG memory allocation failed"
|
|
#define APNG_FORMAT_ERROR "Unsupported APNG format, only APNG8 is supported!"
|
|
#define APNG_LOAD_ERROR "Failed to load APNG"
|
|
#define APNG_UPDATE_ERROR "gbitmap_sequence failed to update bitmap"
|
|
#define APNG_ELAPSED_WARNING "invalid elapsed_ms for gbitmap_sequence, forward progression only"
|
|
|
|
static bool prv_gbitmap_sequence_restart(GBitmapSequence *bitmap_sequence, bool reset_elapsed) {
|
|
if (bitmap_sequence == NULL) {
|
|
return false;
|
|
}
|
|
|
|
// can start seeking after SIG + IHDR
|
|
int32_t metadata_bytes = png_seek_chunk_in_resource(bitmap_sequence->resource_id,
|
|
PNG_HEADER_SIZE, false, NULL);
|
|
|
|
if (metadata_bytes <= 0) {
|
|
return false;
|
|
}
|
|
metadata_bytes += PNG_HEADER_SIZE;
|
|
bitmap_sequence->png_decoder_data.read_cursor = metadata_bytes;
|
|
bitmap_sequence->current_frame = 0;
|
|
bitmap_sequence->current_frame_delay_ms = 0;
|
|
|
|
if (reset_elapsed) {
|
|
bitmap_sequence->elapsed_ms = 0;
|
|
bitmap_sequence->play_index = 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//! Directly modifies dst, blending src into dst using equation
|
|
//! dst = src * (alpha_normalized) + dst * (1 - alpha_normalized)
|
|
static ALWAYS_INLINE void prv_gbitmap_sequence_blend_over(GColor8 src_color, GColor8 *dst) {
|
|
if (src_color.a == 3) {
|
|
// Fast path: 100% opacity
|
|
*dst = src_color;
|
|
} else if (src_color.a == 0) {
|
|
// Fast path: 0% opacity, no-op!
|
|
} else {
|
|
const GColor8 dest_color = *dst;
|
|
const uint8_t f_src = src_color.a;
|
|
const uint8_t f_dst = 3 - f_src;
|
|
GColor8 final = {};
|
|
final.r = (src_color.r * f_src + dest_color.r * f_dst) / 3;
|
|
final.g = (src_color.g * f_src + dest_color.g * f_dst) / 3;
|
|
final.b = (src_color.b * f_src + dest_color.b * f_dst) / 3;
|
|
final.a = src_color.a; // Different than bitblt, required for correct transparency
|
|
*dst = final;
|
|
}
|
|
}
|
|
|
|
GBitmapSequence *gbitmap_sequence_create_with_resource(uint32_t resource_id) {
|
|
ResAppNum app_num = sys_get_current_resource_num();
|
|
return gbitmap_sequence_create_with_resource_system(app_num, resource_id);
|
|
}
|
|
|
|
GBitmapSequence *gbitmap_sequence_create_with_resource_system(ResAppNum app_num,
|
|
uint32_t resource_id) {
|
|
uint8_t *frame_data_buffer = NULL;
|
|
|
|
// Allocate gbitmap
|
|
GBitmapSequence* bitmap_sequence = applib_type_zalloc(GBitmapSequence);
|
|
if (bitmap_sequence == NULL) {
|
|
goto cleanup;
|
|
}
|
|
|
|
bitmap_sequence->resource_id = resource_id;
|
|
bitmap_sequence->data_is_loaded_from_flash = true;
|
|
|
|
if (!prv_gbitmap_sequence_restart(bitmap_sequence, true)) {
|
|
goto cleanup;
|
|
}
|
|
int32_t frame_bytes = bitmap_sequence->png_decoder_data.read_cursor;
|
|
|
|
frame_data_buffer = applib_zalloc(frame_bytes);
|
|
if (frame_data_buffer == NULL) {
|
|
goto cleanup;
|
|
}
|
|
|
|
const size_t bytes_read = sys_resource_load_range(app_num, resource_id,
|
|
0, frame_data_buffer, frame_bytes);
|
|
if (bytes_read != (size_t)frame_bytes) {
|
|
goto cleanup;
|
|
}
|
|
|
|
upng_t *upng = upng_create();
|
|
if (upng == NULL) {
|
|
goto cleanup;
|
|
}
|
|
|
|
bitmap_sequence->png_decoder_data.upng = upng;
|
|
upng_load_bytes(upng, frame_data_buffer, frame_bytes);
|
|
|
|
upng_error upng_state = upng_decode_metadata(upng);
|
|
if (upng_state != UPNG_EOK) {
|
|
APP_LOG(APP_LOG_LEVEL_ERROR,
|
|
(upng_state == UPNG_ENOMEM) ? APNG_MEMORY_ERROR : APNG_DECODE_ERROR);
|
|
goto cleanup;
|
|
}
|
|
// Save metadata to bitmap_sequence
|
|
uint32_t play_count = 0;
|
|
// If png is APNG, get num plays, otherwise play count is 0
|
|
if (upng_is_apng(upng)) {
|
|
play_count = upng_apng_num_plays(upng);
|
|
// At the API level 0 is no loops vs APNG specification uses 0 for infinite
|
|
play_count = (play_count == 0) ? PLAY_COUNT_INFINITE : play_count;
|
|
}
|
|
bitmap_sequence->play_count = play_count;
|
|
bitmap_sequence->bitmap_size = (GSize){.w = upng_get_width(upng), .h = upng_get_height(upng)};
|
|
bitmap_sequence->total_frames = upng_apng_num_frames(upng);
|
|
|
|
if (!gbitmap_png_is_format_supported(upng)) {
|
|
APP_LOG(APP_LOG_LEVEL_ERROR, APNG_FORMAT_ERROR);
|
|
goto cleanup;
|
|
}
|
|
|
|
// Create a color palette in RGBA8 format from RGB24 + ALPHA8 PNG Palettes
|
|
upng_format png_format = upng_get_format(upng);
|
|
if (png_format >= UPNG_INDEXED1 && png_format <= UPNG_INDEXED8) {
|
|
bitmap_sequence->png_decoder_data.palette_entries =
|
|
gbitmap_png_load_palette(upng, &bitmap_sequence->png_decoder_data.palette);
|
|
if (bitmap_sequence->png_decoder_data.palette_entries == 0) {
|
|
APP_LOG(APP_LOG_LEVEL_ERROR, "Failed to load palette");
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
bitmap_sequence->header_loaded = true;
|
|
|
|
cleanup:
|
|
applib_free(frame_data_buffer); // Free compressed image buffer
|
|
|
|
if (!bitmap_sequence || !bitmap_sequence->header_loaded) {
|
|
APP_LOG(APP_LOG_LEVEL_ERROR, APNG_LOAD_ERROR);
|
|
gbitmap_sequence_destroy(bitmap_sequence);
|
|
}
|
|
|
|
return bitmap_sequence;
|
|
}
|
|
|
|
bool gbitmap_sequence_restart(GBitmapSequence *bitmap_sequence) {
|
|
return prv_gbitmap_sequence_restart(bitmap_sequence, true);
|
|
}
|
|
|
|
void gbitmap_sequence_destroy(GBitmapSequence *bitmap_sequence) {
|
|
if (bitmap_sequence) {
|
|
upng_destroy(bitmap_sequence->png_decoder_data.upng, true);
|
|
applib_free(bitmap_sequence->png_decoder_data.palette);
|
|
applib_free(bitmap_sequence);
|
|
}
|
|
}
|
|
|
|
static ALWAYS_INLINE GColor8 *prv_target_pixel_addr(GBitmap *bitmap, apng_fctl *fctl,
|
|
uint32_t x, uint32_t y) {
|
|
uint32_t offset = (fctl->y_offset + y + bitmap->bounds.origin.y) * bitmap->row_size_bytes +
|
|
(fctl->x_offset + x + bitmap->bounds.origin.x);
|
|
GColor8 *pixel_data = bitmap->addr;
|
|
return &pixel_data[offset];
|
|
}
|
|
|
|
static void prv_set_pixel_in_row(uint8_t *row_data, GBitmapFormat bitmap_format,
|
|
uint32_t x, GColor8 color) {
|
|
if (bitmap_format == GBitmapFormat1Bit) {
|
|
if (!gcolor_is_invisible(color)) {
|
|
const bool pixel_is_white = !gcolor_equal(color, GColorBlack);
|
|
bitset8_update(row_data, x, pixel_is_white);
|
|
}
|
|
} else if ((bitmap_format == GBitmapFormat8Bit) ||
|
|
(bitmap_format == GBitmapFormat8BitCircular)) {
|
|
GColor8 *const destination_pixel = (GColor8 *)(row_data + x);
|
|
*destination_pixel = color;
|
|
} else {
|
|
WTF; // Unsupported destination type
|
|
}
|
|
}
|
|
|
|
bool gbitmap_sequence_update_bitmap_next_frame(GBitmapSequence *bitmap_sequence,
|
|
GBitmap *bitmap, uint32_t *delay_ms) {
|
|
bool retval = false;
|
|
uint8_t* buffer = NULL;
|
|
|
|
// Disabled if play count is 0 and not the very first frame
|
|
if (!bitmap_sequence ||
|
|
(bitmap_sequence->play_count == 0 && bitmap_sequence->current_frame != 0)) {
|
|
return false;
|
|
}
|
|
|
|
GBitmapSequencePNGDecoderData *png_decoder_data = &bitmap_sequence->png_decoder_data;
|
|
upng_t *upng = png_decoder_data->upng;
|
|
|
|
// Check bitmap_sequence metadata is loaded, bitmap_sequence size, type & memory constraints
|
|
const GBitmapFormat bitmap_format = gbitmap_get_format(bitmap); // call is NULL-safe
|
|
if (!bitmap_sequence->header_loaded || bitmap == NULL || bitmap->addr == NULL ||
|
|
bitmap_sequence->bitmap_size.w > (bitmap->bounds.size.w) ||
|
|
bitmap_sequence->bitmap_size.h > (bitmap->bounds.size.h)) {
|
|
goto cleanup;
|
|
}
|
|
|
|
if (!((bitmap_format == GBitmapFormat1Bit) ||
|
|
(bitmap_format == GBitmapFormat8Bit) ||
|
|
(bitmap_format == GBitmapFormat8BitCircular))) {
|
|
|
|
APP_LOG(APP_LOG_LEVEL_ERROR, "Invalid destination bitmap format for APNG");
|
|
goto cleanup;
|
|
}
|
|
|
|
// Update current time elapsed using the previous frames current_frame_delay_ms
|
|
bitmap_sequence->elapsed_ms += bitmap_sequence->current_frame_delay_ms;
|
|
|
|
// Check if single animation loop is complete, and restart if there are more loops
|
|
if (bitmap_sequence->current_frame >= bitmap_sequence->total_frames) {
|
|
if ((++bitmap_sequence->play_index < bitmap_sequence->play_count) ||
|
|
(bitmap_sequence->play_count == PLAY_COUNT_INFINITE)) {
|
|
prv_gbitmap_sequence_restart(bitmap_sequence, false);
|
|
} else {
|
|
return false; // animation complete
|
|
}
|
|
}
|
|
|
|
const int32_t metadata_bytes =
|
|
png_seek_chunk_in_resource(bitmap_sequence->resource_id,
|
|
png_decoder_data->read_cursor, true, NULL);
|
|
|
|
if (metadata_bytes <= 0) {
|
|
goto cleanup;
|
|
}
|
|
|
|
buffer = applib_zalloc(metadata_bytes);
|
|
if (buffer == NULL) {
|
|
goto cleanup;
|
|
}
|
|
|
|
ResAppNum app_num = sys_get_current_resource_num();
|
|
const size_t bytes_read = sys_resource_load_range(
|
|
app_num, bitmap_sequence->resource_id,
|
|
png_decoder_data->read_cursor, buffer, metadata_bytes);
|
|
|
|
if (bytes_read != (size_t)metadata_bytes) {
|
|
goto cleanup;
|
|
}
|
|
|
|
png_decoder_data->read_cursor += metadata_bytes;
|
|
|
|
upng_load_bytes(upng, buffer, metadata_bytes);
|
|
upng_error upng_state = upng_decode_image(upng);
|
|
if (upng_state != UPNG_EOK) {
|
|
APP_LOG(APP_LOG_LEVEL_ERROR,
|
|
(upng_state == UPNG_ENOMEM) ? APNG_MEMORY_ERROR : APNG_DECODE_ERROR);
|
|
goto cleanup;
|
|
}
|
|
applib_free(buffer);
|
|
|
|
bitmap_sequence->current_frame++;
|
|
|
|
const uint32_t width = bitmap_sequence->bitmap_size.w;
|
|
const uint32_t height = bitmap_sequence->bitmap_size.h;
|
|
|
|
const bool bitmap_supports_transparency = (bitmap_format != GBitmapFormat1Bit);
|
|
|
|
// DISPOSE_OP_BACKGROUND sets the background to black with transparency (0x00)
|
|
// If we don't support transparency, just do nothing.
|
|
if (bitmap_supports_transparency &&
|
|
(png_decoder_data->last_dispose_op == APNG_DISPOSE_OP_BACKGROUND)) {
|
|
const uint32_t y_origin = bitmap->bounds.origin.y + png_decoder_data->previous_yoffset;
|
|
for (uint32_t y = y_origin; y < y_origin + png_decoder_data->previous_height; y++) {
|
|
const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bitmap, y);
|
|
const uint32_t x_origin = bitmap->bounds.origin.x + png_decoder_data->previous_xoffset;
|
|
const int16_t min_x = MAX((uint32_t)row_info.min_x, x_origin);
|
|
const int16_t max_x = MIN((uint32_t)row_info.max_x,
|
|
(x_origin + png_decoder_data->previous_width - 1));
|
|
|
|
const int16_t num_bytes = max_x - min_x + 1;
|
|
if (num_bytes > 0) {
|
|
memset(row_info.data + min_x, 0, num_bytes);
|
|
}
|
|
}
|
|
}
|
|
|
|
apng_fctl fctl = {0}; // Defaults work for IDAT frame without fctl data
|
|
|
|
// If this frame doesn't have fctl, use the full width & height
|
|
if (!upng_get_apng_fctl(upng, &fctl)) {
|
|
fctl.width = width;
|
|
fctl.height = height;
|
|
// As a PNG image is only a single frame, display it forever
|
|
bitmap_sequence->current_frame_delay_ms = PLAY_DURATION_INFINITE;
|
|
} else {
|
|
png_decoder_data->last_dispose_op = fctl.dispose_op;
|
|
png_decoder_data->previous_xoffset = fctl.x_offset;
|
|
png_decoder_data->previous_yoffset = fctl.y_offset;
|
|
png_decoder_data->previous_width = fctl.width;
|
|
png_decoder_data->previous_height = fctl.height;
|
|
|
|
fctl.delay_den = (fctl.delay_den == 0) ? APNG_DEFAULT_DELAY_UNITS : fctl.delay_den;
|
|
// Update the current_frame_delay_ms for this frame
|
|
bitmap_sequence->current_frame_delay_ms =
|
|
((uint32_t)fctl.delay_num * MS_PER_SECOND) / fctl.delay_den;
|
|
}
|
|
|
|
// Return the delay_ms for the new frame
|
|
if (delay_ms != NULL) {
|
|
*delay_ms = bitmap_sequence->current_frame_delay_ms;
|
|
}
|
|
|
|
uint32_t bpp = upng_get_bpp(upng);
|
|
upng_format png_format = upng_get_format(upng);
|
|
uint8_t *upng_buffer = (uint8_t*)upng_get_buffer(upng);
|
|
|
|
// Byte aligned rows for image at bpp
|
|
uint16_t row_stride_bytes = (fctl.width * bpp + 7) / 8;
|
|
|
|
if (png_format >= UPNG_INDEXED1 && png_format <= UPNG_INDEXED8) {
|
|
const GColor8 *palette = png_decoder_data->palette;
|
|
|
|
for (uint32_t y = 0; y < fctl.height; y++) {
|
|
const uint16_t corrected_dst_y = fctl.y_offset + y + bitmap->bounds.origin.y;
|
|
const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bitmap, corrected_dst_y);
|
|
int16_t delta_x = fctl.x_offset + bitmap->bounds.origin.x;
|
|
for (int32_t x = MAX(0, row_info.min_x - delta_x);
|
|
x < MIN((int32_t)fctl.width, row_info.max_x - delta_x + 1);
|
|
x++) {
|
|
const uint32_t corrected_dst_x = x + delta_x;
|
|
const uint8_t palette_index = raw_image_get_value_for_bitdepth(upng_buffer, x, y,
|
|
row_stride_bytes, bpp);
|
|
|
|
const GColor8 src = palette[palette_index];
|
|
GColor8 *const dst = (GColor8 *)(row_info.data + corrected_dst_x);
|
|
if (fctl.blend_op == APNG_BLEND_OP_OVER) {
|
|
prv_gbitmap_sequence_blend_over(src, dst);
|
|
} else {
|
|
*dst = src;
|
|
}
|
|
}
|
|
}
|
|
} else if (png_format >= UPNG_LUMINANCE1 && png_format <= UPNG_LUMINANCE8) {
|
|
const int32_t transparent_gray = gbitmap_png_get_transparent_gray_value(upng);
|
|
|
|
for (uint32_t y = 0; y < fctl.height; y++) {
|
|
const uint16_t corrected_y = fctl.y_offset + y + bitmap->bounds.origin.y;
|
|
const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bitmap, corrected_y);
|
|
|
|
// delta_x is the first bit of data in this frame relative to the bitmap's coordinate system
|
|
const int16_t delta_x = fctl.x_offset + bitmap->bounds.origin.x;
|
|
|
|
// for each pixel in this frame, clipping to the bitmap geometry
|
|
for (int32_t x = MAX(0, row_info.min_x - delta_x);
|
|
x < MIN((int32_t)fctl.width, row_info.max_x - delta_x + 1);
|
|
x++) {
|
|
|
|
const uint32_t corrected_dst_x = x + delta_x;
|
|
uint8_t channel = raw_image_get_value_for_bitdepth(upng_buffer, x, y,
|
|
row_stride_bytes, bpp);
|
|
if (transparent_gray >= 0 && channel == transparent_gray) {
|
|
// Grayscale only has fully transparent, so only modify pixels
|
|
// during OP_SOURCE to make the area transparent
|
|
if (fctl.blend_op == APNG_BLEND_OP_SOURCE) {
|
|
prv_set_pixel_in_row(row_info.data, bitmap_format, corrected_dst_x, GColorClear);
|
|
}
|
|
} else {
|
|
channel = (channel * 255) / ~(~0 << bpp); // Convert to 8-bit value
|
|
const GColor8 color = GColorFromRGB(channel, channel, channel);
|
|
|
|
prv_set_pixel_in_row(row_info.data, bitmap_format, corrected_dst_x, color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Successfully updated gbitmap from sequence
|
|
retval = true;
|
|
|
|
cleanup:
|
|
if (!retval) {
|
|
APP_LOG(APP_LOG_LEVEL_ERROR, APNG_UPDATE_ERROR);
|
|
applib_free(buffer);
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
// total elapsed from start of animation
|
|
bool gbitmap_sequence_update_bitmap_by_elapsed(GBitmapSequence *bitmap_sequence,
|
|
GBitmap *bitmap, uint32_t elapsed_ms) {
|
|
if (!bitmap_sequence) {
|
|
return false;
|
|
}
|
|
|
|
// Disabled if play count is 0 and not the very first frame
|
|
if (bitmap_sequence->play_count == 0 && bitmap_sequence->current_frame != 0) {
|
|
return false;
|
|
}
|
|
|
|
// If animation has started and specified time is in the past
|
|
if (bitmap_sequence->current_frame_delay_ms != 0 && elapsed_ms <= bitmap_sequence->elapsed_ms) {
|
|
APP_LOG(APP_LOG_LEVEL_WARNING, APNG_ELAPSED_WARNING);
|
|
return false;
|
|
}
|
|
|
|
bool retval = false;
|
|
bool frame_updated = true;
|
|
while (frame_updated && ((elapsed_ms > bitmap_sequence->elapsed_ms) ||
|
|
(bitmap_sequence->current_frame_delay_ms == 0))) {
|
|
frame_updated = gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
|
|
// If frame is updated at least once, return true
|
|
if (frame_updated) {
|
|
retval = true;
|
|
}
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
// Helper functions
|
|
int32_t gbitmap_sequence_get_current_frame_idx(GBitmapSequence *bitmap_sequence) {
|
|
if (bitmap_sequence) {
|
|
return bitmap_sequence->current_frame;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
uint32_t gbitmap_sequence_get_current_frame_delay_ms(GBitmapSequence *bitmap_sequence) {
|
|
if (bitmap_sequence) {
|
|
return bitmap_sequence->current_frame_delay_ms;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
uint32_t gbitmap_sequence_get_total_num_frames(GBitmapSequence *bitmap_sequence) {
|
|
if (bitmap_sequence) {
|
|
return bitmap_sequence->total_frames;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
uint32_t gbitmap_sequence_get_play_count(GBitmapSequence *bitmap_sequence) {
|
|
if (bitmap_sequence) {
|
|
return bitmap_sequence->play_count;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void gbitmap_sequence_set_play_count(GBitmapSequence *bitmap_sequence, uint32_t play_count) {
|
|
// Loop count is not allowed to be set to 0
|
|
if (bitmap_sequence && play_count) {
|
|
bitmap_sequence->play_count = play_count;
|
|
}
|
|
}
|
|
|
|
GSize gbitmap_sequence_get_bitmap_size(GBitmapSequence *bitmap_sequence) {
|
|
GSize size = (GSize){0, 0};
|
|
if (bitmap_sequence) {
|
|
size = bitmap_sequence->bitmap_size;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
uint32_t gbitmap_sequence_get_total_duration(GBitmapSequence *bitmap_sequence) {
|
|
if (bitmap_sequence) {
|
|
return bitmap_sequence->total_duration_ms;
|
|
}
|
|
return 0;
|
|
}
|