| // Copyright 2020 Joe Drago. All rights reserved. |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| #include "testcase.h" |
| |
| #include "compare.h" |
| #include "y4m.h" |
| |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| |
| static const char * choiceToString(avifCodecChoice choice) |
| { |
| switch (choice) { |
| case AVIF_CODEC_CHOICE_AUTO: |
| return "auto"; |
| case AVIF_CODEC_CHOICE_AOM: |
| return "aom"; |
| case AVIF_CODEC_CHOICE_DAV1D: |
| return "dav1d"; |
| case AVIF_CODEC_CHOICE_LIBGAV1: |
| return "libgav1"; |
| case AVIF_CODEC_CHOICE_RAV1E: |
| return "rav1e"; |
| } |
| return "unknown"; |
| } |
| |
| static avifCodecChoice stringToChoice(const char * str) |
| { |
| if (!strcmp(str, "aom")) { |
| return AVIF_CODEC_CHOICE_AOM; |
| } else if (!strcmp(str, "dav1d")) { |
| return AVIF_CODEC_CHOICE_DAV1D; |
| } else if (!strcmp(str, "libgav1")) { |
| return AVIF_CODEC_CHOICE_LIBGAV1; |
| } else if (!strcmp(str, "rav1e")) { |
| return AVIF_CODEC_CHOICE_RAV1E; |
| } |
| return AVIF_CODEC_CHOICE_AUTO; |
| } |
| |
| TestCase * testCaseCreate(void) |
| { |
| TestCase * tc = malloc(sizeof(TestCase)); |
| memset(tc, 0, sizeof(TestCase)); |
| return tc; |
| } |
| |
| void testCaseDestroy(TestCase * tc) |
| { |
| if (tc->name) { |
| free(tc->name); |
| } |
| if (tc->inputFilename) { |
| free(tc->inputFilename); |
| } |
| free(tc); |
| } |
| |
| void testCaseSetInputFilename(TestCase * tc, const char * inputFilename) |
| { |
| if (tc->inputFilename) { |
| free(tc->inputFilename); |
| } |
| tc->inputFilename = strdup(inputFilename); |
| } |
| |
| void testCaseGenerateName(TestCase * tc) |
| { |
| char basenameBuffer[1024]; |
| if (tc->inputFilename) { |
| strcpy(basenameBuffer, tc->inputFilename); |
| char * dotLoc = strrchr(basenameBuffer, '.'); |
| if (dotLoc) { |
| *dotLoc = 0; |
| } |
| } else { |
| basenameBuffer[0] = 0; |
| } |
| |
| char nameBuffer[1024]; |
| if (snprintf(nameBuffer, |
| sizeof(nameBuffer), |
| "%s_%s_to_%s_qp%d_%d_speed%d", |
| basenameBuffer, |
| choiceToString(tc->encodeChoice), |
| choiceToString(tc->decodeChoice), |
| tc->minQuantizer, |
| tc->maxQuantizer, |
| tc->speed) < 0) { |
| nameBuffer[0] = 0; |
| } |
| nameBuffer[sizeof(nameBuffer) - 1] = 0; |
| if (tc->name) { |
| free(tc->name); |
| } |
| tc->name = strdup(nameBuffer); |
| } |
| |
| static const char * jsonGetString(cJSON * parent, const char * key, const char * def) |
| { |
| if (!parent || !cJSON_IsObject(parent)) { |
| return def; |
| } |
| |
| cJSON * childItem = cJSON_GetObjectItem(parent, key); |
| if (!childItem || !cJSON_IsString(childItem)) { |
| return def; |
| } |
| return childItem->valuestring; |
| } |
| |
| static int jsonGetInt(cJSON * parent, const char * key, int def) |
| { |
| if (!parent || !cJSON_IsObject(parent)) { |
| return def; |
| } |
| |
| cJSON * childItem = cJSON_GetObjectItem(parent, key); |
| if (!childItem || !cJSON_IsNumber(childItem)) { |
| return def; |
| } |
| return childItem->valueint; |
| } |
| |
| static float jsonGetFloat(cJSON * parent, const char * key, float def) |
| { |
| if (!parent || !cJSON_IsObject(parent)) { |
| return def; |
| } |
| |
| cJSON * childItem = cJSON_GetObjectItem(parent, key); |
| if (!childItem || !cJSON_IsNumber(childItem)) { |
| return def; |
| } |
| return (float)childItem->valuedouble; |
| } |
| |
| static avifBool jsonGetBool(cJSON * parent, const char * key, avifBool def) |
| { |
| if (!parent || !cJSON_IsObject(parent)) { |
| return def; |
| } |
| |
| cJSON * childItem = cJSON_GetObjectItem(parent, key); |
| if (!childItem || !cJSON_IsBool(childItem)) { |
| return def; |
| } |
| return (childItem->type == cJSON_True); |
| } |
| |
| TestCase * testCaseFromJSON(cJSON * json) |
| { |
| TestCase * tc = testCaseCreate(); |
| tc->name = strdup(jsonGetString(json, "name", "unknown")); |
| tc->inputFilename = strdup(jsonGetString(json, "input", "unknown")); |
| |
| tc->encodeChoice = stringToChoice(jsonGetString(json, "enc", "aom")); |
| tc->decodeChoice = stringToChoice(jsonGetString(json, "dec", "aom")); |
| tc->minQuantizer = jsonGetInt(json, "minQP", 0); |
| tc->maxQuantizer = jsonGetInt(json, "maxQP", 0); |
| tc->speed = jsonGetInt(json, "speed", 0); |
| tc->active = jsonGetBool(json, "active", AVIF_FALSE); |
| |
| tc->maxThreshold = jsonGetInt(json, "max", 0); |
| tc->avgThreshold = jsonGetFloat(json, "avg", 0); |
| return tc; |
| } |
| |
| cJSON * testCaseToJSON(TestCase * tc) |
| { |
| cJSON * json = cJSON_CreateObject(); |
| |
| if (tc->name) { |
| cJSON_AddStringToObject(json, "name", tc->name); |
| } |
| if (tc->inputFilename) { |
| cJSON_AddStringToObject(json, "input", tc->inputFilename); |
| } |
| |
| cJSON_AddStringToObject(json, "enc", choiceToString(tc->encodeChoice)); |
| cJSON_AddStringToObject(json, "dec", choiceToString(tc->decodeChoice)); |
| cJSON_AddNumberToObject(json, "minQP", tc->minQuantizer); |
| cJSON_AddNumberToObject(json, "maxQP", tc->maxQuantizer); |
| cJSON_AddNumberToObject(json, "speed", tc->speed); |
| cJSON_AddBoolToObject(json, "active", tc->active); |
| |
| cJSON_AddNumberToObject(json, "max", tc->maxThreshold); |
| cJSON_AddNumberToObject(json, "avg", tc->avgThreshold); |
| return json; |
| } |
| |
| int testCaseRun(TestCase * tc, const char * dataDir, avifBool generating) |
| { |
| if (!tc->name || !tc->inputFilename) { |
| return AVIF_FALSE; |
| } |
| |
| char y4mFilename[2048]; |
| snprintf(y4mFilename, sizeof(y4mFilename), "%s/%s", dataDir, tc->inputFilename); |
| y4mFilename[sizeof(y4mFilename) - 1] = 0; |
| |
| avifImage * image = avifImageCreateEmpty(); |
| if (!y4mRead(image, y4mFilename)) { |
| avifImageDestroy(image); |
| printf("ERROR[%s]: Can't read y4m: %s\n", tc->name, y4mFilename); |
| return AVIF_FALSE; |
| } |
| |
| avifBool result = AVIF_TRUE; |
| avifRWData encodedData = AVIF_DATA_EMPTY; |
| avifEncoder * encoder = NULL; |
| avifDecoder * decoder = NULL; |
| |
| encoder = avifEncoderCreate(); |
| encoder->codecChoice = tc->encodeChoice; |
| encoder->maxThreads = 4; // TODO: pick something better here |
| if (avifEncoderWrite(encoder, image, &encodedData) != AVIF_RESULT_OK) { |
| printf("ERROR[%s]: Encode failed\n", tc->name); |
| result = AVIF_FALSE; |
| goto cleanup; |
| } |
| |
| decoder = avifDecoderCreate(); |
| decoder->codecChoice = tc->decodeChoice; |
| avifResult decodeResult = avifDecoderParse(decoder, (avifROData *)&encodedData); |
| if (decodeResult != AVIF_RESULT_OK) { |
| printf("ERROR[%s]: Decode failed\n", tc->name); |
| result = AVIF_FALSE; |
| goto cleanup; |
| } |
| |
| avifResult nextImageResult = avifDecoderNextImage(decoder); |
| if (nextImageResult != AVIF_RESULT_OK) { |
| printf("ERROR[%s]: NextImage failed\n", tc->name); |
| result = AVIF_FALSE; |
| goto cleanup; |
| } |
| |
| ImageComparison ic; |
| if (!compareYUVA(&ic, image, decoder->image)) { |
| printf("ERROR[%s]: compare bailed out\n", tc->name); |
| result = AVIF_FALSE; |
| goto cleanup; |
| } |
| |
| if (generating) { |
| int maxThreshold = ic.maxDiff; |
| if (maxThreshold > 0) { |
| // Not lossless, give one more codepoint of wiggle room |
| ++maxThreshold; |
| } |
| float avgThreshold = ic.avgDiff + 0.25f; |
| |
| tc->maxThreshold = maxThreshold; |
| tc->avgThreshold = avgThreshold; |
| printf("Generated[%s]: Thresholds - Max %d, Avg %f\n", tc->name, tc->maxThreshold, tc->avgThreshold); |
| } else { |
| if (ic.maxDiff > tc->maxThreshold) { |
| printf("ERROR[%s]: max diff threshold exceeded: %d > %d\n", tc->name, ic.maxDiff, tc->maxThreshold); |
| result = AVIF_FALSE; |
| goto cleanup; |
| } |
| if (ic.avgDiff > tc->avgThreshold) { |
| printf("FAILED[%s]: avg diff threshold exceeded: %f > %f\n", tc->name, ic.avgDiff, tc->avgThreshold); |
| result = AVIF_FALSE; |
| goto cleanup; |
| } |
| printf("OK[%s]\n", tc->name); |
| } |
| |
| cleanup: |
| if (decoder) { |
| avifDecoderDestroy(decoder); |
| } |
| avifRWDataFree(&encodedData); |
| if (encoder) { |
| avifEncoderDestroy(encoder); |
| } |
| avifImageDestroy(image); |
| (void)generating; |
| return result; |
| } |