summaryrefslogtreecommitdiffstats
path: root/libhb/rendersub.c
diff options
context:
space:
mode:
Diffstat (limited to 'libhb/rendersub.c')
-rw-r--r--libhb/rendersub.c648
1 files changed, 648 insertions, 0 deletions
diff --git a/libhb/rendersub.c b/libhb/rendersub.c
new file mode 100644
index 000000000..26a3ec06d
--- /dev/null
+++ b/libhb/rendersub.c
@@ -0,0 +1,648 @@
+
+#include "hb.h"
+#include "hbffmpeg.h"
+#include <ass/ass.h>
+
+struct hb_filter_private_s
+{
+ // Common
+ int crop[4];
+ int type;
+
+ // VOBSUB
+ hb_list_t * sub_list; // List of active subs
+
+ // SSA
+ ASS_Library * ssa;
+ ASS_Renderer * renderer;
+ ASS_Track * ssaTrack;
+};
+
+// VOBSUB
+static int vobsub_init( hb_filter_object_t * filter, hb_filter_init_t * init );
+
+static int vobsub_work( hb_filter_object_t * filter,
+ hb_buffer_t ** buf_in,
+ hb_buffer_t ** buf_out );
+
+static void vobsub_close( hb_filter_object_t * filter );
+
+
+// SSA
+static int ssa_init( hb_filter_object_t * filter, hb_filter_init_t * init );
+
+static int ssa_work( hb_filter_object_t * filter,
+ hb_buffer_t ** buf_in,
+ hb_buffer_t ** buf_out );
+
+static void ssa_close( hb_filter_object_t * filter );
+
+
+// Entry points
+static int hb_rendersub_init( hb_filter_object_t * filter,
+ hb_filter_init_t * init );
+
+static int hb_rendersub_work( hb_filter_object_t * filter,
+ hb_buffer_t ** buf_in,
+ hb_buffer_t ** buf_out );
+
+static void hb_rendersub_close( hb_filter_object_t * filter );
+
+hb_filter_object_t hb_filter_render_sub =
+{
+ .id = HB_FILTER_RENDER_SUB,
+ .enforce_order = 1,
+ .name = "Subtitle renderer",
+ .settings = NULL,
+ .init = hb_rendersub_init,
+ .work = hb_rendersub_work,
+ .close = hb_rendersub_close,
+};
+
+static void blend( hb_buffer_t *dst, hb_buffer_t *src, int left, int top )
+{
+ int xx, yy;
+ int ww, hh;
+ int x0, y0;
+ uint8_t *y_in, *y_out;
+ uint8_t *u_in, *u_out;
+ uint8_t *v_in, *v_out;
+ uint8_t *a_in, alpha;
+
+ x0 = y0 = 0;
+ if( left < 0 )
+ {
+ x0 = -left;
+ }
+ if( top < 0 )
+ {
+ y0 = -top;
+ }
+
+ ww = src->f.width;
+ if( left + src->f.width > dst->f.width )
+ {
+ ww = dst->f.width - ( left + src->f.width );
+ }
+ hh = src->f.height;
+ if( top + src->f.height > dst->f.height )
+ {
+ hh = dst->f.height - ( top + src->f.height );
+ }
+
+ // Blend luma
+ for( yy = y0; yy < hh; yy++ )
+ {
+ y_in = src->plane[0].data + yy * src->plane[0].stride;
+ y_out = dst->plane[0].data + ( yy + top ) * dst->plane[0].stride;
+ if( a_in )
+ {
+ a_in = src->plane[3].data + yy * src->plane[3].stride;
+ }
+ for( xx = x0; xx < ww; xx++ )
+ {
+ if( a_in )
+ {
+ alpha = a_in[xx];
+ }
+ else
+ {
+ // If source has no alpha channel, use 50%
+ alpha = 128;
+ }
+
+ /*
+ * Merge the luminance and alpha with the picture
+ */
+ y_out[left + xx] =
+ ( (uint16_t)y_out[left + xx] * ( 255 - alpha ) +
+ (uint16_t)y_in[xx] * alpha ) >> 8;
+ }
+ }
+
+ // Blend U & V
+ // Assumes source and dest are the same PIX_FMT
+ int hshift = 0;
+ int wshift = 0;
+ if( dst->plane[1].height < dst->plane[0].height )
+ hshift = 1;
+ if( dst->plane[1].width < dst->plane[0].width )
+ wshift = 1;
+ for( yy = y0 >> hshift; yy < hh >> hshift; yy++ )
+ {
+ u_in = src->plane[1].data + yy * src->plane[1].stride;
+ u_out = dst->plane[1].data + ( yy + ( top >> hshift ) ) * dst->plane[1].stride;
+ v_in = src->plane[2].data + yy * src->plane[2].stride;
+ v_out = dst->plane[2].data + ( yy + ( top >> hshift ) ) * dst->plane[2].stride;
+ if( a_in )
+ {
+ a_in = src->plane[3].data + ( yy << hshift ) * src->plane[3].stride;
+ }
+
+ for( xx = x0 >> wshift; xx < ww >> wshift; xx++ )
+ {
+ if( a_in )
+ {
+ alpha = a_in[xx << wshift];
+ }
+ else
+ {
+ // If source has no alpha channel, use 50%
+ alpha = 128;
+ }
+
+ // Blend averge U and alpha
+ u_out[(left >> wshift) + xx] =
+ ( (uint16_t)u_out[(left >> wshift) + xx] * ( 255 - alpha ) +
+ (uint16_t)u_in[xx] * alpha ) >> 8;
+
+ // Blend V and alpha
+ v_out[(left >> wshift) + xx] =
+ ( (uint16_t)v_out[(left >> wshift) + xx] * ( 255 - alpha ) +
+ (uint16_t)v_in[xx] * alpha ) >> 8;
+ }
+ }
+}
+
+// Assumes that the input buffer has the same dimensions
+// as the original title diminsions
+static void ApplySub( hb_filter_private_t * pv, hb_buffer_t * buf, hb_buffer_t * sub )
+{
+ int top, left, margin_top, margin_percent;
+
+ /*
+ * Percent of height of picture that form a margin that subtitles
+ * should not be displayed within.
+ */
+ margin_percent = 2;
+
+ /*
+ * If necessary, move the subtitle so it is not in a cropped zone.
+ * When it won't fit, we center it so we lose as much on both ends.
+ * Otherwise we try to leave a 20px or 2% margin around it.
+ */
+ margin_top = ( ( buf->f.height - pv->crop[0] - pv->crop[1] ) *
+ margin_percent ) / 100;
+
+ if( margin_top > 20 )
+ {
+ /*
+ * A maximum margin of 20px regardless of height of the picture.
+ */
+ margin_top = 20;
+ }
+
+ if( sub->f.height > buf->f.height - pv->crop[0] - pv->crop[1] -
+ ( margin_top * 2 ) )
+ {
+ /*
+ * The subtitle won't fit in the cropped zone, so center
+ * it vertically so we fit in as much as we can.
+ */
+ top = pv->crop[0] + ( buf->f.height - pv->crop[0] -
+ pv->crop[1] - sub->f.height ) / 2;
+ }
+ else if( sub->f.y < pv->crop[0] + margin_top )
+ {
+ /*
+ * The subtitle fits in the cropped zone, but is currently positioned
+ * within our top margin, so move it outside of our margin.
+ */
+ top = pv->crop[0] + margin_top;
+ }
+ else if( sub->f.y > buf->f.height - pv->crop[1] - margin_top - sub->f.height )
+ {
+ /*
+ * The subtitle fits in the cropped zone, and is not within the top
+ * margin but is within the bottom margin, so move it to be above
+ * the margin.
+ */
+ top = buf->f.height - pv->crop[1] - margin_top - sub->f.height;
+ }
+ else
+ {
+ /*
+ * The subtitle is fine where it is.
+ */
+ top = sub->f.y;
+ }
+
+ if( sub->f.width > buf->f.width - pv->crop[2] - pv->crop[3] - 40 )
+ left = pv->crop[2] + ( buf->f.width - pv->crop[2] -
+ pv->crop[3] - sub->f.width ) / 2;
+ else if( sub->f.x < pv->crop[2] + 20 )
+ left = pv->crop[2] + 20;
+ else if( sub->f.x > buf->f.width - pv->crop[3] - 20 - sub->f.width )
+ left = buf->f.width - pv->crop[3] - 20 - sub->f.width;
+ else
+ left = sub->f.x;
+
+ blend( buf, sub, left, top );
+}
+
+// Assumes that the input buffer has the same dimensions
+// as the original title diminsions
+static void ApplyVOBSubs( hb_filter_private_t * pv, hb_buffer_t * buf )
+{
+ int ii;
+ hb_buffer_t * sub;
+
+ for( ii = 0; ii < hb_list_count( pv->sub_list ); ii++ )
+ {
+ sub = hb_list_item( pv->sub_list, ii );
+ if( sub->s.stop <= buf->s.start )
+ {
+ // Subtitle stop is in the past, delete it
+ hb_list_rem( pv->sub_list, sub );
+ }
+ else if( sub->s.start <= buf->s.start )
+ {
+ // The subtitle has started before this frame and ends
+ // after it. Render the subtitle into the frame.
+ while ( sub )
+ {
+ ApplySub( pv, buf, sub );
+ sub = sub->next;
+ }
+ }
+ else
+ {
+ // The subtitle starts in the future. No need to continue.
+ break;
+ }
+ }
+}
+
+static int vobsub_init( hb_filter_object_t * filter,
+ hb_filter_init_t * init )
+{
+ hb_filter_private_t * pv = filter->private_data;
+
+ // VOBSUB render filter has no settings
+ memcpy( pv->crop, init->crop, sizeof( int[4] ) );
+
+ pv->sub_list = hb_list_init();
+
+ return 0;
+}
+
+static void vobsub_close( hb_filter_object_t * filter )
+{
+ hb_filter_private_t * pv = filter->private_data;
+
+ if( !pv )
+ {
+ return;
+ }
+
+ if( pv->sub_list )
+ hb_list_empty( &pv->sub_list );
+
+ free( pv );
+ filter->private_data = NULL;
+}
+
+static int vobsub_work( hb_filter_object_t * filter,
+ hb_buffer_t ** buf_in,
+ hb_buffer_t ** buf_out )
+{
+ hb_filter_private_t * pv = filter->private_data;
+ hb_buffer_t * in = *buf_in;
+ hb_buffer_t * sub;
+
+ if ( in->size <= 0 )
+ {
+ *buf_in = NULL;
+ *buf_out = in;
+ return HB_FILTER_DONE;
+ }
+
+ // Get any pending subtitles and add them to the active
+ // subtitle list
+ while( ( sub = hb_fifo_get( filter->subtitle->fifo_out ) ) )
+ {
+ hb_list_add( pv->sub_list, sub );
+ }
+
+ ApplyVOBSubs( pv, in );
+ *buf_in = NULL;
+ *buf_out = in;
+
+ return HB_FILTER_OK;
+}
+
+static uint8_t ssaAlpha( ASS_Image *frame, int x, int y )
+{
+ unsigned frameA = ( frame->color ) & 0xff;
+ unsigned gliphA = frame->bitmap[y*frame->stride + x];
+
+ // Alpha for this pixel is the frame opacity (255 - frameA)
+ // multiplied by the gliph alfa (gliphA) for this pixel
+ unsigned alpha = (255 - frameA) * gliphA >> 8;
+
+ return (uint8_t)alpha;
+}
+
+static hb_buffer_t * RenderSSAFrame( ASS_Image * frame )
+{
+ hb_buffer_t *sub;
+ int xx, yy;
+
+ unsigned r = ( frame->color >> 24 ) & 0xff;
+ unsigned g = ( frame->color >> 16 ) & 0xff;
+ unsigned b = ( frame->color >> 8 ) & 0xff;
+
+ int yuv = hb_rgb2yuv((r << 16) | (g << 8) | b );
+
+ unsigned frameY = (yuv >> 16) & 0xff;
+ unsigned frameV = (yuv >> 8 ) & 0xff;
+ unsigned frameU = (yuv >> 0 ) & 0xff;
+
+ sub = hb_pic_buffer_init( PIX_FMT_YUVA420P, frame->w, frame->h );
+ if( sub == NULL )
+ return NULL;
+
+ uint8_t *y_out, *u_out, *v_out, *a_out;
+ y_out = sub->plane[0].data;
+ u_out = sub->plane[1].data;
+ v_out = sub->plane[2].data;
+ a_out = sub->plane[3].data;
+
+ for( yy = 0; yy < frame->h; yy++ )
+ {
+ for( xx = 0; xx < frame->w; xx++ )
+ {
+ y_out[xx] = frameY;
+ if( ( yy & 1 ) == 0 )
+ {
+ u_out[xx>>1] = frameU;
+ v_out[xx>>1] = frameV;
+ }
+ a_out[xx] = ssaAlpha( frame, xx, yy );;
+ }
+ y_out += sub->plane[0].stride;
+ if( ( yy & 1 ) == 0 )
+ {
+ u_out += sub->plane[1].stride;
+ v_out += sub->plane[2].stride;
+ }
+ a_out += sub->plane[3].stride;
+ }
+ sub->f.width = frame->w;
+ sub->f.height = frame->h;
+ sub->f.x = frame->dst_x;
+ sub->f.y = frame->dst_y;
+
+ return sub;
+}
+
+static void ApplySSASubs( hb_filter_private_t * pv, hb_buffer_t * buf )
+{
+ ASS_Image *frameList;
+ hb_buffer_t *sub;
+ frameList = ass_render_frame( pv->renderer, pv->ssaTrack,
+ buf->s.start / 90, NULL );
+ if ( !frameList )
+ return;
+
+ ASS_Image *frame;
+ for (frame = frameList; frame; frame = frame->next) {
+ sub = RenderSSAFrame( frame );
+ if( sub )
+ {
+ ApplySub( pv, buf, sub );
+ hb_buffer_close( &sub );
+ }
+ }
+}
+
+static void ssa_log(int level, const char *fmt, va_list args, void *data)
+{
+ if ( level < 5 ) // same as default verbosity when no callback is set
+ {
+ hb_valog( 1, "[ass]", fmt, args );
+ }
+}
+
+static int ssa_init( hb_filter_object_t * filter,
+ hb_filter_init_t * init )
+{
+ hb_filter_private_t * pv = filter->private_data;
+
+ memcpy( pv->crop, init->crop, sizeof( int[4] ) );
+
+ pv->ssa = ass_library_init();
+ if ( !pv->ssa ) {
+ hb_error( "decssasub: libass initialization failed\n" );
+ return 1;
+ }
+
+ // Redirect libass output to hb_log
+ ass_set_message_cb( pv->ssa, ssa_log, NULL );
+
+ // Load embedded fonts
+ hb_list_t * list_attachment = init->job->title->list_attachment;
+ int i;
+ for ( i = 0; i < hb_list_count(list_attachment); i++ )
+ {
+ hb_attachment_t * attachment = hb_list_item( list_attachment, i );
+
+ if ( attachment->type == FONT_TTF_ATTACH )
+ {
+ ass_add_font(
+ pv->ssa,
+ attachment->name,
+ attachment->data,
+ attachment->size );
+ }
+ }
+
+ ass_set_extract_fonts( pv->ssa, 1 );
+ ass_set_style_overrides( pv->ssa, NULL );
+
+ pv->renderer = ass_renderer_init( pv->ssa );
+ if ( !pv->renderer ) {
+ hb_log( "decssasub: renderer initialization failed\n" );
+ return 1;
+ }
+
+ ass_set_use_margins( pv->renderer, 0 );
+ ass_set_hinting( pv->renderer, ASS_HINTING_LIGHT ); // VLC 1.0.4 uses this
+ ass_set_font_scale( pv->renderer, 1.0 );
+ ass_set_line_spacing( pv->renderer, 1.0 );
+
+ // Setup default font family
+ //
+ // SSA v4.00 requires that "Arial" be the default font
+ const char *font = NULL;
+ const char *family = "Arial";
+ // NOTE: This can sometimes block for several *seconds*.
+ // It seems that process_fontdata() for some embedded fonts is slow.
+ ass_set_fonts( pv->renderer, font, family, /*haveFontConfig=*/1, NULL, 1 );
+
+ // Setup track state
+ pv->ssaTrack = ass_new_track( pv->ssa );
+ if ( !pv->ssaTrack ) {
+ hb_log( "decssasub: ssa track initialization failed\n" );
+ return 1;
+ }
+
+ // NOTE: The codec extradata is expected to be in MKV format
+ ass_process_codec_private( pv->ssaTrack,
+ (char *)filter->subtitle->extradata, filter->subtitle->extradata_size );
+
+ int width = init->width - ( init->crop[2] + init->crop[3] );
+ int height = init->height - ( init->crop[0] + init->crop[1] );
+ ass_set_frame_size( pv->renderer, width, height);
+
+ double par = (double)init->par_width / init->par_height;
+ ass_set_aspect_ratio( pv->renderer, 1, par );
+
+ return 0;
+}
+
+static void ssa_close( hb_filter_object_t * filter )
+{
+ hb_filter_private_t * pv = filter->private_data;
+
+ if( !pv )
+ {
+ return;
+ }
+
+ if ( pv->ssaTrack )
+ ass_free_track( pv->ssaTrack );
+ if ( pv->renderer )
+ ass_renderer_done( pv->renderer );
+ if ( pv->ssa )
+ ass_library_done( pv->ssa );
+
+ free( pv );
+ filter->private_data = NULL;
+}
+
+static int ssa_work( hb_filter_object_t * filter,
+ hb_buffer_t ** buf_in,
+ hb_buffer_t ** buf_out )
+{
+ hb_filter_private_t * pv = filter->private_data;
+ hb_buffer_t * in = *buf_in;
+ hb_buffer_t * sub;
+
+ if ( in->size <= 0 )
+ {
+ *buf_in = NULL;
+ *buf_out = in;
+ return HB_FILTER_DONE;
+ }
+
+ // Get any pending subtitles and add them to the active
+ // subtitle list
+ while( ( sub = hb_fifo_get( filter->subtitle->fifo_out ) ) )
+ {
+ // Parse MKV-SSA packet
+ ass_process_chunk( pv->ssaTrack, (char*)sub->data, sub->size,
+ sub->s.start / 90,
+ (sub->s.stop - sub->s.start) / 90 );
+ }
+
+ ApplySSASubs( pv, in );
+ *buf_in = NULL;
+ *buf_out = in;
+
+ return HB_FILTER_OK;
+}
+
+static int hb_rendersub_init( hb_filter_object_t * filter,
+ hb_filter_init_t * init )
+{
+ filter->private_data = calloc( 1, sizeof(struct hb_filter_private_s) );
+ hb_filter_private_t * pv = filter->private_data;
+ hb_subtitle_t *subtitle;
+ int ii;
+
+ // Find the subtitle we need
+ for( ii = 0; ii < hb_list_count(init->job->title->list_subtitle); ii++ )
+ {
+ subtitle = hb_list_item( init->job->title->list_subtitle, ii );
+ if( subtitle && subtitle->config.dest == RENDERSUB )
+ {
+ // Found it
+ filter->subtitle = subtitle;
+ pv->type = subtitle->source;
+ break;
+ }
+ }
+ if( filter->subtitle == NULL )
+ {
+ hb_error("rendersub: no subtitle marked for burn");
+ return 1;
+ }
+
+ switch( pv->type )
+ {
+ case VOBSUB:
+ {
+ return vobsub_init( filter, init );
+ } break;
+
+ case SSASUB:
+ {
+ return ssa_init( filter, init );
+ } break;
+
+ default:
+ {
+ hb_error("rendersub: unsupported subtitle format %d", pv->type );
+ return 1;
+ } break;
+ }
+}
+
+static int hb_rendersub_work( hb_filter_object_t * filter,
+ hb_buffer_t ** buf_in,
+ hb_buffer_t ** buf_out )
+{
+ hb_filter_private_t * pv = filter->private_data;
+ switch( pv->type )
+ {
+ case VOBSUB:
+ {
+ return vobsub_work( filter, buf_in, buf_out );
+ } break;
+
+ case SSASUB:
+ {
+ return ssa_work( filter, buf_in, buf_out );
+ } break;
+
+ default:
+ {
+ hb_error("rendersub: unsupported subtitle format %d", pv->type );
+ return 1;
+ } break;
+ }
+}
+
+static void hb_rendersub_close( hb_filter_object_t * filter )
+{
+ hb_filter_private_t * pv = filter->private_data;
+ switch( pv->type )
+ {
+ case VOBSUB:
+ {
+ vobsub_close( filter );
+ } break;
+
+ case SSASUB:
+ {
+ ssa_close( filter );
+ } break;
+
+ default:
+ {
+ hb_error("rendersub: unsupported subtitle format %d", pv->type );
+ } break;
+ }
+}
+