summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDr.Smile <vabnick@gmail.com>2019-05-27 00:50:04 +0300
committerDr.Smile <vabnick@gmail.com>2020-05-07 18:36:35 +0300
commit46f44957e54cdd6d0c3554ad44bae5eb17ad74fc (patch)
tree33c7982d758a8e6b24ab8839a71fa3f646348590
parente5140624ff739c3157929bc5e1a1007cdc9cdaa8 (diff)
downloadlibass-46f44957e54cdd6d0c3554ad44bae5eb17ad74fc.tar.bz2
libass-46f44957e54cdd6d0c3554ad44bae5eb17ad74fc.tar.xz
Add infrastructure for regression testing
`test` subdirectory contains complete test example.
-rw-r--r--.gitignore1
-rw-r--r--Makefile.am8
-rw-r--r--compare/Makefile.am7
-rw-r--r--compare/README.md72
-rw-r--r--compare/compare.c627
-rw-r--r--compare/image.c240
-rw-r--r--compare/image.h41
-rw-r--r--compare/test/font1.ttfbin0 -> 16744 bytes
-rw-r--r--compare/test/font2.otfbin0 -> 27644 bytes
-rw-r--r--compare/test/sub1-0500.pngbin0 -> 5509 bytes
-rw-r--r--compare/test/sub1-1500.pngbin0 -> 4684 bytes
-rw-r--r--compare/test/sub1-2500.pngbin0 -> 4465 bytes
-rw-r--r--compare/test/sub1.ass12
-rw-r--r--compare/test/sub2-153000.pngbin0 -> 9547 bytes
-rw-r--r--compare/test/sub2.ass12
-rw-r--r--configure.ac9
16 files changed, 1024 insertions, 5 deletions
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] <input-dir> [-o <output-dir>] [-s <scale:1-8>]`
+
+* `<input-dir>` is a test input directory;
+* `<output-dir>` if present sets directory to store the rendering results;
+* `<scale>` 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 (`<sub_name>.ass`)
+and their corresponding image files in the form of `<sub_name>-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 (`<sub_name>.ass`) scan for the files with names of `<sub_name>-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&ndash;2.0] is marked as `OK`, the range of [2.0&ndash;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 <vabnick@gmail.com>
+ *
+ * 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 <sys/types.h>
+#include <sys/stat.h>
+#include <dirent.h>
+#include <string.h>
+
+
+#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:
+ // <subtitle_name>-<time_in_msec>.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] <input-dir> [-o <output-dir>] [-s <scale:1-8>]\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 <vabnick@gmail.com>
+ *
+ * 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 <png.h>
+
+
+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;