1/*
2 * Copyright (c) 2012 Clément Bœsch
3 *
4 * This file is part of FFmpeg.
5 *
6 * FFmpeg is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * FFmpeg is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with FFmpeg; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
21/**
22 * @file
23 * MicroDVD subtitle decoder
24 *
25 * Based on the specifications found here:
26 * https://trac.videolan.org/vlc/ticket/1825#comment:6
27 */
28
29#include "libavutil/avstring.h"
30#include "libavutil/parseutils.h"
31#include "libavutil/bprint.h"
32#include "avcodec.h"
33#include "ass.h"
34#include "codec_internal.h"
35
36static int indexof(const char *s, int c)
37{
38    char *f = strchr(s, c);
39    return f ? (f - s) : -1;
40}
41
42struct microdvd_tag {
43    char key;
44    int persistent;
45    uint32_t data1;
46    uint32_t data2;
47    char *data_string;
48    int data_string_len;
49};
50
51#define MICRODVD_PERSISTENT_OFF     0
52#define MICRODVD_PERSISTENT_ON      1
53#define MICRODVD_PERSISTENT_OPENED  2
54
55// Color, Font, Size, cHarset, stYle, Position, cOordinate
56#define MICRODVD_TAGS "cfshyYpo"
57
58static void microdvd_set_tag(struct microdvd_tag *tags, struct microdvd_tag tag)
59{
60    int tag_index = indexof(MICRODVD_TAGS, tag.key);
61
62    if (tag_index < 0)
63        return;
64    memcpy(&tags[tag_index], &tag, sizeof(tag));
65}
66
67// italic, bold, underline, strike-through
68#define MICRODVD_STYLES "ibus"
69
70/* some samples have lines that start with a / indicating non persistent italic
71 * marker */
72static char *check_for_italic_slash_marker(struct microdvd_tag *tags, char *s)
73{
74    if (*s == '/') {
75        struct microdvd_tag tag = tags[indexof(MICRODVD_TAGS, 'y')];
76        tag.key = 'y';
77        tag.data1 |= 1 << 0 /* 'i' position in MICRODVD_STYLES */;
78        microdvd_set_tag(tags, tag);
79        s++;
80    }
81    return s;
82}
83
84static char *microdvd_load_tags(struct microdvd_tag *tags, char *s)
85{
86    s = check_for_italic_slash_marker(tags, s);
87
88    while (*s == '{') {
89        char *start = s;
90        char tag_char = *(s + 1);
91        struct microdvd_tag tag = {0};
92
93        if (!tag_char || *(s + 2) != ':')
94            break;
95        s += 3;
96
97        switch (tag_char) {
98
99        /* Style */
100        case 'Y':
101            tag.persistent = MICRODVD_PERSISTENT_ON;
102        case 'y':
103            while (*s && *s != '}' && s - start < 256) {
104                int style_index = indexof(MICRODVD_STYLES, *s);
105
106                if (style_index >= 0)
107                    tag.data1 |= (1 << style_index);
108                s++;
109            }
110            if (*s != '}')
111                break;
112            /* We must distinguish persistent and non-persistent styles
113             * to handle this kind of style tags: {y:ib}{Y:us} */
114            tag.key = tag_char;
115            break;
116
117        /* Color */
118        case 'C':
119            tag.persistent = MICRODVD_PERSISTENT_ON;
120        case 'c':
121            while (*s == '$' || *s == '#')
122                s++;
123            tag.data1 = strtol(s, &s, 16) & 0x00ffffff;
124            if (*s != '}')
125                break;
126            tag.key = 'c';
127            break;
128
129        /* Font name */
130        case 'F':
131            tag.persistent = MICRODVD_PERSISTENT_ON;
132        case 'f': {
133            int len = indexof(s, '}');
134            if (len < 0)
135                break;
136            tag.data_string = s;
137            tag.data_string_len = len;
138            s += len;
139            tag.key = 'f';
140            break;
141        }
142
143        /* Font size */
144        case 'S':
145            tag.persistent = MICRODVD_PERSISTENT_ON;
146        case 's':
147            tag.data1 = strtol(s, &s, 10);
148            if (*s != '}')
149                break;
150            tag.key = 's';
151            break;
152
153        /* Charset */
154        case 'H': {
155            //TODO: not yet handled, just parsed.
156            int len = indexof(s, '}');
157            if (len < 0)
158                break;
159            tag.data_string = s;
160            tag.data_string_len = len;
161            s += len;
162            tag.key = 'h';
163            break;
164        }
165
166        /* Position */
167        case 'P':
168            if (!*s)
169                break;
170            tag.persistent = MICRODVD_PERSISTENT_ON;
171            tag.data1 = (*s++ == '1');
172            if (*s != '}')
173                break;
174            tag.key = 'p';
175            break;
176
177        /* Coordinates */
178        case 'o':
179            tag.persistent = MICRODVD_PERSISTENT_ON;
180            tag.data1 = strtol(s, &s, 10);
181            if (*s != ',')
182                break;
183            s++;
184            tag.data2 = strtol(s, &s, 10);
185            if (*s != '}')
186                break;
187            tag.key = 'o';
188            break;
189
190        default:    /* Unknown tag, we consider it's text */
191            break;
192        }
193
194        if (tag.key == 0)
195            return start;
196
197        microdvd_set_tag(tags, tag);
198        s++;
199    }
200    return check_for_italic_slash_marker(tags, s);
201}
202
203static void microdvd_open_tags(AVBPrint *new_line, struct microdvd_tag *tags)
204{
205    int i, sidx;
206    for (i = 0; i < sizeof(MICRODVD_TAGS) - 1; i++) {
207        if (tags[i].persistent == MICRODVD_PERSISTENT_OPENED)
208            continue;
209        switch (tags[i].key) {
210        case 'Y':
211        case 'y':
212            for (sidx = 0; sidx < sizeof(MICRODVD_STYLES) - 1; sidx++)
213                if (tags[i].data1 & (1 << sidx))
214                    av_bprintf(new_line, "{\\%c1}", MICRODVD_STYLES[sidx]);
215            break;
216
217        case 'c':
218            av_bprintf(new_line, "{\\c&H%06"PRIX32"&}", tags[i].data1);
219            break;
220
221        case 'f':
222            av_bprintf(new_line, "{\\fn%.*s}",
223                       tags[i].data_string_len, tags[i].data_string);
224            break;
225
226        case 's':
227            av_bprintf(new_line, "{\\fs%"PRId32"}", tags[i].data1);
228            break;
229
230        case 'p':
231            if (tags[i].data1 == 0)
232                av_bprintf(new_line, "{\\an8}");
233            break;
234
235        case 'o':
236            av_bprintf(new_line, "{\\pos(%"PRId32",%"PRId32")}",
237                       tags[i].data1, tags[i].data2);
238            break;
239        }
240        if (tags[i].persistent == MICRODVD_PERSISTENT_ON)
241            tags[i].persistent = MICRODVD_PERSISTENT_OPENED;
242    }
243}
244
245static void microdvd_close_no_persistent_tags(AVBPrint *new_line,
246                                              struct microdvd_tag *tags)
247{
248    int i, sidx;
249
250    for (i = sizeof(MICRODVD_TAGS) - 2; i >= 0; i--) {
251        if (tags[i].persistent != MICRODVD_PERSISTENT_OFF)
252            continue;
253        switch (tags[i].key) {
254
255        case 'y':
256            for (sidx = sizeof(MICRODVD_STYLES) - 2; sidx >= 0; sidx--)
257                if (tags[i].data1 & (1 << sidx))
258                    av_bprintf(new_line, "{\\%c0}", MICRODVD_STYLES[sidx]);
259            break;
260
261        case 'c':
262            av_bprintf(new_line, "{\\c}");
263            break;
264
265        case 'f':
266            av_bprintf(new_line, "{\\fn}");
267            break;
268
269        case 's':
270            av_bprintf(new_line, "{\\fs}");
271            break;
272        }
273        tags[i].key = 0;
274    }
275}
276
277static int microdvd_decode_frame(AVCodecContext *avctx, AVSubtitle *sub,
278                                 int *got_sub_ptr, const AVPacket *avpkt)
279{
280    AVBPrint new_line;
281    char *line = avpkt->data;
282    char *end = avpkt->data + avpkt->size;
283    FFASSDecoderContext *s = avctx->priv_data;
284    struct microdvd_tag tags[sizeof(MICRODVD_TAGS) - 1] = {{0}};
285
286    if (avpkt->size <= 0)
287        return avpkt->size;
288
289    av_bprint_init(&new_line, 0, 2048);
290
291    // subtitle content
292    while (line < end && *line) {
293
294        // parse MicroDVD tags, and open them in ASS
295        line = microdvd_load_tags(tags, line);
296        microdvd_open_tags(&new_line, tags);
297
298        // simple copy until EOL or forced carriage return
299        while (line < end && *line && *line != '|') {
300            av_bprint_chars(&new_line, *line, 1);
301            line++;
302        }
303
304        // line split
305        if (line < end && *line == '|') {
306            microdvd_close_no_persistent_tags(&new_line, tags);
307            av_bprintf(&new_line, "\\N");
308            line++;
309        }
310    }
311    if (new_line.len) {
312        int ret = ff_ass_add_rect(sub, new_line.str, s->readorder++, 0, NULL, NULL);
313        av_bprint_finalize(&new_line, NULL);
314        if (ret < 0)
315            return ret;
316    }
317
318    *got_sub_ptr = sub->num_rects > 0;
319    return avpkt->size;
320}
321
322static int microdvd_init(AVCodecContext *avctx)
323{
324    int i, sidx;
325    AVBPrint font_buf;
326    int font_size    = ASS_DEFAULT_FONT_SIZE;
327    int color        = ASS_DEFAULT_COLOR;
328    int bold         = ASS_DEFAULT_BOLD;
329    int italic       = ASS_DEFAULT_ITALIC;
330    int underline    = ASS_DEFAULT_UNDERLINE;
331    int alignment    = ASS_DEFAULT_ALIGNMENT;
332    struct microdvd_tag tags[sizeof(MICRODVD_TAGS) - 1] = {{0}};
333
334    av_bprint_init(&font_buf, 0, AV_BPRINT_SIZE_AUTOMATIC);
335    av_bprintf(&font_buf, "%s", ASS_DEFAULT_FONT);
336
337    if (avctx->extradata) {
338        microdvd_load_tags(tags, avctx->extradata);
339        for (i = 0; i < sizeof(MICRODVD_TAGS) - 1; i++) {
340            switch (av_tolower(tags[i].key)) {
341            case 'y':
342                for (sidx = 0; sidx < sizeof(MICRODVD_STYLES) - 1; sidx++) {
343                    if (tags[i].data1 & (1 << sidx)) {
344                        switch (MICRODVD_STYLES[sidx]) {
345                        case 'i': italic    = 1; break;
346                        case 'b': bold      = 1; break;
347                        case 'u': underline = 1; break;
348                        }
349                    }
350                }
351                break;
352
353            case 'c': color     = tags[i].data1; break;
354            case 's': font_size = tags[i].data1; break;
355            case 'p': alignment =             8; break;
356
357            case 'f':
358                av_bprint_clear(&font_buf);
359                av_bprintf(&font_buf, "%.*s",
360                           tags[i].data_string_len, tags[i].data_string);
361                break;
362            }
363        }
364    }
365    return ff_ass_subtitle_header(avctx, font_buf.str, font_size, color,
366                                  ASS_DEFAULT_BACK_COLOR, bold, italic,
367                                  underline, ASS_DEFAULT_BORDERSTYLE,
368                                  alignment);
369}
370
371const FFCodec ff_microdvd_decoder = {
372    .p.name       = "microdvd",
373    .p.long_name  = NULL_IF_CONFIG_SMALL("MicroDVD subtitle"),
374    .p.type       = AVMEDIA_TYPE_SUBTITLE,
375    .p.id         = AV_CODEC_ID_MICRODVD,
376    .init         = microdvd_init,
377    FF_CODEC_DECODE_SUB_CB(microdvd_decode_frame),
378    .flush        = ff_ass_decoder_flush,
379    .priv_data_size = sizeof(FFASSDecoderContext),
380    .caps_internal  = FF_CODEC_CAP_INIT_THREADSAFE,
381};
382