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