/* * 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: * text stacking * karaoke stuffs * Script File Reading * Script File Parsing * (Support SSA and Simple Idiotic Script (SIS)) * YUV * * 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 RGB mode works! * 2003-03-06 : Image processing too slow. * 2003-03-08 : New processing method. Prelim planning of parsing. * * * 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. * * * * KSS (Keep (it) Simple Script) Format: * | KSS 1.0 * | # file _MUST_ start with the KSS header * | # STYLE * | # (align: T [top] B [bottom] M [middle]) * | # (color: hex value or text-string) * | STYLE default helvetica 12 C #ffffff white * | STYLE truetype @file.ttf 20 B #ff0000 none * | * | # - * | # start-end: start and end frame numbers * | # white space before/after is trimmed * | FRAME 0-9 this is some text * | FRAME 010-90 some more text * | FRAME 40-080 other text * */ #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 IsTrans(x) (ScaleQuantumToChar(x.opacity) == 255) // a = 0 ==> d // a = 255 ==> s #define Blend(s,d,a) ( ( ( (a) * ( (s) - (d) ) ) >> 8 ) + (d) ) //-------------------------------------------------------------------- //-------------------------------------------------------------------- // data structures //-------------------------------------------------------------------- //-------------------------------------------------------------------- typedef enum scriptType { FILE_UNKNOWN, FILE_SSA, FILE_KSS, } scriptType; typedef struct styleData { char *id; int fadedur; DrawInfo *draw_fore; DrawInfo *draw_back; PixelPacket fore_color1; PixelPacket back_color1; unsigned char opacity; // default 0 == show (opaque) struct styleData *next; } styleData; typedef struct frameData { unsigned int fbeg; unsigned int fend; int posx; int posy; int height; char *text; int fadein, fadeout; styleData *style; unsigned char *fore_mask; unsigned char *back_mask; struct frameData *next; } frameData; //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- // global variables //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- static ExceptionInfo exception; static PixelPacket color_black; static PixelPacket color_white; static Image *canvas = NULL; static ImageInfo *iinfo = NULL; static styleData *std = NULL; static frameData *frd = NULL; static int ending_frame = 0; static frameData *end_frame = NULL; static styleData *default_style = NULL; static char *parse; //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- // helper functions //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- //============================================================================= // add a style to the list //============================================================================= static void add_style(styleData *style) { if (style == NULL) return; style->next = std; std = style; } //============================================================================= // find a style //============================================================================= static styleData * find_style(const char *id) { styleData *style = std; while (style) { if (!strcasecmp(style->id, id)) return style; style = style->next; } } //============================================================================= // initialize styleinfo //============================================================================= static styleData * init_style(styleData **style) { if (*style == NULL) { *style = (styleData *) malloc(sizeof(styleData)); if (*style == NULL) return(NULL); memset(*style, 0, sizeof(styleData)); } (*style)->next = NULL; (*style)->id = NULL; (*style)->fadedur = 0; (*style)->opacity = 0; (*style)->fore_color1 = color_white; (*style)->back_color1 = color_black; if ((*style)->draw_fore) DestroyDrawInfo((*style)->draw_fore); (*style)->draw_fore = CloneDrawInfo(iinfo, NULL); if ((*style)->draw_fore == NULL) { free(*style); return(NULL); } (*style)->draw_fore->pointsize = 18; (*style)->draw_fore->gravity = NorthGravity; (*style)->draw_fore->fill = color_black; if ((*style)->draw_back) DestroyDrawInfo((*style)->draw_back); (*style)->draw_back = CloneDrawInfo(iinfo, (*style)->draw_fore); if ((*style)->draw_back == NULL) { DestroyDrawInfo((*style)->draw_fore); free(*style); return(NULL); } (*style)->draw_back->stroke_width = 4; (*style)->draw_back->stroke = color_black; return(*style); } //============================================================================= // add a frame to the list //============================================================================= static void add_frame(frameData *frame) { if (frame == NULL) return; if (frd == NULL || (frame->fbeg <= frd->fbeg)) { frame->next = frd; frd = frame; end_frame = frame; ending_frame = frame->fbeg; } else if (frame->fbeg >= ending_frame) { end_frame->next = frame; end_frame = frame; ending_frame = frame->fbeg; } else { frameData *f = frd; frameData *p = NULL; while (f && (f->fbeg < frame->fbeg)) { p = f; f = f->next; } if (p == NULL) { frame->next = frd; frd = frame; } else { frame->next = p->next; p->next = frame; } } #if 0 { frameData *f = frd; while (f) { printf(".. 0x%x .. %s\n", f, f->text); f = f->next; } } #endif return; } //============================================================================= // initialize frameinfo //============================================================================= static frameData * init_frame(frameData **frame, styleData *style) { if (*frame == NULL) { *frame = (frameData *) malloc(sizeof(frameData)); if (*frame == NULL) return(NULL); memset(*frame, 0, sizeof(frameData)); } if (style == NULL) style = default_style; (*frame)->next = NULL; (*frame)->style = style; (*frame)->fbeg = 0; (*frame)->fend = 0; (*frame)->fadein = style->fadedur; (*frame)->fadeout = style->fadedur; (*frame)->fore_mask = NULL; (*frame)->back_mask = NULL; (*frame)->text = NULL; return(*frame); } //============================================================================= // parse argument //============================================================================= static char *parsearg(char *str) { char *start, *s; if (!str) str = parse; // trim leading spaces while (isspace(*str)) str++; // locate the end of the argument or string start = str; while (*str && !strchr(",\n", *str)) str++; // we were already at the end of the buffer if ((!*str || *str == '\n') && (str == start)) return NULL; s = str; // if we haven't reached the end, set the actual start position if (*str) str++; // trim off the ending spaces while (s > start && isspace(s[-1])) s--; // set the end-of-string *s = '\0'; // save the position for the next parse call parse = str; return start; } static char *parseargfinish() { return parse; } //============================================================================= // convert SSA color to ImageMagick //============================================================================= PixelPacket parsecolor(const char *str) { PixelPacket color; unsigned long value; if (str[0] == '&' && toupper(str[1]) == 'H') { value = strtoul(str+2, NULL, 16); } else { value = strtoul(str, NULL, 0); } color.opacity = 0; // color is in BGR order color.red = value & 0xFF; value >>= 8; color.green = value & 0xFF; value >>= 8; color.blue = value & 0xFF; return color; } //============================================================================= // parse script file //============================================================================= static int parse_script(char *filename, ImageInfo *iinfo) { FILE *fp = NULL; char buf[8192]; char *ptr = NULL; styleData *newstyle = NULL; frameData *newframe = NULL; scriptType type = FILE_UNKNOWN; int version = 0; if (init_style(&default_style) == NULL) { if (verbose) printf("[%s] could not initialize default style\n", MOD_NAME); return(-1); } add_style(default_style); // determine extension, if any ptr = filename + strlen(filename) - 4; if (ptr < filename) ptr = filename; // check filename for assumption of file type if (!strncasecmp(ptr, ".kss", 4)) { type = FILE_KSS; } else if (!strncasecmp(ptr, ".ssa", 4)) { type = FILE_SSA; } // get the file, maybe fp = fopen(filename, "r"); if (fp == NULL) { if (verbose) printf("[%s] could not open file: %s\n", MOD_NAME, filename); return(-1); } // process the file (this may get ugly....) while (fgets(buf, sizeof(buf), fp)) { // unknown file type. determine it if (type == FILE_UNKNOWN) { // comment if ((buf[0] == '#') || (buf[0] == ';')) continue; // KSS files ALWAYS start with a header if (!strncasecmp(buf, "KSS 1.", 6)) { type = FILE_KSS; version = 1; } // everything else is a SSA file else { type = FILE_SSA; } } // if-end FILE_UNKNOWN // KSS file else if (type == FILE_KSS) { // comment if (buf[0] == '#') continue; // style line if (!strncasecmp(buf, "STYLE", 5)) { // STYLE const char *token; int i; if (init_style(&newstyle) == NULL) { if (verbose) printf("[%s] could not allocate style info\n", MOD_NAME); return(-1); } // ... // TODO } // subtitle frame else if (!strncasecmp(buf, "FRAME", 5)) { const char *token; int i; // TODO } } // if-end FILE_KSS // SSA file else if (type == FILE_SSA) { // comment if (buf[0] == ';') continue; // TODO } // if-end FILE_SSA reject: // ignore-path ; } /* std = (styleData *) malloc(sizeof(styleData)); std->fadedur = 5; std->opacity = 0; QueryColorDatabase("white", &std->fore_color1, &exception); QueryColorDatabase("black", &std->back_color1, &exception); std->draw_fore = CloneDrawInfo(iinfo, NULL); std->draw_fore->pointsize = 24; std->draw_fore->font = AllocateString("@tahomabd.ttf"); std->draw_fore->gravity = SouthGravity; std->draw_fore->fill = color_black; std->draw_back = CloneDrawInfo(iinfo, std->draw_fore); std->draw_back->stroke_width = 4; std->draw_back->stroke = color_black; std->next = NULL; */ init_frame(&newframe, NULL); newframe->text = AllocateString("testing 1 2 3"); newframe->fbeg = 5; newframe->fend = 50; newframe->fadeout = 10; add_frame(newframe); newframe = NULL; init_frame(&newframe, NULL); newframe->text = AllocateString("testing 1 2 3 4 5"); newframe->fbeg = 50; newframe->fend = 90; newframe->fadein = 5; newframe->fadeout = 30; add_frame(newframe); newframe = NULL; init_frame(&newframe, NULL); newframe->text = AllocateString("testing"); newframe->fbeg = 100; newframe->fend = 120; add_frame(newframe); newframe = NULL; init_frame(&newframe, NULL); newframe->text = AllocateString("test"); newframe->fbeg = 90; newframe->fend = 100; add_frame(newframe); newframe = NULL; return(0); } //============================================================================= // output options //============================================================================= 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"); } //============================================================================= // process frame (RGB) //============================================================================= static int process_rgb(unsigned char *buf, frameData *frame, unsigned char opacity, unsigned int width, unsigned int height) { unsigned int row, col; styleData *style = frame->style; int fo = 255; // default to transparent int bo = 255; // default to transparent if (opacity == 255) return(0); if (verbose > 2) printf("[%s] processing RGB frame\n", MOD_NAME); // loop over the framebuffer for (row = 0; row < height; row++) { for (col = 0; col < width; col++) { int buf_off = (row * width + col) * 3; int pix_off = (row * width + col); // adjust the pixel opacity by the render opacity if (frame->fore_mask) fo = Blend(255, frame->fore_mask[pix_off], opacity); if (frame->back_mask) bo = Blend(255, frame->back_mask[pix_off], opacity); // draw the shadow, if needed if ((bo < 255) && !IsTrans(frame->style->back_color1)) { buf[buf_off +0] = Blend(buf[buf_off +0], ScaleQuantumToChar(style->back_color1.blue), bo); buf[buf_off +1] = Blend(buf[buf_off +1], ScaleQuantumToChar(style->back_color1.green), bo); buf[buf_off +2] = Blend(buf[buf_off +2], ScaleQuantumToChar(style->back_color1.red), bo); } // draw the front text if ((fo < 255) && !IsTrans(frame->style->fore_color1)) { buf[buf_off +0] = Blend(buf[buf_off +0], ScaleQuantumToChar(style->fore_color1.blue), fo); buf[buf_off +1] = Blend(buf[buf_off +1], ScaleQuantumToChar(style->fore_color1.green), fo); buf[buf_off +2] = Blend(buf[buf_off +2], ScaleQuantumToChar(style->fore_color1.red), fo); } } } return(0); } //-------------------------------------------------------------------- //-------------------------------------------------------------------- // single function interface //-------------------------------------------------------------------- //-------------------------------------------------------------------- int tc_filter(vframe_list_t *ptr, char *options) { char junkbuf[MaxTextExtent]; static vob_t *vob = NULL; static char filename[MaxTextExtent]; static unsigned int size; //------------------------------------------------------------------ // filter get config //------------------------------------------------------------------ if (ptr->tag & TC_FILTER_GET_CONFIG && options) { // V - Video R - RGB Y - YUA M - MultInstance O - POST optstr_filter_desc(options, MOD_NAME, MOD_CAP, MOD_VERSION, MOD_AUTHOR, "VRO", "1"); sprintf(junkbuf, "%s", filename); optstr_param(options, "file", "SSA script file", "%s", junkbuf); } //------------------------------------------------------------------ // filter init //------------------------------------------------------------------ if (ptr->tag & TC_FILTER_INIT) { static styleData *style = NULL; // get video information vob = tc_get_vob(); if (vob == NULL) return(-1); size = vob->ex_v_width * vob->ex_v_height; if (verbose) printf("[%s] %s %s\n", MOD_NAME, MOD_VERSION, MOD_CAP); // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! if (vob->im_v_codec == CODEC_YUV && verbose) { printf("[%s] NO YUV YET (may segfault)\n", MOD_NAME); } // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // set default script filename 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(); } // show the options that are set if (verbose > 1) { printf(" SSA Settings:\n"); printf(" file = %s\n", filename); } // initialize ImageMagick InitializeMagick(""); GetExceptionInfo(&exception); QueryColorDatabase("black", &color_black, &exception); QueryColorDatabase("white", &color_white, &exception); // set up image info, draw info, and image itself iinfo = CloneImageInfo(NULL); if (iinfo == NULL) { if (verbose) printf("[%s] could not initialize imagemagick stuff\n", MOD_NAME); return(-1); } strcpy(iinfo->filename, "xc:white"); FormatString(junkbuf, "%dx%d", vob->ex_v_width, vob->ex_v_height); CloneString(&iinfo->size, junkbuf); canvas = ReadImage(iinfo, &exception); if (canvas == NULL) { if (verbose) printf("[%s] could not initialize imagemagick stuff\n", MOD_NAME); return(-1); } // open and parse the scriptfile if (parse_script(filename, iinfo) < 0) { if (verbose) printf("[%s] could not parse script\n", MOD_NAME); return(-1); } // initialize YUV data structures if (vob->im_v_codec == CODEC_YUV) { // TODO } if (verbose > 2) printf("[%s] Finished initialization\n", MOD_NAME); return(0); } //--------------------------------------------------------------------------- // filter close //--------------------------------------------------------------------------- if (ptr->tag & TC_FILTER_CLOSE) { if (verbose > 2) printf("[%s] Starting shutdown\n", MOD_NAME); // clean style data while (std) { styleData *style = std->next; if (std->draw_fore) DestroyDrawInfo(std->draw_fore); if (std->draw_back) DestroyDrawInfo(std->draw_back); free(style); std = style; } std = NULL; // clean frame data while (frd) { frameData *frame = frd->next; if (frame->text) free(frame->text); if (frame->fore_mask) free(frame->fore_mask); if (frame->back_mask) free(frame->back_mask); free(frame); frd = frame; } frd = NULL; // clean ImageMagick data DestroyImage(canvas); DestroyExceptionInfo(&exception); DestroyImageInfo(iinfo); DestroyMagick(); return(0); } //--------------------------------------------------------------------------- // filter post-process video //--------------------------------------------------------------------------- if ((ptr->tag & TC_POST_PROCESS) && (ptr->tag & TC_VIDEO)) { frameData *prev = NULL; frameData *frame = NULL; PixelPacket *pixel = NULL; unsigned int top_disp = 0; unsigned int bottom_disp = 0; unsigned int i, j; // any frames to process? if (frd == NULL || (ptr->id < frd->fbeg)) return(0); if (verbose > 2) printf("[%s] starting post-process of frame %d\n", MOD_NAME, ptr->id); // loop over applicable frames frame = frd; while (frame && (ptr->id >= frame->fbeg)) { // frame in range if (ptr->id < frame->fend) { unsigned char opacity = frame->style->opacity; // fade-in if ((ptr->id - frame->fbeg) < frame->fadein) { opacity = Blend(opacity, 255, 255 * (ptr->id - frame->fbeg) / frame->fadein); } // fade-out if ((frame->fend - ptr->id) < frame->fadeout) { opacity = Blend(opacity, 255, 255 * (frame->fend - ptr->id) / frame->fadeout); } if (verbose > 2) printf("[%s] setting render opacity at %d\n", MOD_NAME, opacity); // text mask not generated yet if ((frame->fore_mask == NULL) && (frame->style->draw_fore != NULL) && !IsTrans(frame->style->fore_color1)) { // set up for drawing frame->style->draw_fore->text = frame->text; frame->style->draw_fore->geometry = "+0+40"; AnnotateImage(canvas, frame->style->draw_fore); // clean up frame->style->draw_fore->text = NULL; frame->style->draw_back->geometry = NULL; // set up the alphamask frame->fore_mask = malloc(sizeof(char) * size); if (frame->fore_mask == NULL) { if (verbose) printf("[%s] could not allocate frame fore mask\n", MOD_NAME); return(-1); } // copy it to the mask for (i = 0; i < vob->ex_v_height; i++) { pixel = GetImagePixels(canvas, 0, vob->ex_v_height - 1 - i, vob->ex_v_width, 1); for (j = 0; j < vob->ex_v_width; j++) { unsigned int off = (i * vob->ex_v_width) + j; // copy the mask frame->fore_mask[off] = ScaleQuantumToChar(pixel[j].red); // reset the pixel to white pixel[j] = color_white; } } } // background mask not generated yet, and we need it if ((frame->back_mask == NULL) && (frame->style->draw_back != NULL) && !IsTrans(frame->style->back_color1)) { // set up for drawing frame->style->draw_back->text = frame->text; frame->style->draw_back->geometry = "+0+40"; AnnotateImage(canvas, frame->style->draw_back); BlurImage(canvas, 0, 1, &exception); // clean up frame->style->draw_back->text = NULL; frame->style->draw_back->geometry = NULL; // set up the alphamask frame->back_mask = malloc(sizeof(char) * size); if (frame->back_mask == NULL) { if (verbose) printf("[%s] could not allocate frame back mask\n", MOD_NAME); return(-1); } // copy it to the mask for (i = 0; i < vob->ex_v_height; i++) { pixel = GetImagePixels(canvas, 0, vob->ex_v_height - 1 - i, vob->ex_v_width, 1); for (j = 0; j < vob->ex_v_width; j++) { unsigned int off = (i * vob->ex_v_width) + j; // copy the mask frame->back_mask[off] = ScaleQuantumToChar(pixel[j].red); // reset the pixel to white pixel[j] = color_white; } } } // RGB frame if (vob->im_v_codec == CODEC_RGB) { if (process_rgb(ptr->video_buf, frame, opacity, vob->ex_v_width, vob->ex_v_height) < 0) { if (verbose) printf("[%s] error processing RGB frame\n", MOD_NAME); return(-1); } } // YUV frame else { // TODO } prev = frame; frame = frame->next; } // frame out of range. kill it else { frameData *temp = frame->next; if (verbose > 2) printf("[%s] removing frame %d (%d-%d)\n", MOD_NAME, ptr->id, frame->fbeg, frame->fend); if (prev == NULL) frd = frame->next; else prev->next = frame->next; if (frame->text) free(frame->text); if (frame->fore_mask) free(frame->fore_mask); if (frame->back_mask) free(frame->back_mask); free(frame); frame = temp; } // end of loop. next frame } if (verbose > 2) printf("[%s] finished frame %d\n", MOD_NAME, ptr->id); return(0); } // everything else return (0); }