]> git.sesse.net Git - vlc/blob - modules/misc/quartztext.c
quartztext: Make default options settings.
[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, 0 );
121     add_integer( "quartztext-color", 0x00FFFFFF, NULL, COLOR_TEXT,
122                  COLOR_LONGTEXT, false );
123         change_integer_list( pi_color_values, ppsz_color_descriptions, 0 );
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     i_string_length = CFStringGetLength( p_cfString );
369
370     if( pi_strlen )
371         *pi_strlen = i_string_length;
372
373     if( !*ppsz_utf16_str )
374         *ppsz_utf16_str = (UniChar *) calloc( i_string_length, sizeof( UniChar ) );
375
376     CFStringGetCharacters( p_cfString, CFRangeMake( 0, i_string_length ), *ppsz_utf16_str );
377
378     CFRelease( p_cfString );
379 }
380
381 // Renders a text subpicture region into another one.
382 // It is used as pf_add_string callback in the vout method by this module
383 static int RenderText( filter_t *p_filter, subpicture_region_t *p_region_out,
384                        subpicture_region_t *p_region_in )
385 {
386     filter_sys_t *p_sys = p_filter->p_sys;
387     UniChar      *psz_utf16_str = NULL;
388     uint32_t      i_string_length;
389     char         *psz_string;
390     int           i_font_color, i_font_alpha, i_font_size;
391     vlc_value_t val;
392     int i_scale = 1000;
393
394     p_sys->i_font_size    = GetFontSize( p_filter );
395
396     // Sanity check
397     if( !p_region_in || !p_region_out ) return VLC_EGENERIC;
398     psz_string = p_region_in->psz_text;
399     if( !psz_string || !*psz_string ) return VLC_EGENERIC;
400
401     if( VLC_SUCCESS == var_Get( p_filter, "scale", &val ))
402         i_scale = val.i_int;
403
404     if( p_region_in->p_style )
405     {
406         i_font_color = __MAX( __MIN( p_region_in->p_style->i_font_color, 0xFFFFFF ), 0 );
407         i_font_alpha = __MAX( __MIN( p_region_in->p_style->i_font_alpha, 255 ), 0 );
408         i_font_size  = __MAX( __MIN( p_region_in->p_style->i_font_size, 255 ), 0 ) * i_scale / 1000;
409     }
410     else
411     {
412         i_font_color = p_sys->i_font_color;
413         i_font_alpha = 255 - p_sys->i_font_opacity;
414         i_font_size  = p_sys->i_font_size;
415     }
416
417     if( !i_font_alpha ) i_font_alpha = 255 - p_sys->i_font_opacity;
418
419     ConvertToUTF16( EliminateCRLF( psz_string ), &i_string_length, &psz_utf16_str );
420
421     p_region_out->i_x = p_region_in->i_x;
422     p_region_out->i_y = p_region_in->i_y;
423
424     if( psz_utf16_str != NULL )
425     {
426         ATSUStyle p_style = CreateStyle( p_sys->psz_font_name, i_font_size,
427                                          (i_font_color & 0xffffff) |
428                                          ((i_font_alpha & 0xff) << 24),
429                                          false, false, false );
430         if( p_style )
431         {
432             RenderYUVA( p_filter, p_region_out, psz_utf16_str, i_string_length,
433                         1, &i_string_length, &p_style );
434         }
435
436         ATSUDisposeStyle( p_style );
437         free( psz_utf16_str );
438     }
439
440     return VLC_SUCCESS;
441 }
442
443
444 static ATSUStyle CreateStyle( char *psz_fontname, int i_font_size, uint32_t i_font_color,
445                               bool b_bold, bool b_italic, bool b_uline )
446 {
447     ATSUStyle   p_style;
448     OSStatus    status;
449     uint32_t    i_tag_cnt;
450
451     float f_red   = (float)(( i_font_color & 0x00FF0000 ) >> 16) / 255.0;
452     float f_green = (float)(( i_font_color & 0x0000FF00 ) >>  8) / 255.0;
453     float f_blue  = (float)(  i_font_color & 0x000000FF        ) / 255.0;
454     float f_alpha = ( 255.0 - (float)(( i_font_color & 0xFF000000 ) >> 24)) / 255.0;
455
456     ATSUFontID           font;
457     Fixed                font_size  = IntToFixed( i_font_size );
458     ATSURGBAlphaColor    font_color = { f_red, f_green, f_blue, f_alpha };
459     Boolean              bold       = b_bold;
460     Boolean              italic     = b_italic;
461     Boolean              uline      = b_uline;
462
463     ATSUAttributeTag tags[]        = { kATSUSizeTag, kATSURGBAlphaColorTag, kATSUQDItalicTag,
464                                        kATSUQDBoldfaceTag, kATSUQDUnderlineTag, kATSUFontTag };
465     ByteCount sizes[]              = { sizeof( Fixed ), sizeof( ATSURGBAlphaColor ), sizeof( Boolean ),
466                                        sizeof( Boolean ), sizeof( Boolean ), sizeof( ATSUFontID )};
467     ATSUAttributeValuePtr values[] = { &font_size, &font_color, &italic, &bold, &uline, &font };
468
469     i_tag_cnt = sizeof( tags ) / sizeof( ATSUAttributeTag );
470
471     status = ATSUFindFontFromName( psz_fontname,
472                                    strlen( psz_fontname ),
473                                    kFontFullName,
474                                    kFontNoPlatform,
475                                    kFontNoScript,
476                                    kFontNoLanguageCode,
477                                    &font );
478
479     if( status != noErr )
480     {
481         // If we can't find a suitable font, just do everything else
482         i_tag_cnt--;
483     }
484
485     if( noErr == ATSUCreateStyle( &p_style ) )
486     {
487         if( noErr == ATSUSetAttributes( p_style, i_tag_cnt, tags, sizes, values ) )
488         {
489             return p_style;
490         }
491         ATSUDisposeStyle( p_style );
492     }
493     return NULL;
494 }
495
496 static int PushFont( font_stack_t **p_font, const char *psz_name, int i_size,
497                      uint32_t i_color )
498 {
499     font_stack_t *p_new;
500
501     if( !p_font )
502         return VLC_EGENERIC;
503
504     p_new = malloc( sizeof( font_stack_t ) );
505     if( ! p_new )
506         return VLC_ENOMEM;
507
508     p_new->p_next = NULL;
509
510     if( psz_name )
511         p_new->psz_name = strdup( psz_name );
512     else
513         p_new->psz_name = NULL;
514
515     p_new->i_size   = i_size;
516     p_new->i_color  = i_color;
517
518     if( !*p_font )
519     {
520         *p_font = p_new;
521     }
522     else
523     {
524         font_stack_t *p_last;
525
526         for( p_last = *p_font;
527              p_last->p_next;
528              p_last = p_last->p_next )
529         ;
530
531         p_last->p_next = p_new;
532     }
533     return VLC_SUCCESS;
534 }
535
536 static int PopFont( font_stack_t **p_font )
537 {
538     font_stack_t *p_last, *p_next_to_last;
539
540     if( !p_font || !*p_font )
541         return VLC_EGENERIC;
542
543     p_next_to_last = NULL;
544     for( p_last = *p_font;
545          p_last->p_next;
546          p_last = p_last->p_next )
547     {
548         p_next_to_last = p_last;
549     }
550
551     if( p_next_to_last )
552         p_next_to_last->p_next = NULL;
553     else
554         *p_font = NULL;
555
556     free( p_last->psz_name );
557     free( p_last );
558
559     return VLC_SUCCESS;
560 }
561
562 static int PeekFont( font_stack_t **p_font, char **psz_name, int *i_size,
563                      uint32_t *i_color )
564 {
565     font_stack_t *p_last;
566
567     if( !p_font || !*p_font )
568         return VLC_EGENERIC;
569
570     for( p_last=*p_font;
571          p_last->p_next;
572          p_last=p_last->p_next )
573     ;
574
575     *psz_name = p_last->psz_name;
576     *i_size   = p_last->i_size;
577     *i_color  = p_last->i_color;
578
579     return VLC_SUCCESS;
580 }
581
582 static ATSUStyle GetStyleFromFontStack( filter_sys_t *p_sys,
583         font_stack_t **p_fonts, bool b_bold, bool b_italic,
584         bool b_uline )
585 {
586     ATSUStyle   p_style = NULL;
587
588     char     *psz_fontname = NULL;
589     uint32_t  i_font_color = p_sys->i_font_color;
590     int       i_font_size  = p_sys->i_font_size;
591
592     if( VLC_SUCCESS == PeekFont( p_fonts, &psz_fontname, &i_font_size,
593                                  &i_font_color ))
594     {
595         p_style = CreateStyle( psz_fontname, i_font_size, i_font_color,
596                                b_bold, b_italic, b_uline );
597     }
598     return p_style;
599 }
600
601 static int HandleFontAttributes( xml_reader_t *p_xml_reader,
602                                   font_stack_t **p_fonts, int i_scale )
603 {
604     int        rv;
605     char      *psz_fontname = NULL;
606     uint32_t   i_font_color = 0xffffff;
607     int        i_font_alpha = 0;
608     int        i_font_size  = 24;
609
610     // Default all attributes to the top font in the stack -- in case not
611     // all attributes are specified in the sub-font
612     if( VLC_SUCCESS == PeekFont( p_fonts,
613                                  &psz_fontname,
614                                  &i_font_size,
615                                  &i_font_color ))
616     {
617         psz_fontname = strdup( psz_fontname );
618         i_font_size = i_font_size * 1000 / i_scale;
619     }
620     i_font_alpha = (i_font_color >> 24) & 0xff;
621     i_font_color &= 0x00ffffff;
622
623     while ( xml_ReaderNextAttr( p_xml_reader ) == VLC_SUCCESS )
624     {
625         char *psz_name = xml_ReaderName( p_xml_reader );
626         char *psz_value = xml_ReaderValue( p_xml_reader );
627
628         if( psz_name && psz_value )
629         {
630             if( !strcasecmp( "face", psz_name ) )
631             {
632                 free( psz_fontname );
633                 psz_fontname = strdup( psz_value );
634             }
635             else if( !strcasecmp( "size", psz_name ) )
636             {
637                 if( ( *psz_value == '+' ) || ( *psz_value == '-' ) )
638                 {
639                     int i_value = atoi( psz_value );
640
641                     if( ( i_value >= -5 ) && ( i_value <= 5 ) )
642                         i_font_size += ( i_value * i_font_size ) / 10;
643                     else if( i_value < -5 )
644                         i_font_size = - i_value;
645                     else if( i_value > 5 )
646                         i_font_size = i_value;
647                 }
648                 else
649                     i_font_size = atoi( psz_value );
650             }
651             else if( !strcasecmp( "color", psz_name )  &&
652                      ( psz_value[0] == '#' ) )
653             {
654                 i_font_color = strtol( psz_value + 1, NULL, 16 );
655                 i_font_color &= 0x00ffffff;
656             }
657             else if( !strcasecmp( "alpha", psz_name ) &&
658                      ( psz_value[0] == '#' ) )
659             {
660                 i_font_alpha = strtol( psz_value + 1, NULL, 16 );
661                 i_font_alpha &= 0xff;
662             }
663             free( psz_name );
664             free( psz_value );
665         }
666     }
667     rv = PushFont( p_fonts,
668                    psz_fontname,
669                    i_font_size * i_scale / 1000,
670                    (i_font_color & 0xffffff) | ((i_font_alpha & 0xff) << 24) );
671
672     free( psz_fontname );
673
674     return rv;
675 }
676
677 static int ProcessNodes( filter_t *p_filter,
678                          xml_reader_t *p_xml_reader,
679                          text_style_t *p_font_style,
680                          UniChar *psz_text,
681                          int *pi_len,
682
683                          uint32_t *pi_runs,
684                          uint32_t **ppi_run_lengths,
685                          ATSUStyle **ppp_styles )
686 {
687     int           rv             = VLC_SUCCESS;
688     filter_sys_t *p_sys          = p_filter->p_sys;
689     UniChar      *psz_text_orig  = psz_text;
690     font_stack_t *p_fonts        = NULL;
691     vlc_value_t   val;
692     int           i_scale        = 1000;
693
694     char *psz_node  = NULL;
695
696     bool b_italic = false;
697     bool b_bold   = false;
698     bool b_uline  = false;
699
700     if( VLC_SUCCESS == var_Get( p_filter, "scale", &val ))
701         i_scale = val.i_int;
702
703     if( p_font_style )
704     {
705         rv = PushFont( &p_fonts,
706                p_font_style->psz_fontname,
707                p_font_style->i_font_size * i_scale / 1000,
708                (p_font_style->i_font_color & 0xffffff) |
709                    ((p_font_style->i_font_alpha & 0xff) << 24) );
710
711         if( p_font_style->i_style_flags & STYLE_BOLD )
712             b_bold = true;
713         if( p_font_style->i_style_flags & STYLE_ITALIC )
714             b_italic = true;
715         if( p_font_style->i_style_flags & STYLE_UNDERLINE )
716             b_uline = true;
717     }
718     else
719     {
720         rv = PushFont( &p_fonts,
721                        p_sys->psz_font_name,
722                        p_sys->i_font_size,
723                        p_sys->i_font_color );
724     }
725     if( rv != VLC_SUCCESS )
726         return rv;
727
728     while ( ( xml_ReaderRead( p_xml_reader ) == 1 ) )
729     {
730         switch ( xml_ReaderNodeType( p_xml_reader ) )
731         {
732             case XML_READER_NONE:
733                 break;
734             case XML_READER_ENDELEM:
735                 psz_node = xml_ReaderName( p_xml_reader );
736
737                 if( psz_node )
738                 {
739                     if( !strcasecmp( "font", psz_node ) )
740                         PopFont( &p_fonts );
741                     else if( !strcasecmp( "b", psz_node ) )
742                         b_bold   = false;
743                     else if( !strcasecmp( "i", psz_node ) )
744                         b_italic = false;
745                     else if( !strcasecmp( "u", psz_node ) )
746                         b_uline  = false;
747
748                     free( psz_node );
749                 }
750                 break;
751             case XML_READER_STARTELEM:
752                 psz_node = xml_ReaderName( p_xml_reader );
753                 if( psz_node )
754                 {
755                     if( !strcasecmp( "font", psz_node ) )
756                         rv = HandleFontAttributes( p_xml_reader, &p_fonts, i_scale );
757                     else if( !strcasecmp( "b", psz_node ) )
758                         b_bold = true;
759                     else if( !strcasecmp( "i", psz_node ) )
760                         b_italic = true;
761                     else if( !strcasecmp( "u", psz_node ) )
762                         b_uline = true;
763                     else if( !strcasecmp( "br", psz_node ) )
764                     {
765                         uint32_t i_string_length;
766
767                         ConvertToUTF16( "\n", &i_string_length, &psz_text );
768                         psz_text += i_string_length;
769
770                         (*pi_runs)++;
771
772                         if( *ppp_styles )
773                             *ppp_styles = (ATSUStyle *) realloc( *ppp_styles, *pi_runs * sizeof( ATSUStyle ) );
774                         else
775                             *ppp_styles = (ATSUStyle *) malloc( *pi_runs * sizeof( ATSUStyle ) );
776
777                         (*ppp_styles)[ *pi_runs - 1 ] = GetStyleFromFontStack( p_sys, &p_fonts, b_bold, b_italic, b_uline );
778
779                         if( *ppi_run_lengths )
780                             *ppi_run_lengths = (uint32_t *) realloc( *ppi_run_lengths, *pi_runs * sizeof( uint32_t ) );
781                         else
782                             *ppi_run_lengths = (uint32_t *) malloc( *pi_runs * sizeof( uint32_t ) );
783
784                         (*ppi_run_lengths)[ *pi_runs - 1 ] = i_string_length;
785                     }
786                     free( psz_node );
787                 }
788                 break;
789             case XML_READER_TEXT:
790                 psz_node = xml_ReaderValue( p_xml_reader );
791                 if( psz_node )
792                 {
793                     uint32_t i_string_length;
794
795                     // Turn any multiple-whitespaces into single spaces
796                     char *s = strpbrk( psz_node, "\t\r\n " );
797                     while( s )
798                     {
799                         int i_whitespace = strspn( s, "\t\r\n " );
800
801                         if( i_whitespace > 1 )
802                             memmove( &s[1],
803                                      &s[i_whitespace],
804                                      strlen( s ) - i_whitespace + 1 );
805                         *s++ = ' ';
806
807                         s = strpbrk( s, "\t\r\n " );
808                     }
809
810                     ConvertToUTF16( psz_node, &i_string_length, &psz_text );
811                     psz_text += i_string_length;
812
813                     (*pi_runs)++;
814
815                     if( *ppp_styles )
816                         *ppp_styles = (ATSUStyle *) realloc( *ppp_styles, *pi_runs * sizeof( ATSUStyle ) );
817                     else
818                         *ppp_styles = (ATSUStyle *) malloc( *pi_runs * sizeof( ATSUStyle ) );
819
820                     (*ppp_styles)[ *pi_runs - 1 ] = GetStyleFromFontStack( p_sys, &p_fonts, b_bold, b_italic, b_uline );
821
822                     if( *ppi_run_lengths )
823                         *ppi_run_lengths = (uint32_t *) realloc( *ppi_run_lengths, *pi_runs * sizeof( uint32_t ) );
824                     else
825                         *ppi_run_lengths = (uint32_t *) malloc( *pi_runs * sizeof( uint32_t ) );
826
827                     (*ppi_run_lengths)[ *pi_runs - 1 ] = i_string_length;
828
829                     free( psz_node );
830                 }
831                 break;
832         }
833     }
834
835     *pi_len = psz_text - psz_text_orig;
836
837     while( VLC_SUCCESS == PopFont( &p_fonts ) );
838
839     return rv;
840 }
841
842 static int RenderHtml( filter_t *p_filter, subpicture_region_t *p_region_out,
843                        subpicture_region_t *p_region_in )
844 {
845     int          rv = VLC_SUCCESS;
846     stream_t     *p_sub = NULL;
847     xml_t        *p_xml = NULL;
848     xml_reader_t *p_xml_reader = NULL;
849
850     if( !p_region_in || !p_region_in->psz_html )
851         return VLC_EGENERIC;
852
853     /* Reset the default fontsize in case screen metrics have changed */
854     p_filter->p_sys->i_font_size = GetFontSize( p_filter );
855
856     p_sub = stream_MemoryNew( VLC_OBJECT(p_filter),
857                               (uint8_t *) p_region_in->psz_html,
858                               strlen( p_region_in->psz_html ),
859                               true );
860     if( p_sub )
861     {
862         p_xml = xml_Create( p_filter );
863         if( p_xml )
864         {
865             bool b_karaoke = false;
866
867             p_xml_reader = xml_ReaderCreate( p_xml, p_sub );
868             if( p_xml_reader )
869             {
870                 /* Look for Root Node */
871                 if( xml_ReaderRead( p_xml_reader ) == 1 )
872                 {
873                     char *psz_node = xml_ReaderName( p_xml_reader );
874
875                     if( !strcasecmp( "karaoke", psz_node ) )
876                     {
877                         /* We're going to have to render the text a number
878                          * of times to show the progress marker on the text.
879                          */
880                         var_SetBool( p_filter, "text-rerender", true );
881                         b_karaoke = true;
882                     }
883                     else if( !strcasecmp( "text", psz_node ) )
884                     {
885                         b_karaoke = false;
886                     }
887                     else
888                     {
889                         /* Only text and karaoke tags are supported */
890                         xml_ReaderDelete( p_xml, p_xml_reader );
891                         p_xml_reader = NULL;
892                         rv = VLC_EGENERIC;
893                     }
894
895                     free( psz_node );
896                 }
897             }
898
899             if( p_xml_reader )
900             {
901                 UniChar    *psz_text;
902                 int         i_len = 0;
903                 uint32_t    i_runs = 0;
904                 uint32_t   *pi_run_lengths = NULL;
905                 ATSUStyle  *pp_styles = NULL;
906
907                 psz_text = (UniChar *) malloc( strlen( p_region_in->psz_html ) *
908                                                 sizeof( UniChar ) );
909                 if( psz_text )
910                 {
911                     uint32_t k;
912
913                     rv = ProcessNodes( p_filter, p_xml_reader,
914                                   p_region_in->p_style, psz_text, &i_len,
915                                   &i_runs, &pi_run_lengths, &pp_styles );
916
917                     p_region_out->i_x = p_region_in->i_x;
918                     p_region_out->i_y = p_region_in->i_y;
919
920                     if(( rv == VLC_SUCCESS ) && ( i_len > 0 ))
921                     {
922                         RenderYUVA( p_filter, p_region_out, psz_text, i_len, i_runs,
923                              pi_run_lengths, pp_styles);
924                     }
925
926                     for( k=0; k<i_runs; k++)
927                         ATSUDisposeStyle( pp_styles[k] );
928                     free( pp_styles );
929                     free( pi_run_lengths );
930                     free( psz_text );
931                 }
932
933                 xml_ReaderDelete( p_xml, p_xml_reader );
934             }
935             xml_Delete( p_xml );
936         }
937         stream_Delete( p_sub );
938     }
939
940     return rv;
941 }
942
943 static CGContextRef CreateOffScreenContext( int i_width, int i_height,
944                          offscreen_bitmap_t **pp_memory, CGColorSpaceRef *pp_colorSpace )
945 {
946     offscreen_bitmap_t *p_bitmap;
947     CGContextRef        p_context = NULL;
948
949     p_bitmap = (offscreen_bitmap_t *) malloc( sizeof( offscreen_bitmap_t ));
950     if( p_bitmap )
951     {
952         p_bitmap->i_bitsPerChannel = 8;
953         p_bitmap->i_bitsPerPixel   = 4 * p_bitmap->i_bitsPerChannel; // A,R,G,B
954         p_bitmap->i_bytesPerPixel  = p_bitmap->i_bitsPerPixel / 8;
955         p_bitmap->i_bytesPerRow    = i_width * p_bitmap->i_bytesPerPixel;
956
957         p_bitmap->p_data = calloc( i_height, p_bitmap->i_bytesPerRow );
958
959 #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_4
960         *pp_colorSpace = CGColorSpaceCreateWithName( kCGColorSpaceGenericRGB );
961 #else
962         *pp_colorSpace = CreateGenericRGBColorSpace();
963 #endif
964
965         if( p_bitmap->p_data && *pp_colorSpace )
966         {
967             p_context = CGBitmapContextCreate( p_bitmap->p_data, i_width, i_height,
968                                 p_bitmap->i_bitsPerChannel, p_bitmap->i_bytesPerRow,
969                                 *pp_colorSpace, kCGImageAlphaPremultipliedFirst);
970         }
971         if( p_context )
972         {
973 #if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_1
974             // OS X 10.1 doesn't support weak linking of this call which is only available
975             // int 10.4 and later
976             if( CGContextSetAllowsAntialiasing != NULL )
977             {
978                 CGContextSetAllowsAntialiasing( p_context, true );
979             }
980 #endif
981         }
982         *pp_memory = p_bitmap;
983     }
984
985     return p_context;
986 }
987
988 static offscreen_bitmap_t *Compose( int i_text_align, UniChar *psz_utf16_str, uint32_t i_text_len,
989                                     uint32_t i_runs, uint32_t *pi_run_lengths, ATSUStyle *pp_styles,
990                                     int i_width, int i_height, int *pi_textblock_height )
991 {
992     offscreen_bitmap_t  *p_offScreen  = NULL;
993     CGColorSpaceRef      p_colorSpace = NULL;
994     CGContextRef         p_context = NULL;
995
996     p_context = CreateOffScreenContext( i_width, i_height, &p_offScreen, &p_colorSpace );
997
998     if( p_context )
999     {
1000         ATSUTextLayout p_textLayout;
1001         OSStatus status = noErr;
1002
1003         status = ATSUCreateTextLayoutWithTextPtr( psz_utf16_str, 0, i_text_len, i_text_len,
1004                                                   i_runs,
1005                                                   (const UniCharCount *) pi_run_lengths,
1006                                                   pp_styles,
1007                                                   &p_textLayout );
1008         if( status == noErr )
1009         {
1010             // Attach our offscreen Image Graphics Context to the text style
1011             // and setup the line alignment (have to specify the line width
1012             // also in order for our chosen alignment to work)
1013
1014             Fract   alignment  = kATSUStartAlignment;
1015             Fixed   line_width = Long2Fix( i_width - HORIZONTAL_MARGIN * 2 );
1016
1017             ATSUAttributeTag tags[]        = { kATSUCGContextTag, kATSULineFlushFactorTag, kATSULineWidthTag };
1018             ByteCount sizes[]              = { sizeof( CGContextRef ), sizeof( Fract ), sizeof( Fixed ) };
1019             ATSUAttributeValuePtr values[] = { &p_context, &alignment, &line_width };
1020
1021             int i_tag_cnt = sizeof( tags ) / sizeof( ATSUAttributeTag );
1022
1023             if( i_text_align == SUBPICTURE_ALIGN_RIGHT )
1024             {
1025                 alignment = kATSUEndAlignment;
1026             }
1027             else if( i_text_align != SUBPICTURE_ALIGN_LEFT )
1028             {
1029                 alignment = kATSUCenterAlignment;
1030             }
1031
1032             ATSUSetLayoutControls( p_textLayout, i_tag_cnt, tags, sizes, values );
1033
1034             // let ATSUI deal with characters not-in-our-specified-font
1035             ATSUSetTransientFontMatching( p_textLayout, true );
1036
1037             Fixed x = Long2Fix( HORIZONTAL_MARGIN );
1038             Fixed y = Long2Fix( i_height );
1039
1040             // Set the line-breaks and draw individual lines
1041             uint32_t i_start = 0;
1042             uint32_t i_end = i_text_len;
1043
1044             // Set up black outlining of the text --
1045             CGContextSetRGBStrokeColor( p_context, 0, 0, 0, 0.5 );
1046             CGContextSetTextDrawingMode( p_context, kCGTextFillStroke );
1047
1048             do
1049             {
1050                 // ATSUBreakLine will automatically pick up any manual '\n's also
1051                 status = ATSUBreakLine( p_textLayout, i_start, line_width, true, (UniCharArrayOffset *) &i_end );
1052                 if( ( status == noErr ) || ( status == kATSULineBreakInWord ) )
1053                 {
1054                     Fixed     ascent;
1055                     Fixed     descent;
1056                     uint32_t  i_actualSize;
1057
1058                     // Come down far enough to fit the height of this line --
1059                     ATSUGetLineControl( p_textLayout, i_start, kATSULineAscentTag,
1060                                     sizeof( Fixed ), &ascent, (ByteCount *) &i_actualSize );
1061
1062                     // Quartz uses an upside-down co-ordinate space -> y values decrease as
1063                     // you move down the page
1064                     y -= ascent;
1065
1066                     // Set the outlining for this line to be dependent on the size of the line -
1067                     // make it about 5% of the ascent, with a minimum at 1.0
1068                     float f_thickness = FixedToFloat( ascent ) * 0.05;
1069                     CGContextSetLineWidth( p_context, (( f_thickness > 1.0 ) ? 1.0 : f_thickness ));
1070
1071                     ATSUDrawText( p_textLayout, i_start, i_end - i_start, x, y );
1072
1073                     // and now prepare for the next line by coming down far enough for our
1074                     // descent
1075                     ATSUGetLineControl( p_textLayout, i_start, kATSULineDescentTag,
1076                                     sizeof( Fixed ), &descent, (ByteCount *) &i_actualSize );
1077                     y -= descent;
1078
1079                     i_start = i_end;
1080                 }
1081                 else
1082                     break;
1083             }
1084             while( i_end < i_text_len );
1085
1086             *pi_textblock_height = i_height - Fix2Long( y );
1087             CGContextFlush( p_context );
1088
1089             ATSUDisposeTextLayout( p_textLayout );
1090         }
1091
1092         CGContextRelease( p_context );
1093     }
1094     if( p_colorSpace ) CGColorSpaceRelease( p_colorSpace );
1095
1096     return p_offScreen;
1097 }
1098
1099 static int GetFontSize( filter_t *p_filter )
1100 {
1101     return p_filter->fmt_out.video.i_height / __MAX(1, var_CreateGetInteger( p_filter, "quartztext-rel-fontsize" ));
1102 }
1103
1104 static int RenderYUVA( filter_t *p_filter, subpicture_region_t *p_region, UniChar *psz_utf16_str,
1105                        uint32_t i_text_len, uint32_t i_runs, uint32_t *pi_run_lengths, ATSUStyle *pp_styles )
1106 {
1107     offscreen_bitmap_t *p_offScreen = NULL;
1108     int      i_textblock_height = 0;
1109
1110     int i_width = p_filter->fmt_out.video.i_visible_width;
1111     int i_height = p_filter->fmt_out.video.i_visible_height;
1112     int i_text_align = p_region->i_align & 0x3;
1113
1114     if( !psz_utf16_str )
1115     {
1116         msg_Err( p_filter, "Invalid argument to RenderYUVA" );
1117         return VLC_EGENERIC;
1118     }
1119
1120     p_offScreen = Compose( i_text_align, psz_utf16_str, i_text_len,
1121                            i_runs, pi_run_lengths, pp_styles,
1122                            i_width, i_height, &i_textblock_height );
1123
1124     if( !p_offScreen )
1125     {
1126         msg_Err( p_filter, "No offscreen buffer" );
1127         return VLC_EGENERIC;
1128     }
1129
1130     uint8_t *p_dst_y,*p_dst_u,*p_dst_v,*p_dst_a;
1131     video_format_t fmt;
1132     int x, y, i_offset, i_pitch;
1133     uint8_t i_y, i_u, i_v; // YUV values, derived from incoming RGB
1134     subpicture_region_t *p_region_tmp;
1135
1136     // Create a new subpicture region
1137     memset( &fmt, 0, sizeof(video_format_t) );
1138     fmt.i_chroma = VLC_FOURCC('Y','U','V','A');
1139     fmt.i_aspect = 0;
1140     fmt.i_width = fmt.i_visible_width = i_width;
1141     fmt.i_height = fmt.i_visible_height = i_textblock_height + VERTICAL_MARGIN * 2;
1142     fmt.i_x_offset = fmt.i_y_offset = 0;
1143     p_region_tmp = spu_CreateRegion( p_filter, &fmt );
1144     if( !p_region_tmp )
1145     {
1146         msg_Err( p_filter, "cannot allocate SPU region" );
1147         return VLC_EGENERIC;
1148     }
1149     p_region->fmt = p_region_tmp->fmt;
1150     p_region->picture = p_region_tmp->picture;
1151     free( p_region_tmp );
1152
1153     p_dst_y = p_region->picture.Y_PIXELS;
1154     p_dst_u = p_region->picture.U_PIXELS;
1155     p_dst_v = p_region->picture.V_PIXELS;
1156     p_dst_a = p_region->picture.A_PIXELS;
1157     i_pitch = p_region->picture.A_PITCH;
1158
1159     i_offset = VERTICAL_MARGIN *i_pitch;
1160     for( y=0; y<i_textblock_height; y++)
1161     {
1162         for( x=0; x<i_width; x++)
1163         {
1164             int i_alpha = p_offScreen->p_data[ y * p_offScreen->i_bytesPerRow + x * p_offScreen->i_bytesPerPixel     ];
1165             int i_red   = p_offScreen->p_data[ y * p_offScreen->i_bytesPerRow + x * p_offScreen->i_bytesPerPixel + 1 ];
1166             int i_green = p_offScreen->p_data[ y * p_offScreen->i_bytesPerRow + x * p_offScreen->i_bytesPerPixel + 2 ];
1167             int i_blue  = p_offScreen->p_data[ y * p_offScreen->i_bytesPerRow + x * p_offScreen->i_bytesPerPixel + 3 ];
1168
1169             i_y = (uint8_t)__MIN(abs( 2104 * i_red  + 4130 * i_green +
1170                               802 * i_blue + 4096 + 131072 ) >> 13, 235);
1171             i_u = (uint8_t)__MIN(abs( -1214 * i_red  + -2384 * i_green +
1172                              3598 * i_blue + 4096 + 1048576) >> 13, 240);
1173             i_v = (uint8_t)__MIN(abs( 3598 * i_red + -3013 * i_green +
1174                               -585 * i_blue + 4096 + 1048576) >> 13, 240);
1175
1176             p_dst_y[ i_offset + x ] = i_y;
1177             p_dst_u[ i_offset + x ] = i_u;
1178             p_dst_v[ i_offset + x ] = i_v;
1179             p_dst_a[ i_offset + x ] = i_alpha;
1180         }
1181         i_offset += i_pitch;
1182     }
1183
1184     free( p_offScreen->p_data );
1185     free( p_offScreen );
1186
1187     return VLC_SUCCESS;
1188 }