freetype: fix extra squares in Arabic subtitles (fixes #6152 #8117)
[vlc.git] / modules / text_renderer / quartztext.c
1 /*****************************************************************************
2  * quartztext.c : Put text on the video, using Mac OS X Quartz Engine
3  *****************************************************************************
4  * Copyright (C) 2007, 2009, 2012 VLC authors and VideoLAN
5  * $Id$
6  *
7  * Authors: Bernie Purcell <bitmap@videolan.org>
8  *          Pierre d'Herbemont <pdherbemont # videolan dot>
9  *          Felix Paul K├╝hne <fkuehne # videolan # org>
10  *
11  * This program is free software; you can redistribute it and/or modify it
12  * under the terms of the GNU Lesser General Public License as published by
13  * the Free Software Foundation; either version 2.1 of the License, or
14  * (at your option) any later version.
15  *
16  * This program is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19  * GNU Lesser General Public License for more details.
20  *
21  * You should have received a copy of the GNU Lesser General Public License
22  * along with this program; if not, write to the Free Software Foundation,
23  * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
24  *****************************************************************************/
25
26 /*****************************************************************************
27  * Preamble
28  *****************************************************************************/
29
30 #ifdef HAVE_CONFIG_H
31 # include "config.h"
32 #endif
33
34 #include <vlc_common.h>
35 #include <vlc_plugin.h>
36 #include <vlc_stream.h>
37 #include <vlc_xml.h>
38 #include <vlc_input.h>
39 #include <vlc_filter.h>
40
41 #include <TargetConditionals.h>
42
43 #if TARGET_OS_IPHONE
44 #include <CoreText/CoreText.h>
45 #include <CoreGraphics/CoreGraphics.h>
46
47 #else
48 // Fix ourselves ColorSync headers that gets included in ApplicationServices.
49 #define DisposeCMProfileIterateUPP(a) DisposeCMProfileIterateUPP(CMProfileIterateUPP userUPP __attribute__((unused)))
50 #define DisposeCMMIterateUPP(a) DisposeCMMIterateUPP(CMProfileIterateUPP userUPP __attribute__((unused)))
51 #define __MACHINEEXCEPTIONS__
52 #include <ApplicationServices/ApplicationServices.h>
53 #endif
54
55 #define DEFAULT_FONT           "Helvetica-Neue"
56 #define DEFAULT_FONT_COLOR     0xffffff
57 #define DEFAULT_REL_FONT_SIZE  16
58
59 #define VERTICAL_MARGIN 3
60 #define HORIZONTAL_MARGIN 10
61
62 /*****************************************************************************
63  * Local prototypes
64  *****************************************************************************/
65 static int  Create (vlc_object_t *);
66 static void Destroy(vlc_object_t *);
67
68 static int LoadFontsFromAttachments(filter_t *p_filter);
69
70 static int RenderText(filter_t *, subpicture_region_t *,
71                        subpicture_region_t *,
72                        const vlc_fourcc_t *);
73 static int RenderHtml(filter_t *, subpicture_region_t *,
74                        subpicture_region_t *,
75                        const vlc_fourcc_t *);
76
77 static int GetFontSize(filter_t *p_filter);
78 static int RenderYUVA(filter_t *p_filter, subpicture_region_t *p_region,
79                        CFMutableAttributedStringRef p_attrString);
80
81 static void setFontAttibutes(char *psz_fontname, int i_font_size, uint32_t i_font_color,
82                               bool b_bold, bool b_italic, bool b_underline, bool b_halfwidth,
83                               int i_spacing,
84                               CFRange p_range, CFMutableAttributedStringRef p_attrString);
85
86 /*****************************************************************************
87  * Module descriptor
88  *****************************************************************************/
89
90 /* The preferred way to set font style information is for it to come from the
91  * subtitle file, and for it to be rendered with RenderHtml instead of
92  * RenderText. */
93 #define FONT_TEXT N_("Font")
94 #define FONT_LONGTEXT N_("Name for the font you want to use")
95 #define FONTSIZER_TEXT N_("Relative font size")
96 #define FONTSIZER_LONGTEXT N_("This is the relative default size of the " \
97     "fonts that will be rendered on the video. If absolute font size is set, "\
98     "relative size will be overridden.")
99 #define COLOR_TEXT N_("Text default color")
100 #define COLOR_LONGTEXT N_("The color of the text that will be rendered on "\
101     "the video. This must be an hexadecimal (like HTML colors). The first two "\
102     "chars are for red, then green, then blue. #000000 = black, #FF0000 = red,"\
103     " #00FF00 = green, #FFFF00 = yellow (red + green), #FFFFFF = white")
104 #define OUTLINE_TEXT N_("Add outline")
105 #define SHADOW_TEXT N_("Add shadow")
106
107 static const int pi_color_values[] = {
108   0x00000000, 0x00808080, 0x00C0C0C0, 0x00FFFFFF, 0x00800000,
109   0x00FF0000, 0x00FF00FF, 0x00FFFF00, 0x00808000, 0x00008000, 0x00008080,
110   0x0000FF00, 0x00800080, 0x00000080, 0x000000FF, 0x0000FFFF };
111
112 static const char *const ppsz_color_names[] = {
113     "black", "gray", "silver", "white", "maroon",
114     "red", "fuchsia", "yellow", "olive", "green",
115     "teal", "lime", "purple", "navy", "blue", "aqua" };
116
117 static const char *const ppsz_color_descriptions[] = {
118   N_("Black"), N_("Gray"), N_("Silver"), N_("White"), N_("Maroon"),
119   N_("Red"), N_("Fuchsia"), N_("Yellow"), N_("Olive"), N_("Green"), N_("Teal"),
120   N_("Lime"), N_("Purple"), N_("Navy"), N_("Blue"), N_("Aqua") };
121
122 static const int pi_sizes[] = { 20, 18, 16, 12, 6 };
123 static const char *const ppsz_sizes_text[] = {
124     N_("Smaller"), N_("Small"), N_("Normal"), N_("Large"), N_("Larger") };
125
126 vlc_module_begin ()
127     set_shortname(N_("Text renderer for Mac"))
128     set_description(N_("CoreText font renderer"))
129     set_category(CAT_VIDEO)
130     set_subcategory(SUBCAT_VIDEO_SUBPIC)
131
132     add_string("quartztext-font", DEFAULT_FONT, FONT_TEXT, FONT_LONGTEXT,
133               false)
134     add_integer("quartztext-rel-fontsize", DEFAULT_REL_FONT_SIZE, FONTSIZER_TEXT,
135                  FONTSIZER_LONGTEXT, false)
136         change_integer_list(pi_sizes, ppsz_sizes_text)
137     add_integer("quartztext-color", 0x00FFFFFF, COLOR_TEXT,
138                  COLOR_LONGTEXT, false)
139         change_integer_list(pi_color_values, ppsz_color_descriptions)
140     add_bool("quartztext-outline", false, OUTLINE_TEXT, NULL, false)
141     add_bool("quartztext-shadow", true, SHADOW_TEXT, NULL, false)
142     set_capability("text renderer", 50)
143     add_shortcut("text")
144     set_callbacks(Create, Destroy)
145 vlc_module_end ()
146
147 typedef struct font_stack_t font_stack_t;
148 struct font_stack_t
149 {
150     char          *psz_name;
151     int            i_size;
152     uint32_t       i_color;            // ARGB
153
154     font_stack_t  *p_next;
155 };
156
157 typedef struct
158 {
159     int         i_font_size;
160     uint32_t    i_font_color;         /* ARGB */
161     bool  b_italic;
162     bool  b_bold;
163     bool  b_underline;
164     char       *psz_fontname;
165 } ft_style_t;
166
167 typedef struct offscreen_bitmap_t offscreen_bitmap_t;
168 struct offscreen_bitmap_t
169 {
170     uint8_t       *p_data;
171     int            i_bitsPerChannel;
172     int            i_bitsPerPixel;
173     int            i_bytesPerPixel;
174     int            i_bytesPerRow;
175 };
176
177 /*****************************************************************************
178  * filter_sys_t: quartztext local data
179  *****************************************************************************
180  * This structure is part of the video output thread descriptor.
181  * It describes the freetype specific properties of an output thread.
182  *****************************************************************************/
183 struct filter_sys_t
184 {
185     char          *psz_font_name;
186     uint8_t        i_font_opacity;
187     int            i_font_color;
188     int            i_font_size;
189     bool           b_outline;
190     bool           b_shadow;
191
192 #ifndef TARGET_OS_IPHONE
193     ATSFontContainerRef    *p_fonts;
194     int                     i_fonts;
195 #endif
196 };
197
198 /*****************************************************************************
199  * Create: allocates osd-text video thread output method
200  *****************************************************************************
201  * This function allocates and initializes a Clone vout method.
202  *****************************************************************************/
203 static int Create(vlc_object_t *p_this)
204 {
205     filter_t *p_filter = (filter_t *)p_this;
206     filter_sys_t *p_sys;
207
208     // Allocate structure
209     p_filter->p_sys = p_sys = malloc(sizeof(filter_sys_t));
210     if (!p_sys)
211         return VLC_ENOMEM;
212     p_sys->psz_font_name  = var_CreateGetString(p_this, "quartztext-font");
213     p_sys->i_font_opacity = 255;
214     p_sys->i_font_color = VLC_CLIP(var_CreateGetInteger(p_this, "quartztext-color") , 0, 0xFFFFFF);
215     p_sys->b_outline = var_InheritBool(p_this, "quartztext-outline");
216     p_sys->b_shadow = var_InheritBool(p_this, "quartztext-shadow");
217     p_sys->i_font_size = GetFontSize(p_filter);
218
219     p_filter->pf_render_text = RenderText;
220     p_filter->pf_render_html = RenderHtml;
221
222 #ifndef TARGET_OS_IPHONE
223     p_sys->p_fonts = NULL;
224     p_sys->i_fonts = 0;
225 #endif
226
227     LoadFontsFromAttachments(p_filter);
228
229     return VLC_SUCCESS;
230 }
231
232 /*****************************************************************************
233  * Destroy: destroy Clone video thread output method
234  *****************************************************************************
235  * Clean up all data and library connections
236  *****************************************************************************/
237 static void Destroy(vlc_object_t *p_this)
238 {
239     filter_t *p_filter = (filter_t *)p_this;
240     filter_sys_t *p_sys = p_filter->p_sys;
241 #ifndef TARGET_OS_IPHONE
242     if (p_sys->p_fonts) {
243         for (int k = 0; k < p_sys->i_fonts; k++) {
244             ATSFontDeactivate(p_sys->p_fonts[k], NULL, kATSOptionFlagsDefault);
245
246         free(p_sys->p_fonts);
247     }
248 #endif
249     free(p_sys->psz_font_name);
250     free(p_sys);
251 }
252
253 /*****************************************************************************
254  * Make any TTF/OTF fonts present in the attachments of the media file
255  * available to the Quartz engine for text rendering
256  *****************************************************************************/
257 static int LoadFontsFromAttachments(filter_t *p_filter)
258 {
259 #ifdef TARGET_OS_IPHONE
260     VLC_UNUSED(p_filter);
261     return VLC_SUCCESS;
262 #else
263     filter_sys_t         *p_sys = p_filter->p_sys;
264     input_attachment_t  **pp_attachments;
265     int                   i_attachments_cnt;
266
267     if (filter_GetInputAttachments(p_filter, &pp_attachments, &i_attachments_cnt))
268         return VLC_EGENERIC;
269
270     p_sys->i_fonts = 0;
271     p_sys->p_fonts = malloc(i_attachments_cnt * sizeof(ATSFontContainerRef));
272     if (! p_sys->p_fonts)
273         return VLC_ENOMEM;
274
275     for (int k = 0; k < i_attachments_cnt; k++) {
276         input_attachment_t *p_attach = pp_attachments[k];
277
278         if ((!strcmp(p_attach->psz_mime, "application/x-truetype-font") || // TTF
279               !strcmp(p_attach->psz_mime, "application/x-font-otf")) &&    // OTF
280             p_attach->i_data > 0 && p_attach->p_data) {
281             ATSFontContainerRef  container;
282
283             if (noErr == ATSFontActivateFromMemory(p_attach->p_data,
284                                                     p_attach->i_data,
285                                                     kATSFontContextLocal,
286                                                     kATSFontFormatUnspecified,
287                                                     NULL,
288                                                     kATSOptionFlagsDefault,
289                                                     &container))
290                 p_sys->p_fonts[ p_sys->i_fonts++ ] = container;
291         }
292         vlc_input_attachment_Delete(p_attach);
293     }
294     free(pp_attachments);
295     return VLC_SUCCESS;
296 #endif
297 }
298
299 static char *EliminateCRLF(char *psz_string)
300 {
301     char *q;
302
303     for (char * p = psz_string; p && *p; p++) {
304         if ((*p == '\r') && (*(p+1) == '\n')) {
305             for (q = p + 1; *q; q++)
306                 *(q - 1) = *q;
307
308             *(q - 1) = '\0';
309         }
310     }
311     return psz_string;
312 }
313
314 /* Renders a text subpicture region into another one.
315  * It is used as pf_add_string callback in the vout method by this module */
316 static int RenderText(filter_t *p_filter, subpicture_region_t *p_region_out,
317                        subpicture_region_t *p_region_in,
318                        const vlc_fourcc_t *p_chroma_list)
319 {
320     filter_sys_t *p_sys = p_filter->p_sys;
321     char         *psz_string;
322     char         *psz_fontname;
323     int           i_font_size;
324     int           i_spacing = 0;
325     int           i_font_alpha;
326     uint32_t      i_font_color;
327     bool          b_bold, b_uline, b_italic, b_halfwidth;
328     vlc_value_t val;
329     b_bold = b_uline = b_italic = b_halfwidth = FALSE;
330     VLC_UNUSED(p_chroma_list);
331
332     p_sys->i_font_size = GetFontSize(p_filter);
333
334     // Sanity check
335     if (!p_region_in || !p_region_out)
336         return VLC_EGENERIC;
337
338     psz_string = p_region_in->psz_text;
339     if (!psz_string || !*psz_string)
340         return VLC_EGENERIC;
341
342     if (p_region_in->p_style) {
343         psz_fontname = p_region_in->p_style->psz_fontname ?
344             p_region_in->p_style->psz_fontname : p_sys->psz_font_name;
345         i_font_color = VLC_CLIP(p_region_in->p_style->i_font_color, 0, 0xFFFFFF);
346         i_font_size  = VLC_CLIP(p_region_in->p_style->i_font_size, 0, 255);
347         if (p_region_in->p_style->i_style_flags) {
348             if (p_region_in->p_style->i_style_flags & STYLE_BOLD)
349                 b_bold = TRUE;
350             if (p_region_in->p_style->i_style_flags & STYLE_ITALIC)
351                 b_italic = TRUE;
352             if (p_region_in->p_style->i_style_flags & STYLE_UNDERLINE)
353                 b_uline = TRUE;
354             if (p_region_in->p_style->i_style_flags & STYLE_HALFWIDTH)
355                 b_halfwidth = TRUE;
356         }
357         i_spacing = VLC_CLIP(p_region_in->p_style->i_spacing, 0, 255);
358     } else {
359         psz_fontname = p_sys->psz_font_name;
360         i_font_color = p_sys->i_font_color;
361         i_font_size  = p_sys->i_font_size;
362     }
363
364     if (i_font_size <= 0) {
365         msg_Warn(p_filter, "invalid fontsize, using 12");
366         if (VLC_SUCCESS == var_Get(p_filter, "scale", &val))
367             i_font_size = 12 * val.i_int / 1000;
368         else
369             i_font_size = 12;
370     }
371
372     p_region_out->i_x = p_region_in->i_x;
373     p_region_out->i_y = p_region_in->i_y;
374
375     CFMutableAttributedStringRef p_attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
376
377     if (p_attrString) {
378         CFStringRef   p_cfString;
379         int           len;
380
381         EliminateCRLF(psz_string);
382         p_cfString = CFStringCreateWithCString(NULL, psz_string, kCFStringEncodingUTF8);
383         CFAttributedStringReplaceString(p_attrString, CFRangeMake(0, 0), p_cfString);
384         CFRelease(p_cfString);
385         len = CFAttributedStringGetLength(p_attrString);
386
387         setFontAttibutes(psz_fontname, i_font_size, i_font_color, b_bold, b_italic, b_uline, b_halfwidth,
388                                              i_spacing,
389                                              CFRangeMake(0, len), p_attrString);
390
391         RenderYUVA(p_filter, p_region_out, p_attrString);
392         CFRelease(p_attrString);
393     }
394
395     return VLC_SUCCESS;
396 }
397
398
399 static int PushFont(font_stack_t **p_font, const char *psz_name, int i_size,
400                      uint32_t i_color)
401 {
402     font_stack_t *p_new;
403
404     if (!p_font)
405         return VLC_EGENERIC;
406
407     p_new = malloc(sizeof(font_stack_t));
408     if (! p_new)
409         return VLC_ENOMEM;
410
411     p_new->p_next = NULL;
412
413     if (psz_name)
414         p_new->psz_name = strdup(psz_name);
415     else
416         p_new->psz_name = NULL;
417
418     p_new->i_size   = i_size;
419     p_new->i_color  = i_color;
420
421     if (!*p_font)
422         *p_font = p_new;
423     else {
424         font_stack_t *p_last;
425
426         for (p_last = *p_font; p_last->p_next; p_last = p_last->p_next)
427         ;
428
429         p_last->p_next = p_new;
430     }
431     return VLC_SUCCESS;
432 }
433
434 static int PopFont(font_stack_t **p_font)
435 {
436     font_stack_t *p_last, *p_next_to_last;
437
438     if (!p_font || !*p_font)
439         return VLC_EGENERIC;
440
441     p_next_to_last = NULL;
442     for (p_last = *p_font; p_last->p_next; p_last = p_last->p_next)
443         p_next_to_last = p_last;
444
445     if (p_next_to_last)
446         p_next_to_last->p_next = NULL;
447     else
448         *p_font = NULL;
449
450     free(p_last->psz_name);
451     free(p_last);
452
453     return VLC_SUCCESS;
454 }
455
456 static int PeekFont(font_stack_t **p_font, char **psz_name, int *i_size,
457                      uint32_t *i_color)
458 {
459     font_stack_t *p_last;
460
461     if (!p_font || !*p_font)
462         return VLC_EGENERIC;
463
464     for (p_last=*p_font;
465          p_last->p_next;
466          p_last=p_last->p_next)
467     ;
468
469     *psz_name = p_last->psz_name;
470     *i_size   = p_last->i_size;
471     *i_color  = p_last->i_color;
472
473     return VLC_SUCCESS;
474 }
475
476 static int HandleFontAttributes(xml_reader_t *p_xml_reader,
477                                   font_stack_t **p_fonts)
478 {
479     int        rv;
480     char      *psz_fontname = NULL;
481     uint32_t   i_font_color = 0xffffff;
482     int        i_font_alpha = 0;
483     int        i_font_size  = 24;
484     const char *attr, *value;
485
486     /* Default all attributes to the top font in the stack -- in case not
487      * all attributes are specified in the sub-font */
488     if (VLC_SUCCESS == PeekFont(p_fonts,
489                                 &psz_fontname,
490                                 &i_font_size,
491                                 &i_font_color)) {
492         psz_fontname = strdup(psz_fontname);
493         i_font_size = i_font_size;
494     }
495     i_font_alpha = (i_font_color >> 24) & 0xff;
496     i_font_color &= 0x00ffffff;
497
498     while ((attr = xml_ReaderNextAttr(p_xml_reader, &value))) {
499         if (!strcasecmp("face", attr)) {
500             free(psz_fontname);
501             psz_fontname = strdup(value);
502         } else if (!strcasecmp("size", attr)) {
503             if ((*value == '+') || (*value == '-')) {
504                 int i_value = atoi(value);
505
506                 if ((i_value >= -5) && (i_value <= 5))
507                     i_font_size += (i_value * i_font_size) / 10;
508                 else if (i_value < -5)
509                     i_font_size = - i_value;
510                 else if (i_value > 5)
511                     i_font_size = i_value;
512             }
513             else
514                 i_font_size = atoi(value);
515         } else if (!strcasecmp("color", attr)) {
516             if (value[0] == '#') {
517                 i_font_color = strtol(value + 1, NULL, 16);
518                 i_font_color &= 0x00ffffff;
519             } else {
520                 /* color detection fallback */
521                 unsigned int count = sizeof(ppsz_color_names);
522                 for (unsigned x = 0; x < count; x++) {
523                     if (!strcmp(value, ppsz_color_names[x])) {
524                         i_font_color = pi_color_values[x];
525                         break;
526                     }
527                 }
528             }
529         } else if (!strcasecmp("alpha", attr) && (value[0] == '#')) {
530             i_font_alpha = strtol(value + 1, NULL, 16);
531             i_font_alpha &= 0xff;
532         }
533     }
534     rv = PushFont(p_fonts,
535                   psz_fontname,
536                   i_font_size,
537                   (i_font_color & 0xffffff) | ((i_font_alpha & 0xff) << 24));
538
539     free(psz_fontname);
540
541     return rv;
542 }
543
544 static void setFontAttibutes(char *psz_fontname, int i_font_size, uint32_t i_font_color,
545         bool b_bold, bool b_italic, bool b_underline, bool b_halfwidth,
546         int i_spacing,
547         CFRange p_range, CFMutableAttributedStringRef p_attrString)
548 {
549     CFStringRef p_cfString;
550     CTFontRef   p_font;
551
552     int i_font_width = b_halfwidth ? i_font_size / 2 : i_font_size;
553     CGAffineTransform trans = CGAffineTransformMakeScale((float)i_font_width
554                                                          / i_font_size, 1.0);
555
556     // fallback on default
557     if (!psz_fontname)
558         psz_fontname = (char *)DEFAULT_FONT;
559
560     p_cfString = CFStringCreateWithCString(kCFAllocatorDefault,
561                                             psz_fontname,
562                                             kCFStringEncodingUTF8);
563     p_font     = CTFontCreateWithName(p_cfString,
564                                        (float)i_font_size,
565                                        &trans);
566     CFRelease(p_cfString);
567     CFAttributedStringSetAttribute(p_attrString,
568                                     p_range,
569                                     kCTFontAttributeName,
570                                     p_font);
571     CFRelease(p_font);
572
573     // Handle Underline
574     SInt32 _uline;
575     if (b_underline)
576         _uline = kCTUnderlineStyleSingle;
577     else
578         _uline = kCTUnderlineStyleNone;
579
580     CFNumberRef underline = CFNumberCreate(NULL, kCFNumberSInt32Type, &_uline);
581     CFAttributedStringSetAttribute(p_attrString,
582                                     p_range,
583                                     kCTUnderlineStyleAttributeName,
584                                     underline);
585     CFRelease(underline);
586
587     // Handle Bold
588     float _weight;
589     if (b_bold)
590         _weight = 0.5;
591     else
592         _weight = 0.0;
593
594     CFNumberRef weight = CFNumberCreate(NULL, kCFNumberFloatType, &_weight);
595     CFAttributedStringSetAttribute(p_attrString,
596                                     p_range,
597                                     kCTFontWeightTrait,
598                                     weight);
599     CFRelease(weight);
600
601     // Handle Italic
602     float _slant;
603     if (b_italic)
604         _slant = 1.0;
605     else
606         _slant = 0.0;
607
608     CFNumberRef slant = CFNumberCreate(NULL, kCFNumberFloatType, &_slant);
609     CFAttributedStringSetAttribute(p_attrString,
610                                     p_range,
611                                     kCTFontSlantTrait,
612                                     slant);
613     CFRelease(slant);
614
615     // fetch invalid colors
616     if (i_font_color == 0xFFFFFFFF)
617         i_font_color = 0x00FFFFFF;
618
619     // Handle foreground color
620     CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
621     CGFloat components[] = { (float)((i_font_color & 0x00ff0000) >> 16) / 255.0,
622                              (float)((i_font_color & 0x0000ff00) >>  8) / 255.0,
623                              (float)((i_font_color & 0x000000ff)) / 255.0,
624                              (float)(255-((i_font_color & 0xff000000) >> 24)) / 255.0 };
625     CGColorRef fg_text = CGColorCreate(rgbColorSpace, components);
626     CGColorSpaceRelease(rgbColorSpace);
627
628     CFAttributedStringSetAttribute(p_attrString,
629                                     p_range,
630                                     kCTForegroundColorAttributeName,
631                                     fg_text);
632     CFRelease(fg_text);
633
634     // spacing
635     if (i_spacing > 0)
636     {
637         CGFloat spacing = i_spacing;
638         CFNumberRef spacingCFNum = CFNumberCreate(NULL,
639                 kCFNumberCGFloatType, &spacing);
640         CFAttributedStringSetAttribute(p_attrString,
641                                         p_range,
642                                         kCTKernAttributeName,
643                                         spacingCFNum);
644         CFRelease(spacingCFNum);
645     }
646 }
647
648 static void GetAttrStrFromFontStack(font_stack_t **p_fonts,
649         bool b_bold, bool b_italic, bool b_uline,
650         CFRange p_range, CFMutableAttributedStringRef p_attrString)
651 {
652     char       *psz_fontname = NULL;
653     int         i_font_size  = 0;
654     uint32_t    i_font_color = 0;
655
656     if (VLC_SUCCESS == PeekFont(p_fonts, &psz_fontname, &i_font_size,
657                                 &i_font_color)) {
658         setFontAttibutes(psz_fontname,
659                          i_font_size,
660                          i_font_color,
661                          b_bold, b_italic, b_uline, FALSE,
662                          0,
663                          p_range,
664                          p_attrString);
665     }
666 }
667
668 static int ProcessNodes(filter_t *p_filter,
669                          xml_reader_t *p_xml_reader,
670                          text_style_t *p_font_style,
671                          CFMutableAttributedStringRef p_attrString)
672 {
673     int           rv             = VLC_SUCCESS;
674     filter_sys_t *p_sys          = p_filter->p_sys;
675     font_stack_t *p_fonts        = NULL;
676
677     int type;
678     const char *node;
679
680     bool b_italic = false;
681     bool b_bold   = false;
682     bool b_uline  = false;
683
684     if (p_font_style) {
685         rv = PushFont(&p_fonts,
686                p_font_style->psz_fontname,
687                p_font_style->i_font_size,
688                (p_font_style->i_font_color & 0xffffff) |
689                    ((p_font_style->i_font_alpha & 0xff) << 24));
690
691         if (p_font_style->i_style_flags & STYLE_BOLD)
692             b_bold = true;
693         if (p_font_style->i_style_flags & STYLE_ITALIC)
694             b_italic = true;
695         if (p_font_style->i_style_flags & STYLE_UNDERLINE)
696             b_uline = true;
697     } else {
698         rv = PushFont(&p_fonts,
699                        p_sys->psz_font_name,
700                        p_sys->i_font_size,
701                        p_sys->i_font_color);
702     }
703     if (rv != VLC_SUCCESS)
704         return rv;
705
706     while ((type = xml_ReaderNextNode(p_xml_reader, &node)) > 0) {
707         switch (type) {
708             case XML_READER_ENDELEM:
709                 if (!strcasecmp("font", node))
710                     PopFont(&p_fonts);
711                 else if (!strcasecmp("b", node))
712                     b_bold   = false;
713                 else if (!strcasecmp("i", node))
714                     b_italic = false;
715                 else if (!strcasecmp("u", node))
716                     b_uline  = false;
717
718                 break;
719             case XML_READER_STARTELEM:
720                 if (!strcasecmp("font", node))
721                     rv = HandleFontAttributes(p_xml_reader, &p_fonts);
722                 else if (!strcasecmp("b", node))
723                     b_bold = true;
724                 else if (!strcasecmp("i", node))
725                     b_italic = true;
726                 else if (!strcasecmp("u", node))
727                     b_uline = true;
728                 else if (!strcasecmp("br", node)) {
729                     CFMutableAttributedStringRef p_attrnode = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
730                     CFAttributedStringReplaceString(p_attrnode, CFRangeMake(0, 0), CFSTR("\n"));
731
732                     GetAttrStrFromFontStack(&p_fonts, b_bold, b_italic, b_uline,
733                                              CFRangeMake(0, 1),
734                                              p_attrnode);
735                     CFAttributedStringReplaceAttributedString(p_attrString,
736                                     CFRangeMake(CFAttributedStringGetLength(p_attrString), 0),
737                                     p_attrnode);
738                     CFRelease(p_attrnode);
739                 }
740                 break;
741             case XML_READER_TEXT:
742             {
743                 CFStringRef   p_cfString;
744                 int           len;
745
746                 // Turn any multiple-whitespaces into single spaces
747                 char *dup = strdup(node);
748                 if (!dup)
749                     break;
750                 char *s = strpbrk(dup, "\t\r\n ");
751                 while(s)
752                 {
753                     int i_whitespace = strspn(s, "\t\r\n ");
754
755                     if (i_whitespace > 1)
756                         memmove(&s[1],
757                                  &s[i_whitespace],
758                                  strlen(s) - i_whitespace + 1);
759                     *s++ = ' ';
760
761                     s = strpbrk(s, "\t\r\n ");
762                 }
763
764
765                 CFMutableAttributedStringRef p_attrnode = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
766                 p_cfString = CFStringCreateWithCString(NULL, dup, kCFStringEncodingUTF8);
767                 CFAttributedStringReplaceString(p_attrnode, CFRangeMake(0, 0), p_cfString);
768                 CFRelease(p_cfString);
769                 len = CFAttributedStringGetLength(p_attrnode);
770
771                 GetAttrStrFromFontStack(&p_fonts, b_bold, b_italic, b_uline,
772                                          CFRangeMake(0, len),
773                                          p_attrnode);
774
775                 CFAttributedStringReplaceAttributedString(p_attrString,
776                                 CFRangeMake(CFAttributedStringGetLength(p_attrString), 0),
777                                 p_attrnode);
778                 CFRelease(p_attrnode);
779
780                 free(dup);
781                 break;
782             }
783         }
784     }
785
786     while(VLC_SUCCESS == PopFont(&p_fonts));
787
788     return rv;
789 }
790
791 static int RenderHtml(filter_t *p_filter, subpicture_region_t *p_region_out,
792                        subpicture_region_t *p_region_in,
793                        const vlc_fourcc_t *p_chroma_list)
794 {
795     int          rv = VLC_SUCCESS;
796     stream_t     *p_sub = NULL;
797     xml_t        *p_xml = NULL;
798     xml_reader_t *p_xml_reader = NULL;
799     VLC_UNUSED(p_chroma_list);
800
801     if (!p_region_in || !p_region_in->psz_html)
802         return VLC_EGENERIC;
803
804     /* Reset the default fontsize in case screen metrics have changed */
805     p_filter->p_sys->i_font_size = GetFontSize(p_filter);
806
807     p_sub = stream_MemoryNew(VLC_OBJECT(p_filter),
808                               (uint8_t *) p_region_in->psz_html,
809                               strlen(p_region_in->psz_html),
810                               true);
811     if (p_sub) {
812         p_xml = xml_Create(p_filter);
813         if (p_xml) {
814             p_xml_reader = xml_ReaderCreate(p_xml, p_sub);
815             if (p_xml_reader) {
816                 /* Look for Root Node */
817                 const char *name;
818                 if (xml_ReaderNextNode(p_xml_reader, &name)
819                         == XML_READER_STARTELEM) {
820                     if (!strcasecmp("karaoke", name)) {
821                         /* We're going to have to render the text a number
822                          * of times to show the progress marker on the text.
823                          */
824                         var_SetBool(p_filter, "text-rerender", true);
825                     } else if (strcasecmp("text", name)) {
826                         /* Only text and karaoke tags are supported */
827                         msg_Dbg(p_filter, "Unsupported top-level tag "
828                                            "<%s> ignored.", name);
829                         rv = VLC_EGENERIC;
830                     }
831                 } else {
832                     msg_Err(p_filter, "Malformed HTML subtitle");
833                     rv = VLC_EGENERIC;
834                 }
835
836                 if (rv != VLC_SUCCESS) {
837                     xml_ReaderDelete(p_xml_reader);
838                     p_xml_reader = NULL;
839                 }
840             }
841
842             if (p_xml_reader) {
843                 int         i_len;
844
845                 CFMutableAttributedStringRef p_attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
846                 rv = ProcessNodes(p_filter, p_xml_reader,
847                               p_region_in->p_style, p_attrString);
848
849                 i_len = CFAttributedStringGetLength(p_attrString);
850
851                 p_region_out->i_x = p_region_in->i_x;
852                 p_region_out->i_y = p_region_in->i_y;
853
854                 if ((rv == VLC_SUCCESS) && (i_len > 0))
855                     RenderYUVA(p_filter, p_region_out, p_attrString);
856
857                 CFRelease(p_attrString);
858
859                 xml_ReaderDelete(p_xml_reader);
860             }
861             xml_Delete(p_xml);
862         }
863         stream_Delete(p_sub);
864     }
865
866     return rv;
867 }
868
869 static CGContextRef CreateOffScreenContext(int i_width, int i_height,
870                          offscreen_bitmap_t **pp_memory, CGColorSpaceRef *pp_colorSpace)
871 {
872     offscreen_bitmap_t *p_bitmap;
873     CGContextRef        p_context = NULL;
874
875     p_bitmap = (offscreen_bitmap_t *) malloc(sizeof(offscreen_bitmap_t));
876     if (p_bitmap) {
877         p_bitmap->i_bitsPerChannel = 8;
878         p_bitmap->i_bitsPerPixel   = 4 * p_bitmap->i_bitsPerChannel; // A,R,G,B
879         p_bitmap->i_bytesPerPixel  = p_bitmap->i_bitsPerPixel / 8;
880         p_bitmap->i_bytesPerRow    = i_width * p_bitmap->i_bytesPerPixel;
881
882         p_bitmap->p_data = calloc(i_height, p_bitmap->i_bytesPerRow);
883
884         *pp_colorSpace = CGColorSpaceCreateDeviceRGB();
885
886         if (p_bitmap->p_data && *pp_colorSpace)
887             p_context = CGBitmapContextCreate(p_bitmap->p_data, i_width, i_height,
888                                 p_bitmap->i_bitsPerChannel, p_bitmap->i_bytesPerRow,
889                                 *pp_colorSpace, kCGImageAlphaPremultipliedFirst);
890
891         if (p_context) {
892             if (CGContextSetAllowsAntialiasing != NULL)
893                 CGContextSetAllowsAntialiasing(p_context, true);
894         }
895         *pp_memory = p_bitmap;
896     }
897
898     return p_context;
899 }
900
901 static offscreen_bitmap_t *Compose(filter_t *p_filter,
902                                     subpicture_region_t *p_region,
903                                     CFMutableAttributedStringRef p_attrString,
904                                     unsigned i_width,
905                                     unsigned i_height,
906                                     unsigned *pi_textblock_height)
907 {
908     filter_sys_t *p_sys   = p_filter->p_sys;
909     offscreen_bitmap_t  *p_offScreen  = NULL;
910     CGColorSpaceRef      p_colorSpace = NULL;
911     CGContextRef         p_context = NULL;
912
913     p_context = CreateOffScreenContext(i_width, i_height, &p_offScreen, &p_colorSpace);
914
915     *pi_textblock_height = 0;
916     if (p_context) {
917         float horiz_flush;
918
919         CGContextSetTextMatrix(p_context, CGAffineTransformIdentity);
920
921         if (p_region->i_align & SUBPICTURE_ALIGN_RIGHT)
922             horiz_flush = 1.0;
923         else if ((p_region->i_align & SUBPICTURE_ALIGN_LEFT) == 0)
924             horiz_flush = 0.5;
925         else
926             horiz_flush = 0.0;
927
928         // Create the framesetter with the attributed string.
929         CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(p_attrString);
930         if (framesetter) {
931             CTFrameRef frame;
932             CGMutablePathRef p_path = CGPathCreateMutable();
933             CGRect p_bounds = CGRectMake((float)HORIZONTAL_MARGIN,
934                                           (float)VERTICAL_MARGIN,
935                                           (float)(i_width  - HORIZONTAL_MARGIN*2),
936                                           (float)(i_height - VERTICAL_MARGIN  *2));
937             CGPathAddRect(p_path, NULL, p_bounds);
938
939             // Create the frame and draw it into the graphics context
940             frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), p_path, NULL);
941
942             CGPathRelease(p_path);
943
944             // Set up black outlining of the text --
945             if (p_sys->b_outline)
946             {
947                 CGContextSetRGBStrokeColor(p_context, 0, 0, 0, 0.5);
948                 CGContextSetTextDrawingMode(p_context, kCGTextFillStroke);
949             }
950
951             // Shadow
952             if (p_sys->b_shadow)
953             {
954                 // TODO: Use CGContextSetShadowWithColor.
955                 // TODO: Use user defined parrameters (color, distance, etc.)
956                 CGContextSetShadow(p_context, CGSizeMake(3.0f, -3.0f), 2.0f);
957             }
958
959             if (frame != NULL) {
960                 CFArrayRef lines;
961                 CGPoint    penPosition;
962
963                 lines = CTFrameGetLines(frame);
964                 penPosition.y = i_height;
965                 for (int i=0; i<CFArrayGetCount(lines); i++) {
966                     CGFloat  ascent, descent, leading;
967
968                     CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, i);
969                     CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
970
971                     // Set the outlining for this line to be dependant on the size of the line -
972                     // make it about 5% of the ascent, with a minimum at 1.0
973                     float f_thickness = ascent * 0.05;
974                     CGContextSetLineWidth(p_context, ((f_thickness > 1.0) ? 1.0 : f_thickness));
975
976                     double penOffset = CTLineGetPenOffsetForFlush(line, horiz_flush, (i_width  - HORIZONTAL_MARGIN*2));
977                     penPosition.x = HORIZONTAL_MARGIN + penOffset;
978                     if (horiz_flush == 0.0)
979                         penPosition.x = p_region->i_x;
980                     penPosition.y -= ascent;
981                     CGContextSetTextPosition(p_context, penPosition.x, penPosition.y);
982                     CTLineDraw(line, p_context);
983                     penPosition.y -= descent + leading;
984
985                 }
986                 *pi_textblock_height = i_height - penPosition.y;
987
988                 CFRelease(frame);
989             }
990             CFRelease(framesetter);
991         }
992         CGContextFlush(p_context);
993         CGContextRelease(p_context);
994     }
995     if (p_colorSpace) CGColorSpaceRelease(p_colorSpace);
996
997     return p_offScreen;
998 }
999
1000 static int GetFontSize(filter_t *p_filter)
1001 {
1002     int i_size = 0;
1003
1004     int i_ratio = var_CreateGetInteger( p_filter, "quartztext-rel-fontsize" );
1005     if( i_ratio > 0 )
1006         i_size = (int)p_filter->fmt_out.video.i_height / i_ratio;
1007
1008     if( i_size <= 0 )
1009     {
1010         msg_Warn( p_filter, "invalid fontsize, using 12" );
1011         i_size = 12;
1012     }
1013     return i_size;
1014 }
1015
1016 static int RenderYUVA(filter_t *p_filter, subpicture_region_t *p_region,
1017                        CFMutableAttributedStringRef p_attrString)
1018 {
1019     offscreen_bitmap_t *p_offScreen = NULL;
1020     unsigned      i_textblock_height = 0;
1021
1022     unsigned i_width = p_filter->fmt_out.video.i_visible_width;
1023     unsigned i_height = p_filter->fmt_out.video.i_visible_height;
1024
1025     if (!p_attrString) {
1026         msg_Err(p_filter, "Invalid argument to RenderYUVA");
1027         return VLC_EGENERIC;
1028     }
1029
1030     p_offScreen = Compose(p_filter, p_region, p_attrString,
1031                            i_width, i_height, &i_textblock_height);
1032
1033     if (!p_offScreen) {
1034         msg_Err(p_filter, "No offscreen buffer");
1035         return VLC_EGENERIC;
1036     }
1037
1038     uint8_t *p_dst_y,*p_dst_u,*p_dst_v,*p_dst_a;
1039     video_format_t fmt;
1040     int i_offset;
1041     unsigned i_pitch;
1042     uint8_t i_y, i_u, i_v; // YUV values, derived from incoming RGB
1043
1044     // Create a new subpicture region
1045     memset(&fmt, 0, sizeof(video_format_t));
1046     fmt.i_chroma = VLC_CODEC_YUVA;
1047     fmt.i_width = fmt.i_visible_width = i_width;
1048     fmt.i_height = fmt.i_visible_height = __MIN(i_height, i_textblock_height + VERTICAL_MARGIN * 2);
1049     fmt.i_x_offset = fmt.i_y_offset = 0;
1050     fmt.i_sar_num = 1;
1051     fmt.i_sar_den = 1;
1052
1053     p_region->p_picture = picture_NewFromFormat(&fmt);
1054     if (!p_region->p_picture) {
1055         free(p_offScreen->p_data);
1056         free(p_offScreen);
1057         return VLC_EGENERIC;
1058     }
1059     p_region->fmt = fmt;
1060
1061     p_dst_y = p_region->p_picture->Y_PIXELS;
1062     p_dst_u = p_region->p_picture->U_PIXELS;
1063     p_dst_v = p_region->p_picture->V_PIXELS;
1064     p_dst_a = p_region->p_picture->A_PIXELS;
1065     i_pitch = p_region->p_picture->A_PITCH;
1066
1067     i_offset = (i_height + VERTICAL_MARGIN < fmt.i_height) ? VERTICAL_MARGIN *i_pitch : 0 ;
1068     for (unsigned y = 0; y < fmt.i_height; y++) {
1069         for (unsigned x = 0; x < fmt.i_width; x++) {
1070             int i_alpha = p_offScreen->p_data[ y * p_offScreen->i_bytesPerRow + x * p_offScreen->i_bytesPerPixel     ];
1071             int i_red   = p_offScreen->p_data[ y * p_offScreen->i_bytesPerRow + x * p_offScreen->i_bytesPerPixel + 1 ];
1072             int i_green = p_offScreen->p_data[ y * p_offScreen->i_bytesPerRow + x * p_offScreen->i_bytesPerPixel + 2 ];
1073             int i_blue  = p_offScreen->p_data[ y * p_offScreen->i_bytesPerRow + x * p_offScreen->i_bytesPerPixel + 3 ];
1074
1075             i_y = (uint8_t)__MIN(abs(2104 * i_red  + 4130 * i_green +
1076                               802 * i_blue + 4096 + 131072) >> 13, 235);
1077             i_u = (uint8_t)__MIN(abs(-1214 * i_red  + -2384 * i_green +
1078                              3598 * i_blue + 4096 + 1048576) >> 13, 240);
1079             i_v = (uint8_t)__MIN(abs(3598 * i_red + -3013 * i_green +
1080                               -585 * i_blue + 4096 + 1048576) >> 13, 240);
1081
1082             p_dst_y[ i_offset + x ] = i_y;
1083             p_dst_u[ i_offset + x ] = i_u;
1084             p_dst_v[ i_offset + x ] = i_v;
1085             p_dst_a[ i_offset + x ] = i_alpha;
1086         }
1087         i_offset += i_pitch;
1088     }
1089
1090     free(p_offScreen->p_data);
1091     free(p_offScreen);
1092
1093     return VLC_SUCCESS;
1094 }