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
35static int indexof(const char *s, int c)
36{
37    char *f = strchr(s, c);
38    return f ? (f - s) : -1;
39}
40
41struct microdvd_tag {
42    char key;
43    int persistent;
44    uint32_t data1;
45    uint32_t data2;
46    char *data_string;
47    int data_string_len;
48};
49
50#define MICRODVD_PERSISTENT_OFF     0
51#define MICRODVD_PERSISTENT_ON      1
52#define MICRODVD_PERSISTENT_OPENED  2
53
54// Color, Font, Size, cHarset, stYle, Position, cOordinate
55#define MICRODVD_TAGS "cfshyYpo"
56
57static void microdvd_set_tag(struct microdvd_tag *tags, struct microdvd_tag tag)
58{
59    int tag_index = indexof(MICRODVD_TAGS, tag.key);
60
61    if (tag_index < 0)
62        return;
63    memcpy(&tags[tag_index], &tag, sizeof(tag));
64}
65
66// italic, bold, underline, strike-through
67#define MICRODVD_STYLES "ibus"
68
69static char *microdvd_load_tags(struct microdvd_tag *tags, char *s)
70{
71    while (*s == '{') {
72        char *start = s;
73        char tag_char = *(s + 1);
74        struct microdvd_tag tag = {0};
75
76        if (!tag_char || *(s + 2) != ':')
77            break;
78        s += 3;
79
80        switch (tag_char) {
81
82        /* Style */
83        case 'Y':
84            tag.persistent = MICRODVD_PERSISTENT_ON;
85        case 'y':
86            while (*s && *s != '}') {
87                int style_index = indexof(MICRODVD_STYLES, *s);
88
89                if (style_index >= 0)
90                    tag.data1 |= (1 << style_index);
91                s++;
92            }
93            if (*s != '}')
94                break;
95            /* We must distinguish persistent and non-persistent styles
96             * to handle this kind of style tags: {y:ib}{Y:us} */
97            tag.key = tag_char;
98            break;
99
100        /* Color */
101        case 'C':
102            tag.persistent = MICRODVD_PERSISTENT_ON;
103        case 'c':
104            if (*s == '$')
105                s++;
106            tag.data1 = strtol(s, &s, 16) & 0x00ffffff;
107            if (*s != '}')
108                break;
109            tag.key = 'c';
110            break;
111
112        /* Font name */
113        case 'F':
114            tag.persistent = MICRODVD_PERSISTENT_ON;
115        case 'f': {
116            int len = indexof(s, '}');
117            if (len < 0)
118                break;
119            tag.data_string = s;
120            tag.data_string_len = len;
121            s += len;
122            tag.key = 'f';
123            break;
124        }
125
126        /* Font size */
127        case 'S':
128            tag.persistent = MICRODVD_PERSISTENT_ON;
129        case 's':
130            tag.data1 = strtol(s, &s, 10);
131            if (*s != '}')
132                break;
133            tag.key = 's';
134            break;
135
136        /* Charset */
137        case 'H': {
138            //TODO: not yet handled, just parsed.
139            int len = indexof(s, '}');
140            if (len < 0)
141                break;
142            tag.data_string = s;
143            tag.data_string_len = len;
144            s += len;
145            tag.key = 'h';
146            break;
147        }
148
149        /* Position */
150        case 'P':
151            tag.persistent = MICRODVD_PERSISTENT_ON;
152            tag.data1 = (*s++ == '1');
153            if (*s != '}')
154                break;
155            tag.key = 'p';
156            break;
157
158        /* Coordinates */
159        case 'o':
160            tag.persistent = MICRODVD_PERSISTENT_ON;
161            tag.data1 = strtol(s, &s, 10);
162            if (*s != ',')
163                break;
164            s++;
165            tag.data2 = strtol(s, &s, 10);
166            if (*s != '}')
167                break;
168            tag.key = 'o';
169            break;
170
171        default:    /* Unknown tag, we consider it's text */
172            break;
173        }
174
175        if (tag.key == 0)
176            return start;
177
178        microdvd_set_tag(tags, tag);
179        s++;
180    }
181    return s;
182}
183
184static void microdvd_open_tags(AVBPrint *new_line, struct microdvd_tag *tags)
185{
186    int i, sidx;
187    for (i = 0; i < sizeof(MICRODVD_TAGS) - 1; i++) {
188        if (tags[i].persistent == MICRODVD_PERSISTENT_OPENED)
189            continue;
190        switch (tags[i].key) {
191        case 'Y':
192        case 'y':
193            for (sidx = 0; sidx < sizeof(MICRODVD_STYLES) - 1; sidx++)
194                if (tags[i].data1 & (1 << sidx))
195                    av_bprintf(new_line, "{\\%c1}", MICRODVD_STYLES[sidx]);
196            break;
197
198        case 'c':
199            av_bprintf(new_line, "{\\c&H%06X&}", tags[i].data1);
200            break;
201
202        case 'f':
203            av_bprintf(new_line, "{\\fn%.*s}",
204                       tags[i].data_string_len, tags[i].data_string);
205            break;
206
207        case 's':
208            av_bprintf(new_line, "{\\fs%d}", tags[i].data1);
209            break;
210
211        case 'p':
212            if (tags[i].data1 == 0)
213                av_bprintf(new_line, "{\\an8}");
214            break;
215
216        case 'o':
217            av_bprintf(new_line, "{\\pos(%d,%d)}",
218                       tags[i].data1, tags[i].data2);
219            break;
220        }
221        if (tags[i].persistent == MICRODVD_PERSISTENT_ON)
222            tags[i].persistent = MICRODVD_PERSISTENT_OPENED;
223    }
224}
225
226static void microdvd_close_no_persistent_tags(AVBPrint *new_line,
227                                              struct microdvd_tag *tags)
228{
229    int i, sidx;
230
231    for (i = sizeof(MICRODVD_TAGS) - 2; i >= 0; i--) {
232        if (tags[i].persistent != MICRODVD_PERSISTENT_OFF)
233            continue;
234        switch (tags[i].key) {
235
236        case 'y':
237            for (sidx = sizeof(MICRODVD_STYLES) - 2; sidx >= 0; sidx--)
238                if (tags[i].data1 & (1 << sidx))
239                    av_bprintf(new_line, "{\\%c0}", MICRODVD_STYLES[sidx]);
240            break;
241
242        case 'c':
243            av_bprintf(new_line, "{\\c}");
244            break;
245
246        case 'f':
247            av_bprintf(new_line, "{\\fn}");
248            break;
249
250        case 's':
251            av_bprintf(new_line, "{\\fs}");
252            break;
253        }
254        tags[i].key = 0;
255    }
256}
257
258static int microdvd_decode_frame(AVCodecContext *avctx,
259                                 void *data, int *got_sub_ptr, AVPacket *avpkt)
260{
261    AVSubtitle *sub = data;
262    AVBPrint new_line;
263    char c;
264    char *decoded_sub;
265    char *line = avpkt->data;
266    char *end = avpkt->data + avpkt->size;
267    struct microdvd_tag tags[sizeof(MICRODVD_TAGS) - 1] = {{0}};
268
269    if (avpkt->size <= 0)
270        return avpkt->size;
271
272    /* To be removed later */
273    if (sscanf(line, "{%*d}{%*[0123456789]}%c", &c) == 1 &&
274        line[avpkt->size - 1] == '\n') {
275        av_log(avctx, AV_LOG_ERROR, "AVPacket is not clean (contains timing "
276               "information and a trailing line break). You need to upgrade "
277               "your libavformat or sanitize your packet.\n");
278        return AVERROR_INVALIDDATA;
279    }
280
281    av_bprint_init(&new_line, 0, 2048);
282
283    // subtitle content
284    while (line < end && *line) {
285
286        // parse MicroDVD tags, and open them in ASS
287        line = microdvd_load_tags(tags, line);
288        microdvd_open_tags(&new_line, tags);
289
290        // simple copy until EOL or forced carriage return
291        while (line < end && *line && *line != '|') {
292            av_bprint_chars(&new_line, *line, 1);
293            line++;
294        }
295
296        // line split
297        if (line < end && *line == '|') {
298            microdvd_close_no_persistent_tags(&new_line, tags);
299            av_bprintf(&new_line, "\\N");
300            line++;
301        }
302    }
303    if (new_line.len) {
304        av_bprintf(&new_line, "\r\n");
305
306    av_bprint_finalize(&new_line, &decoded_sub);
307    if (*decoded_sub) {
308        int64_t start    = avpkt->pts;
309        int64_t duration = avpkt->duration;
310        int ts_start     = av_rescale_q(start,    avctx->time_base, (AVRational){1,100});
311        int ts_duration  = duration != -1 ?
312                           av_rescale_q(duration, avctx->time_base, (AVRational){1,100}) : -1;
313        ff_ass_add_rect(sub, decoded_sub, ts_start, ts_duration, 0);
314    }
315    av_free(decoded_sub);
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, alignment);
368}
369
370AVCodec ff_microdvd_decoder = {
371    .name         = "microdvd",
372    .long_name    = NULL_IF_CONFIG_SMALL("MicroDVD subtitle"),
373    .type         = AVMEDIA_TYPE_SUBTITLE,
374    .id           = AV_CODEC_ID_MICRODVD,
375    .init         = microdvd_init,
376    .decode       = microdvd_decode_frame,
377};
378