/* * 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) * * Notes: * - Files containing UTF-8 are NOT to be prefixed with a BOM. * Rather, you should use the POSIX locale setting of UTF-8. * (ImageMagick can usually figure things out even without the * locale settings set.) * * To Do: * - karaoke stuffs (requires redesign of frame handling. v2.0) * - SSA Script File Parsing * - SubRip File Parsing (.srt) * - YUV Mode * * 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 : 0.1 Initial * 2003-03-10 : 0.2.1 KSS file 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. * */ #define MOD_NAME "filter_ssa.so" #define MOD_VERSION "v0.2.1 (2003-03-12)" #define MOD_CAP "substation alpha subtitling" #define MOD_AUTHOR "Ted Lin" #include #include #include #include #include // for debugging purposes //#define DEBUGMODE //-------------------------------------------------------------------- //-------------------------------------------------------------------- // 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 // all chars (unsigned) #define Blend(s,d,a) ( ( ( (a) * ( (s) - (d) ) ) >> 8 ) + (d) ) //-------------------------------------------------------------------- //-------------------------------------------------------------------- // data structures //-------------------------------------------------------------------- //-------------------------------------------------------------------- typedef enum scriptType { FILE_UNKNOWN, // unknown style FILE_SSA, // SubStation Alpha FILE_KSS, // Keep (it) Simple Script } scriptType; typedef struct styleData { char *id; // style ID int fadedur; // default fade duration for style int offset; // base text offset DrawInfo *draw_fore; // text drawstuff for imagemagick DrawInfo *draw_back; // shadow drawstuff for imagemagick PixelPacket fore_color1; // text color (primary) PixelPacket back_color1; // shadow color (primary) unsigned char opacity; // default 0 == show fully (opaque) struct styleData *next; // next style in list } styleData; typedef struct frameData { unsigned int fbeg; // frame start time unsigned int fend; // frame end time (frame is not shown) int absolute; // absolute positioned int posx; // alignment offset: horizontal int posy; // alignment offset: vertical int height; // height of the bounding box char *text; // the actual text string int fadein; // fade-in frame count int fadeout; // fade-out frame count styleData *style; // associated style unsigned char *fore_mask; // foreground/text alpha mask unsigned char *back_mask; // background/shadow alpha mask struct frameData *next; // next frame in list } frameData; //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- // global variables //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- static unsigned int width, height; static double fps; static int timeres = 0; static int frameshift = 0; 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 styleData *last_used_style = 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; #ifdef DEBUGMODE // debugging output { printf("\nstyles on the list:\n"); styleData *s = std; while (s) { printf("* 0x%x %s\n", s, s->id); s = s->next; } } #endif } //============================================================================= // find a style //============================================================================= static styleData * find_style(const char *id) { styleData *style = std; // nothing supplied, give the default if (id == NULL) return default_style; // generally, the same style is used repeatedly if (last_used_style && !strcasecmp(last_used_style->id, id)) return last_used_style; // otherwise, search for it while (style) { if (style->id && !strcasecmp(style->id, id)) { last_used_style = style; return style; } style = style->next; } // cannot find style, so just give the default one return default_style; } //============================================================================= // 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)); } // initialize style basic stuff (*style)->next = NULL; if ((*style)->id) free((*style)->id); (*style)->id = NULL; (*style)->fadedur = 0; (*style)->opacity = 0; (*style)->fore_color1 = color_white; (*style)->back_color1 = color_black; // initialize foreground mask if ((*style)->draw_fore) DestroyDrawInfo((*style)->draw_fore); (*style)->draw_fore = CloneDrawInfo(iinfo, NULL); if ((*style)->draw_fore == NULL) { free(*style); *style = NULL; return(NULL); } // foreground mask default settings (*style)->draw_fore->pointsize = 18; (*style)->draw_fore->gravity = SouthGravity; (*style)->draw_fore->fill = color_black; (*style)->draw_fore->font = NULL; // initialize background/shadow mask 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); *style = NULL; return(NULL); } // shadow mask forced settings (*style)->draw_back->stroke_width = 4; (*style)->draw_back->stroke = color_black; // default base offset is based off the pointsize. REMEMBER! (*style)->offset = (*style)->draw_fore->pointsize * 3 / 2; return(*style); } //============================================================================= // add a frame to the list // frames are ordered by time, further organized by longer lasting, // and then by longer text-strings //============================================================================= static void add_frame(frameData *frame) { if (frame == NULL) return; // frame doesn't exist, or is before the head of the list if (frd == NULL || (frame->fbeg < frd->fbeg)) { frame->next = frd; frd = frame; } // frame is after the end of the list else if (frame->fbeg > ending_frame) { end_frame->next = frame; } // frame is somewhere in the middle else { frameData *f = frd; frameData *p = NULL; while (f && f->fbeg <= frame->fbeg) { // things lasting longer and starting at same time go first if (f->fbeg == frame->fbeg && f->fend < frame->fend) { break; } // longer lines go first, in the case of same time start/end if ((f->fbeg == frame->fbeg && f->fend == frame->fend) && (strlen(f->text) < strlen(frame->text))) { break; } p = f; f = f->next; } if (p == NULL) { frame->next = frd; frd = frame; } else { frame->next = p->next; p->next = frame; } } if (frame->next == NULL) { end_frame = frame; ending_frame = frame->fbeg; } #ifdef DEBUGMODE // debugging output { printf("\nframes on the list:\n"); frameData *f = frd; while (f) { printf("* 0x%x (%5d-%5d)\n", f, f->fbeg, f->fend); 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)->absolute = 0; (*frame)->posx = 0; (*frame)->posy = 0; (*frame)->height = 0; (*frame)->fore_mask = NULL; (*frame)->back_mask = NULL; (*frame)->text = NULL; return(*frame); } //============================================================================= // align frames //============================================================================= void align_frames() { frameData *f = frd; frameData *p = NULL; // where to start aligning frames from if (verbose > 1) printf("[%s] aligning subtitle lines\n", MOD_NAME); // loop over the frames. if things overlap, then position/realign them // but only if it is north/south aligned p = f; while (f) { // first, word wrap the lines (even if karaoke) if (f->style->draw_fore->gravity == NorthGravity || f->style->draw_fore->gravity == SouthGravity || f->style->draw_fore->gravity == CenterGravity) { char buf[512]; char *str = f->text; char *tmp = buf; int off = 0; int newlines = 0; TypeMetric metrics; buf[off] = '\0'; f->style->draw_back->text = tmp; // iterate over the string, adding words and newlines as necessary while (*str != '\0') { // copy until space or newline while ((*str != '\0') && (strchr(" \n", *str) == NULL)) { tmp[off++] = *str; str++; } tmp[off] = '\0'; // get the width of the string GetTypeMetrics(canvas, f->style->draw_back, &metrics); // too wide, insert a \n at the first space before the end if (metrics.width > (width - metrics.ascent - 4)) { char *space = strrchr(tmp, ' '); if (space) { *space = '\n'; tmp = space + 1; off = strlen(tmp); f->style->draw_back->text = tmp; newlines++; } } // newline count and reset if (*str == '\n') { tmp[off++] = '\n'; tmp[off] = '\0'; tmp += off; off = 0; f->style->draw_back->text = tmp; newlines++; } // copy the space over else if (*str == ' ') { tmp[off++] = ' '; tmp[off] = '\0'; } if (*str != '\0') str++; } // get the final metric. GetTypeMetrics(canvas, f->style->draw_back, &metrics); f->height = metrics.height * (newlines + 1); // finished. free, and copy the new string in f->style->draw_back->text = NULL; free(f->text); f->text = AllocateString(buf); } // determine where to start frame aligning from while (p && p->fend <= f->fbeg) { p = p->next; } // only adjust if north/south aligned if (f->absolute == 0 && (f->style->draw_fore->gravity == NorthGravity || f->style->draw_fore->gravity == SouthGravity)) { frameData *t = p; int realignmin = height; int realignmax = 0; while (t && (t != f)) { // only align to things that are stackable if (t->absolute == 0 && t->style->draw_fore->gravity == f->style->draw_fore->gravity) { // only align to things that are on-screen if (t->fend > f->fbeg) { if ((t->posy - t->style->offset) < realignmin) realignmin = t->posy - t->style->offset; if ((t->posy - t->style->offset + t->height) > realignmax) realignmax = t->posy - t->style->offset + t->height; } } t = t->next; } // nothing on screen if (realignmin == height) { realignmin = 0; realignmax = 0; } // place it if ((realignmin - f->height) >= 0) f->posy = f->style->offset + 0; else f->posy = f->style->offset + realignmax; } #ifdef DEBUGMODE { printf("[%5d-%5d] (%3dx%3d) %3d\n", f->fbeg, f->fend, f->posx, f->posy, f->height); } #endif f = f->next; } } //============================================================================= // parse argument (KSS) //============================================================================= static char *parseargkss(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; } //============================================================================= // parse argument (SSA) //============================================================================= static char *parseargssa(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 SMPTE time code to a frame number //============================================================================= static unsigned int parsesmpte(char *str) { // [[[hh:]mm:]ss.]ff char *s = NULL; char *t = NULL; double count = 0; if (!str) return 0; // first check if string is valid s = str; while (*s) { if (!(*s == ':' || *s == '.' || (*s >= '0' && *s <= '9'))) return 0; s++; } // decipher the number s = str; count += strtol(s, &t, 10); while ((t != NULL) && (s != t) && (*t != '\0')) { switch (*t) { // number read was either hour or minute case ':': count *= 60; break; // number read was second case '.': count *= timeres; break; default: break; } s = t + 1; count += strtol(s, &t, 10); } return (count * fps / timeres); } //============================================================================= // convert SSA color to ImageMagick //============================================================================= static 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)) { // cannot assume KSS file version type = FILE_UNKNOWN; } 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 1.x file else if (type == FILE_KSS && (version == 1)) { // comment if (buf[0] == '#') continue; // setting if (!strncasecmp(buf, "SET", 3)) { const char *token; if (!(token = parseargkss(buf + 3))) goto reject; // TIME RESOLUTION if (!strncasecmp(token, "time_resolution", 15)) { if (!(token = parseargkss(NULL))) goto reject; timeres = atoi(token); } // end-if TIME RESOLUTION } // end-if SET // time shift if (!strncasecmp(buf, "TIMESHIFT", 9)) { // TIMESHIFT