view libass/ass_parse.c @ 33263:5f527a9a9521

Add an exit function. This function will allow performing clean-up operations. (MPlayer calls guiDone() before exiting, but only if the GUI has been initialized, i.e. if guiInit() has been called successfully. Any exit_player()/exit_player_with_rc() after GUI's cfg_read() until guiInit(), or any exit_player() during guiInit() itself will end the GUI without calling guiDone(). This exit function will at least handle abortions during guiInit() itself. It will be called twice in case of an guiExit() after GUI initialization - first directly, next by guiDone() via MPlayer's exit_player_with_rc().)
author ib
date Tue, 03 May 2011 12:19:22 +0000
parents ac6e48baa03d
children 88eebbbbd6a0
line wrap: on
line source

/*
 * Copyright (C) 2009 Grigori Goronzy <greg@geekmind.org>
 *
 * This file is part of libass.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#include "ass_render.h"
#include "ass_parse.h"

#define MAX_BE 127
#define NBSP 0xa0   // unicode non-breaking space character

#define skip_to(x) while ((*p != (x)) && (*p != '}') && (*p != 0)) { ++p;}
#define skip(x) if (*p == (x)) ++p; else { return p; }
#define skipopt(x) if (*p == (x)) { ++p; }

/**
 * \brief Check if starting part of (*p) matches sample.
 * If true, shift p to the first symbol after the matching part.
 */
static inline int mystrcmp(char **p, const char *sample)
{
    int len = strlen(sample);
    if (strncmp(*p, sample, len) == 0) {
        (*p) += len;
        return 1;
    } else
        return 0;
}

static void change_font_size(ASS_Renderer *render_priv, double sz)
{
    double size = sz * render_priv->font_scale;

    if (size < 1)
        size = 1;
    else if (size > render_priv->height * 2)
        size = render_priv->height * 2;

    ass_font_set_size(render_priv->state.font, size);

    render_priv->state.font_size = sz;
}

/**
 * \brief Change current font, using setting from render_priv->state.
 */
void update_font(ASS_Renderer *render_priv)
{
    unsigned val;
    ASS_FontDesc desc;
    desc.treat_family_as_pattern = render_priv->state.treat_family_as_pattern;

    if (render_priv->state.family[0] == '@') {
        desc.vertical = 1;
        desc.family = strdup(render_priv->state.family + 1);
    } else {
        desc.vertical = 0;
        desc.family = strdup(render_priv->state.family);
    }

    val = render_priv->state.bold;
    // 0 = normal, 1 = bold, >1 = exact weight
    if (val == 1 || val == -1)
        val = 200;              // bold
    else if (val <= 0)
        val = 80;               // normal
    desc.bold = val;

    val = render_priv->state.italic;
    if (val == 1 || val == -1)
        val = 110;              // italic
    else if (val <= 0)
        val = 0;                // normal
    desc.italic = val;

    render_priv->state.font =
        ass_font_new(render_priv->cache.font_cache, render_priv->library,
                     render_priv->ftlibrary, render_priv->fontconfig_priv,
                     &desc);
    free(desc.family);

    if (render_priv->state.font)
        change_font_size(render_priv, render_priv->state.font_size);
}

/**
 * \brief Change border width
 * negative value resets border to style value
 */
void change_border(ASS_Renderer *render_priv, double border_x,
                   double border_y)
{
    int bord;
    if (!render_priv->state.font)
        return;

    if (border_x < 0 && border_y < 0) {
        if (render_priv->state.style->BorderStyle == 1 ||
            render_priv->state.style->BorderStyle == 3)
            border_x = border_y = render_priv->state.style->Outline;
        else
            border_x = border_y = 1.;
    }

    render_priv->state.border_x = border_x;
    render_priv->state.border_y = border_y;

    bord = 64 * border_x * render_priv->border_scale;
    if (bord > 0 && border_x == border_y) {
        if (!render_priv->state.stroker) {
            int error;
            error =
                FT_Stroker_New(render_priv->ftlibrary,
                               &render_priv->state.stroker);
            if (error) {
                ass_msg(render_priv->library, MSGL_V,
                        "failed to get stroker");
                render_priv->state.stroker = 0;
            }
        }
        if (render_priv->state.stroker)
            FT_Stroker_Set(render_priv->state.stroker, bord,
                           FT_STROKER_LINECAP_ROUND,
                           FT_STROKER_LINEJOIN_ROUND, 0);
    } else {
        FT_Stroker_Done(render_priv->state.stroker);
        render_priv->state.stroker = 0;
    }
}

