Rework last change, so that we build subparse, but just disable the sami parse functi...
[platform/upstream/gstreamer.git] / gst / subparse / samiparse.c
1 /* GStreamer SAMI subtitle parser
2  * Copyright (c) 2006 Young-Ho Cha <ganadist at chollian net>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Library General Public License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public
15  * License along with this library; if not, write to the
16  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17  * Boston, MA 02111-1307, USA.
18  */
19
20 #include "samiparse.h"
21
22 #include <libxml/HTMLparser.h>
23 #include <string.h>
24
25 #define ITALIC_TAG 'i'
26 #define SPAN_TAG   's'
27 #define RUBY_TAG   'r'
28 #define RT_TAG     't'
29 #define CLEAR_TAG  '0'
30
31 typedef struct _GstSamiContext GstSamiContext;
32
33 struct _GstSamiContext
34 {
35   GString *buf;                 /* buffer to collect content */
36   GString *rubybuf;             /* buffer to collect ruby content */
37   GString *resultbuf;           /* when opening the next 'sync' tag, move
38                                  * from 'buf' to avoid to append following
39                                  * content */
40   GString *state;               /* in many sami files there are tags that
41                                  * are not closed, so for each open tag the
42                                  * parser will append a tag flag here so
43                                  * that tags can be closed properly on
44                                  * 'sync' tags. See _context_push_state()
45                                  * and _context_pop_state(). */
46   htmlParserCtxtPtr htmlctxt;   /* html parser context */
47   gboolean has_result;          /* set when ready to push out result */
48   gboolean in_sync;             /* flag to avoid appending anything except the
49                                  * content of the sync elements to buf */
50   guint64 time1;                /* previous start attribute in sync tag */
51   guint64 time2;                /* current start attribute in sync tag  */
52 };
53
54 static gchar *
55 has_tag (GString * str, const gchar tag)
56 {
57   return strrchr (str->str, tag);
58 }
59
60 static void
61 sami_context_push_state (GstSamiContext * sctx, char state)
62 {
63   g_string_append_c (sctx->state, state);
64 }
65
66 static void
67 sami_context_pop_state (GstSamiContext * sctx, char state)
68 {
69   GString *str = g_string_new ("");
70   GString *context_state = sctx->state;
71   int i;
72
73   for (i = context_state->len - 1; i >= 0; i--) {
74     switch (context_state->str[i]) {
75       case ITALIC_TAG:         /* <i> */
76       {
77         g_string_append (str, "</i>");
78         break;
79       }
80       case SPAN_TAG:           /* <span foreground= > */
81       {
82         g_string_append (str, "</span>");
83         break;
84       }
85       case RUBY_TAG:           /* <span size= >  -- ruby */
86       {
87         break;
88       }
89       case RT_TAG:             /*  ruby */
90       {
91         /* FIXME: support for furigana/ruby once implemented in pango */
92         g_string_append (sctx->rubybuf, "</span>");
93         if (has_tag (context_state, ITALIC_TAG)) {
94           g_string_append (sctx->rubybuf, "</i>");
95         }
96
97         break;
98       }
99       default:
100         break;
101     }
102     if (context_state->str[i] == state) {
103       g_string_append (sctx->buf, str->str);
104       g_string_free (str, TRUE);
105       g_string_truncate (context_state, i);
106       return;
107     }
108   }
109   if (state == CLEAR_TAG) {
110     g_string_append (sctx->buf, str->str);
111     g_string_truncate (context_state, 0);
112   }
113   g_string_free (str, TRUE);
114 }
115
116 static void
117 handle_start_sync (GstSamiContext * sctx, const xmlChar ** atts)
118 {
119   int i;
120
121   sami_context_pop_state (sctx, CLEAR_TAG);
122   if (atts != NULL) {
123     for (i = 0; (atts[i] != NULL); i += 2) {
124       const xmlChar *key, *value;
125
126       key = atts[i];
127       value = atts[i + 1];
128
129       if (!value)
130         continue;
131       if (!xmlStrncmp ((const xmlChar *) "start", key, 5)) {
132         /* Only set a new start time if we don't have text pending */
133         if (sctx->resultbuf->len == 0)
134           sctx->time1 = sctx->time2;
135
136         sctx->time2 = atoi ((const char *) value) * GST_MSECOND;
137         g_string_append (sctx->resultbuf, sctx->buf->str);
138         sctx->has_result = (sctx->resultbuf->len != 0) ? TRUE : FALSE;
139         g_string_truncate (sctx->buf, 0);
140       }
141     }
142   }
143 }
144
145 static void
146 handle_start_font (GstSamiContext * sctx, const xmlChar ** atts)
147 {
148   int i;
149
150   sami_context_pop_state (sctx, SPAN_TAG);
151   if (atts != NULL) {
152     g_string_append (sctx->buf, "<span");
153     for (i = 0; (atts[i] != NULL); i += 2) {
154       const xmlChar *key, *value;
155
156       key = atts[i];
157       value = atts[i + 1];
158
159       if (!value)
160         continue;
161       if (!xmlStrncmp ((const xmlChar *) "color", key, 5)) {
162         /*
163          * There are invalid color value in many
164          * sami files.
165          * It will fix hex color value that start without '#'
166          */
167         gchar *sharp = "";
168         int len = xmlStrlen (value);
169
170         if (!(*value == '#' && len == 7)) {
171           gchar *r;
172
173           /* check if it looks like hex */
174           if (strtol ((const char *) value, &r, 16) >= 0 &&
175               ((xmlChar *) r == (value + 6) && len == 6)) {
176             sharp = "#";
177           }
178         }
179         /* some colours can be found in many sami files, but X RGB database
180          * doesn't contain a colour by this name, so map explicitly */
181         if (!xmlStrncasecmp (value, (const xmlChar *) "aqua", len)) {
182           value = (const xmlChar *) "#00ffff";
183         } else if (!xmlStrncasecmp (value, (const xmlChar *) "crimson", len)) {
184           value = (const xmlChar *) "#dc143c";
185         } else if (!xmlStrncasecmp (value, (const xmlChar *) "fuchsia", len)) {
186           value = (const xmlChar *) "#ff00ff";
187         } else if (!xmlStrncasecmp (value, (const xmlChar *) "indigo", len)) {
188           value = (const xmlChar *) "#4b0082";
189         } else if (!xmlStrncasecmp (value, (const xmlChar *) "lime", len)) {
190           value = (const xmlChar *) "#00ff00";
191         } else if (!xmlStrncasecmp (value, (const xmlChar *) "olive", len)) {
192           value = (const xmlChar *) "#808000";
193         } else if (!xmlStrncasecmp (value, (const xmlChar *) "silver", len)) {
194           value = (const xmlChar *) "#c0c0c0";
195         } else if (!xmlStrncasecmp (value, (const xmlChar *) "teal", len)) {
196           value = (const xmlChar *) "#008080";
197         }
198         g_string_append_printf (sctx->buf, " foreground=\"%s%s\"", sharp,
199             value);
200       } else if (!xmlStrncasecmp ((const xmlChar *) "face", key, 4)) {
201         g_string_append_printf (sctx->buf, " font_family=\"%s\"", value);
202       }
203     }
204     g_string_append_c (sctx->buf, '>');
205     sami_context_push_state (sctx, SPAN_TAG);
206   }
207 }
208
209 static void
210 start_sami_element (void *ctx, const xmlChar * name, const xmlChar ** atts)
211 {
212   GstSamiContext *sctx = (GstSamiContext *) ctx;
213
214   if (!xmlStrncmp ((const xmlChar *) "sync", name, 4)) {
215     handle_start_sync (sctx, atts);
216     sctx->in_sync = TRUE;
217   } else if (!xmlStrncmp ((const xmlChar *) "font", name, 4)) {
218     handle_start_font (sctx, atts);
219   } else if (!xmlStrncmp ((const xmlChar *) "ruby", name, 4)) {
220     sami_context_push_state (sctx, RUBY_TAG);
221   } else if (!xmlStrncmp ((const xmlChar *) "br", name, 2)) {
222     g_string_append_c (sctx->buf, '\n');
223     /* FIXME: support for furigana/ruby once implemented in pango */
224   } else if (!xmlStrncmp ((const xmlChar *) "rt", name, 2)) {
225     if (has_tag (sctx->state, ITALIC_TAG)) {
226       g_string_append (sctx->rubybuf, "<i>");
227     }
228     g_string_append (sctx->rubybuf, "<span size='xx-small' rise='-100'>");
229     sami_context_push_state (sctx, RT_TAG);
230   } else if (!xmlStrncmp ((const xmlChar *) "p", name, 1)) {
231   } else if (!xmlStrncmp ((const xmlChar *) "i", name, 1)) {
232     g_string_append (sctx->buf, "<i>");
233     sami_context_push_state (sctx, ITALIC_TAG);
234   }
235 }
236
237 static void
238 end_sami_element (void *ctx, const xmlChar * name)
239 {
240   GstSamiContext *sctx = (GstSamiContext *) ctx;
241
242   if (!xmlStrncmp ((const xmlChar *) "sync", name, 4)) {
243     sctx->in_sync = FALSE;
244   } else if (!xmlStrncmp ((const xmlChar *) "body", name, 4)) {
245     /* We will usually have one buffer left when the body is closed
246      * as we need the next sync to actually send it */
247     if (sctx->buf->len != 0) {
248       /* Only set a new start time if we don't have text pending */
249       if (sctx->resultbuf->len == 0)
250         sctx->time1 = sctx->time2;
251
252       sctx->time2 = GST_CLOCK_TIME_NONE;
253       g_string_append (sctx->resultbuf, sctx->buf->str);
254       sctx->has_result = (sctx->resultbuf->len != 0) ? TRUE : FALSE;
255       g_string_truncate (sctx->buf, 0);
256     }
257   } else if (!xmlStrncmp ((const xmlChar *) "font", name, 4)) {
258     sami_context_pop_state (sctx, SPAN_TAG);
259   } else if (!xmlStrncmp ((const xmlChar *) "ruby", name, 4)) {
260     sami_context_pop_state (sctx, RUBY_TAG);
261   } else if (!xmlStrncmp ((const xmlChar *) "i", name, 1)) {
262     sami_context_pop_state (sctx, ITALIC_TAG);
263   }
264 }
265
266 static void
267 characters_sami (void *ctx, const xmlChar * ch, int len)
268 {
269   GstSamiContext *sctx = (GstSamiContext *) ctx;
270   gchar *escaped;
271   gchar *tmp;
272   gint i;
273
274   /* Skip everything except content of the sync elements */
275   if (!sctx->in_sync)
276     return;
277
278   escaped = g_markup_escape_text ((const gchar *) ch, len);
279   g_strstrip (escaped);
280
281   /* Remove double spaces forom the string as those are
282    * usually added by newlines and indention */
283   tmp = escaped;
284   for (i = 0; i <= strlen (escaped); i++) {
285     escaped[i] = *tmp;
286     if (*tmp != ' ') {
287       tmp++;
288       continue;
289     }
290     while (*tmp == ' ')
291       tmp++;
292   }
293
294   if (has_tag (sctx->state, RT_TAG)) {
295     g_string_append_c (sctx->rubybuf, ' ');
296     g_string_append (sctx->rubybuf, escaped);
297     g_string_append_c (sctx->rubybuf, ' ');
298   } else {
299     g_string_append (sctx->buf, escaped);
300   }
301   g_free (escaped);
302 }
303
304 static xmlSAXHandler samiSAXHandlerStruct = {
305   NULL,                         /* internalSubset */
306   NULL,                         /* isStandalone */
307   NULL,                         /* hasInternalSubset */
308   NULL,                         /* hasExternalSubset */
309   NULL,                         /* resolveEntity */
310   NULL,                         /* getEntity */
311   NULL,                         /* entityDecl */
312   NULL,                         /* notationDecl */
313   NULL,                         /* attributeDecl */
314   NULL,                         /* elementDecl */
315   NULL,                         /* unparsedEntityDecl */
316   NULL,                         /* setDocumentLocator */
317   NULL,                         /* startDocument */
318   NULL,                         /* endDocument */
319   start_sami_element,           /* startElement */
320   end_sami_element,             /* endElement */
321   NULL,                         /* reference */
322   characters_sami,              /* characters */
323   NULL,                         /* ignorableWhitespace */
324   NULL,                         /* processingInstruction */
325   NULL,                         /* comment */
326   NULL,                         /* xmlParserWarning */
327   NULL,                         /* xmlParserError */
328   NULL,                         /* xmlParserError */
329   NULL,                         /* getParameterEntity */
330   NULL,                         /* cdataBlock */
331   NULL,                         /* externalSubset */
332   1,                            /* initialized */
333   NULL,                         /* private */
334   NULL,                         /* startElementNsSAX2Func */
335   NULL,                         /* endElementNsSAX2Func */
336   NULL                          /* xmlStructuredErrorFunc */
337 };
338 static xmlSAXHandlerPtr samiSAXHandler = &samiSAXHandlerStruct;
339
340 void
341 sami_context_init (ParserState * state)
342 {
343   GstSamiContext *context;
344
345   g_assert (state->user_data == NULL);
346   state->user_data = (gpointer) g_new0 (GstSamiContext, 1);
347   context = (GstSamiContext *) state->user_data;
348
349   context->htmlctxt = htmlCreatePushParserCtxt (samiSAXHandler, context,
350       "", 0, NULL, XML_CHAR_ENCODING_UTF8);
351   context->buf = g_string_new ("");
352   context->rubybuf = g_string_new ("");
353   context->resultbuf = g_string_new ("");
354   context->state = g_string_new ("");
355 }
356
357 void
358 sami_context_deinit (ParserState * state)
359 {
360   GstSamiContext *context = (GstSamiContext *) state->user_data;
361
362   if (context) {
363     htmlParserCtxtPtr htmlctxt = context->htmlctxt;
364
365     /* destroy sax context */
366     htmlDocPtr doc;
367
368     htmlParseChunk (htmlctxt, "", 0, 1);
369     doc = htmlctxt->myDoc;
370     htmlFreeParserCtxt (htmlctxt);
371     context->htmlctxt = NULL;
372     if (doc)
373       xmlFreeDoc (doc);
374     g_string_free (context->buf, TRUE);
375     g_string_free (context->rubybuf, TRUE);
376     g_string_free (context->resultbuf, TRUE);
377     g_string_free (context->state, TRUE);
378     g_free (context);
379     state->user_data = NULL;
380   }
381 }
382
383 void
384 sami_context_reset (ParserState * state)
385 {
386   GstSamiContext *context = (GstSamiContext *) state->user_data;
387
388   if (context) {
389     g_string_truncate (context->buf, 0);
390     g_string_truncate (context->rubybuf, 0);
391     g_string_truncate (context->resultbuf, 0);
392     g_string_truncate (context->state, 0);
393     context->has_result = FALSE;
394     context->in_sync = FALSE;
395     context->time1 = 0;
396     context->time2 = 0;
397   }
398 }
399
400 static gchar *
401 fix_invalid_entities (const gchar * line)
402 {
403   const gchar *cp, *pp;         /* current pointer, previous pointer */
404   gssize size;
405   GString *ret = g_string_new (NULL);
406
407   pp = line;
408   cp = strchr (line, '&');
409   while (cp) {
410     size = cp - pp;
411     ret = g_string_append_len (ret, pp, size);
412     cp++;
413     if (g_ascii_strncasecmp (cp, "nbsp;", 5)
414         && (!g_ascii_strncasecmp (cp, "nbsp", 4))) {
415       /* translate "&nbsp" to "&nbsp;" */
416       ret = g_string_append_len (ret, "&nbsp;", 6);
417       cp += 4;
418     } else if (g_ascii_strncasecmp (cp, "quot;", 5)
419         && g_ascii_strncasecmp (cp, "amp;", 4)
420         && g_ascii_strncasecmp (cp, "apos;", 5)
421         && g_ascii_strncasecmp (cp, "lt;", 3)
422         && g_ascii_strncasecmp (cp, "gt;", 3)
423         && g_ascii_strncasecmp (cp, "nbsp;", 5)
424         && cp[0] != '#') {
425       /* translate "&" to "&amp;" */
426       ret = g_string_append_len (ret, "&amp;", 5);
427     } else {
428       /* do not translate */
429       ret = g_string_append_c (ret, '&');
430     }
431
432     pp = cp;
433     cp = strchr (pp, '&');
434   }
435   ret = g_string_append (ret, pp);
436   return g_string_free (ret, FALSE);
437 }
438
439 gchar *
440 parse_sami (ParserState * state, const gchar * line)
441 {
442   gchar *fixed_line;
443   GstSamiContext *context = (GstSamiContext *) state->user_data;
444
445   fixed_line = fix_invalid_entities (line);
446   htmlParseChunk (context->htmlctxt, fixed_line, strlen (fixed_line), 0);
447   g_free (fixed_line);
448
449   if (context->has_result) {
450     gchar *r;
451
452     if (context->rubybuf->len) {
453       context->rubybuf = g_string_append_c (context->rubybuf, '\n');
454       g_string_prepend (context->resultbuf, context->rubybuf->str);
455       context->rubybuf = g_string_truncate (context->rubybuf, 0);
456     }
457
458     r = g_string_free (context->resultbuf, FALSE);
459     context->resultbuf = g_string_new ("");
460     state->start_time = context->time1;
461     state->duration = context->time2 - context->time1;
462     context->has_result = FALSE;
463     return r;
464   }
465   return NULL;
466 }