]> git.sesse.net Git - vlc/blob - modules/misc/quartztext.c
e307ce779d3b4126eee70cd4959a7e6b1d44c67c
[vlc] / modules / misc / quartztext.c
1 /*****************************************************************************
2  * quartztext.c : Put text on the video, using Mac OS X Quartz Engine
3  *****************************************************************************
4  * Copyright (C) 2007 the VideoLAN team
5  * $Id$
6  *
7  * Authors: Bernie Purcell <bitmap@videolan.org>
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
22  *****************************************************************************/
23
24 //////////////////////////////////////////////////////////////////////////////
25 // Preamble
26 //////////////////////////////////////////////////////////////////////////////
27
28 #ifdef HAVE_CONFIG_H
29 # include "config.h"
30 #endif
31
32 #include <vlc_common.h>
33 #include <vlc_plugin.h>
34 #include <vlc_vout.h>
35 #include <vlc_osd.h>
36 #include <vlc_block.h>
37 #include <vlc_filter.h>
38 #include <vlc_stream.h>
39 #include <vlc_xml.h>
40 #include <vlc_input.h>
41
42 #include <math.h>
43
44 #include <Carbon/Carbon.h>
45
46 #define DEFAULT_FONT           "Verdana"
47 #define DEFAULT_FONT_COLOR     0xffffff
48 #define DEFAULT_REL_FONT_SIZE  16
49
50 #define VERTICAL_MARGIN 3
51 #define HORIZONTAL_MARGIN 10
52
53 //////////////////////////////////////////////////////////////////////////////
54 // Local prototypes
55 //////////////////////////////////////////////////////////////////////////////
56 static int  Create ( vlc_object_t * );
57 static void Destroy( vlc_object_t * );
58
59 static int LoadFontsFromAttachments( filter_t *p_filter );
60
61 static int RenderText( filter_t *, subpicture_region_t *,
62                        subpicture_region_t * );
63 static int RenderHtml( filter_t *, subpicture_region_t *,
64                        subpicture_region_t * );
65
66 static int GetFontSize( filter_t *p_filter );
67 static int RenderYUVA( filter_t *p_filter, subpicture_region_t *p_region,
68                        UniChar *psz_utfString, uint32_t i_text_len,
69                        uint32_t i_runs, uint32_t *pi_run_lengths,
70                        ATSUStyle *pp_styles );
71 static ATSUStyle CreateStyle( char *psz_fontname, int i_font_size,
72                               uint32_t i_font_color,
73                               bool b_bold, bool b_italic,
74                               bool b_uline );
75 //////////////////////////////////////////////////////////////////////////////
76 // Module descriptor
77 //////////////////////////////////////////////////////////////////////////////
78
79 // The preferred way to set font style information is for it to come from the
80 // subtitle file, and for it to be rendered with RenderHtml instead of
81 // RenderText. This module, unlike Freetype, doesn't provide any options to
82 // override the fallback font selection used when this style information is
83 // absent.
84 #define FONT_TEXT N_("Font")
85 #define FONT_LONGTEXT N_("Name for the font you want to use")
86 #define FONTSIZER_TEXT N_("Relative font size")
87 #define FONTSIZER_LONGTEXT N_("This is the relative default size of the " \
88     "fonts that will be rendered on the video. If absolute font size is set, "\
89     "relative size will be overriden." )
90 #define COLOR_TEXT N_("Text default color")
91 #define COLOR_LONGTEXT N_("The color of the text that will be rendered on "\
92     "the video. This must be an hexadecimal (like HTML colors). The first two "\
93     "chars are for red, then green, then blue. #000000 = black, #FF0000 = red,"\
94     " #00FF00 = green, #FFFF00 = yellow (red + green), #FFFFFF = white" )
95
96 static const int pi_color_values[] = {
97   0x00000000, 0x00808080, 0x00C0C0C0, 0x00FFFFFF, 0x00800000,
98   0x00FF0000, 0x00FF00FF, 0x00FFFF00, 0x00808000, 0x00008000, 0x00008080,
99   0x0000FF00, 0x00800080, 0x00000080, 0x000000FF, 0x0000FFFF };
100
101 static const char *const ppsz_color_descriptions[] = {
102   N_("Black"), N_("Gray"), N_("Silver"), N_("White"), N_("Maroon"),
103   N_("Red"), N_("Fuchsia"), N_("Yellow"), N_("Olive"), N_("Green"), N_("Teal"),
104   N_("Lime"), N_("Purple"), N_("Navy"), N_("Blue"), N_("Aqua") };
105
106 static const int pi_sizes[] = { 20, 18, 16, 12, 6 };
107 static const char *const ppsz_sizes_text[] = {
108     N_("Smaller"), N_("Small"), N_("Normal"), N_("Large"), N_("Larger") };
109
110 vlc_module_begin();
111     set_shortname( N_("Mac Text renderer"));
112     set_description( N_("Quartz font renderer") );
113     set_category( CAT_VIDEO );
114     set_subcategory( SUBCAT_VIDEO_SUBPIC );
115
116     add_string( "quartztext-font", DEFAULT_FONT, NULL, FONT_TEXT, FONT_LONGTEXT,
117               false );
118     add_integer( "quartztext-rel-fontsize", DEFAULT_REL_FONT_SIZE, NULL, FONTSIZER_TEXT,
119                  FONTSIZER_LONGTEXT, false );
120         change_integer_list( pi_sizes, ppsz_sizes_text, NULL );
121     add_integer( "quartztext-color", 0x00FFFFFF, NULL, COLOR_TEXT,
122                  COLOR_LONGTEXT, false );
123         change_integer_list( pi_color_values, ppsz_color_descriptions, NULL );
124     set_capability( "text renderer", 120 );
125     add_shortcut( "text" );
126     set_callbacks( Create, Destroy );
127 vlc_module_end();
128
129 typedef struct font_stack_t font_stack_t;
130 struct font_stack_t
131 {
132     char          *psz_name;
133     int            i_size;
134     uint32_t       i_color;            // ARGB
135
136     font_stack_t  *p_next;
137 };
138
139 typedef struct offscreen_bitmap_t offscreen_bitmap_t;
140 struct offscreen_bitmap_t
141 {
142     uint8_t       *p_data;
143     int            i_bitsPerChannel;
144     int            i_bitsPerPixel;
145     int            i_bytesPerPixel;
146     int            i_bytesPerRow;
147 };
148
149 //////////////////////////////////////////////////////////////////////////////
150 // filter_sys_t: quartztext local data
151 //////////////////////////////////////////////////////////////////////////////
152 // This structure is part of the video output thread descriptor.
153 // It describes the freetype specific properties of an output thread.
154 //////////////////////////////////////////////////////////////////////////////
155 struct filter_sys_t
156 {
157     char          *psz_font_name;
158     uint8_t        i_font_opacity;
159     int            i_font_color;
160     int            i_font_size;
161
162     ATSFontContainerRef    *p_fonts;
163     int                     i_fonts;
164 };
165
166 //////////////////////////////////////////////////////////////////////////////
167 // Create: allocates osd-text video thread output method
168 //////////////////////////////////////////////////////////////////////////////
169 // This function allocates and initializes a Clone vout method.
170 //////////////////////////////////////////////////////////////////////////////
171 static int Create( vlc_object_t *p_this )
172 {
173     filter_t *p_filter = (filter_t *)p_this;
174     filter_sys_t *p_sys;
175
176     // Allocate structure
177     p_filter->p_sys = p_sys = malloc( sizeof( filter_sys_t ) );
178     if( !p_sys )
179         return VLC_ENOMEM;
180     p_sys->psz_font_name  = var_CreateGetString( p_this, "quartztext-font" );
181     p_sys->i_font_opacity = 255;
182     p_sys->i_font_color = __MAX( __MIN( var_CreateGetInteger( p_this, "quartztext-color" ) , 0xFFFFFF ), 0 );
183     p_sys->i_font_size    = GetFontSize( p_filter );
184
185     p_filter->pf_render_text = RenderText;
186     p_filter->pf_render_html = RenderHtml;
187
188     p_sys->p_fonts = NULL;
189     p_sys->i_fonts = 0;
190
191     LoadFontsFromAttachments( p_filter );
192
193     return VLC_SUCCESS;
194 }
195
196 //////////////////////////////////////////////////////////////////////////////
197 // Destroy: destroy Clone video thread output method
198 //////////////////////////////////////////////////////////////////////////////
199 // Clean up all data and library connections
200 //////////////////////////////////////////////////////////////////////////////
201 static void Destroy( vlc_object_t *p_this )
202 {
203     filter_t *p_filter = (filter_t *)p_this;
204     filter_sys_t *p_sys = p_filter->p_sys;
205
206     if( p_sys->p_fonts )
207     {
208         int   k;
209
210         for( k = 0; k < p_sys->i_fonts; k++ )
211         {
212             ATSFontDeactivate( p_sys->p_fonts[k], NULL, kATSOptionFlagsDefault );
213         }
214
215         free( p_sys->p_fonts );
216     }
217
218     free( p_sys->psz_font_name );
219     free( p_sys );
220 }
221
222 //////////////////////////////////////////////////////////////////////////////
223 // Make any TTF/OTF fonts present in the attachments of the media file
224 // available to the Quartz engine for text rendering
225 //////////////////////////////////////////////////////////////////////////////
226 static int LoadFontsFromAttachments( filter_t *p_filter )
227 {
228     filter_sys_t         *p_sys = p_filter->p_sys;
229     input_thread_t       *p_input;
230     input_attachment_t  **pp_attachments;
231     int                   i_attachments_cnt;
232     int                   k;
233     int                   rv = VLC_SUCCESS;
234
235     p_input = (input_thread_t *)vlc_object_find( p_filter, VLC_OBJECT_INPUT, FIND_PARENT );
236     if( ! p_input )
237         return VLC_EGENERIC;
238
239     if( VLC_SUCCESS != input_Control( p_input, INPUT_GET_ATTACHMENTS, &pp_attachments, &i_attachments_cnt ))
240     {
241         vlc_object_release(p_input);
242         return VLC_EGENERIC;
243     }
244
245     p_sys->i_fonts = 0;
246     p_sys->p_fonts = malloc( i_attachments_cnt * sizeof( ATSFontContainerRef ) );
247     if(! p_sys->p_fonts )
248         rv = VLC_ENOMEM;
249
250     for( k = 0; k < i_attachments_cnt; k++ )
251     {
252         input_attachment_t *p_attach = pp_attachments[k];
253
254         if( p_sys->p_fonts )
255         {
256             if(( !strcmp( p_attach->psz_mime, "application/x-truetype-font" ) || // TTF
257                  !strcmp( p_attach->psz_mime, "application/x-font-otf" ) ) &&    // OTF
258                ( p_attach->i_data > 0 ) &&
259                ( p_attach->p_data != NULL ) )
260             {
261                 ATSFontContainerRef  container;
262
263                 if( noErr == ATSFontActivateFromMemory( p_attach->p_data,
264                                                         p_attach->i_data,
265                                                         kATSFontContextLocal,
266                                                         kATSFontFormatUnspecified,
267                                                         NULL,
268                                                         kATSOptionFlagsDefault,
269                                                         &container ))
270                 {
271                     p_sys->p_fonts[ p_sys->i_fonts++ ] = container;
272                 }
273             }
274         }
275         vlc_input_attachment_Delete( p_attach );
276     }
277     free( pp_attachments );
278
279     vlc_object_release(p_input);
280
281     return rv;
282 }
283
284 #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_4
285 // Original version of these functions available on:
286 // http://developer.apple.com/documentation/Carbon/Conceptual/QuickDrawToQuartz2D/tq_color/chapter_4_section_3.html
287
288 #define kGenericRGBProfilePathStr "/System/Library/ColorSync/Profiles/Generic RGB Profile.icc"
289
290 static CMProfileRef OpenGenericProfile( void )
291 {
292     static CMProfileRef cached_rgb_prof = NULL;
293
294     // Create the profile reference only once
295     if( cached_rgb_prof == NULL )
296     {
297         OSStatus            err;
298         CMProfileLocation   loc;
299
300         loc.locType = cmPathBasedProfile;
301         strcpy( loc.u.pathLoc.path, kGenericRGBProfilePathStr );
302
303         err = CMOpenProfile( &cached_rgb_prof, &loc );
304
305         if( err != noErr )
306         {
307             cached_rgb_prof = NULL;
308         }
309     }
310
311     if( cached_rgb_prof )
312     {
313         // Clone the profile reference so that the caller has
314         // their own reference, not our cached one.
315         CMCloneProfileRef( cached_rgb_prof );
316     }
317
318     return cached_rgb_prof;
319 }
320
321 static CGColorSpaceRef CreateGenericRGBColorSpace( void )
322 {
323     static CGColorSpaceRef p_generic_rgb_cs = NULL;
324
325     if( p_generic_rgb_cs == NULL )
326     {
327         CMProfileRef generic_rgb_prof = OpenGenericProfile();
328
329         if( generic_rgb_prof )
330         {
331             p_generic_rgb_cs = CGColorSpaceCreateWithPlatformColorSpace( generic_rgb_prof );
332
333             CMCloseProfile( generic_rgb_prof );
334         }
335     }
336
337     return p_generic_rgb_cs;
338 }
339 #endif
340
341 static char *EliminateCRLF( char *psz_string )
342 {
343     char *p;
344     char *q;
345
346     for( p = psz_string; p && *p; p++ )
347     {
348         if( ( *p == '\r' ) && ( *(p+1) == '\n' ) )
349         {
350             for( q = p + 1; *q; q++ )
351                 *( q - 1 ) = *q;
352
353             *( q - 1 ) = '\0';
354         }
355     }
356     return psz_string;
357 }
358
359 // Convert UTF-8 string to UTF-16 character array -- internal Mac Endian-ness ;
360 // we don't need to worry about bidirectional text conversion as ATSUI should
361 // handle that for us automatically
362 static void ConvertToUTF16( const char *psz_utf8_str, uint32_t *pi_strlen, UniChar **ppsz_utf16_str )
363 {
364     CFStringRef   p_cfString;
365     int           i_string_length;
366
367     p_cfString = CFStringCreateWithCString( NULL, psz_utf8_str, kCFStringEncodingUTF8 );
368     if( !p_cfString )
369         return;
370
371     i_string_length = CFStringGetLength( p_cfString );
372
373     if( pi_strlen )
374         *pi_strlen = i_string_length;
375
376     if( !*ppsz_utf16_str )
377         *ppsz_utf16_str = (UniChar *) calloc( i_string_length, sizeof( UniChar ) );
378
379     CFStringGetCharacters( p_cfString, CFRangeMake( 0, i_string_length ), *ppsz_utf16_str );
380
381     CFRelease( p_cfString );
382 }
383
384 // Renders a text subpicture region into another one.
385 // It is used as pf_add_string callback in the vout method by this module
386 static int RenderText( filter_t *p_filter, subpicture_region_t *p_region_out,
387                        subpicture_region_t *p_region_in )
388 {
389     filter_sys_t *p_sys = p_filter->p_sys;
390     UniChar      *psz_utf16_str = NULL;
391     uint32_t      i_string_length;
392     char         *psz_string;
393     int           i_font_color, i_font_alpha, i_font_size;
394     vlc_value_t val;
395     int i_scale = 1000;
396
397     p_sys->i_font_size    = GetFontSize( p_filter );
398
399     // Sanity check
400     if( !p_region_in || !p_region_out ) return VLC_EGENERIC;
401     psz_string = p_region_in->psz_text;
402     if( !psz_string || !*psz_string ) return VLC_EGENERIC;
403
404     if( VLC_SUCCESS == var_Get( p_filter, "scale", &val ))
405         i_scale = val.i_int;
406
407     if( p_region_in->p_style )
408     {
409         i_font_color = __MAX( __MIN( p_region_in->p_style->i_font_color, 0xFFFFFF ), 0 );
410         i_font_alpha = __MAX( __MIN( p_region_in->p_style->i_font_alpha, 255 ), 0 );
411         i_font_size  = __MAX( __MIN( p_region_in->p_style->i_font_size, 255 ), 0 ) * i_scale / 1000;
412     }
413     else
414     {
415         i_font_color = p_sys->i_font_color;
416         i_font_alpha = 255 - p_sys->i_font_opacity;
417         i_font_size  = p_sys->i_font_size;
418     }
419
420     if( !i_font_alpha ) i_font_alpha = 255 - p_sys->i_font_opacity;
421
422     ConvertToUTF16( EliminateCRLF( psz_string ), &i_string_length, &psz_utf16_str );
423
424     p_region_out->i_x = p_region_in->i_x;
425     p_region_out->i_y = p_region_in->i_y;
426
427     if( psz_utf16_str != NULL )
428     {
429         ATSUStyle p_style = CreateStyle( p_sys->psz_font_name, i_font_size,
430                                          (i_font_color & 0xffffff) |
431                                          ((i_font_alpha & 0xff) << 24),
432                                          false, false, false );
433         if( p_style )
434         {
435             RenderYUVA( p_filter, p_region_out, psz_utf16_str, i_string_length,
436                         1, &i_string_length, &p_style );
437         }
438
439         ATSUDisposeStyle( p_style );
440         free( psz_utf16_str );
441     }
442
443     return VLC_SUCCESS;
444 }
445
446
447 static ATSUStyle CreateStyle( char *psz_fontname, int i_font_size, uint32_t i_font_color,
448                               bool b_bold, bool b_italic, bool b_uline )
449 {
450     ATSUStyle   p_style;
451     OSStatus    status;
452     uint32_t    i_tag_cnt;
453
454     float f_red   = (float)(( i_font_color & 0x00FF0000 ) >> 16) / 255.0;
455     float f_green = (float)(( i_font_color & 0x0000FF00 ) >>  8) / 255.0;
456     float f_blue  = (float)(  i_font_color & 0x000000FF        ) / 255.0;
457     float f_alpha = ( 255.0 - (float)(( i_font_color & 0xFF000000 ) >> 24)) / 255.0;
458
459     ATSUFontID           font;
460     Fixed                font_size  = IntToFixed( i_font_size );
461     ATSURGBAlphaColor    font_color = { f_red, f_green, f_blue, f_alpha };
462     Boolean              bold       = b_bold;
463     Boolean              italic     = b_italic;
464     Boolean              uline      = b_uline;
465
466     ATSUAttributeTag tags[]        = { kATSUSizeTag, kATSURGBAlphaColorTag, kATSUQDItalicTag,
467                                        kATSUQDBoldfaceTag, kATSUQDUnderlineTag, kATSUFontTag };
468     ByteCount sizes[]              = { sizeof( Fixed ), sizeof( ATSURGBAlphaColor ), sizeof( Boolean ),
469                                        sizeof( Boolean ), sizeof( Boolean ), sizeof( ATSUFontID )};
470     ATSUAttributeValuePtr values[] = { &font_size, &font_color, &italic, &bold, &uline, &font };
471
472     i_tag_cnt = sizeof( tags ) / sizeof( ATSUAttributeTag );
473
474     status = ATSUFindFontFromName( psz_fontname,
475                                    strlen( psz_fontname ),
476                                    kFontFullName,
477                                    kFontNoPlatform,
478                                    kFontNoScript,
479                                    kFontNoLanguageCode,
480                                    &font );
481
482     if( status != noErr )
483     {
484         // If we can't find a suitable font, just do everything else
485         i_tag_cnt--;
486     }
487
488     if( noErr == ATSUCreateStyle( &p_style ) )
489     {
490         if( noErr == ATSUSetAttributes( p_style, i_tag_cnt, tags, sizes, values ) )
491         {
492             return p_style;
493         }
494         ATSUDisposeStyle( p_style );
495     }
496     return NULL;
497 }
498
499 static int PushFont( font_stack_t **p_font, const char *psz_name, int i_size,
500                      uint32_t i_color )
501 {
502     font_stack_t *p_new;
503
504     if( !p_font )
505         return VLC_EGENERIC;
506
507     p_new = malloc( sizeof( font_stack_t ) );
508     if( ! p_new )
509         return VLC_ENOMEM;
510
511     p_new->p_next = NULL;
512
513     if( psz_name )
514         p_new->psz_name = strdup( psz_name );
515     else
516         p_new->psz_name = NULL;
517
518     p_new->i_size   = i_size;
519     p_new->i_color  = i_color;
520
521     if( !*p_font )
522     {
523         *p_font = p_new;
524     }
525     else
526     {
527         font_stack_t *p_last;
528
529         for( p_last = *p_font;
530              p_last->p_next;
531              p_last = p_last->p_next )
532         ;
533
534         p_last->p_next = p_new;
535     }
536     return VLC_SUCCESS;
537 }
538
539 static int PopFont( font_stack_t **p_font )
540 {
541     font_stack_t *p_last, *p_next_to_last;
542
543     if( !p_font || !*p_font )
544         return VLC_EGENERIC;
545
546     p_next_to_last = NULL;
547     for( p_last = *p_font;
548          p_last->p_next;
549          p_last = p_last->p_next )
550     {
551         p_next_to_last = p_last;
552     }
553
554     if( p_next_to_last )
555         p_next_to_last->p_next = NULL;
556     else
557         *p_font = NULL;
558
559     free( p_last->psz_name );
560     free( p_last );
561
562     return VLC_SUCCESS;
563 }
564
565 static int PeekFont( font_stack_t **p_font, char **psz_name, int *i_size,
566                      uint32_t *i_color )
567 {
568     font_stack_t *p_last;
569
570     if( !p_font || !*p_font )
571         return VLC_EGENERIC;
572
573     for( p_last=*p_font;
574          p_last->p_next;
575          p_last=p_last->p_next )
576     ;
577
578     *psz_name = p_last->psz_name;
579     *i_size   = p_last->i_size;
580     *i_color  = p_last->i_color;
581
582     return VLC_SUCCESS;
583 }
584
585 static ATSUStyle GetStyleFromFontStack( filter_sys_t *p_sys,
586         font_stack_t **p_fonts, bool b_bold, bool b_italic,
587         bool b_uline )
588 {
589     ATSUStyle   p_style = NULL;
590
591     char     *psz_fontname = NULL;
592     uint32_t  i_font_color = p_sys->i_font_color;
593     int       i_font_size  = p_sys->i_font_size;
594
595     if( VLC_SUCCESS == PeekFont( p_fonts, &psz_fontname, &i_font_size,
596                                  &i_font_color ))
597     {
598         p_style = CreateStyle( psz_fontname, i_font_size, i_font_color,
599                                b_bold, b_italic, b_uline );
600     }
601     return p_style;
602 }
603
604 static int HandleFontAttributes( xml_reader_t *p_xml_reader,
605                                   font_stack_t **p_fonts, int i_scale )
606 {
607     int        rv;
608     char      *psz_fontname = NULL;
609     uint32_t   i_font_color = 0xffffff;
610     int        i_font_alpha = 0;
611     int        i_font_size  = 24;
612
613     // Default all attributes to the top font in the stack -- in case not
614     // all attributes are specified in the sub-font
615     if( VLC_SUCCESS == PeekFont( p_fonts,
616                                  &psz_fontname,
617                                  &i_font_size,
618                                  &i_font_color ))
619     {
620         psz_fontname = strdup( psz_fontname );
621         i_font_size = i_font_size * 1000 / i_scale;
622     }
623     i_font_alpha = (i_font_color >> 24) & 0xff;
624     i_font_color &= 0x00ffffff;
625
626     while ( xml_ReaderNextAttr( p_xml_reader ) == VLC_SUCCESS )
627     {
628         char *psz_name = xml_ReaderName( p_xml_reader );
629         char *psz_value = xml_ReaderValue( p_xml_reader );
630
631         if( psz_name && psz_value )
632         {
633             if( !strcasecmp( "face", psz_name ) )
634             {
635                 free( psz_fontname );
636                 psz_fontname = strdup( psz_value );
637             }
638             else if( !strcasecmp( "size", psz_name ) )
639             {
640                 if( ( *psz_value == '+' ) || ( *psz_value == '-' ) )
641                 {
642                     int i_value = atoi( psz_value );
643
644                     if( ( i_value >= -5 ) && ( i_value <= 5 ) )
645                         i_font_size += ( i_value * i_font_size ) / 10;
646                     else if( i_value < -5 )
647                         i_font_size = - i_value;
648                     else if( i_value > 5 )
649                         i_font_size = i_value;
650                 }
651                 else
652                     i_font_size = atoi( psz_value );
653             }
654             else if( !strcasecmp( "color", psz_name )  &&
655                      ( psz_value[0] == '#' ) )
656             {
657                 i_font_color = strtol( psz_value + 1, NULL, 16 );
658                 i_font_color &= 0x00ffffff;
659             }
660             else if( !strcasecmp( "alpha", psz_name ) &&
661                      ( psz_value[0] == '#' ) )
662             {
663                 i_font_alpha = strtol( psz_value + 1, NULL, 16 );
664                 i_font_alpha &= 0xff;
665             }
666         }
667         free( psz_name );
668         free( psz_value );
669     }
670     rv = PushFont( p_fonts,
671                    psz_fontname,
672                    i_font_size * i_scale / 1000,
673                    (i_font_color & 0xffffff) | ((i_font_alpha & 0xff) << 24) );
674
675     free( psz_fontname );
676
677     return rv;
678 }
679
680 static int ProcessNodes( filter_t *p_filter,
681                          xml_reader_t *p_xml_reader,
682                          text_style_t *p_font_style,
683                          UniChar *psz_text,
684                          int *pi_len,
685
686                          uint32_t *pi_runs,
687                          uint32_t **ppi_run_lengths,
688                          ATSUStyle **ppp_styles )
689 {
690     int           rv             = VLC_SUCCESS;
691     filter_sys_t *p_sys          = p_filter->p_sys;
692     UniChar      *psz_text_orig  = psz_text;
693     font_stack_t *p_fonts        = NULL;
694     vlc_value_t   val;
695     int           i_scale        = 1000;
696
697     char *psz_node  = NULL;
698
699     bool b_italic = false;
700     bool b_bold   = false;
701     bool b_uline  = false;
702
703     if( VLC_SUCCESS == var_Get( p_filter, "scale", &val ))
704         i_scale = val.i_int;
705
706     if( p_font_style )
707     {
708         rv = PushFont( &p_fonts,
709                p_font_style->psz_fontname,
710                p_font_style->i_font_size * i_scale / 1000,
711                (p_font_style->i_font_color & 0xffffff) |
712                    ((p_font_style->i_font_alpha & 0xff) << 24) );
713
714         if( p_font_style->i_style_flags & STYLE_BOLD )
715             b_bold = true;
716         if( p_font_style->i_style_flags & STYLE_ITALIC )
717             b_italic = true;
718         if( p_font_style->i_style_flags & STYLE_UNDERLINE )
719             b_uline = true;
720     }
721     else
722     {
723         rv = PushFont( &p_fonts,
724                        p_sys->psz_font_name,
725                        p_sys->i_font_size,
726                        p_sys->i_font_color );
727     }
728     if( rv != VLC_SUCCESS )
729         return rv;
730
731     while ( ( xml_ReaderRead( p_xml_reader ) == 1 ) )
732     {
733         switch ( xml_ReaderNodeType( p_xml_reader ) )
734         {
735             case XML_READER_NONE:
736                 break;
737             case XML_READER_ENDELEM:
738                 psz_node = xml_ReaderName( p_xml_reader );
739                 if( psz_node )
740                 {
741                     if( !strcasecmp( "font", psz_node ) )
742                         PopFont( &p_fonts );
743                     else if( !strcasecmp( "b", psz_node ) )
744                         b_bold   = false;
745                     else if( !strcasecmp( "i", psz_node ) )
746                         b_italic = false;
747                     else if( !strcasecmp( "u", psz_node ) )
748                         b_uline  = false;
749
750                     free( psz_node );
751                 }
752                 break;
753             case XML_READER_STARTELEM:
754                 psz_node = xml_ReaderName( p_xml_reader );
755                 if( psz_node )
756                 {
757                     if( !strcasecmp( "font", psz_node ) )
758                         rv = HandleFontAttributes( p_xml_reader, &p_fonts, i_scale );
759                     else if( !strcasecmp( "b", psz_node ) )
760                         b_bold = true;
761                     else if( !strcasecmp( "i", psz_node ) )
762                         b_italic = true;
763                     else if( !strcasecmp( "u", psz_node ) )
764                         b_uline = true;
765                     else if( !strcasecmp( "br", psz_node ) )
766                     {
767                         uint32_t i_string_length;
768
769                         ConvertToUTF16( "\n", &i_string_length, &psz_text );
770                         psz_text += i_string_length;
771
772                         (*pi_runs)++;
773
774                         if( *ppp_styles )
775                             *ppp_styles = (ATSUStyle *) realloc( *ppp_styles, *pi_runs * sizeof( ATSUStyle ) );
776                         else
777                             *ppp_styles = (ATSUStyle *) malloc( *pi_runs * sizeof( ATSUStyle ) );
778
779                         (*ppp_styles)[ *pi_runs - 1 ] = GetStyleFromFontStack( p_sys, &p_fonts, b_bold, b_italic, b_uline );
780
781                         if( *ppi_run_lengths )
782                             *ppi_run_lengths = (uint32_t *) realloc( *ppi_run_lengths, *pi_runs * sizeof( uint32_t ) );
783                         else
784                             *ppi_run_lengths = (uint32_t *) malloc( *pi_runs * sizeof( uint32_t ) );
785
786                         (*ppi_run_lengths)[ *pi_runs - 1 ] = i_string_length;
787                     }
788                     free( psz_node );
789                 }
790                 break;
791             case XML_READER_TEXT:
792                 psz_node = xml_ReaderValue( p_xml_reader );
793                 if( psz_node )
794                 {
795                     uint32_t i_string_length;
796
797                     // Turn any multiple-whitespaces into single spaces
798                     char *s = strpbrk( psz_node, "\t\r\n " );
799                     while( s )
800                     {
801                         int i_whitespace = strspn( s, "\t\r\n " );
802
803                         if( i_whitespace > 1 )
804                             memmove( &s[1],
805                                      &s[i_whitespace],
806                                      strlen( s ) - i_whitespace + 1 );
807                         *s++ = ' ';
808
809                         s = strpbrk( s, "\t\r\n " );
810                     }
811
812                     ConvertToUTF16( psz_node, &i_string_length, &psz_text );
813                     psz_text += i_string_length;
814
815                     (*pi_runs)++;
816
817                     if( *ppp_styles )
818                         *ppp_styles = (ATSUStyle *) realloc( *ppp_styles, *pi_runs * sizeof( ATSUStyle ) );
819                     else
820                         *ppp_styles = (ATSUStyle *) malloc( *pi_runs * sizeof( ATSUStyle ) );
821
822                     (*ppp_styles)[ *pi_runs - 1 ] = GetStyleFromFontStack( p_sys, &p_fonts, b_bold, b_italic, b_uline );
823
824                     if( *ppi_run_lengths )
825                         *ppi_run_lengths = (uint32_t *) realloc( *ppi_run_lengths, *pi_runs * sizeof( uint32_t ) );
826                     else
827                         *ppi_run_lengths = (uint32_t *) malloc( *pi_runs * sizeof( uint32_t ) );
828
829                     (*ppi_run_lengths)[ *pi_runs - 1 ] = i_string_length;
830
831                     free( psz_node );
832                 }
833                 break;
834         }
835     }
836
837     *pi_len = psz_text - psz_text_orig;
838
839     while( VLC_SUCCESS == PopFont( &p_fonts ) );
840
841     return rv;
842 }
843
844 static int RenderHtml( filter_t *p_filter, subpicture_region_t *p_region_out,
845                        subpicture_region_t *p_region_in )
846 {
847     int          rv = VLC_SUCCESS;
848     stream_t     *p_sub = NULL;
849     xml_t        *p_xml = NULL;
850     xml_reader_t *p_xml_reader = NULL;
851
852     if( !p_region_in || !p_region_in->psz_html )
853         return VLC_EGENERIC;
854
855     /* Reset the default fontsize in case screen metrics have changed */
856     p_filter->p_sys->i_font_size = GetFontSize( p_filter );
857
858     p_sub = stream_MemoryNew( VLC_OBJECT(p_filter),
859                               (uint8_t *) p_region_in->psz_html,
860                               strlen( p_region_in->psz_html ),
861                               true );
862     if( p_sub )
863     {
864         p_xml = xml_Create( p_filter );
865         if( p_xml )
866         {
867             bool b_karaoke = false;
868
869             p_xml_reader = xml_ReaderCreate( p_xml, p_sub );
870             if( p_xml_reader )
871             {
872                 /* Look for Root Node */
873                 if( xml_ReaderRead( p_xml_reader ) == 1 )
874                 {
875                     char *psz_node = xml_ReaderName( p_xml_reader );
876
877                     if( !strcasecmp( "karaoke", psz_node ) )
878                     {
879                         /* We're going to have to render the text a number
880                          * of times to show the progress marker on the text.
881                          */
882                         var_SetBool( p_filter, "text-rerender", true );
883                         b_karaoke = true;
884                     }
885                     else if( !strcasecmp( "text", psz_node ) )
886                     {
887                         b_karaoke = false;
888                     }
889                     else
890                     {
891                         /* Only text and karaoke tags are supported */
892                         xml_ReaderDelete( p_xml, p_xml_reader );
893                         p_xml_reader = NULL;
894                         rv = VLC_EGENERIC;
895                     }
896
897                     free( psz_node );
898                 }
899             }
900
901             if( p_xml_reader )
902             {
903                 UniChar    *psz_text;
904                 int         i_len = 0;
905                 uint32_t    i_runs = 0;
906                 uint32_t   *pi_run_lengths = NULL;
907                 ATSUStyle  *pp_styles = NULL;
908
909                 psz_text = (UniChar *) malloc( strlen( p_region_in->psz_html ) *
910                                                 sizeof( UniChar ) );
911                 if( psz_text )
912                 {
913                     uint32_t k;
914
915                     rv = ProcessNodes( p_filter, p_xml_reader,
916                                   p_region_in->p_style, psz_text, &i_len,
917                                   &i_runs, &pi_run_lengths, &pp_styles );
918
919                     p_region_out->i_x = p_region_in->i_x;
920                     p_region_out->i_y = p_region_in->i_y;
921
922                     if(( rv == VLC_SUCCESS ) && ( i_len > 0 ))
923                     {
924                         RenderYUVA( p_filter, p_region_out, psz_text, i_len, i_runs,
925                              pi_run_lengths, pp_styles);
926                     }
927
928                     for( k=0; k<i_runs; k++)
929                         ATSUDisposeStyle( pp_styles[k] );
930                     free( pp_styles );
931                     free( pi_run_lengths );
932                     free( psz_text );
933                 }
934
935                 xml_ReaderDelete( p_xml, p_xml_reader );
936             }
937             xml_Delete( p_xml );
938         }
939         stream_Delete( p_sub );
940     }
941
942     return rv;
943 }
944
945 static CGContextRef CreateOffScreenContext( int i_width, int i_height,
946                          offscreen_bitmap_t **pp_memory, CGColorSpaceRef *pp_colorSpace )
947 {
948     offscreen_bitmap_t *p_bitmap;
949     CGContextRef        p_context = NULL;
950
951     p_bitmap = (offscreen_bitmap_t *) malloc( sizeof( offscreen_bitmap_t ));
952     if( p_bitmap )
953     {
954         p_bitmap->i_bitsPerChannel = 8;
955         p_bitmap->i_bitsPerPixel   = 4 * p_bitmap->i_bitsPerChannel; // A,R,G,B
956         p_bitmap->i_bytesPerPixel  = p_bitmap->i_bitsPerPixel / 8;
957         p_bitmap->i_bytesPerRow    = i_width * p_bitmap->i_bytesPerPixel;
958
959         p_bitmap->p_data = calloc( i_height, p_bitmap->i_bytesPerRow );
960
961 #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_4
962         *pp_colorSpace = CGColorSpaceCreateWithName( kCGColorSpaceGenericRGB );
963 #else
964         *pp_colorSpace = CreateGenericRGBColorSpace();
965 #endif
966
967         if( p_bitmap->p_data && *pp_colorSpace )
968         {
969             p_context = CGBitmapContextCreate( p_bitmap->p_data, i_width, i_height,
970                                 p_bitmap->i_bitsPerChannel, p_bitmap->i_bytesPerRow,
971                                 *pp_colorSpace, kCGImageAlphaPremultipliedFirst);
972         }
973         if( p_context )
974         {
975 #if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_1
976             // OS X 10.1 doesn't support weak linking of this call which is only available
977             // int 10.4 and later
978             if( CGContextSetAllowsAntialiasing != NULL )
979             {
980                 CGContextSetAllowsAntialiasing( p_context, true );
981             }
982 #endif
983         }
984         *pp_memory = p_bitmap;
985     }
986
987     return p_context;
988 }
989
990 static offscreen_bitmap_t *Compose( int i_text_align, UniChar *psz_utf16_str, uint32_t i_text_len,
991                                     uint32_t i_runs, uint32_t *pi_run_lengths, ATSUStyle *pp_styles,
992                                     int i_width, int i_height, int *pi_textblock_height )
993 {
994     offscreen_bitmap_t  *p_offScreen  = NULL;
995     CGColorSpaceRef      p_colorSpace = NULL;
996     CGContextRef         p_context = NULL;
997
998     p_context = CreateOffScreenContext( i_width, i_height, &p_offScreen, &p_colorSpace );
999
1000     if( p_context )
1001     {
1002         ATSUTextLayout p_textLayout;
1003         OSStatus status = noErr;
1004
1005         status = ATSUCreateTextLayoutWithTextPtr( psz_utf16_str, 0, i_text_len, i_text_len,
1006                                                   i_runs,
1007                                                   (const UniCharCount *) pi_run_lengths,
1008                                                   pp_styles,
1009                                                   &p_textLayout );
1010         if( status == noErr )
1011         {
1012             // Attach our offscreen Image Graphics Context to the text style
1013             // and setup the line alignment (have to specify the line width
1014             // also in order for our chosen alignment to work)
1015
1016             Fract   alignment  = kATSUStartAlignment;
1017             Fixed   line_width = Long2Fix( i_width - HORIZONTAL_MARGIN * 2 );
1018
1019             ATSUAttributeTag tags[]        = { kATSUCGContextTag, kATSULineFlushFactorTag, kATSULineWidthTag };
1020             ByteCount sizes[]              = { sizeof( CGContextRef ), sizeof( Fract ), sizeof( Fixed ) };
1021             ATSUAttributeValuePtr values[] = { &p_context, &alignment, &line_width };
1022
1023             int i_tag_cnt = sizeof( tags ) / sizeof( ATSUAttributeTag );
1024
1025             if( i_text_align == SUBPICTURE_ALIGN_RIGHT )
1026             {
1027                 alignment = kATSUEndAlignment;
1028             }
1029             else if( i_text_align != SUBPICTURE_ALIGN_LEFT )
1030             {
1031                 alignment = kATSUCenterAlignment;
1032             }
1033
1034             ATSUSetLayoutControls( p_textLayout, i_tag_cnt, tags, sizes, values );
1035
1036             // let ATSUI deal with characters not-in-our-specified-font
1037             ATSUSetTransientFontMatching( p_textLayout, true );
1038
1039             Fixed x = Long2Fix( HORIZONTAL_MARGIN );
1040             Fixed y = Long2Fix( i_height );
1041
1042             // Set the line-breaks and draw individual lines
1043             uint32_t i_start = 0;
1044             uint32_t i_end = i_text_len;
1045
1046             // Set up black outlining of the text --
1047             CGContextSetRGBStrokeColor( p_context, 0, 0, 0, 0.5 );
1048             CGContextSetTextDrawingMode( p_context, kCGTextFillStroke );
1049
1050             do
1051             {
1052                 // ATSUBreakLine will automatically pick up any manual '\n's also
1053                 status = ATSUBreakLine( p_textLayout, i_start, line_width, true, (UniCharArrayOffset *) &i_end );
1054                 if( ( status == noErr ) || ( status == kATSULineBreakInWord ) )
1055                 {
1056                     Fixed     ascent;
1057                     Fixed     descent;
1058                     uint32_t  i_actualSize;
1059
1060                     // Come down far enough to fit the height of this line --
1061                     ATSUGetLineControl( p_textLayout, i_start, kATSULineAscentTag,
1062                                     sizeof( Fixed ), &ascent, (ByteCount *) &i_actualSize );
1063
1064                     // Quartz uses an upside-down co-ordinate space -> y values decrease as
1065                     // you move down the page
1066                     y -= ascent;
1067
1068                     // Set the outlining for this line to be dependent on the size of the line -
1069                     // make it about 5% of the ascent, with a minimum at 1.0
1070                     float f_thickness = FixedToFloat( ascent ) * 0.05;
1071                     CGContextSetLineWidth( p_context, (( f_thickness > 1.0 ) ? 1.0 : f_thickness ));
1072
1073                     ATSUDrawText( p_textLayout, i_start, i_end - i_start, x, y );
1074
1075                     // and now prepare for the next line by coming down far enough for our
1076                     // descent
1077                     ATSUGetLineControl( p_textLayout, i_start, kATSULineDescentTag,
1078                                     sizeof( Fixed ), &descent, (ByteCount *) &i_actualSize );
1079                     y -= descent;
1080
1081                     i_start = i_end;
1082                 }
1083                 else
1084                     break;
1085             }
1086             while( i_end < i_text_len );
1087
1088             *pi_textblock_height = i_height - Fix2Long( y );
1089             CGContextFlush( p_context );
1090
1091             ATSUDisposeTextLayout( p_textLayout );
1092         }
1093
1094         CGContextRelease( p_context );
1095     }
1096     if( p_colorSpace ) CGColorSpaceRelease( p_colorSpace );
1097
1098     return p_offScreen;
1099 }
1100
1101 static int GetFontSize( filter_t *p_filter )
1102 {
1103     return p_filter->fmt_out.video.i_height / __MAX(1, var_CreateGetInteger( p_filter, "quartztext-rel-fontsize" ));
1104 }
1105
1106 static int RenderYUVA( filter_t *p_filter, subpicture_region_t *p_region, UniChar *psz_utf16_str,
1107                        uint32_t i_text_len, uint32_t i_runs, uint32_t *pi_run_lengths, ATSUStyle *pp_styles )
1108 {
1109     offscreen_bitmap_t *p_offScreen = NULL;
1110     int      i_textblock_height = 0;
1111
1112     int i_width = p_filter->fmt_out.video.i_visible_width;
1113     int i_height = p_filter->fmt_out.video.i_visible_height;
1114     int i_text_align = p_region->i_align & 0x3;
1115
1116     if( !psz_utf16_str )
1117     {
1118         msg_Err( p_filter, "Invalid argument to RenderYUVA" );
1119         return VLC_EGENERIC;
1120     }
1121
1122     p_offScreen = Compose( i_text_align, psz_utf16_str, i_text_len,
1123                            i_runs, pi_run_lengths, pp_styles,
1124                            i_width, i_height, &i_textblock_height );
1125
1126     if( !p_offScreen )
1127     {
1128         msg_Err( p_filter, "No offscreen buffer" );
1129         return VLC_EGENERIC;
1130     }
1131
1132     uint8_t *p_dst_y,*p_dst_u,*p_dst_v,*p_dst_a;
1133     video_format_t fmt;
1134     int x, y, i_offset, i_pitch;
1135     uint8_t i_y, i_u, i_v; // YUV values, derived from incoming RGB
1136     subpicture_region_t *p_region_tmp;
1137
1138     // Create a new subpicture region
1139     memset( &fmt, 0, sizeof(video_format_t) );
1140     fmt.i_chroma = VLC_FOURCC('Y','U','V','A');
1141     fmt.i_aspect = 0;
1142     fmt.i_width = fmt.i_visible_width = i_width;
1143     fmt.i_height = fmt.i_visible_height = i_textblock_height + VERTICAL_MARGIN * 2;
1144     fmt.i_x_offset = fmt.i_y_offset = 0;
1145     p_region_tmp = spu_CreateRegion( p_filter, &fmt );
1146     if( !p_region_tmp )
1147     {
1148         msg_Err( p_filter, "cannot allocate SPU region" );
1149         return VLC_EGENERIC;
1150     }
1151     p_region->fmt = p_region_tmp->fmt;
1152     p_region->picture = p_region_tmp->picture;
1153     free( p_region_tmp );
1154
1155     p_dst_y = p_region->picture.Y_PIXELS;
1156     p_dst_u = p_region->picture.U_PIXELS;
1157     p_dst_v = p_region->picture.V_PIXELS;
1158     p_dst_a = p_region->picture.A_PIXELS;
1159     i_pitch = p_region->picture.A_PITCH;
1160
1161     i_offset = VERTICAL_MARGIN *i_pitch;
1162     for( y=0; y<i_textblock_height; y++)
1163     {
1164         for( x=0; x<i_width; x++)
1165         {
1166             int i_alpha = p_offScreen->p_data[ y * p_offScreen->i_bytesPerRow + x * p_offScreen->i_bytesPerPixel     ];
1167             int i_red   = p_offScreen->p_data[ y * p_offScreen->i_bytesPerRow + x * p_offScreen->i_bytesPerPixel + 1 ];
1168             int i_green = p_offScreen->p_data[ y * p_offScreen->i_bytesPerRow + x * p_offScreen->i_bytesPerPixel + 2 ];
1169             int i_blue  = p_offScreen->p_data[ y * p_offScreen->i_bytesPerRow + x * p_offScreen->i_bytesPerPixel + 3 ];
1170
1171             i_y = (uint8_t)__MIN(abs( 2104 * i_red  + 4130 * i_green +
1172                               802 * i_blue + 4096 + 131072 ) >> 13, 235);
1173             i_u = (uint8_t)__MIN(abs( -1214 * i_red  + -2384 * i_green +
1174                              3598 * i_blue + 4096 + 1048576) >> 13, 240);
1175             i_v = (uint8_t)__MIN(abs( 3598 * i_red + -3013 * i_green +
1176                               -585 * i_blue + 4096 + 1048576) >> 13, 240);
1177
1178             p_dst_y[ i_offset + x ] = i_y;
1179             p_dst_u[ i_offset + x ] = i_u;
1180             p_dst_v[ i_offset + x ] = i_v;
1181             p_dst_a[ i_offset + x ] = i_alpha;
1182         }
1183         i_offset += i_pitch;
1184     }
1185
1186     free( p_offScreen->p_data );
1187     free( p_offScreen );
1188
1189     return VLC_SUCCESS;
1190 }