/**
 * \brief Calculate a weighted average of two colors
 * calculates c1*(1-a) + c2*a, but separately for each component except alpha
 */
static void change_color(uint32_t *var, uint32_t new, double pwr)
{
    (*var) = ((uint32_t) (_r(*var) * (1 - pwr) + _r(new) * pwr) << 24) +
        ((uint32_t) (_g(*var) * (1 - pwr) + _g(new) * pwr) << 16) +
        ((uint32_t) (_b(*var) * (1 - pwr) + _b(new) * pwr) << 8) + _a(*var);
}

// like change_color, but for alpha component only
inline void change_alpha(uint32_t *var, uint32_t new, double pwr)
{
    *var =
        (_r(*var) << 24) + (_g(*var) << 16) + (_b(*var) << 8) +
        (uint32_t) (_a(*var) * (1 - pwr) + _a(new) * pwr);
}

/**
 * \brief Multiply two alpha values
 * \param a first value
 * \param b second value
 * \return result of multiplication
 * Parameters and result are limited by 0xFF.
 */
inline uint32_t mult_alpha(uint32_t a, uint32_t b)
{
    return 0xFF - (0xFF - a) * (0xFF - b) / 0xFF;
}

/**
 * \brief Calculate alpha value by piecewise linear function
 * Used for \fad, \fade implementation.
 */
static unsigned
interpolate_alpha(long long now, long long t1, long long t2, long long t3,
                  long long t4, unsigned a1, unsigned a2, unsigned a3)
{
    unsigned a;
    double cf;
    if (now <= t1) {
        a = a1;
    } else if (now >= t4) {
        a = a3;
    } else if (now < t2) {      // and > t1
        cf = ((double) (now - t1)) / (t2 - t1);
        a = a1 * (1 - cf) + a2 * cf;
    } else if (now > t3) {
        cf = ((double) (now - t3)) / (t4 - t3);
        a = a2 * (1 - cf) + a3 * cf;
    } else {                    // t2 <= now <= t3
        a = a2;
    }

    return a;
}

/**
 * Parse a vector clip into an outline, using the proper scaling
 * parameters.  Translate it to correct for screen borders, if needed.
 */
static char *parse_vector_clip(ASS_Renderer *render_priv, char *p)
{
    int scale = 1;
    int res = 0;
    ASS_Drawing *drawing = render_priv->state.clip_drawing;

    if (drawing && drawing->glyph)
        FT_Done_Glyph((FT_Glyph) drawing->glyph);
    ass_drawing_free(drawing);
    render_priv->state.clip_drawing = ass_drawing_new(
        render_priv->fontconfig_priv,
        render_priv->state.font,
        render_priv->ftlibrary);
    drawing = render_priv->state.clip_drawing;
    skipopt('(');
    res = mystrtoi(&p, &scale);
    skipopt(',')
    if (!res)
        scale = 1;
    drawing->scale = scale;
    drawing->scale_x = render_priv->font_scale_x * render_priv->font_scale;
    drawing->scale_y = render_priv->font_scale;
    while (*p != ')' && *p != '}' && p != 0)
        ass_drawing_add_char(drawing, *p++);
    skipopt(')');

    return p;
}

/**
 * \brief Parse style override tag.
 * \param p string to parse
 * \param pwr multiplier for some tag effects (comes from \t tags)
 */
