diff options
Diffstat (limited to 'libhb/rendersub.c')
-rw-r--r-- | libhb/rendersub.c | 648 |
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; + } +} + |