/* * 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. */ #pragma once #include "test_graphics.h" #include "applib/graphics/gbitmap_png.h" #include "util/graphics.h" #include "util/math.h" #include "clar.h" #include #include #include #include #include #include #define PATH_STRING_LENGTH 512 extern GBitmapDataRowInfo prv_gbitmap_get_data_row_info(const GBitmap *bitmap, uint16_t y); #include "util_pbi.h" // The following macros append the ~platform (unless default) and the filetype extension // unless file_name contains Xbit, then appends .(native_bitdepth)bit and filetype extension #define TEST_NAMED_PBI_FILE(file_name) namecat(file_name, ".pbi") #define TEST_NAMED_PNG_FILE(file_name) namecat(file_name, ".png") // The following macros create a file name based on the function name plus extension #define TEST_PBI_FILE namecat(__func__, ".pbi") #define TEST_PBI_FILE_FMT(fmt) namecat(__func__, "." #fmt ".pbi") #define TEST_PNG_FILE namecat(__func__, ".png") #define TEST_PNG_FILE_FMT(fmt) namecat(__func__, "." #fmt ".png") #define TEST_PDC_FILE namecat(__func__, ".pdc") #define TEST_PDC_PBI_FILE namecat(__func__, ".pdc.pbi") #define TEST_APNG_FILE namecat(__func__, ".apng") #define TEST_PBI_FILE_X(x) namecat(__func__, "_" #x ".pbi") bool tests_write_gbitmap_to_pbi(GBitmap *bmp, const char *filename) { char full_path[PATH_STRING_LENGTH]; snprintf(full_path, sizeof(full_path), "%s/%s", TEST_OUTPUT_PATH, filename); return write_gbitmap_to_pbi(bmp, full_path, PBI2PNG_EXE); } // Used to work around __func__ not being a string literal (necessary for macro concatenation) static const char *namecat(const char* str1, const char* str2){ char *filename = malloc(PATH_STRING_LENGTH); filename[0] = '\0'; strcat(filename, str1); char *filename_xbit = strstr(filename, ".Xbit"); if (filename_xbit) { // Support using ".Xbit" for the native bitdepth filename_xbit[1] = (SCREEN_COLOR_DEPTH_BITS == 8) ? '8' : '1'; printf("filename and filename_xbit %s : %s\n", filename, filename_xbit); } else { #if !PLATFORM_DEFAULT // Add ~platform to files with unit-tests built for a specific platform strcat(filename, "~"); strcat(filename, PLATFORM_NAME); #endif } strcat(filename, str2); return filename; } static char get_terminal_color(uint8_t c) { switch (c) { case GColorBlackARGB8: // black return 'B'; case GColorWhiteARGB8: // white return 'W'; case GColorRedARGB8: // red return 'R'; case GColorGreenARGB8: // green return 'G'; case GColorBlueARGB8: // blue return 'b'; default: return ' '; } } // A simple functon for printing 8-bit gbitmaps to the console. // Makes it easy to quickly review failing test cases. void print_bitmap(const GBitmap *bmp) { printf("Row Size Bytes: %d\n", bmp->row_size_bytes); printf("Bounds: "); printf(GRECT_PRINTF_FORMAT, GRECT_PRINTF_FORMAT_EXPLODE(bmp->bounds)); printf("\n"); GSize size = bmp->bounds.size; uint8_t *data = (uint8_t *)bmp->addr; // Build a coordinate system, 3 rows up top for the col number for (uint8_t y = 0; y < 3; ++y) { printf("\t"); // leave space for row # for (uint8_t x = 0; x < size.w; ++x) { int num = -1; switch (y) { case 0: // hundreds if (x < 100) break; num = (x / 100) % 10; break; case 1: // tens if (x < 10) break; num = (x / 10) % 10; break; case 2: // ones num = x % 10; break; } if (num < 0) { printf(" "); } else { printf("%d", num); } } printf("\n"); } const uint8_t start_x = bmp->bounds.origin.x; const uint8_t end_x = start_x + bmp->bounds.size.w; const uint8_t start_y = bmp->bounds.origin.y; const uint8_t end_y = start_y + bmp->bounds.size.h; for (uint8_t y = start_y; y < end_y; ++y) { printf("\n%d\t", y); for (uint8_t x = start_x; x < end_x; ++x) { uint8_t color = data[y * bmp->row_size_bytes + x]; printf("%c", get_terminal_color(color)); } } printf("\n\n\n\n"); } // Will load the PBI at location $TEST_IMAGES_PATH/filename into a gbitmap. GBitmap *get_gbitmap_from_pbi(const char *filename) { char full_path[PATH_STRING_LENGTH]; snprintf(full_path, PATH_STRING_LENGTH, "%s/%s", TEST_IMAGES_PATH, filename); // Support using ".Xbit" for the native bitdepth char *filename_xbit = strstr(full_path, ".Xbit"); if (filename_xbit) { filename_xbit[1] = (SCREEN_COLOR_DEPTH_BITS == 8) ? '8' : '1'; } FILE *file = fopen(full_path, "r"); if (!file) { printf("Unable to open file: %s\n", full_path); return NULL; } GBitmap *bmp = malloc(sizeof(*bmp)); *bmp = (GBitmap){0}; // Read bitmap header fread(&bmp->row_size_bytes, sizeof(bmp->row_size_bytes), 1, file); fread(&bmp->info_flags, sizeof(bmp->info_flags), 1, file); fread(&bmp->bounds, sizeof(bmp->bounds), 1, file); size_t data_size = bmp->row_size_bytes * bmp->bounds.size.h; bmp->addr = malloc(data_size); bmp->info.is_bitmap_heap_allocated = true; fread(bmp->addr, 1, data_size, file); uint8_t palette_size = gbitmap_get_palette_size(gbitmap_get_format(bmp)); if (palette_size > 0) { // Allocate palette of GColor8 entries in ARGB8 format bmp->palette = malloc(palette_size * sizeof(GColor8)); fread(bmp->palette, 1, palette_size * sizeof(GColor8), file); } fclose(file); return bmp; } #define ACTUAL_PBI_FILE_EXTENSION "-actual.pbi" #define EXPECTED_PBI_FILE_EXTENSION "-expected.pbi" #define DIFF_PBI_FILE_EXTENSION "-diff.pbi" #define DIFF_COLOR GColorMagenta static GColor8 prv_convert_to_gcolor8(GBitmapFormat format, uint8_t raw_value, GColor *palette) { uint8_t color8 = raw_value; switch (format) { case GBitmapFormat1Bit: color8 = (raw_value) ? GColorWhite.argb : GColorBlack.argb; break; case GBitmapFormat1BitPalette: case GBitmapFormat2BitPalette: case GBitmapFormat4BitPalette: color8 = palette[raw_value].argb; break; case GBitmapFormat8Bit: default: break; } return (GColor8)color8; } static uint8_t prv_raw_image_get_value_for_format(const uint8_t *raw_image_buffer, uint32_t x, uint32_t y, uint16_t row_stride_bytes, uint8_t bitdepth, GBitmapFormat format) { if (format == GBitmapFormat1Bit){ // Retrieve the byte from the image buffer containing the requested pixel uint32_t pixel_in_byte = raw_image_buffer[y * row_stride_bytes + (x / 8)]; // Find the index of the pixel in terms of coordinates and aligned_width uint32_t pixel_index = y * (row_stride_bytes * 8) + x; // Shift and mask the requested pixel data from the byte containing it and return return (uint8_t)(pixel_in_byte >> ((pixel_index % 8)) & 1); } else { return raw_image_get_value_for_bitdepth(raw_image_buffer, x, y, row_stride_bytes, bitdepth); } } static void prv_write_diff_to_file(const char *filename, GBitmap *expected_bmp, GBitmap *actual_bmp, GBitmap *diff_bmp) { // Write the expected output to filename-expected.png char bmp_filename[PATH_STRING_LENGTH]; if (expected_bmp) { strncpy(bmp_filename, filename, PATH_STRING_LENGTH - strlen(EXPECTED_PBI_FILE_EXTENSION) - 1); char *ext = strrchr(bmp_filename, '.'); strncpy(ext, EXPECTED_PBI_FILE_EXTENSION, strlen(EXPECTED_PBI_FILE_EXTENSION) + 1); cl_assert(tests_write_gbitmap_to_pbi(expected_bmp, bmp_filename)); // TODO: PBL-20932 Add 1-bit and palletized support if (actual_bmp->info.format == GBitmapFormat8Bit && diff_bmp) { // Only write the diff file if there is an expected image strncpy(bmp_filename, filename, PATH_STRING_LENGTH - strlen(DIFF_PBI_FILE_EXTENSION) - 1); ext = strrchr(bmp_filename, '.'); strncpy(ext, DIFF_PBI_FILE_EXTENSION, strlen(DIFF_PBI_FILE_EXTENSION) + 1); cl_assert(tests_write_gbitmap_to_pbi(diff_bmp, bmp_filename)); } } // Write the actual output to the filename-actual.png strncpy(bmp_filename, filename, PATH_STRING_LENGTH - strlen(ACTUAL_PBI_FILE_EXTENSION) - 1); char *ext = strrchr(bmp_filename, '.'); strncpy(ext, ACTUAL_PBI_FILE_EXTENSION, strlen(ACTUAL_PBI_FILE_EXTENSION) + 1); cl_assert(tests_write_gbitmap_to_pbi(actual_bmp, bmp_filename)); } // declared in gbitmap.c GBitmap *prv_gbitmap_create_blank_internal_no_platform_checks(GSize size, GBitmapFormat format); // Compare two bitmap and return whether or not they are the same // Note that if both passed bitmaps are NULL, this test will succeed! bool gbitmap_eq(GBitmap *actual_bmp, GBitmap *expected_bmp, const char *filename) { bool rc = false; GBitmap *diff_bmp = NULL; if (!actual_bmp && !expected_bmp) { return true; } else if (!actual_bmp || !expected_bmp) { goto done; } if (!grect_equal(&actual_bmp->bounds, &expected_bmp->bounds)) { printf("Unmatched bounds\n"); printf("\tExpected: "); printf(GRECT_PRINTF_FORMAT, GRECT_PRINTF_FORMAT_EXPLODE(expected_bmp->bounds)); printf("\n\tGot: "); printf(GRECT_PRINTF_FORMAT, GRECT_PRINTF_FORMAT_EXPLODE(actual_bmp->bounds)); goto done; } uint8_t actual_bmp_palette_size = gbitmap_get_palette_size(gbitmap_get_format(actual_bmp)); uint8_t expected_bmp_palette_size = gbitmap_get_palette_size(gbitmap_get_format(expected_bmp)); uint8_t *expected_bmp_data = (uint8_t *)expected_bmp->addr; uint8_t actual_bmp_bpp = gbitmap_get_bits_per_pixel(gbitmap_get_format(actual_bmp)); uint8_t expected_bmp_bpp = gbitmap_get_bits_per_pixel(gbitmap_get_format(expected_bmp)); const int16_t start_y = actual_bmp->bounds.origin.y; const int16_t end_y = start_y + actual_bmp->bounds.size.h; rc = true; // Create a bitmap for the diff image - force 8-bit // The diff image contains first the actual image, then the diff image, and then the expected image // These images are separated by one pixel column (transparent) GSize diff_bmp_size = actual_bmp->bounds.size; diff_bmp_size.w = (3 * diff_bmp_size.w) + 2; // 2 pixels to divide the three images diff_bmp = prv_gbitmap_create_blank_internal_no_platform_checks(diff_bmp_size, GBitmapFormat8Bit); if (!diff_bmp) { printf("Unable to create diff bitmap\n"); rc = false; goto done; } for (int y = start_y; y < end_y; ++y) { uint8_t *line = ((uint8_t*)diff_bmp->addr) + (diff_bmp->row_size_bytes * y); // TODO: PBL-20932 Add 1-bit and palletized support if (actual_bmp->info.format == GBitmapFormat8Bit) { line[(diff_bmp->row_size_bytes / 3) + 1] = GColorClear.argb; // Separator pixel between images line[(2 * diff_bmp->row_size_bytes / 3) + 1] = GColorClear.argb; // Separator pixel between images } // Needs to be prv_gbitmap_get_data_row_info to avoid unit test mocked version const GBitmapDataRowInfo dest_row_info = prv_gbitmap_get_data_row_info(actual_bmp, y); const int16_t start_x = MAX(actual_bmp->bounds.origin.x, dest_row_info.min_x); const int16_t end_x = MIN(grect_get_max_x(&actual_bmp->bounds), dest_row_info.max_x + 1); const int16_t y_line = 0; // line is constant zero below now that we are retrieving row if (end_x < start_x) { continue; } for (int x = start_x; x < end_x; ++x) { uint8_t *actual_bmp_data = dest_row_info.data; uint8_t actual_bmp_val = prv_raw_image_get_value_for_format(actual_bmp_data, x, y_line, actual_bmp->row_size_bytes, actual_bmp_bpp, actual_bmp->info.format); uint8_t expected_bmp_val = prv_raw_image_get_value_for_format(expected_bmp_data, x, y, expected_bmp->row_size_bytes, expected_bmp_bpp, expected_bmp->info.format); GColor8 actual_bmp_color = prv_convert_to_gcolor8(actual_bmp->info.format, actual_bmp_val, actual_bmp->palette); GColor8 expected_bmp_color = prv_convert_to_gcolor8(expected_bmp->info.format, expected_bmp_val, expected_bmp->palette); if (!gcolor_equal(actual_bmp_color, expected_bmp_color)) { if (rc) { // Only print out the first mismatch printf("Mismatch at x: %d y: %d\n", x, y); printf("value for end_x was:%d\n", end_x); printf("format was %d\n", actual_bmp->info.format); } rc = false; } // TODO: PBL-20932 Add 1-bit and palletized support if (actual_bmp->info.format == GBitmapFormat8Bit) { if (actual_bmp_color.argb != expected_bmp_color.argb) { GColor8 diff_bmp_color = DIFF_COLOR; line[(diff_bmp->row_size_bytes / 3) + x + 1] = diff_bmp_color.argb; } else { line[(diff_bmp->row_size_bytes / 3) + x + 1] = actual_bmp_color.argb; } // Fill in the actual and expected pixels on either side of the diff image line[x] = actual_bmp_color.argb; line[(2 * diff_bmp->row_size_bytes / 3) + x + 1] = expected_bmp_color.argb; } } } done: if (!rc) { prv_write_diff_to_file(filename, expected_bmp, actual_bmp, diff_bmp); } gbitmap_destroy(diff_bmp); return rc; } // Compare the given gbitmap to a gbmitmap loaded from a PBI in the filename given. bool gbitmap_pbi_eq_with_bounds(GBitmap *bmp, const char *filename, const GRect *bounds) { GBitmap *pbi_bmp = get_gbitmap_from_pbi(filename); if (pbi_bmp && bounds) { pbi_bmp->bounds = *bounds; } bool rc = gbitmap_eq(bmp, pbi_bmp, filename); gbitmap_destroy(pbi_bmp); return rc; } bool gbitmap_pbi_eq(GBitmap *bmp, const char *filename) { return gbitmap_pbi_eq_with_bounds(bmp, filename, NULL); } size_t load_file(const char* filename, uint8_t** data) { char full_path[PATH_STRING_LENGTH]; snprintf(full_path, sizeof(full_path), "%s/%s", TEST_IMAGES_PATH, filename); FILE *file = fopen(full_path,"rb"); if(file == NULL){ printf("Error: couldn't open file: %s\n", filename); cl_assert(false); } fseek(file,0,SEEK_END); int data_size = ftell(file); fseek(file,0,SEEK_SET); *data = (unsigned char*)malloc(data_size); fread(*data,1, data_size, file); fclose(file); return data_size; } // Mask flags to indicate what to setup in the draw_state of GContext within setup_test_context #define CTX_FLAG_DS_ALL 0x00000010 #define CTX_FLAG_DS_CLIP_BOX 0x00000020 #define CTX_FLAG_DS_DRAWING_BOX 0x00000040 #define CTX_FLAG_DS_STROKE_COLOR 0x00000080 #define CTX_FLAG_DS_FILL_COLOR 0x00000100 #define CTX_FLAG_DS_TEXT_COLOR 0x00000200 #define CTX_FLAG_DS_COMPOSITING_MODE 0x00000400 #define CTX_FLAG_DS_ANTIALIASED 0x00000800 #define CTX_FLAG_DS_STROKE_WIDTH 0x00001000 void setup_test_context(GContext* ctx, uint32_t flags, GDrawState *draw_state, bool *lock) { if (draw_state) { if (flags & CTX_FLAG_DS_CLIP_BOX) { ctx->draw_state.clip_box = draw_state->clip_box; } if (flags & CTX_FLAG_DS_DRAWING_BOX) { ctx->draw_state.drawing_box = draw_state->drawing_box; } if (flags & CTX_FLAG_DS_STROKE_COLOR) { graphics_context_set_stroke_color(ctx, draw_state->stroke_color); } if (flags & CTX_FLAG_DS_FILL_COLOR) { graphics_context_set_fill_color(ctx, draw_state->fill_color); } if (flags & CTX_FLAG_DS_TEXT_COLOR) { graphics_context_set_text_color(ctx, draw_state->text_color); } if (flags & CTX_FLAG_DS_COMPOSITING_MODE) { graphics_context_set_compositing_mode(ctx, draw_state->compositing_mode); } if (flags & CTX_FLAG_DS_ANTIALIASED) { #if PBL_COLOR graphics_context_set_antialiased(ctx, draw_state->antialiased); #endif } if (flags & CTX_FLAG_DS_STROKE_WIDTH) { graphics_context_set_stroke_width(ctx, draw_state->stroke_width); } } if (lock) { ctx->lock = lock; } } GBitmap* setup_pbi_test(const char *filename) { uint8_t *pbi_data = NULL; size_t pbi_size = 0; pbi_size = load_file(filename, &pbi_data); cl_assert(pbi_size > 0); cl_assert(pbi_data); return gbitmap_create_with_data(pbi_data); } static GBitmap* setup_png_test(const char *filename) { uint8_t *png_data = NULL; size_t png_size = 0; png_size = load_file(filename, &png_data); cl_assert(png_size > 0); cl_assert(png_data); return gbitmap_create_from_png_data(png_data, png_size); } void setup_test_aa_sw(GContext *ctx, FrameBuffer *fb, GRect clip_box, GRect drawing_box, bool antialiased, uint8_t stroke_width) { test_graphics_context_reset(ctx, fb); GDrawState draw_state = { .clip_box = clip_box, .drawing_box = drawing_box, #if PBL_COLOR .antialiased = antialiased, #endif .stroke_width = stroke_width }; setup_test_context(ctx, (CTX_FLAG_DS_CLIP_BOX | CTX_FLAG_DS_DRAWING_BOX | CTX_FLAG_DS_ANTIALIASED | CTX_FLAG_DS_STROKE_WIDTH), &draw_state, NULL); } #if PLATFORM_SPALDING bool gbitmap_8bit_to_8bit_circular(GBitmap *bitmap) { // Only allow conversion of 8Bit 180x180 rectangular bitmaps to circular if (!bitmap || (gbitmap_get_format(bitmap) != GBitmapFormat8Bit) || (bitmap->bounds.size.w != DISP_COLS) || (bitmap->bounds.size.h != DISP_COLS)) { return false; } // Using realloc or copying to a new buffer has high memory overhead // attempt to shuffle bytes in place to allow 3rd party watchapps use uint8_t *data = gbitmap_get_data(bitmap); // Convert format and link to data_row_infos table bitmap->info.format = GBitmapFormat8BitCircular; bitmap->data_row_infos = g_gbitmap_spalding_data_row_infos; for (uint32_t y = 0; y < DISP_ROWS; y++) { GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bitmap, y); // copy (using memmove for overlapping region) valid bytes between min_x and max_x memmove(&row_info.data[row_info.min_x], &data[y * DISP_COLS + row_info.min_x], row_info.max_x - row_info.min_x + 1); } return true; } #endif