static char *parse_tag(ASS_Renderer *render_priv, char *p, double pwr)
{
    skip_to('\\');
    skip('\\');
    if ((*p == '}') || (*p == 0))
        return p;

    // New tags introduced in vsfilter 2.39
    if (mystrcmp(&p, "xbord")) {
        double val;
        if (mystrtod(&p, &val))
            val = render_priv->state.border_x * (1 - pwr) + val * pwr;
        else
            val = -1.;
        change_border(render_priv, val, render_priv->state.border_y);
    } else if (mystrcmp(&p, "ybord")) {
        double val;
        if (mystrtod(&p, &val))
            val = render_priv->state.border_y * (1 - pwr) + val * pwr;
        else
            val = -1.;
        change_border(render_priv, render_priv->state.border_x, val);
    } else if (mystrcmp(&p, "xshad")) {
        double val;
        if (mystrtod(&p, &val))
            val = render_priv->state.shadow_x * (1 - pwr) + val * pwr;
        else
            val = 0.;
        render_priv->state.shadow_x = val;
    } else if (mystrcmp(&p, "yshad")) {
        double val;
        if (mystrtod(&p, &val))
            val = render_priv->state.shadow_y * (1 - pwr) + val * pwr;
        else
            val = 0.;
        render_priv->state.shadow_y = val;
    } else if (mystrcmp(&p, "fax")) {
        double val;
        if (mystrtod(&p, &val))
            render_priv->state.fax =
                val * pwr + render_priv->state.fax * (1 - pwr);
        else
            render_priv->state.fax = 0.;
    } else if (mystrcmp(&p, "fay")) {
        double val;
        if (mystrtod(&p, &val))
            render_priv->state.fay =
                val * pwr + render_priv->state.fay * (1 - pwr);
        else
            render_priv->state.fay = 0.;
    } else if (mystrcmp(&p, "iclip")) {
        int x0, y0, x1, y1;
        int res = 1;
        char *start = p;
        skipopt('(');
        res &= mystrtoi(&p, &x0);
        skipopt(',');
        res &= mystrtoi(&p, &y0);
        skipopt(',');
        res &= mystrtoi(&p, &x1);
        skipopt(',');
        res &= mystrtoi(&p, &y1);
        skipopt(')');
        if (res) {
            render_priv->state.clip_x0 =
                render_priv->state.clip_x0 * (1 - pwr) + x0 * pwr;
            render_priv->state.clip_x1 =
                render_priv->state.clip_x1 * (1 - pwr) + x1 * pwr;
            render_priv->state.clip_y0 =
                render_priv->state.clip_y0 * (1 - pwr) + y0 * pwr;
            render_priv->state.clip_y1 =
                render_priv->state.clip_y1 * (1 - pwr) + y1 * pwr;
            render_priv->state.clip_mode = 1;
        } else if (!render_priv->state.clip_drawing) {
            p = parse_vector_clip(render_priv, start);
            render_priv->state.clip_drawing_mode = 1;
        } else
            render_priv->state.clip_mode = 0;
    } else if (mystrcmp(&p, "blur")) {
        double val;
        if (mystrtod(&p, &val)) {
            val = render_priv->state.blur * (1 - pwr) + val * pwr;
            val = (val < 0) ? 0 : val;
            val = (val > BLUR_MAX_RADIUS) ? BLUR_MAX_RADIUS : val;
            render_priv->state.blur = val;
        } else
            render_priv->state.blur = 0.0;
        // ASS standard tags
    } else if (mystrcmp(&p, "fsc")) {
        char tp = *p++;
        double val;
        if (tp == 'x') {
            if (mystrtod(&p, &val)) {
                val /= 100;
                render_priv->state.scale_x =
                    render_priv->state.scale_x * (1 - pwr) + val * pwr;
            } else
                render_priv->state.scale_x =
                    render_priv->state.style->ScaleX;
        } else if (tp == 'y') {
            if (mystrtod(&p, &val)) {
                val /= 100;
                render_priv->state.scale_y =
                    render_priv->state.scale_y * (1 - pwr) + val * pwr;
            } else
                render_priv->state.scale_y =
                    render_priv->state.style->ScaleY;
        }
    } else if (mystrcmp(&p, "fsp")) {
        double val;
        if (mystrtod(&p, &val))
            render_priv->state.hspacing =
                render_priv->state.hspacing * (1 - pwr) + val * pwr;
        else
            render_priv->state.hspacing = render_priv->state.style->Spacing;
    } else if (mystrcmp(&p, "fs")) {
        double val;
        if (mystrtod(&p, &val))
            val = render_priv->state.font_size * (1 - pwr) + val * pwr;
        else
            val = render_priv->state.style->FontSize;
        if (render_priv->state.font)
            change_font_size(render_priv, val);
    } else if (mystrcmp(&p, "bord")) {
        double val;
        if (mystrtod(&p, &val)) {
            if (render_priv->state.border_x == render_priv->state.border_y)
                val = render_priv->state.border_x * (1 - pwr) + val * pwr;
        } else
            val = -1.;          // reset to default
        change_border(render_priv, val, val);
    } else if (mystrcmp(&p, "move")) {
        double x1, x2, y1, y2;
        long long t1, t2, delta_t, t;
        double x, y;
        double k;
        skip('(');
        mystrtod(&p, &x1);
        skip(',');
        mystrtod(&p, &y1);
        skip(',');
        mystrtod(&p, &x2);
        skip(',');
        mystrtod(&p, &y2);
        if (*p == ',') {
            skip(',');
            mystrtoll(&p, &t1);
            skip(',');
            mystrtoll(&p, &t2);
            ass_msg(render_priv->library, MSGL_DBG2,
                   "movement6: (%f, %f) -> (%f, %f), (%" PRId64 " .. %"
                   PRId64 ")\n", x1, y1, x2, y2, (int64_t) t1,
                   (int64_t) t2);
        } else {
            t1 = 0;
            t2 = render_priv->state.event->Duration;
            ass_msg(render_priv->library, MSGL_DBG2,
                   "movement: (%f, %f) -> (%f, %f)", x1, y1, x2, y2);
        }
        skip(')');
        delta_t = t2 - t1;
        t = render_priv->time - render_priv->state.event->Start;
        if (t < t1)
            k = 0.;
        else if (t > t2)
            k = 1.;
        else
            k = ((double) (t - t1)) / delta_t;
        x = k * (x2 - x1) + x1;
        y = k * (y2 - y1) + y1;
        if (render_priv->state.evt_type != EVENT_POSITIONED) {
            render_priv->state.pos_x = x;
            render_priv->state.pos_y = y;
            render_priv->state.detect_collisions = 0;
            render_priv->state.evt_type = EVENT_POSITIONED;
        }
    } else if (mystrcmp(&p, "frx")) {
        double val;
        if (mystrtod(&p, &val)) {
            val *= M_PI / 180;
            render_priv->state.frx =
                val * pwr + render_priv->state.frx * (1 - pwr);
        } else
            render_priv->state.frx = 0.;
    } else if (mystrcmp(&p, "fry")) {
        double val;
        if (mystrtod(&p, &val)) {
            val *= M_PI / 180;
            render_priv->state.fry =
                val * pwr + render_priv->state.fry * (1 - pwr);
        } else
            render_priv->state.fry = 0.;
    } else if (mystrcmp(&p, "frz") || mystrcmp(&p, "fr")) {
        double val;
        if (mystrtod(&p, &val)) {
            val *= M_PI / 180;
            render_priv->state.frz =
                val * pwr + render_priv->state.frz * (1 - pwr);
        } else
            render_priv->state.frz =
                M_PI * render_priv->state.style->Angle / 180.;
    } else if (mystrcmp(&p, "fn")) {
        char *start = p;
        char *family;
        skip_to('\\');
        if (p > start) {
            family = malloc(p - start + 1);
            strncpy(family, start, p - start);
            family[p - start] = '\0';
        } else
            family = strdup(render_priv->state.style->FontName);
        free(render_priv->state.family);
        render_priv->state.family = family;
        update_font(render_priv);
    } else if (mystrcmp(&p, "alpha")) {
        uint32_t val;
        int i;
        int hex = render_priv->track->track_type == TRACK_TYPE_ASS;
        if (strtocolor(render_priv->library, &p, &val, hex)) {
            unsigned char a = val >> 24;
            for (i = 0; i < 4; ++i)
                change_alpha(&render_priv->state.c[i], a, pwr);
        } else {
            change_alpha(&render_priv->state.c[0],
                         render_priv->state.style->PrimaryColour, pwr);
            change_alpha(&render_priv->state.c[1],
                         render_priv->state.style->SecondaryColour, pwr);
            change_alpha(&render_priv->state.c[2],
                         render_priv->state.style->OutlineColour, pwr);
            change_alpha(&render_priv->state.c[3],
                         render_priv->state.style->BackColour, pwr);
        }
        // FIXME: simplify
    } else if (mystrcmp(&p, "an")) {
        int val;
        if (mystrtoi(&p, &val) && val) {
            int v = (val - 1) / 3;      // 0, 1 or 2 for vertical alignment
            ass_msg(render_priv->library, MSGL_DBG2, "an %d", val);
            if (v != 0)
                v = 3 - v;
            val = ((val - 1) % 3) + 1;  // horizontal alignment
            val += v * 4;
            ass_msg(render_priv->library, MSGL_DBG2, "align %d", val);
            render_priv->state.alignment = val;
        } else
            render_priv->state.alignment =
                render_priv->state.style->Alignment;
    } else if (mystrcmp(&p, "a")) {
        int val;
        if (mystrtoi(&p, &val) && val)
            // take care of a vsfilter quirk: handle illegal \a8 like \a5
            render_priv->state.alignment = (val == 8) ? 5 : val;
        else
            render_priv->state.alignment =
                render_priv->state.style->Alignment;
    } else if (mystrcmp(&p, "pos")) {
        double v1, v2;
        skip('(');
        mystrtod(&p, &v1);
        skip(',');
        mystrtod(&p, &v2);
        skip(')');
        ass_msg(render_priv->library, MSGL_DBG2, "pos(%f, %f)", v1, v2);
        if (render_priv->state.evt_type == EVENT_POSITIONED) {
            ass_msg(render_priv->library, MSGL_V, "Subtitle has a new \\pos "
                   "after \\move or \\pos, ignoring");
        } else {
            render_priv->state.evt_type = EVENT_POSITIONED;
            render_priv->state.detect_collisions = 0;
            render_priv->state.pos_x = v1;
            render_priv->state.pos_y = v2;
        }
    } else if (mystrcmp(&p, "fad")) {
        int a1, a2, a3;
        long long t1, t2, t3, t4;
        if (*p == 'e')
            ++p;                // either \fad or \fade
        skip('(');
        mystrtoi(&p, &a1);
        skip(',');
        mystrtoi(&p, &a2);
        if (*p == ')') {
            // 2-argument version (\fad, according to specs)
            // a1 and a2 are fade-in and fade-out durations
            t1 = 0;
            t4 = render_priv->state.event->Duration;
            t2 = a1;
            t3 = t4 - a2;
            a1 = 0xFF;
            a2 = 0;
            a3 = 0xFF;
        } else {
            // 6-argument version (\fade)
            // a1 and a2 (and a3) are opacity values
            skip(',');
            mystrtoi(&p, &a3);
            skip(',');
            mystrtoll(&p, &t1);
            skip(',');
            mystrtoll(&p, &t2);
            skip(',');
            mystrtoll(&p, &t3);
            skip(',');
            mystrtoll(&p, &t4);
        }
        skip(')');
        render_priv->state.fade =
            interpolate_alpha(render_priv->time -
                              render_priv->state.event->Start, t1, t2,
                              t3, t4, a1, a2, a3);
    } else if (mystrcmp(&p, "org")) {
        int v1, v2;
        skip('(');
        mystrtoi(&p, &v1);
        skip(',');
        mystrtoi(&p, &v2);
        skip(')');
        ass_msg(render_priv->library, MSGL_DBG2, "org(%d, %d)", v1, v2);
        if (!render_priv->state.have_origin) {
            render_priv->state.org_x = v1;
            render_priv->state.org_y = v2;
            render_priv->state.have_origin = 1;
            render_priv->state.detect_collisions = 0;
        }
    } else if (mystrcmp(&p, "t")) {
        double v[3];
        int v1, v2;
        double v3;
        int cnt;
        long long t1, t2, t, delta_t;
        double k;
        skip('(');
        for (cnt = 0; cnt < 3; ++cnt) {
            if (*p == '\\')
                break;
            mystrtod(&p, &v[cnt]);
            skip(',');
        }
        if (cnt == 3) {
            v1 = v[0];
            v2 = (v[1] < v1) ? render_priv->state.event->Duration : v[1];
            v3 = v[2];
        } else if (cnt == 2) {
            v1 = v[0];
            v2 = (v[1] < v1) ? render_priv->state.event->Duration : v[1];
            v3 = 1.;
        } else if (cnt == 1) {
            v1 = 0;
            v2 = render_priv->state.event->Duration;
            v3 = v[0];
        } else {                // cnt == 0
            v1 = 0;
            v2 = render_priv->state.event->Duration;
            v3 = 1.;
        }
        render_priv->state.detect_collisions = 0;
        t1 = v1;
        t2 = v2;
        delta_t = v2 - v1;
        if (v3 < 0.)
            v3 = 0.;
        t = render_priv->time - render_priv->state.event->Start;        // FIXME: move to render_context
        if (t <= t1)
            k = 0.;
        else if (t >= t2)
            k = 1.;
        else {
            assert(delta_t != 0.);
            k = pow(((double) (t - t1)) / delta_t, v3);
        }
        while (*p == '\\')
            p = parse_tag(render_priv, p, k);   // maybe k*pwr ? no, specs forbid nested \t's
        skip_to(')');           // in case there is some unknown tag or a comment
        skip(')');
    } else if (mystrcmp(&p, "clip")) {
        char *start = p;
        int x0, y0, x1, y1;
        int res = 1;
        skipopt('(');
        res &= mystrtoi(&p, &x0);
        skipopt(',');
        res &= mystrtoi(&p, &y0);
        skipopt(',');
        res &= mystrtoi(&p, &x1);
        skipopt(',');
        res &= mystrtoi(&p, &y1);
        skipopt(')');
        if (res) {
            render_priv->state.clip_x0 =
                render_priv->state.clip_x0 * (1 - pwr) + x0 * pwr;
            render_priv->state.clip_x1 =
                render_priv->state.clip_x1 * (1 - pwr) + x1 * pwr;
            render_priv->state.clip_y0 =
                render_priv->state.clip_y0 * (1 - pwr) + y0 * pwr;
            render_priv->state.clip_y1 =
                render_priv->state.clip_y1 * (1 - pwr) + y1 * pwr;
        // Might be a vector clip
        } else if (!render_priv->state.clip_drawing) {
            p = parse_vector_clip(render_priv, start);
            render_priv->state.clip_drawing_mode = 0;
        } else {
            render_priv->state.clip_x0 = 0;
            render_priv->state.clip_y0 = 0;
            render_priv->state.clip_x1 = render_priv->track->PlayResX;
            render_priv->state.clip_y1 = render_priv->track->PlayResY;
        }
    } else if (mystrcmp(&p, "c")) {
        uint32_t val;
        int hex = render_priv->track->track_type == TRACK_TYPE_ASS;
        if (!strtocolor(render_priv->library, &p, &val, hex))
            val = render_priv->state.style->PrimaryColour;
        ass_msg(render_priv->library, MSGL_DBG2, "color: %X", val);
        change_color(&render_priv->state.c[0], val, pwr);
    } else if ((*p >= '1') && (*p <= '4') && (++p)
               && (mystrcmp(&p, "c") || mystrcmp(&p, "a"))) {
        char n = *(p - 2);
        int cidx = n - '1';
        char cmd = *(p - 1);
        uint32_t val;
        int hex = render_priv->track->track_type == TRACK_TYPE_ASS;
        assert((n >= '1') && (n <= '4'));
        if (!strtocolor(render_priv->library, &p, &val, hex))
            switch (n) {
            case '1':
                val = render_priv->state.style->PrimaryColour;
                break;
            case '2':
                val = render_priv->state.style->SecondaryColour;
                break;
            case '3':
                val = render_priv->state.style->OutlineColour;
                break;
            case '4':
                val = render_priv->state.style->BackColour;
                break;
            default:
                val = 0;
                break;          // impossible due to assert; avoid compilation warning
            }
        switch (cmd) {
        case 'c':
            change_color(render_priv->state.c + cidx, val, pwr);
            break;
        case 'a':
            change_alpha(render_priv->state.c + cidx, val >> 24, pwr);
            break;
        default:
            ass_msg(render_priv->library, MSGL_WARN, "Bad command: %c%c",
                    n, cmd);
            break;
        }
        ass_msg(render_priv->library, MSGL_DBG2, "single c/a at %f: %c%c = %X",
               pwr, n, cmd, render_priv->state.c[cidx]);
    } else if (mystrcmp(&p, "r")) {
        reset_render_context(render_priv);
    } else if (mystrcmp(&p, "be")) {
        int val;
        if (mystrtoi(&p, &val)) {
            // Clamp to a safe upper limit, since high values need excessive CPU
            val = (val < 0) ? 0 : val;
            val = (val > MAX_BE) ? MAX_BE : val;
            render_priv->state.be = val;
        } else
            render_priv->state.be = 0;
    } else if (mystrcmp(&p, "b")) {
        int b;
        if (mystrtoi(&p, &b)) {
            if (pwr >= .5)
                render_priv->state.bold = b;
        } else
            render_priv->state.bold = render_priv->state.style->Bold;
        update_font(render_priv);
    } else if (mystrcmp(&p, "i")) {
        int i;
        if (mystrtoi(&p, &i)) {
            if (pwr >= .5)
                render_priv->state.italic = i;
        } else
            render_priv->state.italic = render_priv->state.style->Italic;
        update_font(render_priv);
    } else if (mystrcmp(&p, "kf") || mystrcmp(&p, "K")) {
        int val = 0;
        mystrtoi(&p, &val);
        render_priv->state.effect_type = EF_KARAOKE_KF;
        if (render_priv->state.effect_timing)
            render_priv->state.effect_skip_timing +=
                render_priv->state.effect_timing;
        render_priv->state.effect_timing = val * 10;
    } else if (mystrcmp(&p, "ko")) {
        int val = 0;
        mystrtoi(&p, &val);
        render_priv->state.effect_type = EF_KARAOKE_KO;
        if (render_priv->state.effect_timing)
            render_priv->state.effect_skip_timing +=
                render_priv->state.effect_timing;
        render_priv->state.effect_timing = val * 10;
    } else if (mystrcmp(&p, "k")) {
        int val = 0;
        mystrtoi(&p, &val);
        render_priv->state.effect_type = EF_KARAOKE;
        if (render_priv->state.effect_timing)
            render_priv->state.effect_skip_timing +=
                render_priv->state.effect_timing;
        render_priv->state.effect_timing = val * 10;
    } else if (mystrcmp(&p, "shad")) {
        double val;
        if (mystrtod(&p, &val)) {
            if (render_priv->state.shadow_x == render_priv->state.shadow_y)
                val = render_priv->state.shadow_x * (1 - pwr) + val * pwr;
        } else
            val = 0.;
        render_priv->state.shadow_x = render_priv->state.shadow_y = val;
    } else if (mystrcmp(&p, "s")) {
        int val;
        if (mystrtoi(&p, &val) && val)
            render_priv->state.flags |= DECO_STRIKETHROUGH;
        else
            render_priv->state.flags &= ~DECO_STRIKETHROUGH;
    } else if (mystrcmp(&p, "u")) {
        int val;
        if (mystrtoi(&p, &val) && val)
            render_priv->state.flags |= DECO_UNDERLINE;
        else
            render_priv->state.flags &= ~DECO_UNDERLINE;
    } else if (mystrcmp(&p, "pbo")) {
        double val = 0;
        if (mystrtod(&p, &val))
            render_priv->state.drawing->pbo = val;
    } else if (mystrcmp(&p, "p")) {
        int val;
        if (!mystrtoi(&p, &val))
            val = 0;
        if (val)
            render_priv->state.drawing->scale = val;
        render_priv->state.drawing_mode = !!val;
    } else if (mystrcmp(&p, "q")) {
        int val;
        if (!mystrtoi(&p, &val))
            val = render_priv->track->WrapStyle;
        render_priv->state.wrap_style = val;
    }

    return p;
}

