1 /*
2  * Copyright (C) 2017 Vabishchevich Nikolay <vabnick@gmail.com>
3  *
4  * This file is part of libass.
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  */
18 
19 #include "image.h"
20 #include "../libass/ass.h"
21 #include <sys/types.h>
22 #include <sys/stat.h>
23 #include <dirent.h>
24 #include <string.h>
25 
26 #if defined(_WIN32) && !defined(__CYGWIN__)
27 #include <direct.h>
28 #define mkdir(path, mode) _mkdir(path)
29 #endif
30 
31 
32 #define FFMAX(a,b) ((a) > (b) ? (a) : (b))
33 #define FFMIN(a,b) ((a) > (b) ? (b) : (a))
34 
blend_image(Image8 * frame,int32_t x0,int32_t y0,const ASS_Image * img)35 static void blend_image(Image8 *frame, int32_t x0, int32_t y0,
36                         const ASS_Image *img)
37 {
38     int32_t x1 = img->dst_x, x_min = FFMAX(x0, x1);
39     int32_t y1 = img->dst_y, y_min = FFMAX(y0, y1);
40     x0 = x_min - x0;  x1 = x_min - x1;
41     y0 = y_min - y0;  y1 = y_min - y1;
42 
43     int32_t w = FFMIN(x0 + frame->width,  x1 + img->w);
44     int32_t h = FFMIN(y0 + frame->height, y1 + img->h);
45     if (w <= 0 || h <= 0)
46         return;
47 
48     uint8_t r = img->color >> 24;
49     uint8_t g = img->color >> 16;
50     uint8_t b = img->color >>  8;
51     uint8_t a = img->color >>  0;
52 
53     int32_t mul = 129 * (255 - a);
54     const int32_t offs = (int32_t) 1 << 22;
55 
56     int32_t stride = 4 * frame->width;
57     uint8_t *dst = frame->buffer + y0 * stride + 4 * x0;
58     const uint8_t *src = img->bitmap + y1 * img->stride + x1;
59     for (int32_t y = 0; y < h; y++) {
60         for (int32_t x = 0; x < w; x++) {
61             int32_t k = src[x] * mul;
62             dst[4 * x + 0] -= ((dst[4 * x + 0] - r) * k + offs) >> 23;
63             dst[4 * x + 1] -= ((dst[4 * x + 1] - g) * k + offs) >> 23;
64             dst[4 * x + 2] -= ((dst[4 * x + 2] - b) * k + offs) >> 23;
65             dst[4 * x + 3] -= ((dst[4 * x + 3] - 0) * k + offs) >> 23;
66         }
67         dst += stride;
68         src += img->stride;
69     }
70 }
71 
blend_all(Image8 * frame,int32_t x0,int32_t y0,const ASS_Image * img)72 static void blend_all(Image8 *frame, int32_t x0, int32_t y0,
73                       const ASS_Image *img)
74 {
75     uint8_t *dst = frame->buffer;
76     size_t size = (size_t) frame->width * frame->height;
77     for (size_t i = 0; i < size; i++) {
78         dst[0] = dst[1] = dst[2] = 0;
79         dst[3] = 255;
80         dst += 4;
81     }
82     for (; img; img = img->next)
83         blend_image(frame, x0, y0, img);
84 }
85 
abs_diff(uint16_t a,uint16_t b)86 inline static uint16_t abs_diff(uint16_t a, uint16_t b)
87 {
88     return a > b ? a - b : b - a;
89 }
90 
abs_diff4(const uint16_t a[4],const uint16_t b[4])91 inline static uint16_t abs_diff4(const uint16_t a[4], const uint16_t b[4])
92 {
93     uint16_t res = 0;
94     for (int k = 0; k < 4; k++) {
95         uint16_t diff = abs_diff(a[k], b[k]);
96         res = FFMAX(res, diff);
97     }
98     return res;
99 }
100 
101 // Calculate error visibility scale according to formula:
102 // max_pixel_value / 255 + max(max_side_gradient / 4, max_diagonal_gradient / 8).
calc_grad(const Image16 * target,uint16_t * grad)103 static void calc_grad(const Image16 *target, uint16_t *grad)
104 {
105     const int base = 257;
106     const int border = base + 65535 / 4;
107 
108     int32_t w = target->width;
109     int32_t h = target->height;
110     int32_t stride = 4 * target->width;
111 
112     for (int32_t x = 0; x < w; x++)
113         *grad++ = border;
114     const uint16_t *tg = target->buffer + stride + 4;
115     for (int32_t y = 1; y < h - 1; y++) {
116         *grad++ = border;
117         for (int32_t x = 1; x < w - 1; x++) {
118             uint16_t g[8];
119             g[0] = abs_diff4(tg, tg - 4) / 4;
120             g[1] = abs_diff4(tg, tg + 4) / 4;
121             g[2] = abs_diff4(tg, tg - stride) / 4;
122             g[3] = abs_diff4(tg, tg + stride) / 4;
123             g[4] = abs_diff4(tg, tg - stride - 4) / 8;
124             g[5] = abs_diff4(tg, tg - stride + 4) / 8;
125             g[6] = abs_diff4(tg, tg + stride - 4) / 8;
126             g[7] = abs_diff4(tg, tg + stride + 4) / 8;
127             uint16_t gg = g[0];
128             for (int k = 1; k < 8; k++)
129                 gg = FFMAX(gg, g[k]);
130             *grad++ = base + gg;
131             tg += 4;
132         }
133         *grad++ = border;
134         tg += 8;
135     }
136     for (int32_t x = 0; x < w; x++)
137         *grad++ = border;
138 }
139 
compare1(const Image16 * target,const uint16_t * grad,const ASS_Image * img,const char * path,double * result)140 static int compare1(const Image16 *target, const uint16_t *grad,
141                     const ASS_Image *img, const char *path, double *result)
142 {
143     Image8 frame;
144     frame.width  = target->width;
145     frame.height = target->height;
146     size_t size = (size_t) frame.width * frame.height;
147     frame.buffer = malloc(4 * size);
148     if (!frame.buffer)
149         return 0;
150 
151     blend_all(&frame, 0, 0, img);
152 
153     double max_err = 0;
154     const uint8_t *ptr = frame.buffer;
155     const uint16_t *tg = target->buffer;
156     for (size_t i = 0; i < size; i++) {
157         uint16_t cmp[4];
158         for (int k = 0; k < 4; k++)
159             cmp[k] = 257u * ptr[k];
160         double err = (double) abs_diff4(cmp, tg) / *grad++;
161         if (max_err < err)
162             max_err = err;
163         ptr += 4;
164         tg += 4;
165     }
166     int flag = path && !write_png8(path, &frame) ? -1 : 1;
167     free(frame.buffer);
168     *result = max_err;
169     return flag;
170 }
171 
compare(const Image16 * target,const uint16_t * grad,const ASS_Image * img,const char * path,double * result,int scale)172 static int compare(const Image16 *target, const uint16_t *grad,
173                    const ASS_Image *img, const char *path,
174                    double *result, int scale)
175 {
176     if (scale == 1)
177         return compare1(target, grad, img, path, result);
178     int scale2 = scale * scale;
179 
180     Image16 frame;
181     frame.width  = target->width;
182     frame.height = target->height;
183     size_t size = (size_t) frame.width * frame.height;
184     frame.buffer = malloc(8 * size);
185     if (!frame.buffer)
186         return 0;
187 
188     Image8 temp;
189     temp.width  = scale * target->width;
190     temp.height = scale * target->height;
191     temp.buffer = malloc(4 * scale2 * size);
192     if (!temp.buffer) {
193         free(frame.buffer);
194         return 0;
195     }
196     blend_all(&temp, 0, 0, img);
197 
198     uint16_t *dst = frame.buffer;
199     const uint8_t *src = temp.buffer;
200     int32_t stride = 4 * temp.width;
201     const uint32_t offs = ((uint32_t) 1 << 18) - 1;
202     uint32_t mul = ((uint32_t) 257 << 19) / scale2;
203     for (int32_t y = 0; y < frame.height; y++) {
204         for (int32_t x = 0; x < frame.width; x++) {
205             uint16_t res[4] = {0};
206             const uint8_t *ptr = src;
207             for (int i = 0; i < scale; i++) {
208                 for (int j = 0; j < scale; j++)
209                     for (int k = 0; k < 4; k++)
210                         res[k] += ptr[4 * j + k];
211                 ptr += stride;
212             }
213             for (int k = 0; k < 4; k++)
214                 // equivalent to (257 * res[k] + (scale2 - 1) / 2) / scale2;
215                 *dst++ = (res[k] * (uint64_t) mul + offs) >> 19;
216             src += 4 * scale;
217         }
218         src += (scale - 1) * stride;
219     }
220 
221     free(temp.buffer);
222 
223     double max_err = 0;
224     const uint16_t *ptr = frame.buffer;
225     const uint16_t *tg = target->buffer;
226     for (size_t i = 0; i < size; i++) {
227         double err = (double) abs_diff4(ptr, tg) / *grad++;
228         if (max_err < err)
229             max_err = err;
230         ptr += 4;
231         tg += 4;
232     }
233     int flag = path && !write_png16(path, &frame) ? -1 : 1;
234     free(frame.buffer);
235     *result = max_err;
236     return flag;
237 }
238 
239 
load_font(ASS_Library * lib,const char * dir,const char * file)240 static bool load_font(ASS_Library *lib, const char *dir, const char *file)
241 {
242     char path[4096];
243     snprintf(path, sizeof(path), "%s/%s", dir, file);
244     FILE *fp = fopen(path, "rb");
245     if (!fp)
246         return false;
247 
248     if (fseek(fp, 0, SEEK_END) == -1) {
249         fclose(fp);
250         return false;
251     }
252 
253     long size = ftell(fp);
254     if (size <= 0 || size > (1l << 30)) {
255         fclose(fp);
256         return false;
257     }
258     rewind(fp);
259 
260     char *buf = malloc(size);
261     if (!buf) {
262         fclose(fp);
263         return false;
264     }
265 
266     long pos = 0;
267     while (pos < size) {
268         size_t n = fread(buf + pos, 1, size - pos, fp);
269         if (!n) {
270             free(buf);
271             fclose(fp);
272             return false;
273         }
274         pos += n;
275     }
276     fclose(fp);
277 
278     printf("Loading font '%s'.\n", file);
279     ass_add_font(lib, file, buf, size);
280     free(buf);
281     return true;
282 }
283 
load_track(ASS_Library * lib,const char * dir,const char * file)284 static ASS_Track *load_track(ASS_Library *lib,
285                              const char *dir, const char *file)
286 {
287     char path[4096];
288     snprintf(path, sizeof(path), "%s/%s", dir, file);
289     ASS_Track *track = ass_read_file(lib, path, NULL);
290     if (!track) {
291         printf("Cannot load subtitle file '%s'!\n", file);
292         return NULL;
293     }
294     printf("Processing '%s':\n", file);
295     return track;
296 }
297 
out_of_memory()298 static bool out_of_memory()
299 {
300     printf("Not enough memory!\n");
301     return false;
302 }
303 
304 typedef enum {
305     R_SAME, R_GOOD, R_BAD, R_FAIL, R_ERROR
306 } Result;
307 
308 static const char *result_text[R_ERROR] = {
309     "SAME", "GOOD", "BAD", "FAIL"
310 };
311 
classify_result(double error)312 Result classify_result(double error)
313 {
314     if (error == 0)
315         return R_SAME;
316     else if (error < 2)
317         return R_GOOD;
318     else if (error < 4)
319         return R_BAD;
320     else
321         return R_FAIL;
322 }
323 
process_image(ASS_Renderer * renderer,ASS_Track * track,const char * input,const char * output,const char * file,int64_t time,int scale)324 static Result process_image(ASS_Renderer *renderer, ASS_Track *track,
325                             const char *input, const char *output,
326                             const char *file, int64_t time, int scale)
327 {
328     uint64_t tm = time;
329     unsigned msec = tm % 1000;  tm /= 1000;
330     unsigned sec  = tm %   60;  tm /=   60;
331     unsigned min  = tm %   60;  tm /=   60;
332     printf("  Time %u:%02u:%02u.%03u - ", (unsigned) tm, min, sec, msec);
333 
334     char path[4096];
335     snprintf(path, sizeof(path), "%s/%s", input, file);
336 
337     Image16 target;
338     if (!read_png(path, &target)) {
339         printf("PNG reading failed!\n");
340         return R_ERROR;
341     }
342 
343     uint16_t *grad = malloc(2 * target.width * target.height);
344     if (!grad) {
345         free(target.buffer);
346         out_of_memory();
347         return R_ERROR;
348     }
349     calc_grad(&target, grad);
350 
351     ass_set_storage_size(renderer, target.width, target.height);
352     ass_set_frame_size(renderer, scale * target.width, scale * target.height);
353     ASS_Image *img = ass_render_frame(renderer, track, time, NULL);
354 
355     const char *out_file = NULL;
356     if (output) {
357         snprintf(path, sizeof(path), "%s/%s", output, file);
358         out_file = path;
359     }
360     double max_err;
361     int res = compare(&target, grad, img, out_file, &max_err, scale);
362     free(target.buffer);
363     free(grad);
364     if (!res) {
365         out_of_memory();
366         return R_ERROR;
367     }
368     Result flag = classify_result(max_err);
369     printf("%.3f %s\n", max_err, result_text[flag]);
370     if (res < 0)
371         printf("Cannot write PNG to file '%s'!\n", path);
372     return flag;
373 }
374 
375 
376 typedef struct {
377     char *name;
378     size_t prefix;
379     const char *dir;
380     int64_t time;
381 } Item;
382 
383 typedef struct {
384     size_t n_items, max_items;
385     Item *items;
386 } ItemList;
387 
add_item(ItemList * list)388 static bool add_item(ItemList *list)
389 {
390     if (list->n_items < list->max_items)
391         return true;
392 
393     size_t n = list->max_items ? 2 * list->max_items : 256;
394     Item *next = realloc(list->items, n * sizeof(Item));
395     if (!next)
396         return out_of_memory();
397     list->max_items = n;
398     list->items = next;
399     return true;
400 }
401 
delete_items(ItemList * list)402 static void delete_items(ItemList *list)
403 {
404     for (size_t i = 0; i < list->n_items; i++)
405         free(list->items[i].name);
406     free(list->items);
407 }
408 
item_compare(const void * ptr1,const void * ptr2)409 static int item_compare(const void *ptr1, const void *ptr2)
410 {
411     const Item *e1 = ptr1, *e2 = ptr2;
412 
413     int cmp_len = 0;
414     size_t len = e1->prefix;
415     if (len > e2->prefix) {
416         cmp_len = +1;
417         len = e2->prefix;
418     } else if (len < e2->prefix) {
419         cmp_len = -1;
420     }
421     int cmp = memcmp(e1->name, e2->name, len);
422     if (cmp)
423         return cmp;
424     if (cmp_len)
425         return cmp_len;
426     if (e1->time > e2->time)
427         return +1;
428     if (e1->time < e2->time)
429         return -1;
430     return 0;
431 }
432 
433 
add_sub_item(ItemList * list,const char * dir,const char * file,size_t len)434 static bool add_sub_item(ItemList *list, const char *dir, const char *file, size_t len)
435 {
436     if (!add_item(list))
437         return false;
438 
439     Item *item = &list->items[list->n_items];
440     item->name = strdup(file);
441     if (!item->name)
442         return out_of_memory();
443     item->prefix = len;
444     item->dir = dir;
445     item->time = -1;
446     list->n_items++;
447     return true;
448 }
449 
add_img_item(ItemList * list,const char * dir,const char * file,size_t len)450 static bool add_img_item(ItemList *list, const char *dir, const char *file, size_t len)
451 {
452     // Parse image name:
453     // <subtitle_name>-<time_in_msec>.png
454 
455     size_t pos = len, first = len;
456     while (true) {
457         if (!pos--)
458             return true;
459         if (file[pos] == '-')
460             break;
461         if (file[pos] < '0' || file[pos] > '9')
462             return true;
463         if (file[pos] != '0')
464             first = pos;
465     }
466     if (pos + 1 == len || first + 15 < len)
467         return true;
468 
469     if (!add_item(list))
470         return false;
471 
472     Item *item = &list->items[list->n_items];
473     item->name = strdup(file);
474     if (!item->name)
475         return out_of_memory();
476     item->prefix = pos;
477     item->dir = dir;
478     item->time = 0;
479     for (size_t i = first; i < len; i++)
480         item->time = 10 * item->time + (file[i] - '0');
481     list->n_items++;
482     return true;
483 }
484 
process_input(ItemList * list,const char * path,ASS_Library * lib)485 static bool process_input(ItemList *list, const char *path, ASS_Library *lib)
486 {
487     DIR *dir = opendir(path);
488     if (!dir) {
489         printf("Cannot open input directory '%s'!\n", path);
490         return false;
491     }
492     struct dirent *file;
493     while ((file = readdir(dir))) {
494         const char *name = file->d_name;
495         if (name[0] == '.')
496             continue;
497         const char *ext = strrchr(name + 1, '.');
498         if (!ext)
499             continue;
500 
501         char ext_lc[5];
502         size_t pos = 0;
503         while (pos < sizeof(ext_lc) - 1) {
504             char c = ext[pos + 1];
505             if (!c)
506                 break;
507             if (c >= 'A' && c <= 'Z')
508                 c += 'a' - 'A';
509             ext_lc[pos] = c;
510             pos++;
511         }
512         ext_lc[pos] = '\0';
513 
514         if (!strcmp(ext_lc, "png")) {
515             if (add_img_item(list, path, name, ext - name))
516                 continue;
517         } else if (!strcmp(ext_lc, "ass")) {
518             if (add_sub_item(list, path, name, ext - name))
519                 continue;
520         } else if (!strcmp(ext_lc, "ttf") ||
521                    !strcmp(ext_lc, "otf") ||
522                    !strcmp(ext_lc, "pfb")) {
523             if (load_font(lib, path, name))
524                 continue;
525             printf("Cannot load font '%s'!\n", name);
526         } else {
527             continue;
528         }
529         closedir(dir);
530         return false;
531     }
532     closedir(dir);
533     return true;
534 }
535 
536 
537 enum {
538     OUTPUT, SCALE, LEVEL, INPUT
539 };
540 
parse_cmdline(int argc,char * argv[])541 static int *parse_cmdline(int argc, char *argv[])
542 {
543     int *pos = calloc(INPUT + argc, sizeof(int));
544     if (!pos) {
545         out_of_memory();
546         return NULL;
547     }
548     int input = INPUT;
549     for (int i = 1; i < argc; i++) {
550         if (argv[i][0] != '-') {
551             pos[input++] = i;
552             continue;
553         }
554         int index;
555         switch (argv[i][1]) {
556         case 'i':  index = input++;  break;
557         case 'o':  index = OUTPUT;   break;
558         case 's':  index = SCALE;    break;
559         case 'p':  index = LEVEL;    break;
560         default:   goto fail;
561         }
562         if (argv[i][2] || ++i >= argc || pos[index])
563             goto fail;
564         pos[index] = i;
565     }
566     if (pos[INPUT])
567         return pos;
568 
569 fail:
570     free(pos);
571     const char *fmt =
572         "Usage: %s ([-i] <input-dir>)+ [-o <output-dir>] [-s <scale:1-8>] [-p <pass-level:0-3>]\n";
573     printf(fmt, argv[0]);
574     return NULL;
575 }
576 
msg_callback(int level,const char * fmt,va_list va,void * data)577 void msg_callback(int level, const char *fmt, va_list va, void *data)
578 {
579     if (level > 3)
580         return;
581     fprintf(stderr, "libass: ");
582     vfprintf(stderr, fmt, va);
583     fprintf(stderr, "\n");
584 }
585 
main(int argc,char * argv[])586 int main(int argc, char *argv[])
587 {
588     int *pos = parse_cmdline(argc, argv);
589     if (!pos)
590         return R_ERROR;
591 
592     ASS_Library *lib = NULL;
593     ItemList list = {0};
594     int result = R_ERROR;
595 
596     int scale = 1;
597     if (pos[SCALE]) {
598         const char *arg = argv[pos[SCALE]];
599         if (arg[0] < '1' || arg[0] > '8' || arg[1]) {
600             printf("Invalid scale value, should be 1-8!\n");
601             goto end;
602         }
603         scale = arg[0] - '0';
604     }
605 
606     int level = R_BAD;
607     if (pos[LEVEL]) {
608         const char *arg = argv[pos[LEVEL]];
609         if (arg[0] < '0' || arg[0] > '3' || arg[1]) {
610             printf("Invalid pass level value, should be 0-3!\n");
611             goto end;
612         }
613         level = arg[0] - '0';
614     }
615 
616     const char *output = NULL;
617     if (pos[OUTPUT]) {
618         output = argv[pos[OUTPUT]];
619         struct stat st;
620         if (stat(output, &st)) {
621             if (mkdir(output, 0755)) {
622                 printf("Cannot create output directory '%s'!\n", output);
623                 goto end;
624             }
625         } else if (!(st.st_mode & S_IFDIR)) {
626             printf("Invalid output directory '%s'!\n", output);
627             goto end;
628         }
629     }
630 
631     lib = ass_library_init();
632     if (!lib) {
633         printf("ass_library_init failed!\n");
634         goto end;
635     }
636     ass_set_message_cb(lib, msg_callback, NULL);
637     ass_set_extract_fonts(lib, true);
638 
639     for (int *input = pos + INPUT; *input; input++) {
640         if (!process_input(&list, argv[*input], lib))
641             goto end;
642     }
643 
644     ASS_Renderer *renderer = ass_renderer_init(lib);
645     if (!renderer) {
646         printf("ass_renderer_init failed!\n");
647         goto end;
648     }
649     ass_set_fonts(renderer, NULL, NULL, ASS_FONTPROVIDER_NONE, NULL, 0);
650 
651     result = 0;
652     size_t prefix = 0;
653     const char *prev = "";
654     ASS_Track *track = NULL;
655     unsigned total = 0, good = 0;
656     qsort(list.items, list.n_items, sizeof(Item), item_compare);
657     for (size_t i = 0; i < list.n_items; i++) {
658         char *name = list.items[i].name;
659         size_t len = list.items[i].prefix;
660         if (prefix != len || memcmp(prev, name, len)) {
661             if (track) {
662                 ass_free_track(track);
663                 track = NULL;
664             }
665             prev = name;
666             prefix = len;
667             if (list.items[i].time >= 0) {
668                 printf("Missing subtitle file '%.*s.ass'!\n", (int) len, name);
669                 total++;
670             } else if (i + 1 < list.n_items && list.items[i + 1].time >= 0)
671                 track = load_track(lib, list.items[i].dir, prev);
672             continue;
673         }
674         if (list.items[i].time < 0) {
675             printf("Multiple subtitle files '%.*s.ass'!\n", (int) len, name);
676             continue;
677         }
678         total++;
679         if (!track)
680             continue;
681         Result res = process_image(renderer, track, list.items[i].dir, output,
682                                    name, list.items[i].time, scale);
683         result = FFMAX(result, res);
684         if (res <= level)
685             good++;
686     }
687     if (track)
688         ass_free_track(track);
689     ass_renderer_done(renderer);
690 
691     if (!total) {
692         printf("No images found!\n");
693         result = R_ERROR;
694     } else if (good < total) {
695         printf("Only %u of %u images have passed test (%s or better)\n",
696                good, total, result_text[level]);
697     } else {
698         printf("All %u images have passed test (%s or better)\n",
699                total, result_text[level]);
700         result = 0;
701     }
702 
703 end:
704     delete_items(&list);
705     if (lib)
706         ass_library_done(lib);
707     free(pos);
708     return result;
709 }
710