From 46f44957e54cdd6d0c3554ad44bae5eb17ad74fc Mon Sep 17 00:00:00 2001 From: "Dr.Smile" Date: Mon, 27 May 2019 00:50:04 +0300 Subject: Add infrastructure for regression testing `test` subdirectory contains complete test example. --- .gitignore | 1 + Makefile.am | 8 +- compare/Makefile.am | 7 + compare/README.md | 72 +++++ compare/compare.c | 627 +++++++++++++++++++++++++++++++++++++++++++ compare/image.c | 240 +++++++++++++++++ compare/image.h | 41 +++ compare/test/font1.ttf | Bin 0 -> 16744 bytes compare/test/font2.otf | Bin 0 -> 27644 bytes compare/test/sub1-0500.png | Bin 0 -> 5509 bytes compare/test/sub1-1500.png | Bin 0 -> 4684 bytes compare/test/sub1-2500.png | Bin 0 -> 4465 bytes compare/test/sub1.ass | 12 + compare/test/sub2-153000.png | Bin 0 -> 9547 bytes compare/test/sub2.ass | 12 + configure.ac | 9 +- 16 files changed, 1024 insertions(+), 5 deletions(-) create mode 100644 compare/Makefile.am create mode 100644 compare/README.md create mode 100644 compare/compare.c create mode 100644 compare/image.c create mode 100644 compare/image.h create mode 100644 compare/test/font1.ttf create mode 100644 compare/test/font2.otf create mode 100644 compare/test/sub1-0500.png create mode 100644 compare/test/sub1-1500.png create mode 100644 compare/test/sub1-2500.png create mode 100644 compare/test/sub1.ass create mode 100644 compare/test/sub2-153000.png create mode 100644 compare/test/sub2.ass diff --git a/.gitignore b/.gitignore index 1f6ecff..534da44 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ Makefile.in # Test programs /test/test* +/compare/compare* /profile/profile* # pkgconfig diff --git a/Makefile.am b/Makefile.am index 4cd26e6..7f72ed7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -5,13 +5,17 @@ EXTRA_DIST = libass.pc.in Changelog pkgconfigdir = $(libdir)/pkgconfig pkgconfig_DATA = libass.pc -if HAVE_LIBPNG +if ENABLE_TEST test = test endif +if ENABLE_COMPARE + compare = compare +endif + if ENABLE_PROFILE profile = profile endif -SUBDIRS = libass $(test) $(profile) +SUBDIRS = libass $(test) $(compare) $(profile) diff --git a/compare/Makefile.am b/compare/Makefile.am new file mode 100644 index 0000000..54645c1 --- /dev/null +++ b/compare/Makefile.am @@ -0,0 +1,7 @@ +AM_CFLAGS = -Wall + +noinst_PROGRAMS = compare +compare_SOURCES = image.h image.c compare.c +compare_CPPFLAGS = -I$(top_srcdir)/libass +compare_LDADD = $(top_builddir)/libass/.libs/libass.a +compare_LDFLAGS = $(AM_LDFLAGS) $(LIBPNG_LIBS) -static diff --git a/compare/README.md b/compare/README.md new file mode 100644 index 0000000..50b1021 --- /dev/null +++ b/compare/README.md @@ -0,0 +1,72 @@ +Regression Testing +================== + +To build a test utility configure libass with the `--enable-compare` flag. +The utility works with `png` image files so there is external dependency of libpng. + +Test program command line: +`compare [-i] [-o ] [-s ]` + +* `` is a test input directory; +* `` if present sets directory to store the rendering results; +* `` sets an oversampling factor (positive integer up to 8, default 1). + +An input directory consists of font files (`*.ttf`, `*.otf` and `*.pfb`), subtitle files (`*.ass`), and image files (`*.png`). +All the fonts required for rendering should be present in the input directory as +libass is configured to not use anything external outside of it (`ASS_FONTPROVIDER_NONE`). +After loading all the fonts in the directory, the test program scans for subtitle files (`.ass`) +and their corresponding image files in the form of `-NNNN.png`, +where `NNNN` is an arbitrary number of decimal digits. +The subtitle file then would be rendered at the time of `NNNN` milliseconds and compared with the loaded image file. +For example, an input directory can have the following structure: + +``` +test/ + font1.ttf + font2.otf + sub1.ass + sub1-0500.png + sub1-1500.png + sub1-2500.png + sub2.ass + sub2-153000.png +``` + +More precisely, the test program (`compare`) would scan the input directory and do the following: +1) load all the fonts found (`*.ttf`, `*.otf`, `*.pfb`); +2) search for subtitle files (`*.ass`); +3) for every file found (`.ass`) scan for the files with names of `-NNNN.png`; +4) interpret `NNNN` as a time in milliseconds and render the subtitle file at that time with a transparent background; +5) fuzzy compare the rendering result with the corresponding png file; +6) save the rendering result in the output directory if requested. + +Target images should be transparent RGBA png files with 8 or 16 bits per channel. +A subtitle rendering resolution is determined by the extents of the target image files multiplied by an optional scale factor (`-s` switch). +In the case of nontrivial scale factor, the result would be downsampled using 16-bit precision before comparison. +Downsampling is done assuming a linear color space which is physically incorrect but commonly used in computer rendering. + +It's possible to save rendering results by using `-o` switch. +Saved files would have the same names as the target image files. +That functionality can be used for an initial generation of target images +by supplying arbitrary source images with correct names and extents. + +Test program output can look like the following: +``` +Loading font 'font1.ttf'. +Loading font 'font2.otf'. +Processing 'sub1.ass': + Time 0:00:00.0500 - 2.464 BAD + Time 0:00:01.0500 - 1.412 OK + Time 0:00:02.0500 - 4.919 FAIL +Processing 'sub2.ass': + Time 0:02:33.0000 - 0.728 OK +Only 3 of 4 images have passed test +``` +For each target image file the program reports a maximal ratio of the per pixel comparison error to the baseline error scale. +The baseline error scale tries to take account of neighboring pixels to estimate visibility of an error in that specific pixel location. +Currently the range of [0–2.0] is marked as `OK`, the range of [2.0–4.0] as `BAD` and more than 4.0 as `FAIL`. +At the end there is a summary of how many images don't fail tests. +If all images pass that test the program returns 0 otherwise 1. + +Note that almost any type of a rendering error can be greatly exaggerated by the specially tailored test cases. +Therefore test cases should be chosen to represent generic real world scenarios only. diff --git a/compare/compare.c b/compare/compare.c new file mode 100644 index 0000000..726e13c --- /dev/null +++ b/compare/compare.c @@ -0,0 +1,627 @@ +/* + * Copyright (C) 2017 Vabishchevich Nikolay + * + * This file is part of libass. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "image.h" +#include "../libass/ass.h" +#include +#include +#include +#include + + +#define FFMAX(a,b) ((a) > (b) ? (a) : (b)) +#define FFMIN(a,b) ((a) > (b) ? (b) : (a)) + +static void blend_image(Image8 *frame, int32_t x0, int32_t y0, + const ASS_Image *img) +{ + int32_t x1 = img->dst_x, x_min = FFMAX(x0, x1); + int32_t y1 = img->dst_y, y_min = FFMAX(y0, y1); + x0 = x_min - x0; x1 = x_min - x1; + y0 = y_min - y0; y1 = y_min - y1; + + int32_t w = FFMIN(x0 + frame->width, x1 + img->w); + int32_t h = FFMIN(y0 + frame->height, y1 + img->h); + if (w <= 0 || h <= 0) + return; + + uint8_t r = img->color >> 24; + uint8_t g = img->color >> 16; + uint8_t b = img->color >> 8; + uint8_t a = img->color >> 0; + + int32_t mul = 129 * (255 - a); + const int32_t offs = (int32_t) 1 << 22; + + int32_t stride = 4 * frame->width; + uint8_t *dst = frame->buffer + y0 * stride + 4 * x0; + const uint8_t *src = img->bitmap + y1 * img->stride + x1; + for (int32_t y = 0; y < h; y++) { + for (int32_t x = 0; x < w; x++) { + int32_t k = src[x] * mul; + dst[4 * x + 0] -= ((dst[4 * x + 0] - r) * k + offs) >> 23; + dst[4 * x + 1] -= ((dst[4 * x + 1] - g) * k + offs) >> 23; + dst[4 * x + 2] -= ((dst[4 * x + 2] - b) * k + offs) >> 23; + dst[4 * x + 3] -= ((dst[4 * x + 3] - 0) * k + offs) >> 23; + } + dst += stride; + src += img->stride; + } +} + +static void blend_all(Image8 *frame, int32_t x0, int32_t y0, + const ASS_Image *img) +{ + uint8_t *dst = frame->buffer; + size_t size = (size_t) frame->width * frame->height; + for (size_t i = 0; i < size; i++) { + dst[0] = dst[1] = dst[2] = 0; + dst[3] = 255; + dst += 4; + } + for (; img; img = img->next) + blend_image(frame, x0, y0, img); +} + +inline static uint16_t abs_diff(uint16_t a, uint16_t b) +{ + return a > b ? a - b : b - a; +} + +inline static uint16_t abs_diff4(const uint16_t a[4], const uint16_t b[4]) +{ + uint16_t res = 0; + for (int k = 0; k < 4; k++) { + uint16_t diff = abs_diff(a[k], b[k]); + res = FFMAX(res, diff); + } + return res; +} + +// Calculate error visibility scale according to formula: +// max_pixel_value / 255 + max(max_side_gradient / 4, max_diagonal_gradient / 8). +static void calc_grad(const Image16 *target, uint16_t *grad) +{ + const int base = 257; + const int border = base + 65535 / 4; + + int32_t w = target->width; + int32_t h = target->height; + int32_t stride = 4 * target->width; + + for (int32_t x = 0; x < w; x++) + *grad++ = border; + const uint16_t *tg = target->buffer + stride + 4; + for (int32_t y = 1; y < h - 1; y++) { + *grad++ = border; + for (int32_t x = 1; x < w - 1; x++) { + uint16_t g[8]; + g[0] = abs_diff4(tg, tg - 4) / 4; + g[1] = abs_diff4(tg, tg + 4) / 4; + g[2] = abs_diff4(tg, tg - stride) / 4; + g[3] = abs_diff4(tg, tg + stride) / 4; + g[4] = abs_diff4(tg, tg - stride - 4) / 8; + g[5] = abs_diff4(tg, tg - stride + 4) / 8; + g[6] = abs_diff4(tg, tg + stride - 4) / 8; + g[7] = abs_diff4(tg, tg + stride + 4) / 8; + uint16_t gg = g[0]; + for (int k = 1; k < 8; k++) + gg = FFMAX(gg, g[k]); + *grad++ = base + gg; + tg += 4; + } + *grad++ = border; + tg += 8; + } + for (int32_t x = 0; x < w; x++) + *grad++ = border; +} + +static int compare1(const Image16 *target, const uint16_t *grad, + const ASS_Image *img, const char *path, double *result) +{ + Image8 frame; + frame.width = target->width; + frame.height = target->height; + size_t size = (size_t) frame.width * frame.height; + frame.buffer = malloc(4 * size); + if (!frame.buffer) + return 0; + + blend_all(&frame, 0, 0, img); + + double max_err = 0; + const uint8_t *ptr = frame.buffer; + const uint16_t *tg = target->buffer; + for (size_t i = 0; i < size; i++) { + uint16_t cmp[4]; + for (int k = 0; k < 4; k++) + cmp[k] = 257u * ptr[k]; + double err = (double) abs_diff4(cmp, tg) / *grad++; + if (max_err < err) + max_err = err; + ptr += 4; + tg += 4; + } + int flag = path && !write_png8(path, &frame) ? -1 : 1; + free(frame.buffer); + *result = max_err; + return flag; +} + +static int compare(const Image16 *target, const uint16_t *grad, + const ASS_Image *img, const char *path, + double *result, int scale) +{ + if (scale == 1) + return compare1(target, grad, img, path, result); + int scale2 = scale * scale; + + Image16 frame; + frame.width = target->width; + frame.height = target->height; + size_t size = (size_t) frame.width * frame.height; + frame.buffer = malloc(8 * size); + if (!frame.buffer) + return 0; + + Image8 temp; + temp.width = scale * target->width; + temp.height = scale * target->height; + temp.buffer = malloc(4 * scale2 * size); + if (!temp.buffer) { + free(frame.buffer); + return 0; + } + blend_all(&temp, 0, 0, img); + + uint16_t *dst = frame.buffer; + const uint8_t *src = temp.buffer; + int32_t stride = 4 * temp.width; + const uint32_t offs = ((uint32_t) 1 << 18) - 1; + uint32_t mul = ((uint32_t) 257 << 19) / scale2; + for (int32_t y = 0; y < frame.height; y++) { + for (int32_t x = 0; x < frame.width; x++) { + uint16_t res[4] = {0}; + const uint8_t *ptr = src; + for (int i = 0; i < scale; i++) { + for (int j = 0; j < scale; j++) + for (int k = 0; k < 4; k++) + res[k] += ptr[4 * j + k]; + ptr += stride; + } + for (int k = 0; k < 4; k++) + // equivalent to (257 * res[k] + (scale2 - 1) / 2) / scale2; + *dst++ = (res[k] * (uint64_t) mul + offs) >> 19; + src += 4 * scale; + } + src += (scale - 1) * stride; + } + + free(temp.buffer); + + double max_err = 0; + const uint16_t *ptr = frame.buffer; + const uint16_t *tg = target->buffer; + for (size_t i = 0; i < size; i++) { + double err = (double) abs_diff4(ptr, tg) / *grad++; + if (max_err < err) + max_err = err; + ptr += 4; + tg += 4; + } + int flag = path && !write_png16(path, &frame) ? -1 : 1; + free(frame.buffer); + *result = max_err; + return flag; +} + + +static bool load_font(ASS_Library *lib, const char *dir, const char *file) +{ + char path[4096]; + snprintf(path, sizeof(path), "%s/%s", dir, file); + FILE *fp = fopen(path, "rb"); + if (!fp) + return false; + + if (fseek(fp, 0, SEEK_END) == -1) { + fclose(fp); + return false; + } + + long size = ftell(fp); + if (size <= 0 || size > (1l << 30)) { + fclose(fp); + return false; + } + rewind(fp); + + char *buf = malloc(size); + if (!buf) { + fclose(fp); + return false; + } + + long pos = 0; + while (pos < size) { + size_t n = fread(buf + pos, 1, size - pos, fp); + if (!n) { + free(buf); + fclose(fp); + return false; + } + pos += n; + } + fclose(fp); + + printf("Loading font '%s'.\n", file); + ass_add_font(lib, (char *) file, buf, size); + free(buf); + return true; +} + +static ASS_Track *load_track(ASS_Library *lib, + const char *dir, const char *file) +{ + char path[4096]; + snprintf(path, sizeof(path), "%s/%s.ass", dir, file); + ASS_Track *track = ass_read_file(lib, path, NULL); + if (!track) { + printf("Cannot load subtitle file '%s.ass'!\n", file); + return NULL; + } + printf("Processing '%s.ass':\n", file); + return track; +} + +static bool out_of_memory() +{ + printf("Not enough memory!\n"); + return false; +} + +static bool process_image(ASS_Renderer *renderer, ASS_Track *track, + const char *input, const char *output, + const char *file, int64_t time, int scale) +{ + uint64_t tm = time; + unsigned msec = tm % 1000; tm /= 1000; + unsigned sec = tm % 60; tm /= 60; + unsigned min = tm % 60; tm /= 60; + printf(" Time %u:%02u:%02u.%04u - ", (unsigned) tm, min, sec, msec); + + char path[4096]; + snprintf(path, sizeof(path), "%s/%s", input, file); + + Image16 target; + if (!read_png(path, &target)) { + printf("PNG reading failed!\n"); + return false; + } + + uint16_t *grad = malloc(2 * target.width * target.height); + if (!grad) { + free(target.buffer); + return out_of_memory(); + } + calc_grad(&target, grad); + + ass_set_frame_size(renderer, scale * target.width, scale * target.height); + ASS_Image *img = ass_render_frame(renderer, track, time, NULL); + + const char *out_file = NULL; + if (output) { + snprintf(path, sizeof(path), "%s/%s", output, file); + out_file = path; + } + double max_err; + int res = compare(&target, grad, img, out_file, &max_err, scale); + free(target.buffer); + free(grad); + if (!res) + return out_of_memory(); + bool flag = max_err < 4; + printf("%.3f %s\n", max_err, flag ? (max_err < 2 ? "OK" : "BAD") : "FAIL"); + if (res < 0) + printf("Cannot write PNG to file '%s'!\n", path); + return flag; +} + + +typedef struct { + char *name; + int64_t time; +} Item; + +typedef struct { + size_t n_items, max_items; + Item *items; +} ItemList; + +static bool init_items(ItemList *list) +{ + int n = 256; + list->n_items = list->max_items = 0; + list->items = malloc(n * sizeof(Item)); + if (!list->items) + return out_of_memory(); + list->max_items = n; + return true; +} + +static bool add_item(ItemList *list) +{ + if (list->n_items < list->max_items) + return true; + + int n = 2 * list->max_items; + Item *next = realloc(list->items, n * sizeof(Item)); + if (!next) + return out_of_memory(); + list->max_items = n; + list->items = next; + return true; +} + +static void delete_items(ItemList *list) +{ + for (size_t i = 0; i < list->n_items; i++) + free(list->items[i].name); + free(list->items); +} + +static int item_compare(const void *ptr1, const void *ptr2) +{ + const Item *e1 = ptr1, *e2 = ptr2; + int cmp = strcmp(e1->name, e2->name); + if (cmp) + return cmp; + if (e1->time > e2->time) + return +1; + if (e1->time < e2->time) + return -1; + return 0; +} + + +static bool add_sub_item(ItemList *list, const char *file, int len) +{ + if (!add_item(list)) + return false; + + Item *item = &list->items[list->n_items]; + item->name = strndup(file, len); + if (!item->name) + return out_of_memory(); + item->time = -1; + list->n_items++; + return true; +} + +static bool add_img_item(ItemList *list, const char *file, int len) +{ + // Parse image name: + // -.png + + int pos = len, first = len; + while (true) { + if (!pos--) + return true; + if (file[pos] == '-') + break; + if (file[pos] < '0' || file[pos] > '9') + return true; + if (file[pos] != '0') + first = pos; + } + if (pos + 1 == len || first + 15 < len) + return true; + + if (!add_item(list)) + return false; + + Item *item = &list->items[list->n_items]; + item->name = strdup(file); + if (!item->name) + return out_of_memory(); + item->name[pos] = '\0'; + item->time = 0; + for (int i = first; i < len; i++) + item->time = 10 * item->time + (file[i] - '0'); + list->n_items++; + return true; +} + + +static int print_usage(const char *program) +{ + const char *fmt = + "Usage: %s [-i] [-o ] [-s ]\n"; + printf(fmt, program); + return 1; +} + +void msg_callback(int level, const char *fmt, va_list va, void *data) +{ + if (level > 3) + return; + printf("libass: "); + vprintf(fmt, va); + printf("\n"); +} + +int main(int argc, char *argv[]) +{ + enum { + INPUT, OUTPUT, SCALE + }; + int pos[3] = {0}; + for (int i = 1; i < argc; i++) { + if (argv[i][0] != '-') { + if (pos[INPUT]) + return print_usage(argv[0]); + pos[INPUT] = i; + continue; + } + int index; + switch (argv[i][1]) { + case 'i': index = INPUT; break; + case 'o': index = OUTPUT; break; + case 's': index = SCALE; break; + default: return print_usage(argv[0]); + } + if (argv[i][2] || ++i >= argc || pos[index]) + return print_usage(argv[0]); + pos[index] = i; + } + if (!pos[INPUT]) + return print_usage(argv[0]); + + int scale = 1; + if (pos[SCALE]) { + const char *arg = argv[pos[SCALE]]; + if (arg[0] < '1' || arg[0] > '8' || arg[1]) { + printf("Invalid scale value, should be 1-8!\n"); + return 1; + } + scale = arg[0] - '0'; + } + + const char *input = argv[pos[INPUT]]; + DIR *dir = opendir(input); + if (!dir) { + printf("Cannot open input directory '%s'!\n", input); + return 1; + } + + const char *output = NULL; + if (pos[OUTPUT]) { + output = argv[pos[OUTPUT]]; + struct stat st; + if (stat(output, &st)) { + if (mkdir(output, 0755)) { + printf("Cannot create output directory '%s'!\n", output); + closedir(dir); + return 1; + } + } else if (!(st.st_mode & S_IFDIR)) { + printf("Invalid output directory '%s'!\n", output); + closedir(dir); + return 1; + } + } + + ASS_Library *lib = ass_library_init(); + if (!lib) { + printf("ass_library_init failed!\n"); + closedir(dir); + return 1; + } + ass_set_message_cb(lib, msg_callback, NULL); + + ItemList list; + if (!init_items(&list)) { + ass_library_done(lib); + closedir(dir); + return 1; + } + + while (true) { + struct dirent *file = readdir(dir); + if (!file) + break; + const char *name = file->d_name; + if (name[0] == '.') + continue; + const char *ext = strrchr(name + 1, '.'); + if (!ext) + continue; + + if (!strcmp(ext, ".png")) { + if (add_img_item(&list, name, ext - name)) + continue; + } else if (!strcmp(ext, ".ass")) { + if (add_sub_item(&list, name, ext - name)) + continue; + } else if (!strcmp(ext, ".ttf") || !strcmp(ext, ".otf") || !strcmp(ext, ".pfb")) { + if (load_font(lib, input, name)) + continue; + printf("Cannot load font '%s'!\n", name); + } else { + continue; + } + delete_items(&list); + ass_library_done(lib); + closedir(dir); + return 1; + } + closedir(dir); + + ASS_Renderer *renderer = ass_renderer_init(lib); + if (!renderer) { + printf("ass_renderer_init failed!\n"); + delete_items(&list); + ass_library_done(lib); + return 1; + } + ass_set_fonts(renderer, NULL, NULL, ASS_FONTPROVIDER_NONE, NULL, 0); + + int prefix; + const char *prev = ""; + ASS_Track *track = NULL; + unsigned total = 0, good = 0; + qsort(list.items, list.n_items, sizeof(Item), item_compare); + for (size_t i = 0; i < list.n_items; i++) { + if (strcmp(prev, list.items[i].name)) { + if (track) + ass_free_track(track); + prev = list.items[i].name; + prefix = strlen(prev); + if (list.items[i].time < 0) + track = load_track(lib, input, prev); + else { + printf("Missing subtitle file '%s.ass'!\n", prev); + track = NULL; + total++; + } + continue; + } + + total++; + if (!track) + continue; + char *name = list.items[i].name; + name[prefix] = '-'; // restore initial filename + if (process_image(renderer, track, input, output, + name, list.items[i].time, scale)) + good++; + } + if (track) + ass_free_track(track); + delete_items(&list); + ass_renderer_done(renderer); + ass_library_done(lib); + + if (good < total) { + printf("Only %u of %u images have passed test\n", good, total); + return 1; + } + printf("All %u images have passed test\n", total); + return 0; +} diff --git a/compare/image.c b/compare/image.c new file mode 100644 index 0000000..5263b93 --- /dev/null +++ b/compare/image.c @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2017 Vabishchevich Nikolay + * + * This file is part of libass. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "image.h" +#include + + +bool read_png(const char *path, Image16 *img) +{ + FILE *fp = fopen(path, "rb"); + if (!fp) + return false; + + png_structp png = + png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png) { + fclose(fp); + return false; + } + + png_infop info = png_create_info_struct(png); + if (!info) { + png_destroy_read_struct(&png, NULL, NULL); + fclose(fp); + return false; + } + + png_byte *volatile buf = NULL; + png_byte **volatile rows = NULL; + if (setjmp(png_jmpbuf(png))) { + free(buf); + free(rows); + png_destroy_read_struct(&png, &info, NULL); + fclose(fp); + return false; + } + + png_init_io(png, fp); + png_read_info(png, info); + + uint32_t w = png_get_image_width(png, info); + uint32_t h = png_get_image_height(png, info); + int type = png_get_color_type(png, info); + int depth = png_get_bit_depth(png, info); + + if (w > 0xFFFF || h > 0xFFFF || type != PNG_COLOR_TYPE_RGBA) { + png_destroy_read_struct(&png, &info, NULL); + fclose(fp); + return false; + } + + ptrdiff_t stride = 8 * w; + buf = malloc(stride * h); + rows = malloc(h * sizeof(png_byte *)); + if (!buf || !rows) { + free(buf); + free(rows); + png_destroy_read_struct(&png, &info, NULL); + fclose(fp); + return false; + } + + png_byte *ptr = buf; + ptrdiff_t half = 4 * w; + if (depth == 8) + ptr += half; + else + png_set_swap(png); + + for (uint32_t i = 0; i < h; i++) { + rows[i] = ptr; + ptr += stride; + } + + png_read_image(png, rows); + png_read_end(png, NULL); + + free(rows); + png_destroy_read_struct(&png, &info, NULL); + fclose(fp); + + // convert to premultiplied with inverted alpha + if (depth == 8) { + uint8_t *ptr = (uint8_t *) buf; + for (uint32_t y = 0; y < h; y++) { + for (uint32_t x = 0; x < w; x++) { + uint8_t r = ptr[half + 4 * x + 0]; + uint8_t g = ptr[half + 4 * x + 1]; + uint8_t b = ptr[half + 4 * x + 2]; + uint8_t a = ptr[half + 4 * x + 3]; + uint16_t ra = (uint16_t) r * a; + uint16_t ga = (uint16_t) g * a; + uint16_t ba = (uint16_t) b * a; + ptr[8 * x + 0] = ptr[8 * x + 1] = (ra + (ra >> 8) + 128) >> 8; + ptr[8 * x + 2] = ptr[8 * x + 3] = (ga + (ga >> 8) + 128) >> 8; + ptr[8 * x + 4] = ptr[8 * x + 5] = (ba + (ba >> 8) + 128) >> 8; + ptr[8 * x + 6] = ptr[8 * x + 7] = ~a; + } + ptr += stride; + } + } else { + uint16_t *ptr = (uint16_t *) buf; + for (uint32_t y = 0; y < h; y++) { + for (uint32_t x = 0; x < w; x++) { + uint16_t r = ptr[4 * x + 0]; + uint16_t g = ptr[4 * x + 1]; + uint16_t b = ptr[4 * x + 2]; + uint16_t a = ptr[4 * x + 3]; + uint32_t ra = (uint32_t) r * a; + uint32_t ga = (uint32_t) g * a; + uint32_t ba = (uint32_t) b * a; + ptr[4 * x + 0] = (ra + (ra >> 16) + (1 << 15)) >> 16; + ptr[4 * x + 1] = (ga + (ga >> 16) + (1 << 15)) >> 16; + ptr[4 * x + 2] = (ba + (ba >> 16) + (1 << 15)) >> 16; + ptr[4 * x + 3] = ~a; + } + ptr += half; + } + } + + img->width = w; + img->height = h; + img->buffer = (uint16_t *) buf; + return true; +} + +static bool write_png(const char *path, uint32_t width, uint32_t height, + ptrdiff_t stride, const void *buffer, int depth) +{ + FILE *fp = fopen(path, "wb"); + if (!fp) + return false; + + png_structp png = + png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png) { + fclose(fp); + return false; + } + + png_infop info = png_create_info_struct(png); + if (!info) { + png_destroy_write_struct(&png, NULL); + fclose(fp); + return false; + } + + png_byte **rows = malloc(height * sizeof(png_byte *)); + if (!rows) { + png_destroy_write_struct(&png, &info); + fclose(fp); + return false; + } + + png_byte *ptr = (png_byte *) buffer; + for (uint32_t i = 0; i < height; i++) { + rows[i] = (png_byte *) ptr; + ptr += stride; + } + + if (setjmp(png_jmpbuf(png))) { + free(rows); + png_destroy_write_struct(&png, &info); + fclose(fp); + return false; + } + + png_init_io(png, fp); + png_set_IHDR(png, info, width, height, depth, + PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + png_write_info(png, info); + + if (depth > 8) + png_set_swap(png); + png_write_image(png, rows); + png_write_end(png, NULL); + + free(rows); + png_destroy_write_struct(&png, &info); + fclose(fp); + return true; +} + +bool write_png8(const char *path, Image8 *img) +{ + uint8_t *ptr = img->buffer; + size_t size = (size_t) img->width * img->height; + for (size_t i = 0; i < size; i++) { + uint8_t alpha = ~ptr[3]; + if (alpha) { + const uint32_t offs = (uint32_t) 1 << 15; + uint32_t inv = ((uint32_t) 255 << 16) / alpha + 1; + // equivalent to (255 * ptr[k] + alpha / 2) / alpha + ptr[0] = (ptr[0] * inv + offs) >> 16; + ptr[1] = (ptr[1] * inv + offs) >> 16; + ptr[2] = (ptr[2] * inv + offs) >> 16; + } + ptr[3] = alpha; + ptr += 4; + } + return write_png(path, img->width, img->height, + 4 * img->width, img->buffer, 8); +} + +bool write_png16(const char *path, Image16 *img) +{ + uint16_t *ptr = img->buffer; + size_t size = (size_t) img->width * img->height; + for (size_t i = 0; i < size; i++) { + uint16_t alpha = ~ptr[3]; + if (alpha) { + const uint64_t offs = (uint64_t) 1 << 32; + uint64_t inv = ((uint64_t) 65535 << 33) / alpha + 1; + // equivalent to (65535 * ptr[k] + alpha / 2) / alpha + ptr[0] = (ptr[0] * inv + offs) >> 33; + ptr[1] = (ptr[1] * inv + offs) >> 33; + ptr[2] = (ptr[2] * inv + offs) >> 33; + } + ptr[3] = alpha; + ptr += 4; + } + return write_png(path, img->width, img->height, + 8 * img->width, img->buffer, 16); +} diff --git a/compare/image.h b/compare/image.h new file mode 100644 index 0000000..5f24801 --- /dev/null +++ b/compare/image.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017 Vabishchevich Nikolay + * + * This file is part of libass. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef COMPARE_IMAGE_H +#define COMPARE_IMAGE_H + +#include +#include +#include +#include + +typedef struct { + int32_t width, height; + uint8_t *buffer; +} Image8; + +typedef struct { + int32_t width, height; + uint16_t *buffer; +} Image16; + +bool read_png(const char *path, Image16 *img); +bool write_png8(const char *path, Image8 *img); +bool write_png16(const char *path, Image16 *img); + +#endif /* COMPARE_IMAGE_H */ diff --git a/compare/test/font1.ttf b/compare/test/font1.ttf new file mode 100644 index 0000000..215d4a2 Binary files /dev/null and b/compare/test/font1.ttf differ diff --git a/compare/test/font2.otf b/compare/test/font2.otf new file mode 100644 index 0000000..4248f6f Binary files /dev/null and b/compare/test/font2.otf differ diff --git a/compare/test/sub1-0500.png b/compare/test/sub1-0500.png new file mode 100644 index 0000000..342c0e3 Binary files /dev/null and b/compare/test/sub1-0500.png differ diff --git a/compare/test/sub1-1500.png b/compare/test/sub1-1500.png new file mode 100644 index 0000000..cf6b593 Binary files /dev/null and b/compare/test/sub1-1500.png differ diff --git a/compare/test/sub1-2500.png b/compare/test/sub1-2500.png new file mode 100644 index 0000000..db83483 Binary files /dev/null and b/compare/test/sub1-2500.png differ diff --git a/compare/test/sub1.ass b/compare/test/sub1.ass new file mode 100644 index 0000000..a1718a5 --- /dev/null +++ b/compare/test/sub1.ass @@ -0,0 +1,12 @@ +[Script Info] +PlayResX: 320 +PlayResY: 240 +ScaledBorderAndShadow: yes + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Aileron,200,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,8,0,5,10,10,10,1 + +[Events] +Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text +Dialogue: 0,0:00:00.00,0:00:04.00,Default,,0,0,0,,{\t(\frz+180)}A diff --git a/compare/test/sub2-153000.png b/compare/test/sub2-153000.png new file mode 100644 index 0000000..11dc45b Binary files /dev/null and b/compare/test/sub2-153000.png differ diff --git a/compare/test/sub2.ass b/compare/test/sub2.ass new file mode 100644 index 0000000..d4d9c26 --- /dev/null +++ b/compare/test/sub2.ass @@ -0,0 +1,12 @@ +[Script Info] +PlayResX: 320 +PlayResY: 180 +ScaledBorderAndShadow: yes + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Pixel Operator Mono,100,&H00FF0000,&H00FF0000,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,5,10,10,10,1 + +[Events] +Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text +Dialogue: 0,0:02:30.00,0:02:35.00,Default,,0,0,0,,{\blur1}Test_ diff --git a/configure.ac b/configure.ac index 4c03edf..258ca9c 100644 --- a/configure.ac +++ b/configure.ac @@ -35,6 +35,8 @@ LIBS="$OLDLIBS $LIBS" # Check for libraries via pkg-config AC_ARG_ENABLE([test], AS_HELP_STRING([--enable-test], [enable test program (requires libpng) @<:@default=no@:>@])) +AC_ARG_ENABLE([compare], AS_HELP_STRING([--enable-compare], + [enable compare program (requires libpng) @<:@default=no@:>@])) AC_ARG_ENABLE([profile], AS_HELP_STRING([--enable-profile], [enable profiling program @<:@default=no@:>@])) AC_ARG_ENABLE([fontconfig], AS_HELP_STRING([--disable-fontconfig], @@ -218,14 +220,15 @@ PKG_CHECK_MODULES([HARFBUZZ], harfbuzz >= 0.9.5, [ fi libpng=false -if test x$enable_test = xyes; then +if test x$enable_test = xyes || test x$enable_compare = xyes; then PKG_CHECK_MODULES([LIBPNG], libpng >= 1.2.0, [ CFLAGS="$CFLAGS $LIBPNG_CFLAGS" AC_DEFINE(CONFIG_LIBPNG, 1, [found libpng via pkg-config]) libpng=true]) fi -AM_CONDITIONAL([HAVE_LIBPNG], [test x$libpng = xtrue]) +AM_CONDITIONAL([ENABLE_TEST], [test x$enable_test = xyes && test x$libpng = xtrue]) +AM_CONDITIONAL([ENABLE_COMPARE], [test x$enable_compare = xyes && test x$libpng = xtrue]) AM_CONDITIONAL([ENABLE_PROFILE], [test x$enable_profile = xyes]) @@ -265,5 +268,5 @@ AC_SUBST([PKG_REQUIRES_PRIVATE], [$(test x$enable_shared = xno || echo ${pkg_req # Setup output beautifier. m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) -AC_CONFIG_FILES([Makefile libass/Makefile test/Makefile profile/Makefile libass.pc]) +AC_CONFIG_FILES([Makefile libass/Makefile test/Makefile compare/Makefile profile/Makefile libass.pc]) AC_OUTPUT -- cgit v1.2.3