void apply_transition_effects(ASS_Renderer *render_priv, ASS_Event *event)
{
    int v[4];
    int cnt;
    char *p = event->Effect;

    if (!p || !*p)
        return;

    cnt = 0;
    while (cnt < 4 && (p = strchr(p, ';'))) {
        v[cnt++] = atoi(++p);
    }

    if (strncmp(event->Effect, "Banner;", 7) == 0) {
        int delay;
        if (cnt < 1) {
            ass_msg(render_priv->library, MSGL_V,
                    "Error parsing effect: '%s'", event->Effect);
            return;
        }
        if (cnt >= 2 && v[1] == 0)      // right-to-left
            render_priv->state.scroll_direction = SCROLL_RL;
        else                    // left-to-right
            render_priv->state.scroll_direction = SCROLL_LR;

        delay = v[0];
        if (delay == 0)
            delay = 1;          // ?
        render_priv->state.scroll_shift =
            (render_priv->time - render_priv->state.event->Start) / delay;
        render_priv->state.evt_type = EVENT_HSCROLL;
        return;
    }

    if (strncmp(event->Effect, "Scroll up;", 10) == 0) {
        render_priv->state.scroll_direction = SCROLL_BT;
    } else if (strncmp(event->Effect, "Scroll down;", 12) == 0) {
        render_priv->state.scroll_direction = SCROLL_TB;
    } else {
        ass_msg(render_priv->library, MSGL_DBG2,
                "Unknown transition effect: '%s'", event->Effect);
        return;
    }
    // parse scroll up/down parameters
    {
        int delay;
        int y0, y1;
        if (cnt < 3) {
            ass_msg(render_priv->library, MSGL_V,
                    "Error parsing effect: '%s'", event->Effect);
            return;
        }
        delay = v[2];
        if (delay == 0)
            delay = 1;          // ?
        render_priv->state.scroll_shift =
            (render_priv->time - render_priv->state.event->Start) / delay;
        if (v[0] < v[1]) {
            y0 = v[0];
            y1 = v[1];
        } else {
            y0 = v[1];
            y1 = v[0];
        }
        if (y1 == 0)
            y1 = render_priv->track->PlayResY;  // y0=y1=0 means fullscreen scrolling
        render_priv->state.clip_y0 = y0;
        render_priv->state.clip_y1 = y1;
        render_priv->state.evt_type = EVENT_VSCROLL;
        render_priv->state.detect_collisions = 0;
    }

}

