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