/* * filter_ssa.c * * Copyright (C) 2003 Ted Lin * * Parts of this plugin is based on ideas and code from: * - Thomas Wehrspann (filter_logoaway) * - Tilmann Bitterberg (filter_logo) * - Avery Lee (VirtualDub Subtitler) * * Random Notes: * Yes, lots of this code is ugly. * At the moment, this is only a proof-of-concept. * Also, it needs/eats LOTS of memory. * Colorization may not work. * YUV may not work 100% either. * * To Do: * Fade-In/Out * Script File Reading * Script File Parsing * (Support SSA and Simple Idiotic Script (SIS)) * * Random URLs: * Color FAQ * http://www.scarse.org/docs/color_faq.html * Alpha Transparency Calculation * http://www.algonet.se/~afb/gamedev/translucency.html * * Change Log: * 2003-03-04 : Initial * 2003-03-05 : Preliminary YUV/RGB modes work! * * * * This file is part of transcode, a linux video stream processing tool * * transcode is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * transcode is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GNU Make; see the file COPYING. If not, write to * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. * */ #define MOD_NAME "filter_ssa.so" #define MOD_VERSION "v0.1 (2003-03-04)" #define MOD_CAP "substation alpha subtitling" #define MOD_AUTHOR "Ted Lin" #include #include #include #include #include /*------------------------------------------------- * * mandatory include files * *-------------------------------------------------*/ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "transcode.h" #include "framebuffer.h" #include "filter.h" #include "optstr.h" #include "../export/vid_aux.h" /*------------------------------------------------- * * macro fun * *-------------------------------------------------*/ #define BLEND(s,d,a) ((s - d) * (1.0 - a/255.0) + d) /*------------------------------------------------- * * data structures * *-------------------------------------------------*/ typedef struct styleData { char *id; int fadedur; char *font; double fontsize; struct styleData *next; } styleData; typedef struct frameData { unsigned int fbeg; unsigned int fend; int posx; int posy; char *text; styleData *style; struct frameData *next; } frameData; /*------------------------------------------------- * * global variables * *-------------------------------------------------*/ static styleData *std = NULL; static frameData *frd = NULL; static char filename[MaxTextExtent]; /*------------------------------------------------- * * single function interface * *-------------------------------------------------*/ static void help_optstr(void) { printf("[%s] (%s) help\n", MOD_NAME, MOD_CAP); printf("* Overview\n"); printf(" This filter renders a SubStation Alpha (SSA) subtitle script\n"); printf(" into the video.\n"); printf("* Options\n"); printf(" 'file' SSA filename (required) [script.ssa]\n"); } int tc_filter(vframe_list_t *ptr, char *options) { static vob_t *vob = NULL; static ExceptionInfo exception; static ImageInfo *image_info = NULL; static Image *canvas_base = NULL; static Image *canvas_fore = NULL; static Image *canvas_back = NULL; static Image *canvas_mask = NULL; static PixelPacket *pixel_fore = NULL; static PixelPacket *pixel_back = NULL; static PixelPacket *pixel_mask = NULL; static char *yuv = NULL; static frameData *frames = NULL; static frameData *frame = NULL; static unsigned int frame_expire = 0; int col, row, i; // API explanation: // ================ // // (1) need more infos, than get pointer to transcode global // information structure vob_t as defined in transcode.h. // // (2) 'tc_get_vob' and 'verbose' are exported by transcode. // // (3) filter is called first time with TC_FILTER_INIT flag set. // // (4) make sure to exit immediately if context (video/audio) or // placement of call (pre/post) is not compatible with the filters // intended purpose, since the filter is called 4 times per frame. // // (5) see framebuffer.h for a complete list of frame_list_t variables. // // (6) filter is last time with TC_FILTER_CLOSE flag set //---------------------------------- // // filter get config // //---------------------------------- if (ptr->tag & TC_FILTER_GET_CONFIG && options) { char buf[255]; optstr_filter_desc(options, MOD_NAME, MOD_CAP, MOD_VERSION, MOD_AUTHOR, "VRYO", "1"); sprintf(buf, "%s", filename); optstr_param(options, "file", "SSA script file", "%s", buf); } //---------------------------------- // // filter init // //---------------------------------- if (ptr->tag & TC_FILTER_INIT) { char size[MaxTextExtent]; vob = tc_get_vob(); if (vob == NULL) return(-1); if (verbose) printf("[%s] %s %s\n", MOD_NAME, MOD_VERSION, MOD_CAP); // set default script strcpy(filename, "script.ssa"); // parse the options if (options != NULL) { if (verbose) printf("[%s] options=%s\n", MOD_NAME, options); optstr_get(options, "file", "%[^:]", &filename); if (optstr_get(options, "help", "") >= 0) help_optstr(); } if (verbose > 1) { printf(" SSA Settings:\n"); printf(" file = %s\n", filename); } // initialize ImageMagick and create a base Image InitializeMagick(""); GetExceptionInfo(&exception); image_info = CloneImageInfo((ImageInfo *) NULL); strcpy(image_info->filename, "xc:white"); FormatString(size, "%dx%d", vob->ex_v_width, vob->ex_v_height); CloneString(&image_info->size, size); canvas_base = ReadImage(image_info, &exception); if (canvas_base == NULL) { MagickWarning(exception.severity, exception.reason, exception.description); return(-1); } // TODO: open the file // TODO: parse the ssa // TODO: foreach style, set the data // TODO: foreach frame, set the data // !!! Just toss in some generic sample data std = (styleData *) malloc(sizeof(styleData)); if (std == NULL) return(-1); std->fadedur = 5; std->font = "@tahomabd.ttf"; std->fontsize = 24; std->next = NULL; frd = (frameData *) malloc(sizeof(frameData)); if (frd == NULL) return(-1); frd->style = std; frd->fbeg = 5; frd->fend = 35; frd->text = malloc(sizeof(char)*20); strcpy(frd->text, "testing"); frd->next = (frameData *) malloc(sizeof(frameData)); frd->next->style = std; frd->next->fbeg = 40; frd->next->fend = 60; frd->next->text = malloc(sizeof(char)*80); strcpy(frd->next->text, "testing testing testing\ntesting testing test"); frd->next->next = NULL; if (vob->im_v_codec == CODEC_YUV) { yuv = (char *) malloc(sizeof(char) * 3 * vob->ex_v_width * vob->ex_v_height); if (yuv == NULL) { fprintf(stderr, "[%s:%d] ERROR out of memory\n", __FILE__, __LINE__); return(-1); } if (tc_rgb2yuv_init(vob->ex_v_width, vob->ex_v_height) < 0) { fprintf(stderr, "[%s] ERROR rgb2yuv init failed\n", MOD_NAME); return(-1); } } // !!! end sample data // set initial frame location frames = frd; if (verbose) printf("[%s] Finished initialization\n", MOD_NAME); return(0); } //---------------------------------- // // filter close // //---------------------------------- if (ptr->tag & TC_FILTER_CLOSE) { if (verbose) printf("[%s] Starting shutdown\n", MOD_NAME); // cleanup canvas if (canvas_fore) DestroyImage(canvas_fore); if (canvas_back) DestroyImage(canvas_back); if (canvas_mask) DestroyImage(canvas_mask); // cleanup style data while (std) { styleData *temp = std->next; free(std); std = temp; } std = NULL; // cleanup frame data frd = frames; while (frd) { frameData *temp = frd->next; free(frd); frd = temp; } frd = NULL; // cleanup imagemagick data DestroyExceptionInfo(&exception); DestroyImage(canvas_base); DestroyImageInfo(image_info); DestroyMagick(); return(0); } //---------------------------------- // // filter frame routine // //---------------------------------- // tag variable indicates, if we are called before // transcodes internal video/audo frame processing routines // or after and determines video/audio context if ((ptr->tag & TC_POST_PROCESS) && (ptr->tag & TC_VIDEO)) { int seq; frameData *prev = NULL; DrawContext context_fore, context_back, context_mask; // any frames? if (frames == NULL) return(0); // any valid frames? if (ptr->id < frames->fbeg) return(0); if (verbose > 2) printf("[%s] frame %6d %s\n", MOD_NAME, ptr->id); // expire canvas forcefully if any of them are blank if (!canvas_fore || !canvas_back || !canvas_mask) frame_expire = ptr->id; // clear the canvas if (ptr->id >= frame_expire) { if (verbose > 2) printf("[%s] frame %6d expiring canvas\n", MOD_NAME, ptr->id); if (canvas_fore) { DestroyImage(canvas_fore); canvas_fore = NULL; pixel_fore = NULL; } if (canvas_back) { DestroyImage(canvas_back); canvas_back = NULL; pixel_back = NULL; } if (canvas_mask) { DestroyImage(canvas_mask); canvas_mask = NULL; pixel_mask = NULL; } if (verbose > 3) printf("[%s] frame %6d creating canvas\n", MOD_NAME, ptr->id); // create new canvas canvas_fore = CloneImage(canvas_base, 0, 0, 0, &exception); if (canvas_fore == NULL) { MagickWarning(exception.severity, exception.reason, exception.description); return(-1); } canvas_back = CloneImage(canvas_base, 0, 0, 0, &exception); if (canvas_back == NULL) { MagickWarning(exception.severity, exception.reason, exception.description); return(-1); } canvas_mask = CloneImage(canvas_base, 0, 0, 0, &exception); if (canvas_mask == NULL) { MagickWarning(exception.severity, exception.reason, exception.description); return(-1); } if (verbose > 3) printf("[%s] frame %6d drawing context\n", MOD_NAME, ptr->id); // create drawing context context_fore = DrawAllocateContext((DrawInfo *) NULL, canvas_fore); context_back = DrawAllocateContext((DrawInfo *) NULL, canvas_back); context_mask = DrawAllocateContext((DrawInfo *) NULL, canvas_mask); frame_expire = frames->fend; // determine when we should expire the canvas frame = frames; while (frame && (frame->fbeg <= ptr->id)) { if (frame->style) { DrawPushGraphicContext(context_fore); { if (frame->style->font) DrawSetFont(context_fore, frame->style->font); if (frame->style->fontsize) DrawSetFontSize(context_fore, frame->style->fontsize); DrawSetFillColorString(context_fore, "#ffffff"); DrawSetGravity(context_fore, SouthGravity); DrawAnnotation(context_fore, 0, 40, frame->text); } DrawPopGraphicContext(context_fore); DrawPushGraphicContext(context_back); { if (frame->style->font) DrawSetFont(context_back, frame->style->font); if (frame->style->fontsize) DrawSetFontSize(context_back, frame->style->fontsize); DrawSetStrokeWidth(context_back, 4); DrawSetStrokeColorString(context_back, "#000000"); DrawSetFillColorString(context_back, "#000000"); DrawSetGravity(context_back, SouthGravity); DrawAnnotation(context_back, 0, 40, frame->text); } DrawPopGraphicContext(context_back); DrawPushGraphicContext(context_mask); { if (frame->style->font) DrawSetFont(context_mask, frame->style->font); if (frame->style->fontsize) DrawSetFontSize(context_mask, frame->style->fontsize); DrawSetFillColorString(context_mask, "#00000"); DrawSetGravity(context_mask, SouthGravity); DrawAnnotation(context_mask, 0, 40, frame->text); } DrawPopGraphicContext(context_mask); } // end-if // TODO: if karaoke frame, set expire every frame of karaoke if (frame->fend < frame_expire) frame_expire = frame->fend; frame = frame->next; } // end-while if (frame && (frame->fbeg < frame_expire)) frame_expire = frame->fbeg; // draw the images DrawRender(context_fore); DrawRender(context_back); DrawRender(context_mask); // don't need drawing context anymore DrawDestroyContext(context_fore); DrawDestroyContext(context_back); DrawDestroyContext(context_mask); // blur the back canvas to make shadowy thing BlurImage(canvas_back, 0, 1, &exception); if (verbose > 3) printf("[%s] frame %6d finished drawing context\n", MOD_NAME, ptr->id); // if we're in YUV mode, we need to convert to a yuv structure if (vob->im_v_codec == CODEC_YUV) { // process the foreground to YUV for (row = 0; row < vob->ex_v_height; row++) { pixel_fore = GetImagePixels(canvas_fore, 0, vob->ex_v_height - row - 1, vob->ex_v_width, 1); for (col = 0; col < vob->ex_v_width; col++) { int off = (row * vob->ex_v_width + col) * 3; int r = pixel_fore[col].red >> 8; int g = pixel_fore[col].green >> 8; int b = pixel_fore[col].blue >> 8; yuv[off +0] = pixel_fore[col].blue; yuv[off +1] = pixel_fore[col].green; yuv[off +2] = pixel_fore[col].red; } } if (tc_rgb2yuv_core(yuv) < 0) { fprintf("[%s] ERROR rgb2yuv conversion failed\n", MOD_NAME); return(-1); } } if (verbose) printf("[%s] frame %6d frame_expire set to %d\n", MOD_NAME, ptr->id, frame_expire); } // process the buffer if (vob->im_v_codec == CODEC_RGB) { unsigned char *buf = ptr->video_buf; int buf_off, pak_off; int bmask, fmask; if (verbose > 2) printf("[%s] frame %6d processing rgb\n", MOD_NAME, ptr->id); for (row = 0; row < vob->ex_v_height; row++) { pixel_fore = GetImagePixels(canvas_fore, 0, vob->ex_v_height - row - 1, vob->ex_v_width, 1); pixel_back = GetImagePixels(canvas_back, 0, vob->ex_v_height - row - 1, vob->ex_v_width, 1); pixel_mask = GetImagePixels(canvas_mask, 0, vob->ex_v_height - row - 1, vob->ex_v_width, 1); for (col = 0; col < vob->ex_v_width; col++) { buf_off = (row * vob->ex_v_width + col) * 3; pak_off = (col); // ForEach Pixel // (1) Overlay the background onto the buffer // (2) Mask the foreground onto the buffer // reminder: 255 is WHITE bmask = pixel_back[pak_off].red >> 8; fmask = pixel_mask[pak_off].red >> 8; // TODO: handle fade-in/out // background "shadow" (grayscale) if (bmask < 255) { buf[buf_off +0] = BLEND(0, buf[buf_off +0], bmask); buf[buf_off +1] = BLEND(0, buf[buf_off +1], bmask); buf[buf_off +2] = BLEND(0, buf[buf_off +2], bmask); } // foreground (color) using mask (grayscale) if (fmask < 255) { buf[buf_off +0] = BLEND(pixel_fore[pak_off].red >> 8, 0, fmask); buf[buf_off +1] = BLEND(pixel_fore[pak_off].green >> 8, 0, fmask); buf[buf_off +2] = BLEND(pixel_fore[pak_off].blue >> 8, 0, fmask); } } } } else // CODEC_YUV { /* I don't think any of this works... */ unsigned char *buf = ptr->video_buf; int buf_off, pak_off, yuv_off; int bmask, fmask; unsigned char *p1, *p2; unsigned char *y1, *y2; int craddr = (vob->ex_v_width * vob->ex_v_height); int cbaddr = (vob->ex_v_width * vob->ex_v_height) * 5 / 4; if (verbose > 2) printf("[%s] frame %6d processing yuv\n", MOD_NAME, ptr->id); // Y' for (row = 0; row < vob->ex_v_height; row++) { pixel_back = GetImagePixels(canvas_back, 0, row, vob->ex_v_width, 1); pixel_mask = GetImagePixels(canvas_mask, 0, row, vob->ex_v_width, 1); for (col = 0; col < vob->ex_v_width; col++) { yuv_off = (row * vob->ex_v_width + col); buf_off = (row * vob->ex_v_width + col); pak_off = (col); bmask = pixel_back[pak_off].red >> 8; fmask = pixel_mask[pak_off].red >> 8; // TODO: handle fade-in/out // (linear fade-in/out. therefore, just divide by the num of frames) // shadow is luminance adjuster. blend them if (bmask < 255) { buf[buf_off] = BLEND(0, buf[buf_off], bmask); } // foreground. use the set value. if (fmask < 255) { // buf[buf_off] = BLEND(yuv[yuv_off], 16, fmask); buf[buf_off] = 255 - fmask; } } } // Cb, Cr p1 = buf + craddr; y1 = yuv + craddr; p2 = buf + cbaddr; y2 = yuv + cbaddr; for (row = 0; row < vob->ex_v_height / 2; row++) { pixel_back = GetImagePixels(canvas_back, 0, row * 2, vob->ex_v_width, 1); pixel_mask = GetImagePixels(canvas_mask, 0, row * 2, vob->ex_v_width, 1); for (col = 0; col < vob->ex_v_width / 2; col++) { pak_off = col * 2; bmask = pixel_back[pak_off].red >> 8; fmask = pixel_mask[pak_off].red >> 8; // TODO: handle fade-in/out // background only affects luminance /* if (bmask < 255) { p1[col] = BLEND(128, p1[col], bmask); p2[col] = BLEND(128, p2[col], bmask); } */ // foreground... if (fmask < 255) { /* p1[col] = BLEND(y1[col], 128, fmask); p2[col] = BLEND(y2[col], 128, fmask); */ p1[col] = y1[col]; p2[col] = y2[col]; } } p1 += vob->ex_v_width / 2; y1 += vob->ex_v_width / 2; p2 += vob->ex_v_width / 2; y2 += vob->ex_v_width / 2; } } if (verbose > 2) printf("[%s] frame %6d cleaning frames\n", MOD_NAME, ptr->id); // loop over all the frames and remove the unneeded frame = frames; prev = NULL; while (frame && (frame->fbeg < ptr->id)) { // we've passed the point where the frame is useful if (frame->fend <= ptr->id) { frameData *temp = frame->next; if (prev == NULL) frames = frame->next; else prev->next = frame->next; if (verbose > 2) printf("[%s] frame %6d removing (%d-%d) %s\n", MOD_NAME, ptr->id, frame->fbeg, frame->fend); // cleanup the frame free(frame); frame = temp; } // otherwise, next frame else { prev = frame; frame = frame->next; } } if (verbose) printf("[%s] frame %d done\n", MOD_NAME, ptr->id); return(0); } // everything else return (0); }