/**
 * \brief Get next ucs4 char from string, parsing and executing style overrides
 * \param str string pointer
 * \return ucs4 code of the next char
 * On return str points to the unparsed part of the string
 */
unsigned get_next_char(ASS_Renderer *render_priv, char **str)
{
    char *p = *str;
    unsigned chr;
    if (*p == '{') {            // '\0' goes here
        p++;
        while (1) {
            p = parse_tag(render_priv, p, 1.);
            if (*p == '}') {    // end of tag
                p++;
                if (*p == '{') {
                    p++;
                    continue;
                } else
                    break;
            } else if (*p != '\\')
                ass_msg(render_priv->library, MSGL_V,
                        "Unable to parse: '%.30s'", p);
            if (*p == 0)
                break;
        }
    }
    if (*p == '\t') {
        ++p;
        *str = p;
        return ' ';
    }
    if (*p == '\\') {
        if ((p[1] == 'N') || ((p[1] == 'n') &&
                              (render_priv->state.wrap_style == 2))) {
            p += 2;
            *str = p;
            return '\n';
        } else if (p[1] == 'n') {
            p += 2;
            *str = p;
            return ' ';
        } else if (p[1] == 'h') {
            p += 2;
            *str = p;
            return NBSP;
        } else if (p[1] == '{') {
            p += 2;
            *str = p;
            return '{';
        } else if (p[1] == '}') {
            p += 2;
            *str = p;
            return '}';
        }
    }
    chr = ass_utf8_get_char((char **) &p);
    *str = p;
    return chr;
}