#include /* This program (testil) is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif /* HAVE_CONFIG_H */ #define _USE_MATH_DEFINES // Have to add for MSVC++ to use M_PI properly. #include #include #include #ifdef WORDS_BIGENDIAN enum colors {ALPHA, BLUE, GREEN, RED}; #else /* not WORDS_BIGENDIAN */ enum colors {RED, GREEN, BLUE, ALPHA}; #endif /* not WORDS_BIGENDIAN */ /* We would need ILU just because of iluErrorString() function... */ /* So make it possible for both with and without ILU! */ #ifdef ILU_ENABLED #include #define ERROR_LOADING_FILE_MACRO(filename, code) fprintf(stderr, "Error loading file '%s'\nReason: %s\n", (filename), iluErrorString((code))) #define ERROR_SAVING_FILE_MACRO(filename, code) fprintf(stderr, "Error saving file '%s'\nReason: %s\n", (filename), iluErrorString((code))) #else /* not ILU_ENABLED */ #define ERROR_LOADING_FILE_MACRO(filename, code) fprintf(stderr, "Error loading file '%s'\nError code: 0x%X\n", (filename), (unsigned int)(code)) #define ERROR_SAVING_FILE_MACRO(filename, code) fprintf(stderr, "Error saving file '%s'\nError code: 0x%X\n", (filename), (unsigned int)(code)) #endif /* not ILU_ENABLED */ /** How did the tests ended? */ enum test_results {TEST_OK = 0, TEST_FAIL = 0x1, TEST_FAIL_QUANTIL = 0x2, TEST_FAIL_INTEGRAL = 0x4 }; /** Parsing and parsing results (Parameters.flags) related: What options were passed? */ enum {ACTION_HELP = 0x1, ACTION_VERBOSE = 0x2, ACTION_PRESERVE_TESTFILE = 0x4, ACTION_ROUGH_MODE = 0x8, ACTION_TEST_ALPHA = 0x10 }; /** Parsing only: What sort of options to expect? */ enum {EXPECT_EXTENSION = 0x1, EXPECT_QUANTILE = 0x2, EXPECT_FILENAME = 0x4, EXPECT_RESOLUTION = 0x8, EXPECT_IMAGES = 0x10}; /** What to test for? */ enum {TEST_NOTHING, TEST_EXTENSION, TEST_IS_TEST_IMAGE, TEST_IMAGES_ARE_SAME}; int compare_ILubyte (const void * a, const void * b) { return ( *(ILubyte * )a - *(ILubyte * )b ); } /* struct PIXEL { ILubyte color[4]; }; typedef struct PIXEL pixel;*/ /** What we collect from the comand line... */ struct parameters { /* ** Input parameters ** */ /** In the case we want to test filename */ char first_filename[64]; /** In the case we want to compare two filenames */ char second_filename[64]; /** or test extension... */ char * extension; /*** Tests parameters ***/ /** how talkative should we be? */ int verbose; /** what about preserve-testfile, rough mode? */ int flags; /* How many bytes per pixel? */ int bpp; /** If we want to tweak test image sizes... */ int resolution[2]; /** Quantile test specs */ double quantile_spec; int quantile_treshold; /** Integral test specs */ double integral_treshold; }; typedef struct parameters Parameters; /** Function generates test pattern when given data and dimensions * \param data Image data. 32bits per pixel * \param w, h Width and height of the image */ void generate_test_image(ILubyte * data, int w, int h, Parameters params) { int test_alpha = params.flags & ACTION_TEST_ALPHA; int bpp = params.bpp; int i, j, index, j_index; double red, green, blue, sum; /* For easier notations */ int h_5 = h / 5; for (j = 0, index = 0; j < h_5 * 3; j++) for (i = 0; i < w * bpp; i += bpp) /* We jump bpp bytes ahead with each pixel */ {/* Making 27 samples from the RGB cube. */ index = 9 * (j / h_5) + (i * 9) / w / bpp; j_index = j * w * bpp; /* just to make stuff look nice */ data[j_index + i + RED] = 255 / 6 + (255 / 3) * (index % 3) ; data[j_index + i + GREEN] = 255 / 6 + (255 / 3) * ((index % 9) / 3 ); data[j_index + i + BLUE] = 255 / 6 + (255 / 3) * (index / 9); if (test_alpha) data[j_index + i + ALPHA] = 255 - 63 * ((j * 4 / h_5) % 4); } for (j = 3 * h_5; j < 4 * h_5; j++) for (i = 0; i < w * bpp; i += bpp) {/* Making color gradient */ red = cos(2 * M_PI / (double)w * i / bpp) / 2.0 + 0.5; green = cos(2 * M_PI / (double)w * i / bpp + M_PI * 1.0 / 3.0) / 2.0 + 0.5; blue = cos(2 * M_PI / (double)w * i / bpp + M_PI * 2.0 / 3.0) / 2.0 + 0.5; sum = sqrt(red * red + green * green + blue * blue); j_index = j * w * bpp; data[j_index + i + RED] = 255 * red / sum; data[j_index + i + GREEN] = 255 * green / sum; data[j_index + i + BLUE] = 255 * blue / sum; if (test_alpha) data[j_index + i + ALPHA] = 255; } for (j = 4 * h_5; j < 5 * h_5; j++) for (i = 0; i < w * bpp; i += bpp) {/* Making smooth B&W gradient */ j_index = j * w * bpp; data[j_index + i + RED] = (i / bpp * 255) / w; data[j_index + i + GREEN] = (i / bpp * 255) / w; data[j_index + i + BLUE] = (i / bpp * 255) / w; if (test_alpha) data[j_index + i + ALPHA] = 255; } } /** Functions saves the test image of given dimensions and given name * \param name Filename * \param w, h Image dimensions * * The image is defined in the generate_test_image function and this function just calls it... */ int save_test_image(const char * name, int w, int h, Parameters params) { /* Set constants so that there are no div rounding errors */ ILuint handle; /* Generate the paperwork for our test image... */ ilGenImages(1, & handle); ilBindImage(handle); /* how much memory will we need? */ int memory_needed = w * h * params.bpp; ILubyte * data = (ILubyte * )malloc(memory_needed); if (data == NULL) { fprintf(stderr, "Out of memory, %s:%d\n", __FILE__, __LINE__); return IL_OUT_OF_MEMORY; } generate_test_image(data, w, h, params); /* finally set the image data */ ilTexImage(w, h, 1, params.bpp, ( (params.flags & ACTION_TEST_ALPHA) == 0 ? IL_RGB : IL_RGBA), IL_UNSIGNED_BYTE, data); /* and dump them to the disc... */ ILboolean saved = ilSaveImage(name); int return_value = IL_NO_ERROR; if (saved == IL_FALSE) { return_value = ilGetError(); ERROR_SAVING_FILE_MACRO(name,return_value); } /* Finally, clean the mess! */ ilDeleteImages(1, & handle); free(data); return return_value; } /** Returns the sum value of quantile of absolute differences among subpixels. * \param reference Left hand side image (LHS == RHS ?) * \param sample Right hand side image * \param num_subpixels BPP of image, usually 4 (RGBA) * \param quantile What quantile value are we interested in * \return Value of the quantile * * First of all, absolute differences are computed. * Then they are stored in an array and sorted using quicksort. * Finally, the quantile is determined and returned. */ int calculate_median_pixel_distance_abs(ILubyte * reference, ILubyte * sample, int num_subpixels, double quantile) { ILubyte * differences = (ILubyte *)malloc(sizeof(ILubyte) * num_subpixels); if (differences == NULL) { fprintf(stderr, "Out of memory, %s:%d\n", __FILE__, __LINE__); return IL_OUT_OF_MEMORY; } int ii, difference; for (ii = 0; ii < num_subpixels; ii++) {/* Going through all pixel components and assigning the absolute difference to differences array */ difference = (int)sample[ii] - reference[ii]; /* avoiding: overflow + need to have abs() */ differences[ii] = (difference >= 0) ? difference : - difference; } /* We need the quantile, so we have to sort the array */ qsort (differences, num_subpixels, sizeof(ILubyte), compare_ILubyte); if (quantile > 1.0 || quantile < 0.0) /* The quantile has totally wrong value... */ { fprintf(stderr, "Come on... Quantile should be between 0.0 and 1.0 (you wanted %.3f)\n", quantile); return -1; } int index = (int)(quantile * num_subpixels); /* Which member of the sorted array is the quantile? */ int result = differences[index]; free(differences); /* Clean the mess */ differences = 0; return result; } /** Returns the sum of squared differences between component color values, normed. * */ double calculate_averaged_distance_sqr_integral(ILubyte * reference, ILubyte * sample, int num_subpixels) { long long differences = 0; int ii, difference; for (ii = 0; ii < num_subpixels; ii++) {/* Going through all pixel components and assigning the absolute difference to differences array */ difference = sample[ii] - reference[ii]; /* avoiding overflow + need to have abs() */ differences += difference * difference; } return sqrt(differences / (double)num_subpixels); } /** Do the comparison and return results through parameters. */ void compare_fields(int w, int h, ILubyte * sample, ILubyte * reference, Parameters params, int * quantile, double * integral) { * quantile = calculate_median_pixel_distance_abs(reference, sample, w * h * params.bpp, params.quantile_spec); * integral = calculate_averaged_distance_sqr_integral(reference, sample, w * h * params.bpp); } /** We have some results of test of similarity of two images. Are they the same? * \param filename What filename are we actually testing? (Needed gor report purposes only.) * \param quantile What is the value of the quantile we have acquired earlier? * \param integral And what is the value of normed integral distance? * \return 0 on success or TEST_FAIL or'd with the cause. */ int interpret_results(const char * filename, Parameters params, int quantile, double integral) { int return_value = 0; if (quantile > params.quantile_treshold) { printf("Quantile test failed: More than %d%% of subpixels have greater error than %d (should be less than %d)\n", (int)(100 - 100 * params.quantile_spec), quantile, params.quantile_treshold); return_value |= TEST_FAIL_QUANTIL | TEST_FAIL; } if (integral > params.integral_treshold) { printf("Integral test failed: Deviation is %.3f, should be below %.3f\n", integral, params.integral_treshold); return_value |= TEST_FAIL_INTEGRAL | TEST_FAIL; } if (params.verbose > 0) printf("Testing file '%s'. \nQuantile[%.3f]: %d (max %d)\nIngegral: %f (max %f)\n", filename, params.quantile_spec, quantile, params.quantile_treshold, integral, params.integral_treshold); return return_value; } /** Is the passed params.first_filename a test image? * Useful for testing whether the various format load functions alre OK * if we can't save the test image in DevIL. * So we take some test image, convert it to another format in some exernal editor * and then we try to load it. Smart, isn't it? :-) */ int test_is_testimage(Parameters params) { /* First, we load the generated and saved image */ ILuint handle; ilGenImages(1, & handle); ilBindImage(handle); int loaded = ilLoadImage(params.first_filename); if (loaded == IL_FALSE) {/* something went wrong */ ERROR_LOADING_FILE_MACRO(params.first_filename, loaded); return loaded; } /* getting image width and height */ int w, h; w = ilGetInteger(IL_IMAGE_WIDTH); h = ilGetInteger(IL_IMAGE_HEIGHT); /* Then we save image data into arrays that we allocate now */ ILubyte * reference, * sample; reference = malloc(w * h * params.bpp * sizeof(ILubyte)); sample = malloc(w * h * params.bpp * sizeof(ILubyte)); if (reference == NULL || sample == NULL) { if ((params.flags & ACTION_PRESERVE_TESTFILE) == 0) remove(params.first_filename); fprintf(stderr, "Out of memory, %s:%d\n", __FILE__, __LINE__); return IL_OUT_OF_MEMORY; } /* Allocation went fine and now we get what SHOULD BE on the loaded image... */ generate_test_image((ILubyte *)reference, w, h, params); ilCopyPixels(0, 0, 0, w, h, 1, ((params.flags & ACTION_TEST_ALPHA) == 0 ? IL_RGB : IL_RGBA), IL_UNSIGNED_BYTE, sample); int quantile; double integral; compare_fields(w, h, sample, reference, params, & quantile, & integral); int return_value = interpret_results(params.first_filename, params, quantile, integral); free(sample); free(reference); /* We do not want to keep the saved test image by default... */ if ((params.flags & ACTION_PRESERVE_TESTFILE) == 0) remove(params.first_filename); return return_value; } /** Tests whether we can save and load the test data to a specified extension * \param params Test options in a nice box * \return 0 if all is OK, or an error code (see interpret_results) */ int test_format(Parameters params) { /* First, let's generate and save a test image */ const char * base_name = "test."; char filename [64]; if (strlen(params.extension) + strlen(base_name) >= sizeof(filename)) return -1; /* buffer overflow */ sprintf(filename, "%s%s", base_name, params.extension); int saved = save_test_image(filename, params.resolution[0], params.resolution[1], params); if (saved != 0) /* something went wrong, no point in continuing, the user already knows */ return saved; strncpy(params.first_filename, filename, sizeof(params.first_filename)); /* Will we be able to load that very test image? */ return test_is_testimage(params); } int test_are_same(Parameters params) { const int images_count = 2; ILuint handle[images_count]; int i; ILubyte * image_data[images_count]; for (i = 0; i < images_count; i++) image_data[i] = NULL; ilGenImages(images_count, & handle[0]); const char * filenames [] = {params.first_filename, params.second_filename}; int w, h; for (i = 0; i < images_count; i++) { ilBindImage(handle[i]); int loaded = ilLoadImage(filenames[i]); if (loaded == IL_FALSE) {/* something went wrong */ ERROR_LOADING_FILE_MACRO(filenames[i], loaded); for (i--; i >= 0; i--) {/* We might allocated something, so let's clean it up :)) */ free(image_data[i]); image_data[i] = NULL; } return loaded; } /* getting image width and height */ if (i == 0) { w = ilGetInteger(IL_IMAGE_WIDTH); h = ilGetInteger(IL_IMAGE_HEIGHT); } else { if (w != ilGetInteger(IL_IMAGE_WIDTH) || h != ilGetInteger(IL_IMAGE_HEIGHT)) { printf("Of course that images '%s' and '%s' are not the same. How could they be as they have different dimensions?\n", filenames[0], filenames[i]); return -1; } } /* Then we save image data into arrays that we allocate now */ image_data[i] = malloc(w * h * params.bpp * sizeof(ILubyte)); if (image_data[i] == NULL) { fprintf(stderr, "Out of memory, %s:%d\n", __FILE__, __LINE__); return IL_OUT_OF_MEMORY; } ilCopyPixels(0, 0, 0, w, h, 1, ((params.flags & ACTION_TEST_ALPHA) == 0 ? IL_RGB : IL_RGBA), IL_UNSIGNED_BYTE, image_data[i]); } int return_value = 0, result; int quantile; double integral; if (params.verbose > 0) printf("Testing against '%s'\n", filenames[0]); for (i = 1; i < images_count; i++) { compare_fields(w, h, image_data[0], image_data[i], params, & quantile, & integral); result = interpret_results(filenames[i], params, quantile, integral); return_value = (result > return_value) ? result : return_value; free(image_data[i]); image_data[i] = 0; } free(image_data[0]); image_data[0] = 0; ilDeleteImages(images_count, & handle[0]); return return_value; } /** Examines the command line like no tomorrow :-) * * Huge code, I am sorry for that. * On the other hand, cross-platformness of popt is catastrophical * and usage of boost is also too much for this simple purpose. */ int parse_commandline(int argc, char ** argv, Parameters * params) { int actions = 0, expectations = 0; const int long_str = 63; int return_value = TEST_NOTHING; int i; for (i = 1; i < argc; i++) { actions = 0; /* Find out what to do and what to expect */ if (argv[i][0] == '-') {/* Ho, an option was passed... */ if (argv[i][1] == '-') {/* Deal with long options */ if(strncmp(argv[i], "--extension", long_str)) { expectations |= EXPECT_EXTENSION; } else if (strncmp(argv[i], "--quantile", long_str)) { expectations |= EXPECT_QUANTILE; } else if (strncmp(argv[i], "--help", long_str)) { actions |= ACTION_HELP; } else if (strncmp(argv[i], "--verbose", long_str)) { actions |= ACTION_VERBOSE; } else if (strncmp(argv[i], "--test-alpha", long_str)) { actions |= ACTION_TEST_ALPHA; } else if (strncmp(argv[i], "--preserve", long_str)) { actions |= ACTION_PRESERVE_TESTFILE; } else if (strncmp(argv[i], "--use-resolution", long_str)) { expectations |= EXPECT_RESOLUTION; } else if (strncmp(argv[i], "--are-same", long_str)) { expectations |= EXPECT_IMAGES; } else if (strncmp(argv[i], "--image-complies", long_str)) { expectations |= EXPECT_FILENAME; } } else /* if not (argv[i][1] == '-') */ {/* Deal with short options */ int j; for (j = 1; j < strlen(argv[i]); j++) switch(argv[i][j]) { case 'h': case '?': actions |= ACTION_HELP; break; case 'v': actions |= ACTION_VERBOSE; break; case 'a': actions |= ACTION_TEST_ALPHA; break; case 'e': expectations |= EXPECT_EXTENSION; break; case 'q': expectations |= EXPECT_QUANTILE; break; case 'p': actions |= ACTION_PRESERVE_TESTFILE; break; case 'r': expectations |= EXPECT_RESOLUTION; break; case 's': expectations |= EXPECT_IMAGES; break; case 'c': expectations |= EXPECT_FILENAME; break; }/* end switch(argv[i][j]) */ }/* if not (argv[i][1] == '-') */ } else /* (! argv[i][0] == '-') */ {/* Now handle expectations... */ if (expectations & EXPECT_EXTENSION) {/* point extension to the extension string so we will be able to read it afterwards */ params->extension = argv[i]; return_value = TEST_EXTENSION; } else if (expectations & EXPECT_QUANTILE) {/* This parameter SHOULD BE the quantile we wanted... */ sscanf(argv[i], "%lf", & params->quantile_spec); } else if (expectations & EXPECT_RESOLUTION) {/* This parameter SHOULD specify the geometry... */ sscanf(argv[i], "%dx%d", & params->resolution[0], & params->resolution[1]); } else if (expectations & EXPECT_FILENAME) {/* This parameter SHOULD specify one filename... */ strncpy(params->first_filename, argv[i], sizeof(params->first_filename)); if ((params->flags & ACTION_PRESERVE_TESTFILE) == 0) { params->flags |= ACTION_PRESERVE_TESTFILE; printf("Issued a command to preserve the input file :-)\n"); } return_value = TEST_IS_TEST_IMAGE; } else if (expectations & EXPECT_IMAGES) {/* Masterpiece, this parameter SHOULD specify two comma separated filenames... */ sscanf(argv[i], "%30[^,],%30s", params->first_filename, params->second_filename); if ((params->flags & ACTION_PRESERVE_TESTFILE) == 0) { params->flags |= ACTION_PRESERVE_TESTFILE; printf("Issued a command to preserve the input file :-)\n"); } return_value = TEST_IMAGES_ARE_SAME; /* just debug: * printf ("Acquired strings: %s %s\n", params->first_filename, params->second_filename); **/ } expectations = 0; }/* (! argv[i][0] == '-') */ /* now handle actions... */ if (actions & ACTION_VERBOSE) { params->verbose++; } if (actions & ACTION_HELP) { printf(" *** Beware, manually generated help (=> may not be 100%% up-to-date :-) ***\n"); printf(" If you miss something, examine the source code\n"); printf("Run %s with this arguments:\n", argv[0]); printf("\t-h, -? | --help: This help message\n"); printf("\t-v | --verbose: Verbose run\n"); printf("\t-a | --test-alpha: Make and test image alpha channel as well\n"); printf("\t-e | --extension : Test saving and loading of this extension\n"); printf("\t-p | --preserve: Don't remove any generated files\n"); printf("\t-c | --image-complies : Test whether the filename is the image of our test pattern (useful for testing opening capabilities)\n"); printf("\t-r | --use-resolution : If this makes sense, use this resolution), default 603x300\n"); printf("\t-s | --are-same : Tests whether the first and second images are the same. Pass two filenames separated by comma.\n"); printf("This will be helpful one day...\n"); } if (actions & ACTION_PRESERVE_TESTFILE) { params->flags |= ACTION_PRESERVE_TESTFILE; } if (actions & ACTION_TEST_ALPHA) { params->flags |= ACTION_TEST_ALPHA; params->bpp = 4; } }/* endfor */ return return_value; } int main(int argc, char ** argv) { /* has to be done */ ilInit(); #ifdef ILU_ENABLED iluInit(); #endif /* Consistent loading stuff... */ ilEnable(IL_ORIGIN_SET); Parameters params = { /* ***SHOULD BE*** first_filename*/ "", /* second filename */ "", /* extension */ (char *)NULL, /* verbose */ (int)0, /* flags */(int)0, /* bpp */(int)3, /* resolution */ {603, 300}, /* quantile spec */ (double)0.80, /* quantile tresh */ (int)5, /* integral tresh */ (double)5}; int to_do = parse_commandline(argc, argv, & params); int return_value = 0; if (to_do & TEST_EXTENSION && params.extension != NULL) { return_value = test_format(params); if (return_value != 0) fprintf(stderr, "Format test: FAIL (.%s)\n", params.extension); } else if (to_do & TEST_IS_TEST_IMAGE && strlen(params.first_filename) > 0 && strlen(params.second_filename) == 0) { if (params.verbose > 0) printf ("Testing whether %s is the test image\n", params.first_filename); return_value = test_is_testimage(params); if (return_value != 0) fprintf(stderr, "Is test image test: FAIL (%s)\n", params.first_filename); } else if (to_do & TEST_IMAGES_ARE_SAME && strlen(params.first_filename) > 0 && strlen(params.second_filename) > 0 ) { if (params.verbose > 0) printf ("Testing whether %s and %s are +- the same\n", params.first_filename, params.second_filename); return_value = test_are_same(params); if (return_value != 0) fprintf(stderr, "Images are the same test: FAIL ('%s' != '%s')\n", params.first_filename, params.second_filename); } return return_value; }