summaryrefslogtreecommitdiffstats
path: root/libass
diff options
context:
space:
mode:
authoreugeni <eugeni@b3059339-0415-0410-9bf9-f77b7e298cf2>2006-07-07 18:26:51 +0000
committereugeni <eugeni@b3059339-0415-0410-9bf9-f77b7e298cf2>2006-07-07 18:26:51 +0000
commite15ae9a60043d8d9c1b110607c5059f13ca86421 (patch)
tree22b3b039cdc936ae7063c5fd0092c2c837292d0f /libass
parent2f2d8cef15ccef7f2e0882a482a4fa071054778d (diff)
downloadmpv-e15ae9a60043d8d9c1b110607c5059f13ca86421.tar.bz2
mpv-e15ae9a60043d8d9c1b110607c5059f13ca86421.tar.xz
Initial libass release (without mencoder support).
git-svn-id: svn://svn.mplayerhq.hu/mplayer/trunk@18942 b3059339-0415-0410-9bf9-f77b7e298cf2
Diffstat (limited to 'libass')
-rw-r--r--libass/Makefile50
-rw-r--r--libass/ass.c857
-rw-r--r--libass/ass.h156
-rw-r--r--libass/ass_cache.c209
-rw-r--r--libass/ass_cache.h50
-rw-r--r--libass/ass_fontconfig.c192
-rw-r--r--libass/ass_fontconfig.h11
-rw-r--r--libass/ass_mp.c10
-rw-r--r--libass/ass_mp.h12
-rw-r--r--libass/ass_render.c1766
-rw-r--r--libass/ass_types.h83
-rw-r--r--libass/ass_utils.c63
-rw-r--r--libass/ass_utils.h9
13 files changed, 3468 insertions, 0 deletions
diff --git a/libass/Makefile b/libass/Makefile
new file mode 100644
index 0000000000..0604dd32e2
--- /dev/null
+++ b/libass/Makefile
@@ -0,0 +1,50 @@
+
+include ../config.mak
+
+LIBNAME=libass.a
+
+LIBS=$(LIBNAME)
+
+SRCS=ass.c ass_cache.c ass_fontconfig.c ass_render.c ass_utils.c ass_mp.c
+
+OBJS=$(SRCS:.c=.o)
+
+CFLAGS = $(OPTFLAGS) \
+ -I. -I.. \
+ -I../libmpcodecs \
+ $(EXTRA_INC) \
+ -D_GNU_SOURCE \
+ $(FREETYPE_INC) \
+ $(FONTCONFIG_INC) \
+
+.SUFFIXES: .c .o
+
+# .PHONY: all clean
+
+.c.o:
+ $(CC) -c $(CFLAGS) -o $@ $<
+
+all: $(LIBS)
+
+$(LIBNAME): $(OBJS)
+ $(AR) r $(LIBNAME) $(OBJS)
+ $(RANLIB) $(LIBNAME)
+
+clean:
+ rm -f *.o *.a *~
+
+distclean: clean
+ rm -f .depend
+
+dep: depend
+
+depend:
+ $(CC) -MM $(CFLAGS) $(SRCS) 1>.depend
+
+#
+# include dependency files if they exist
+#
+ifneq ($(wildcard .depend),)
+include .depend
+endif
+
diff --git a/libass/ass.c b/libass/ass.c
new file mode 100644
index 0000000000..4f9b8908ec
--- /dev/null
+++ b/libass/ass.c
@@ -0,0 +1,857 @@
+#include "config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <assert.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#ifdef HAVE_ENCA
+#include "subreader.h" // for guess_buffer_cp
+#endif
+
+#ifdef USE_ICONV
+#include <iconv.h>
+extern char *sub_cp;
+#endif
+
+#include "mp_msg.h"
+#include "ass.h"
+#include "ass_utils.h"
+#include "libvo/sub.h" // for utf8_get_char
+
+char *get_path(char *);
+
+#define ASS_STYLES_ALLOC 20
+#define ASS_EVENTS_ALLOC 200
+
+void ass_free_track(ass_track_t* track) {
+ int i;
+
+ if (track->style_format)
+ free(track->style_format);
+ if (track->event_format)
+ free(track->event_format);
+ if (track->styles) {
+ for (i = 0; i < track->n_styles; ++i) {
+ ass_style_t* style = track->styles + i;
+ if (style->Name)
+ free(style->Name);
+ if (style->FontName)
+ free(style->FontName);
+ }
+ free(track->styles);
+ }
+ if (track->events) {
+ for (i = 0; i < track->n_events; ++i) {
+ ass_event_t* event = track->events + i;
+ if (event->Name)
+ free(event->Name);
+ if (event->Effect)
+ free(event->Effect);
+ if (event->Text)
+ free(event->Text);
+ }
+ free(track->events);
+ }
+}
+
+/// \brief Allocate a new style struct
+/// \param track track
+/// \return style id
+int ass_alloc_style(ass_track_t* track) {
+ int sid;
+
+ assert(track->n_styles <= track->max_styles);
+
+ if (track->n_styles == track->max_styles) {
+ track->max_styles += ASS_STYLES_ALLOC;
+ track->styles = (ass_style_t*)realloc(track->styles, sizeof(ass_style_t)*track->max_styles);
+ }
+
+ sid = track->n_styles++;
+ memset(track->styles + sid, 0, sizeof(ass_style_t));
+ return sid;
+}
+
+/// \brief Allocate a new event struct
+/// \param track track
+/// \return event id
+int ass_alloc_event(ass_track_t* track) {
+ int eid;
+
+ assert(track->n_events <= track->max_events);
+
+ if (track->n_events == track->max_events) {
+ track->max_events += ASS_EVENTS_ALLOC;
+ track->events = (ass_event_t*)realloc(track->events, sizeof(ass_event_t)*track->max_events);
+ }
+
+ eid = track->n_events++;
+ memset(track->events + eid, 0, sizeof(ass_event_t));
+ return eid;
+}
+
+static void free_event(ass_track_t* track, int eid) {
+ if (track->n_events > eid + 1) // not last event
+ memcpy(track->events + eid, track->events + eid + 1, sizeof(ass_event_t) * (track->n_events - eid - 1));
+ track->n_events--;
+}
+
+static int events_compare_f(const void* a_, const void* b_) {
+ ass_event_t* a = (ass_event_t*)a_;
+ ass_event_t* b = (ass_event_t*)b_;
+ if (a->Start < b->Start)
+ return -1;
+ else if (a->Start > b->Start)
+ return 1;
+ else
+ return 0;
+}
+
+/// \brief Sort events by start time
+/// \param tid track id
+static void sort_events(ass_track_t* track) {
+ qsort(track->events, track->n_events, sizeof(ass_event_t), events_compare_f);
+}
+
+// ==============================================================================================
+
+static void skip_spaces(char** str) {
+ char* p = *str;
+ while ((*p==' ') || (*p=='\t'))
+ ++p;
+ *str = p;
+}
+
+static void rskip_spaces(char** str, char* limit) {
+ char* p = *str;
+ while ((p >= limit) && ((*p==' ') || (*p=='\t')))
+ --p;
+ *str = p;
+}
+
+/**
+ * \brief find style by name
+ * \param track track
+ * \param name style name
+ * \return index in track->styles
+ * Returnes 0 if no styles found => expects at least 1 style.
+ * Parsing code always adds "Default" style in the end.
+ */
+static int lookup_style(ass_track_t* track, char* name) {
+ int i;
+ for (i=0; i<track->n_styles; ++i) {
+ // FIXME: mb strcasecmp ?
+ if (strcmp(track->styles[i].Name, name) == 0)
+ return i;
+ }
+ i = track->default_style;
+ mp_msg(MSGT_GLOBAL, MSGL_WARN, "[%p] Warning: no style named '%s' found, using '%s'\n", track, name, track->styles[i].Name);
+ return i; // use the first style
+}
+
+static uint32_t string2color(char* p) {
+ uint32_t tmp;
+ (void)strtocolor(&p, &tmp);
+ return tmp;
+}
+
+static long long string2timecode(char* p) {
+ unsigned h, m, s, ms;
+ long long tm;
+ int res = sscanf(p, "%1d:%2d:%2d.%2d", &h, &m, &s, &ms);
+ if (res < 4) {
+ mp_msg(MSGT_GLOBAL, MSGL_WARN, "bad timestamp\n");
+ return 0;
+ }
+ tm = ((h * 60 + m) * 60 + s) * 1000 + ms * 10;
+ return tm;
+}
+
+/**
+ * \brief converts numpad-style align to align.
+ */
+static int numpad2align(int val) {
+ int res, v;
+ v = (val - 1) / 3; // 0, 1 or 2 for vertical alignment
+ if (v != 0) v = 3 - v;
+ res = ((val - 1) % 3) + 1; // horizontal alignment
+ res += v*4;
+ return res;
+}
+
+#define NEXT(str,token) \
+ token = next_token(&str); \
+ if (!token) break;
+
+#define ANYVAL(name,func) \
+ } else if (strcasecmp(tname, #name) == 0) { \
+ target->name = func(token); \
+ mp_msg(MSGT_GLOBAL, MSGL_DBG2, "%s = %s\n", #name, token);
+#define STRVAL(name) ANYVAL(name,strdup)
+#define COLORVAL(name) ANYVAL(name,string2color)
+#define INTVAL(name) ANYVAL(name,atoi)
+#define FPVAL(name) ANYVAL(name,atof)
+#define TIMEVAL(name) ANYVAL(name,string2timecode)
+#define STYLEVAL(name) \
+ } else if (strcasecmp(tname, #name) == 0) { \
+ target->name = lookup_style(track, token); \
+ mp_msg(MSGT_GLOBAL, MSGL_DBG2, "%s = %s\n", #name, token);
+
+#define ALIAS(alias,name) \
+ if (strcasecmp(tname, #alias) == 0) {tname = #name;}
+
+static char* next_token(char** str) {
+ char* p = *str;
+ char* start;
+ skip_spaces(&p);
+ if (*p == '\0') {
+ *str = p;
+ return 0;
+ }
+ start = p; // start of the token
+ for (; (*p != '\0') && (*p != ','); ++p) {}
+ if (*p == '\0') {
+ *str = p; // eos found, str will point to '\0' at exit
+ } else {
+ *p = '\0';
+ *str = p + 1; // ',' found, str will point to the next char (beginning of the next token)
+ }
+ --p; // end of current token
+ rskip_spaces(&p, start);
+ if (p < start)
+ p = start; // empty token
+ else
+ ++p; // the first space character, or '\0'
+ *p = '\0';
+ return start;
+}
+/**
+ * \brief Parse the tail of Dialogue line
+ * \param track track
+ * \param event parsed data goes here
+ * \param str string to parse, zero-terminated
+ * \param n_ignored number of format options to skip at the beginning
+*/
+static int process_event_tail(ass_track_t* track, ass_event_t* event, char* str, int n_ignored)
+{
+ char* token;
+ char* tname;
+ char* p = str;
+ int i;
+ ass_event_t* target = event;
+
+ char* format = strdup(track->event_format);
+ char* q = format; // format scanning pointer
+
+ for (i = 0; i < n_ignored; ++i) {
+ NEXT(q, tname);
+ }
+
+ while (1) {
+ NEXT(q, tname);
+ if (strcasecmp(tname, "Text") == 0) {
+ char* last;
+ event->Text = strdup(p);
+ last = event->Text + strlen(event->Text) - 1;
+ if (*last == '\r')
+ *last = 0;
+ mp_msg(MSGT_GLOBAL, MSGL_DBG2, "Text = %s\n", event->Text);
+ event->Duration -= event->Start;
+ free(format);
+ return 0; // "Text" is always the last
+ }
+ NEXT(p, token);
+
+ ALIAS(End,Duration) // temporarily store end timecode in event->Duration
+ if (0) { // cool ;)
+ STYLEVAL(Style)
+ STRVAL(Name)
+ STRVAL(Effect)
+ INTVAL(MarginL)
+ INTVAL(MarginR)
+ INTVAL(MarginV)
+ TIMEVAL(Start)
+ TIMEVAL(Duration)
+ }
+ }
+ free(format);
+ return 1;
+}
+
+/**
+ * \brief Parse the Style line
+ * \param track track
+ * \param str string to parse, zero-terminated
+ * Allocates a new style struct.
+*/
+static int process_style(ass_track_t* track, char *str)
+{
+
+ char* token;
+ char* tname;
+ char* p = str;
+ char* format;
+ char* q; // format scanning pointer
+ int sid;
+ ass_style_t* style;
+ ass_style_t* target;
+
+ if (!track->style_format) {
+ // no style format header
+ // probably an ancient script version
+ if (track->track_type == TRACK_TYPE_SSA)
+ track->style_format = strdup("Name, Fontname, Fontsize, PrimaryColour, SecondaryColour,"
+ "TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline,"
+ "Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding");
+ else
+ track->style_format = strdup("Name, Fontname, Fontsize, PrimaryColour, SecondaryColour,"
+ "OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut,"
+ "ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow,"
+ "Alignment, MarginL, MarginR, MarginV, Encoding");
+ }
+
+ q = format = strdup(track->style_format);
+
+ mp_msg(MSGT_GLOBAL, MSGL_V, "[%p] Style: %s\n", track, str);
+
+ sid = ass_alloc_style(track);
+
+ style = track->styles + sid;
+ target = style;
+// fill style with some default values
+ style->ScaleX = 100.;
+ style->ScaleY = 100.;
+
+ while (1) {
+ NEXT(q, tname);
+ NEXT(p, token);
+
+// ALIAS(TertiaryColour,OutlineColour) // ignore TertiaryColour; it appears only in SSA, and is overridden by BackColour
+
+ if (0) { // cool ;)
+ STRVAL(Name)
+ if ((strcmp(target->Name, "Default")==0) || (strcmp(target->Name, "*Default")==0))
+ track->default_style = sid;
+ STRVAL(FontName)
+ COLORVAL(PrimaryColour)
+ COLORVAL(SecondaryColour)
+ COLORVAL(OutlineColour) // TertiaryColor
+ COLORVAL(BackColour)
+ // SSA uses BackColour for both outline and shadow
+ // this will destroy SSA's TertiaryColour, but i'm not going to use it anyway
+ if (track->track_type == TRACK_TYPE_SSA)
+ target->OutlineColour = target->BackColour;
+ INTVAL(FontSize)
+ INTVAL(Bold)
+ INTVAL(Italic)
+ INTVAL(Underline)
+ INTVAL(StrikeOut)
+ INTVAL(Spacing)
+ INTVAL(Angle)
+ INTVAL(BorderStyle)
+ INTVAL(Alignment)
+ if (track->track_type == TRACK_TYPE_ASS)
+ target->Alignment = numpad2align(target->Alignment);
+ INTVAL(MarginL)
+ INTVAL(MarginR)
+ INTVAL(MarginV)
+ INTVAL(Encoding)
+ FPVAL(ScaleX)
+ FPVAL(ScaleY)
+ FPVAL(Outline)
+ FPVAL(Shadow)
+ }
+ }
+ style->ScaleX /= 100.;
+ style->ScaleY /= 100.;
+ if (!style->Name)
+ style->Name = strdup("Default");
+ if (!style->FontName)
+ style->FontName = strdup("Arial");
+ free(format);
+ return 0;
+
+}
+
+/**
+ * \brief Parse a header line
+ * \param track track
+ * \param str string to parse, zero-terminated
+*/
+static int process_header_line(ass_track_t* track, char *str)
+{
+ static int events_section_started = 0;
+
+ mp_msg(MSGT_GLOBAL, MSGL_DBG2, "=== Header: %s\n", str);
+ if (strncmp(str, "PlayResX:", 9)==0) {
+ track->PlayResX = atoi(str + 9);
+ } else if (strncmp(str,"PlayResY:", 9)==0) {
+ track->PlayResY = atoi(str + 9);
+ } else if (strncmp(str,"Timer:", 6)==0) {
+ track->Timer = atof(str + 6);
+ } else if (strstr(str,"Styles]")) {
+ events_section_started = 0;
+ if (strchr(str, '+'))
+ track->track_type = TRACK_TYPE_ASS;
+ else
+ track->track_type = TRACK_TYPE_SSA;
+ } else if (strncmp(str,"[Events]", 8)==0) {
+ events_section_started = 1;
+ } else if (strncmp(str,"Format:", 7)==0) {
+ char* p = str + 7;
+ skip_spaces(&p);
+ if (events_section_started) {
+ track->event_format = strdup(p);
+ mp_msg(MSGT_GLOBAL, MSGL_DBG2, "Event format: %s\n", track->event_format);
+ } else {
+ track->style_format = strdup(p);
+ mp_msg(MSGT_GLOBAL, MSGL_DBG2, "Style format: %s\n", track->style_format);
+ }
+ } else if (strncmp(str,"Style:", 6)==0) {
+ char* p = str + 6;
+ skip_spaces(&p);
+ process_style(track, p);
+ } else if (strncmp(str,"WrapStyle:", 10)==0) {
+ track->WrapStyle = atoi(str + 10);
+ }
+ return 0;
+}
+
+/**
+ * \brief Process CodecPrivate section of subtitle stream
+ * \param track track
+ * \param data string to parse
+ * \param size length of data
+ CodecPrivate section contains [Stream Info] and [V4+ Styles] sections
+*/
+void ass_process_chunk(ass_track_t* track, char *data, int size)
+{
+ char* str = malloc(size + 1);
+ char* p;
+ int sid;
+
+ memcpy(str, data, size);
+ str[size] = '\0';
+
+ p = str;
+ while(1) {
+ char* q;
+ for (;((*p=='\r')||(*p=='\n'));++p) {}
+ for (q=p; ((*q!='\0')&&(*q!='\r')&&(*q!='\n')); ++q) {};
+ if (q==p)
+ break;
+ if (*q != '\0')
+ *(q++) = '\0';
+ process_header_line(track, p);
+ if (*q == '\0')
+ break;
+ p = q;
+ }
+ free(str);
+
+ // add "Default" style to the end
+ // will be used if track does not contain a default style (or even does not contain styles at all)
+ sid = ass_alloc_style(track);
+ track->styles[sid].Name = strdup("Default");
+ track->styles[sid].FontName = strdup("Arial");
+
+ if (!track->event_format) {
+ // probably an mkv produced by ancient mkvtoolnix
+ // such files don't have [Events] and Format: headers
+ if (track->track_type == TRACK_TYPE_SSA)
+ track->event_format = strdup("Format: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
+ else
+ track->event_format = strdup("Format: Layer, Start, End, Style, Actor, MarginL, MarginR, MarginV, Effect, Text");
+ }
+}
+
+static int check_duplicate_event(ass_track_t* track, int ReadOrder)
+{
+ int i;
+ for (i = 0; i<track->n_events - 1; ++i) // ignoring last event, it is the one we are comparing with
+ if (track->events[i].ReadOrder == ReadOrder)
+ return 1;
+ return 0;
+}
+
+/**
+ * \brief Process a chunk of subtitle stream data. In matroska, this containes exactly 1 event (or a commentary)
+ * \param track track
+ * \param data string to parse
+ * \param size length of data
+ * \param timecode starting time of the event (milliseconds)
+ * \param duration duration of the event (milliseconds)
+*/
+void ass_process_line(ass_track_t* track, char *data, int size, long long timecode, long long duration)
+{
+ char* str;
+ int eid;
+ char* p;
+ char* token;
+ ass_event_t* event;
+
+ if (!track->event_format) {
+ mp_msg(MSGT_GLOBAL, MSGL_WARN, "Event format header missing\n");
+ return;
+ }
+
+ str = malloc(size + 1);
+ memcpy(str, data, size);
+ str[size] = '\0';
+ mp_msg(MSGT_GLOBAL, MSGL_V, "\nline at timecode %lld, duration %lld: \n%s\n", timecode, duration, str);
+
+ eid = ass_alloc_event(track);
+ event = track->events + eid;
+
+ p = str;
+
+ do {
+ NEXT(p, token);
+ event->ReadOrder = atoi(token);
+ if (check_duplicate_event(track, event->ReadOrder))
+ break;
+
+ NEXT(p, token);
+ event->Layer = atoi(token);
+
+ process_event_tail(track, event, p, 3);
+
+ event->Start = timecode;
+ event->Duration = duration;
+
+ free(str);
+ return;
+// dump_events(tid);
+ } while (0);
+ // some error
+ free_event(track, eid);
+ free(str);
+}
+
+/**
+ * \brief Process a line from external file.
+ * \param track track
+ * \param str string to parse
+ * \param size length of data
+*/
+static void ass_process_external_line(ass_track_t* track, char *str, int size)
+{
+ int eid;
+ ass_event_t* event;
+
+ eid = ass_alloc_event(track);
+ event = track->events + eid;
+
+ if (strncmp("Dialogue:", str, 9) != 0)
+ return;
+
+ str += 9;
+ while (*str == ' ') {++str;}
+
+ process_event_tail(track, event, str, 0);
+}
+
+#ifdef USE_ICONV
+/** \brief recode buffer to utf-8
+ * constraint: sub_cp != 0
+ * \param data pointer to text buffer
+ * \param size buffer size
+ * \return a pointer to recoded buffer, caller is responsible for freeing it
+**/
+static char* sub_recode(char* data, size_t size)
+{
+ static iconv_t icdsc = (iconv_t)(-1);
+ char* tocp = "UTF-8";
+ char* outbuf;
+ assert(sub_cp);
+
+ {
+ char* cp_tmp = sub_cp;
+#ifdef HAVE_ENCA
+ char enca_lang[3], enca_fallback[100];
+ if (sscanf(sub_cp, "enca:%2s:%99s", enca_lang, enca_fallback) == 2
+ || sscanf(sub_cp, "ENCA:%2s:%99s", enca_lang, enca_fallback) == 2) {
+ cp_tmp = guess_buffer_cp((unsigned char*)data, size, enca_lang, enca_fallback);
+ }
+#endif
+ if ((icdsc = iconv_open (tocp, cp_tmp)) != (iconv_t)(-1)){
+ mp_msg(MSGT_SUBREADER,MSGL_V,"LIBSUB: opened iconv descriptor.\n");
+ } else
+ mp_msg(MSGT_SUBREADER,MSGL_ERR,"LIBSUB: error opening iconv descriptor.\n");
+#ifdef HAVE_ENCA
+ if (cp_tmp) free(cp_tmp);
+#endif
+ }
+
+ {
+ size_t osize = size;
+ size_t ileft = size;
+ size_t oleft = size - 1;
+ char* ip;
+ char* op;
+ size_t rc;
+
+ outbuf = malloc(size);
+ ip = data;
+ op = outbuf;
+
+ while (ileft) {
+ rc = iconv(icdsc, &ip, &ileft, &op, &oleft);
+ if (rc == (size_t)(-1)) {
+ if (errno == E2BIG) {
+ int offset = op - outbuf;
+ outbuf = (char*)realloc(outbuf, osize + size);
+ op = outbuf + offset;
+ osize += size;
+ oleft += size;
+ } else {
+ mp_msg(MSGT_SUBREADER, MSGL_WARN, "LIBSUB: error recoding file.\n");
+ return NULL;
+ }
+ }
+ }
+ outbuf[osize - oleft - 1] = 0;
+ }
+
+ if (icdsc != (iconv_t)(-1)) {
+ (void)iconv_close(icdsc);
+ icdsc = (iconv_t)(-1);
+ mp_msg(MSGT_SUBREADER,MSGL_V,"LIBSUB: closed iconv descriptor.\n");
+ }
+
+ return outbuf;
+}
+#endif // ICONV
+
+/**
+ * \brief Read subtitles from file.
+ * \param fname file name
+ * \return newly allocated track
+*/
+ass_track_t* ass_read_file(char* fname)
+{
+ int res;
+ long sz;
+ long bytes_read;
+ char* buf;
+ char* p;
+ int events_reached;
+ ass_track_t* track;
+
+ FILE* fp = fopen(fname, "rb");
+ if (!fp) {
+ mp_msg(MSGT_GLOBAL, MSGL_WARN, "ass_read_file(%s): fopen failed\n", fname);
+ return 0;
+ }
+ res = fseek(fp, 0, SEEK_END);
+ if (res == -1) {
+ mp_msg(MSGT_GLOBAL, MSGL_WARN, "ass_read_file(%s): fseek failed\n", fname);
+ fclose(fp);
+ return 0;
+ }
+
+ sz = ftell(fp);
+ rewind(fp);
+
+ if (sz > 10*1024*1024) {
+ mp_msg(MSGT_GLOBAL, MSGL_INFO, "ass_read_file(%s): Refusing to load subtitles larger than 10M\n", fname);
+ fclose(fp);
+ return 0;
+ }
+
+ mp_msg(MSGT_GLOBAL, MSGL_V, "file size: %ld\n", sz);
+
+ buf = malloc(sz + 1);
+ assert(buf);
+ bytes_read = 0;
+ do {
+ res = fread(buf + bytes_read, 1, sz - bytes_read, fp);
+ if (res <= 0) {
+ mp_msg(MSGT_GLOBAL, MSGL_INFO, "Read failed, %d: %s\n", errno, strerror(errno));
+ fclose(fp);
+ free(buf);
+ return 0;
+ }
+ bytes_read += res;
+ } while (sz - bytes_read > 0);
+ buf[sz] = '\0';
+ fclose(fp);
+
+#ifdef USE_ICONV
+ if (sub_cp) {
+ char* tmpbuf = sub_recode(buf, sz);
+ free(buf);
+ if (!tmpbuf)
+ return 0;
+ buf = tmpbuf;
+ }
+#endif
+
+ track = ass_new_track();
+ track->name = strdup(fname);
+
+ // process header
+ events_reached = 0;
+ p = buf;
+ while (p && (*p)) {
+ while (*p == '\n') {++p;}
+ if (strncmp(p, "[Events]", 8) == 0) {
+ events_reached = 1;
+ } else if ((strncmp(p, "Format:", 7) == 0) && (events_reached)) {
+ p = strchr(p, '\n');
+ if (p == 0) {
+ mp_msg(MSGT_GLOBAL, MSGL_WARN, "Incomplete subtitles\n");
+ free(buf);
+ return 0;
+ }
+ ass_process_chunk(track, buf, p - buf + 1);
+ ++p;
+ break;
+ }
+ p = strchr(p, '\n');
+ }
+ // process events
+ while (p && (*p)) {
+ char* next;
+ int len;
+ while (*p == '\n') {++p;}
+ next = strchr(p, '\n');
+ len = 0;
+ if (next) {
+ len = next - p;
+ *next = 0;
+ } else {
+ len = strlen(p);
+ }
+ ass_process_external_line(track, p, len);
+ if (next) {
+ p = next + 1;
+ continue;
+ } else
+ break;
+ }
+
+ free(buf);
+
+ if (!events_reached) {
+ ass_free_track(track);
+ return 0;
+ }
+
+ mp_msg(MSGT_GLOBAL, MSGL_INFO, "LIBASS: added subtitle file: %s (%d styles, %d events)\n", fname, track->n_styles, track->n_events);
+
+ sort_events(track);
+
+// dump_events(forced_tid);
+ return track;
+}
+
+static char* validate_fname(char* name)
+{
+ char* fname;
+ char* p;
+ char* q;
+ unsigned code;
+ int sz = strlen(name);
+
+ q = fname = malloc(sz + 1);
+ p = name;
+ while (*p) {
+ code = utf8_get_char(&p);
+ if (code == 0)
+ break;
+ if ( (code > 0x7F) ||
+ (code == '\\') ||
+ (code == '/') ||
+ (code == ':') ||
+ (code == '*') ||
+ (code == '?') ||
+ (code == '<') ||
+ (code == '>') ||
+ (code == '|') ||
+ (code == 0))
+ {
+ *q++ = '_';
+ } else {
+ *q++ = code;
+ }
+ if (p - name > sz)
+ break;
+ }
+ *q = 0;
+ return fname;
+}
+
+/**
+ * \brief Process embedded matroska font. Saves it to ~/.mplayer/fonts.
+ * \param name attachment name
+ * \param data binary font data
+ * \param data_size data size
+*/
+void ass_process_font(const char* name, char* data, int data_size)
+{
+ char buf[1000];
+ FILE* fp = 0;
+ int rc;
+ struct stat st;
+ char* fname;
+
+ char* fonts_dir = get_path("fonts");
+ rc = stat(fonts_dir, &st);
+ if (rc) {
+ int res;
+#ifndef __MINGW32__
+ res = mkdir(fonts_dir, 0700);
+#else
+ res = mkdir(fonts_dir);
+#endif
+ if (res) {
+ mp_msg(MSGT_GLOBAL, MSGL_WARN, "Failed to create: %s\n", fonts_dir);
+ }
+ } else if (!S_ISDIR(st.st_mode)) {
+ mp_msg(MSGT_GLOBAL, MSGL_WARN, "Not a directory: %s\n", fonts_dir);
+ }
+
+ fname = validate_fname((char*)name);
+
+ snprintf(buf, 1000, "%s/%s", fonts_dir, fname);
+ free(fname);
+ free(fonts_dir);
+
+ fp = fopen(buf, "wb");
+ if (!fp) return;
+
+ fwrite(data, data_size, 1, fp);
+ fclose(fp);
+}
+
+long long ass_step_sub(ass_track_t* track, long long now, int movement) {
+ int i;
+
+ if (movement == 0) return 0;
+ if (track->n_events == 0) return 0;
+
+ if (movement < 0)
+ for (i = 0; (i < track->n_events) && ((long long)(track->events[i].Start + track->events[i].Duration) <= now); ++i) {}
+ else
+ for (i = track->n_events - 1; (i >= 0) && ((long long)(track->events[i].Start) > now); --i) {}
+
+ // -1 and n_events are ok
+ assert(i >= -1); assert(i <= track->n_events);
+ i += movement;
+ if (i < 0) i = 0;
+ if (i >= track->n_events) i = track->n_events - 1;
+ return ((long long)track->events[i].Start) - now;
+}
+
+ass_track_t* ass_new_track(void) {
+ ass_track_t* track = calloc(1, sizeof(ass_track_t));
+ return track;
+}
+
diff --git a/libass/ass.h b/libass/ass.h
new file mode 100644
index 0000000000..6049fd184b
--- /dev/null
+++ b/libass/ass.h
@@ -0,0 +1,156 @@
+#ifndef __ASS_H__
+#define __ASS_H__
+
+#include "ass_types.h"
+
+/// Libass "library object". Contents are private.
+typedef struct ass_instance_s ass_instance_t;
+
+/// used in ass_configure
+typedef struct ass_settings_s {
+ int frame_width;
+ int frame_height;
+ double font_size_coeff; // font size multiplier
+ double line_spacing; // additional line spacing (in frame pixels)
+ int top_margin; // height of top margin. Everything except toptitles is shifted down by top_margin.
+ int bottom_margin; // height of bottom margin. (frame_height - top_margin - bottom_margin) is original video height.
+ double aspect; // frame aspect ratio, d_width / d_height.
+} ass_settings_t;
+
+/// a linked list of images produced by ass renderer
+typedef struct ass_image_s {
+ int w, h; // bitmap width/height
+ int stride; // bitmap stride
+ unsigned char* bitmap; // 1bpp stride*h alpha buffer
+ uint32_t color; // RGBA
+ int dst_x, dst_y; // bitmap placement inside the video frame
+
+ struct ass_image_s* next; // linked list
+} ass_image_t;
+
+/**
+ * \brief initialize the library
+ * \return library handle or NULL if failed
+ */
+ass_instance_t* ass_init(void);
+
+/**
+ * \brief finalize the library
+ * \param priv library handle
+ */
+void ass_done(ass_instance_t* priv);
+
+/**
+ * \brief configure the library
+ * \param priv library handle
+ * \param config struct with configuration parameters. Caller is free to reuse it after this function returns.
+ */
+void ass_configure(ass_instance_t* priv, const ass_settings_t* config);
+
+/**
+ * \brief start rendering a frame
+ * \param priv library
+ * \param track subtitle track
+ * \param now video timestamp in milliseconds
+ */
+int ass_start_frame(ass_instance_t *priv, ass_track_t* track, long long now);
+
+/**
+ * \brief render a single event
+ * uses library, track and timestamp from the previous call to ass_start_frame
+ */
+int ass_render_event(ass_event_t* event);
+
+/**
+ * \brief done rendering frame, give out the results
+ * \return a list of images for blending
+ */
+ass_image_t* ass_end_frame(void); // returns linked list of images to render
+
+/**
+ * \brief render a frame, producing a list of ass_image_t
+ * \param priv library
+ * \param track subtitle track
+ * \param now video timestamp in milliseconds
+ * This function is equivalent to
+ * ass_start_frame()
+ * for events: start <= now < end:
+ * ass_render_event()
+ * ass_end_frame()
+ */
+ass_image_t* ass_render_frame(ass_instance_t *priv, ass_track_t* track, long long now);
+
+
+// The following functions operate on track objects and do not need an ass_instance //
+
+/**
+ * \brief allocate a new empty track object
+ * \return pointer to empty track
+ */
+ass_track_t* ass_new_track(void);
+
+/**
+ * \brief deallocate track and all its child objects (styles and events)
+ * \param track track to deallocate
+ */
+void ass_free_track(ass_track_t* track);
+
+/**
+ * \brief allocate new style
+ * \param track track
+ * \return newly allocated style id
+ */
+int ass_alloc_style(ass_track_t* track);
+
+/**
+ * \brief allocate new event
+ * \param track track
+ * \return newly allocated event id
+ */
+int ass_alloc_event(ass_track_t* track);
+
+/**
+ * \brief Process Codec Private section of subtitle stream
+ * \param track target track
+ * \param data string to parse
+ * \param size length of data
+ */
+void ass_process_chunk(ass_track_t* track, char *data, int size);
+
+/**
+ * \brief Process a chunk of subtitle stream data. In matroska, this containes exactly 1 event (or a commentary)
+ * \param track track
+ * \param data string to parse
+ * \param size length of data
+ * \param timecode starting time of the event (milliseconds)
+ * \param duration duration of the event (milliseconds)
+*/
+void ass_process_line(ass_track_t* track, char *data, int size, long long timecode, long long duration);
+
+/**
+ * \brief Read subtitles from file.
+ * \param fname file name
+ * \return newly allocated track
+*/
+ass_track_t* ass_read_file(char* fname);
+
+/**
+ * \brief Process embedded matroska font. Saves it to ~/.mplayer/fonts.
+ * \param name attachment name
+ * \param data binary font data
+ * \param data_size data size
+*/
+void ass_process_font(const char* name, char* data, int data_size);
+
+/**
+ * \brief Calculates timeshift from now to the start of some other subtitle event, depending on movement parameter
+ * \param track subtitle track
+ * \param now current time, ms
+ * \param movement how many events to skip from the one currently displayed
+ * +2 means "the one after the next", -1 means "previous"
+ * \return timeshift, ms
+ */
+long long ass_step_sub(ass_track_t* track, long long now, int movement);
+
+#endif
+
diff --git a/libass/ass_cache.c b/libass/ass_cache.c
new file mode 100644
index 0000000000..5d400ed6e7
--- /dev/null
+++ b/libass/ass_cache.c
@@ -0,0 +1,209 @@
+#include "config.h"
+
+#include <ft2build.h>
+#include FT_FREETYPE_H
+
+#include <assert.h>
+
+#include "mp_msg.h"
+#include "ass_fontconfig.h"
+#include "ass_cache