You've already forked FFmpeg
mirror of
https://github.com/FFmpeg/FFmpeg.git
synced 2025-11-23 21:54:53 +02:00
2709 lines
75 KiB
C
2709 lines
75 KiB
C
|
|
/*
|
||
|
|
* This file is part of FFmpeg.
|
||
|
|
*
|
||
|
|
* FFmpeg is free software; you can redistribute it and/or
|
||
|
|
* modify it under the terms of the GNU Lesser General Public
|
||
|
|
* License as published by the Free Software Foundation; either
|
||
|
|
* version 2.1 of the License, or (at your option) any later version.
|
||
|
|
*
|
||
|
|
* FFmpeg 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
|
||
|
|
* Lesser General Public License for more details.
|
||
|
|
*
|
||
|
|
* You should have received a copy of the GNU Lesser General Public
|
||
|
|
* License along with FFmpeg; if not, write to the Free Software
|
||
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @file
|
||
|
|
*
|
||
|
|
* drawvg filter, draw vector graphics with cairo.
|
||
|
|
*
|
||
|
|
* This file contains the parser and the interpreter for VGS, and the
|
||
|
|
* AVClass definitions for the drawvg filter.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <cairo.h>
|
||
|
|
|
||
|
|
#include "libavutil/avassert.h"
|
||
|
|
#include "libavutil/avstring.h"
|
||
|
|
#include "libavutil/bswap.h"
|
||
|
|
#include "libavutil/eval.h"
|
||
|
|
#include "libavutil/internal.h"
|
||
|
|
#include "libavutil/macros.h"
|
||
|
|
#include "libavutil/mem.h"
|
||
|
|
#include "libavutil/opt.h"
|
||
|
|
#include "libavutil/pixdesc.h"
|
||
|
|
#include "libavutil/sfc64.h"
|
||
|
|
|
||
|
|
#include "avfilter.h"
|
||
|
|
#include "filters.h"
|
||
|
|
#include "textutils.h"
|
||
|
|
#include "video.h"
|
||
|
|
|
||
|
|
/*
|
||
|
|
* == AVExpr Integration ==
|
||
|
|
*
|
||
|
|
* Definitions to use variables and functions in the expressions from
|
||
|
|
* `av_expr_*` functions.
|
||
|
|
*
|
||
|
|
* For user-variables, created with commands like `setvar` or `defhsla`,
|
||
|
|
* the VGS parser updates a copy of the `vgs_default_vars` array. The
|
||
|
|
* first user-variable is stored in the slot for `VAR_U0`.
|
||
|
|
*/
|
||
|
|
|
||
|
|
enum {
|
||
|
|
VAR_N, ///< Frame number.
|
||
|
|
VAR_T, ///< Timestamp in seconds.
|
||
|
|
VAR_TS, ///< Time in seconds of the first frame.
|
||
|
|
VAR_W, ///< Frame width.
|
||
|
|
VAR_H, ///< Frame height.
|
||
|
|
VAR_DURATION, ///< Frame duration.
|
||
|
|
VAR_CX, ///< X coordinate for current point.
|
||
|
|
VAR_CY, ///< Y coordinate for current point.
|
||
|
|
VAR_I, ///< Loop counter, to use with `repeat {}`.
|
||
|
|
VAR_U0, ///< User variables.
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Number of user variables that can be created with `setvar`.
|
||
|
|
///
|
||
|
|
/// It is possible to allow any number of variables, but this
|
||
|
|
/// approach simplifies the implementation, and 20 variables
|
||
|
|
/// is more than enough for the expected use of this filter.
|
||
|
|
#define USER_VAR_COUNT 20
|
||
|
|
|
||
|
|
/// Total number of variables (default- and user-variables).
|
||
|
|
#define VAR_COUNT (VAR_U0 + USER_VAR_COUNT)
|
||
|
|
|
||
|
|
static const char *const vgs_default_vars[] = {
|
||
|
|
"n",
|
||
|
|
"t",
|
||
|
|
"ts",
|
||
|
|
"w",
|
||
|
|
"h",
|
||
|
|
"duration",
|
||
|
|
"cx",
|
||
|
|
"cy",
|
||
|
|
"i",
|
||
|
|
NULL, // User variables. Name is assigned by commands like `setvar`.
|
||
|
|
};
|
||
|
|
|
||
|
|
// Functions used in expressions.
|
||
|
|
|
||
|
|
static const char *const vgs_func1_names[] = {
|
||
|
|
"pathlen",
|
||
|
|
"randomg",
|
||
|
|
NULL,
|
||
|
|
};
|
||
|
|
|
||
|
|
static double vgs_fn_pathlen(void *, double);
|
||
|
|
static double vgs_fn_randomg(void *, double);
|
||
|
|
|
||
|
|
static double (*const vgs_func1_impls[])(void *, double) = {
|
||
|
|
vgs_fn_pathlen,
|
||
|
|
vgs_fn_randomg,
|
||
|
|
NULL,
|
||
|
|
};
|
||
|
|
|
||
|
|
static const char *const vgs_func2_names[] = {
|
||
|
|
"p",
|
||
|
|
NULL,
|
||
|
|
};
|
||
|
|
|
||
|
|
static double vgs_fn_p(void *, double, double);
|
||
|
|
|
||
|
|
static double (*const vgs_func2_impls[])(void *, double, double) = {
|
||
|
|
vgs_fn_p,
|
||
|
|
NULL,
|
||
|
|
};
|
||
|
|
|
||
|
|
/*
|
||
|
|
* == Command Declarations ==
|
||
|
|
*
|
||
|
|
* Each command is defined by an opcode (used later by the interpreter), a name,
|
||
|
|
* and a set of parameters.
|
||
|
|
*
|
||
|
|
* Inspired by SVG, some commands can be repeated when the next token after the
|
||
|
|
* last parameter is a numeric value (for example, `L 1 2 3 4` is equivalent to
|
||
|
|
* `L 1 2 L 3 4`). In these commands, the last parameter is `PARAM_MAY_REPEAT`.
|
||
|
|
*/
|
||
|
|
|
||
|
|
enum VGSCommand {
|
||
|
|
CMD_ARC = 1, ///< arc (cx cy radius angle1 angle2)
|
||
|
|
CMD_ARC_NEG, ///< arcn (cx cy radius angle1 angle2)
|
||
|
|
CMD_BREAK, ///< break
|
||
|
|
CMD_CIRCLE, ///< circle (cx cy radius)
|
||
|
|
CMD_CLIP, ///< clip
|
||
|
|
CMD_CLIP_EO, ///< eoclip
|
||
|
|
CMD_CLOSE_PATH, ///< Z, z, closepath
|
||
|
|
CMD_COLOR_STOP, ///< colorstop (offset color)
|
||
|
|
CMD_CURVE_TO, ///< C, curveto (x1 y1 x2 y2 x y)
|
||
|
|
CMD_DEF_HSLA, ///< defhsla (varname h s l a)
|
||
|
|
CMD_DEF_RGBA, ///< defrgba (varname r g b a)
|
||
|
|
CMD_CURVE_TO_REL, ///< c, rcurveto (dx1 dy1 dx2 dy2 dx dy)
|
||
|
|
CMD_ELLIPSE, ///< ellipse (cx cy rx ry)
|
||
|
|
CMD_FILL, ///< fill
|
||
|
|
CMD_FILL_EO, ///< eofill
|
||
|
|
CMD_GET_METADATA, ///< getmetadata varname key
|
||
|
|
CMD_HORZ, ///< H (x)
|
||
|
|
CMD_HORZ_REL, ///< h (dx)
|
||
|
|
CMD_IF, ///< if (condition) { subprogram }
|
||
|
|
CMD_LINEAR_GRAD, ///< lineargrad (x0 y0 x1 y1)
|
||
|
|
CMD_LINE_TO, ///< L, lineto (x y)
|
||
|
|
CMD_LINE_TO_REL, ///< l, rlineto (dx dy)
|
||
|
|
CMD_MOVE_TO, ///< M, moveto (x y)
|
||
|
|
CMD_MOVE_TO_REL, ///< m, rmoveto (dx dy)
|
||
|
|
CMD_NEW_PATH, ///< newpath
|
||
|
|
CMD_PRESERVE, ///< preserve
|
||
|
|
CMD_PRINT, ///< print (expr)*
|
||
|
|
CMD_PROC_ASSIGN, ///< proc name varnames* { subprogram }
|
||
|
|
CMD_PROC_CALL, ///< call name (expr)*
|
||
|
|
CMD_Q_CURVE_TO, ///< Q (x1 y1 x y)
|
||
|
|
CMD_Q_CURVE_TO_REL, ///< q (dx1 dy1 dx dy)
|
||
|
|
CMD_RADIAL_GRAD, ///< radialgrad (cx0 cy0 radius0 cx1 cy1 radius1)
|
||
|
|
CMD_RECT, ///< rect (x y width height)
|
||
|
|
CMD_REPEAT, ///< repeat (count) { subprogram }
|
||
|
|
CMD_RESET_CLIP, ///< resetclip
|
||
|
|
CMD_RESET_DASH, ///< resetdash
|
||
|
|
CMD_RESET_MATRIX, ///< resetmatrix
|
||
|
|
CMD_RESTORE, ///< restore
|
||
|
|
CMD_ROTATE, ///< rotate (angle)
|
||
|
|
CMD_ROUNDEDRECT, ///< roundedrect (x y width height radius)
|
||
|
|
CMD_SAVE, ///< save
|
||
|
|
CMD_SCALE, ///< scale (s)
|
||
|
|
CMD_SCALEXY, ///< scalexy (sx sy)
|
||
|
|
CMD_SET_COLOR, ///< setcolor (color)
|
||
|
|
CMD_SET_DASH, ///< setdash (length)
|
||
|
|
CMD_SET_DASH_OFFSET, ///< setdashoffset (offset)
|
||
|
|
CMD_SET_HSLA, ///< sethsla (h s l a)
|
||
|
|
CMD_SET_LINE_CAP, ///< setlinecap (cap)
|
||
|
|
CMD_SET_LINE_JOIN, ///< setlinejoin (join)
|
||
|
|
CMD_SET_LINE_WIDTH, ///< setlinewidth (width)
|
||
|
|
CMD_SET_RGBA, ///< setrgba (r g b a)
|
||
|
|
CMD_SET_VAR, ///< setvar (varname value)
|
||
|
|
CMD_STROKE, ///< stroke
|
||
|
|
CMD_S_CURVE_TO, ///< S (x2 y2 x y)
|
||
|
|
CMD_S_CURVE_TO_REL, ///< s (dx2 dy2 dx dy)
|
||
|
|
CMD_TRANSLATE, ///< translate (tx ty)
|
||
|
|
CMD_T_CURVE_TO, ///< T (x y)
|
||
|
|
CMD_T_CURVE_TO_REL, ///< t (dx dy)
|
||
|
|
CMD_VERT, ///< V (y)
|
||
|
|
CMD_VERT_REL, ///< v (dy)
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Constants for some commands, like `setlinejoin`.
|
||
|
|
struct VGSConstant {
|
||
|
|
const char* name;
|
||
|
|
int value;
|
||
|
|
};
|
||
|
|
|
||
|
|
static const struct VGSConstant vgs_consts_line_cap[] = {
|
||
|
|
{ "butt", CAIRO_LINE_CAP_BUTT },
|
||
|
|
{ "round", CAIRO_LINE_CAP_ROUND },
|
||
|
|
{ "square", CAIRO_LINE_CAP_SQUARE },
|
||
|
|
{ NULL, 0 },
|
||
|
|
};
|
||
|
|
|
||
|
|
static const struct VGSConstant vgs_consts_line_join[] = {
|
||
|
|
{ "bevel", CAIRO_LINE_JOIN_BEVEL },
|
||
|
|
{ "miter", CAIRO_LINE_JOIN_MITER },
|
||
|
|
{ "round", CAIRO_LINE_JOIN_ROUND },
|
||
|
|
{ NULL, 0 },
|
||
|
|
};
|
||
|
|
|
||
|
|
struct VGSParameter {
|
||
|
|
enum {
|
||
|
|
PARAM_COLOR = 1,
|
||
|
|
PARAM_CONSTANT,
|
||
|
|
PARAM_END,
|
||
|
|
PARAM_MAY_REPEAT,
|
||
|
|
PARAM_NUMERIC,
|
||
|
|
PARAM_NUMERIC_METADATA,
|
||
|
|
PARAM_PROC_ARGS,
|
||
|
|
PARAM_PROC_NAME,
|
||
|
|
PARAM_PROC_PARAMS,
|
||
|
|
PARAM_RAW_IDENT,
|
||
|
|
PARAM_SUBPROGRAM,
|
||
|
|
PARAM_VARIADIC,
|
||
|
|
PARAM_VAR_NAME,
|
||
|
|
} type;
|
||
|
|
|
||
|
|
const struct VGSConstant *constants; ///< Array for PARAM_CONSTANT.
|
||
|
|
};
|
||
|
|
|
||
|
|
// Max number of parameters for a command.
|
||
|
|
#define MAX_COMMAND_PARAMS 8
|
||
|
|
|
||
|
|
// Max number of arguments when calling a procedure. Subtract 2 to
|
||
|
|
// `MAX_COMMAND_PARAMS` because the call to `proc` needs 2 arguments
|
||
|
|
// (the procedure name and its body). The rest can be variable names
|
||
|
|
// for the arguments.
|
||
|
|
#define MAX_PROC_ARGS (MAX_COMMAND_PARAMS - 2)
|
||
|
|
|
||
|
|
// Definition of each command.
|
||
|
|
|
||
|
|
struct VGSCommandSpec {
|
||
|
|
const char* name;
|
||
|
|
enum VGSCommand cmd;
|
||
|
|
const struct VGSParameter *params;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Parameter lists.
|
||
|
|
#define PARAMS(...) (const struct VGSParameter[]){ __VA_ARGS__ }
|
||
|
|
#define L(...) PARAMS(__VA_ARGS__, { PARAM_END })
|
||
|
|
#define R(...) PARAMS(__VA_ARGS__, { PARAM_MAY_REPEAT })
|
||
|
|
#define NONE PARAMS({ PARAM_END })
|
||
|
|
|
||
|
|
// Common parameter types.
|
||
|
|
#define N { PARAM_NUMERIC }
|
||
|
|
#define V { PARAM_VAR_NAME }
|
||
|
|
#define P { PARAM_SUBPROGRAM }
|
||
|
|
#define C(c) { PARAM_CONSTANT, .constants = c }
|
||
|
|
|
||
|
|
// Declarations table.
|
||
|
|
//
|
||
|
|
// The array must be sorted by `name` in ascending order.
|
||
|
|
static const struct VGSCommandSpec vgs_commands[] = {
|
||
|
|
{ "C", CMD_CURVE_TO, R(N, N, N, N, N, N) },
|
||
|
|
{ "H", CMD_HORZ, R(N) },
|
||
|
|
{ "L", CMD_LINE_TO, R(N, N) },
|
||
|
|
{ "M", CMD_MOVE_TO, R(N, N) },
|
||
|
|
{ "Q", CMD_Q_CURVE_TO, R(N, N, N, N) },
|
||
|
|
{ "S", CMD_S_CURVE_TO, R(N, N, N, N) },
|
||
|
|
{ "T", CMD_T_CURVE_TO, R(N, N) },
|
||
|
|
{ "V", CMD_VERT, R(N) },
|
||
|
|
{ "Z", CMD_CLOSE_PATH, NONE },
|
||
|
|
{ "arc", CMD_ARC, R(N, N, N, N, N) },
|
||
|
|
{ "arcn", CMD_ARC_NEG, R(N, N, N, N, N) },
|
||
|
|
{ "break", CMD_BREAK, NONE },
|
||
|
|
{ "c", CMD_CURVE_TO_REL, R(N, N, N, N, N, N) },
|
||
|
|
{ "call", CMD_PROC_CALL, L({ PARAM_PROC_NAME }, { PARAM_PROC_ARGS }) },
|
||
|
|
{ "circle", CMD_CIRCLE, R(N, N, N) },
|
||
|
|
{ "clip", CMD_CLIP, NONE },
|
||
|
|
{ "closepath", CMD_CLOSE_PATH, NONE },
|
||
|
|
{ "colorstop", CMD_COLOR_STOP, R(N, { PARAM_COLOR }) },
|
||
|
|
{ "curveto", CMD_CURVE_TO, R(N, N, N, N, N, N) },
|
||
|
|
{ "defhsla", CMD_DEF_HSLA, L(V, N, N, N, N) },
|
||
|
|
{ "defrgba", CMD_DEF_RGBA, L(V, N, N, N, N) },
|
||
|
|
{ "ellipse", CMD_ELLIPSE, R(N, N, N, N) },
|
||
|
|
{ "eoclip", CMD_CLIP_EO, NONE },
|
||
|
|
{ "eofill", CMD_FILL_EO, NONE },
|
||
|
|
{ "fill", CMD_FILL, NONE },
|
||
|
|
{ "getmetadata", CMD_GET_METADATA, L(V, { PARAM_RAW_IDENT }) },
|
||
|
|
{ "h", CMD_HORZ_REL, R(N) },
|
||
|
|
{ "if", CMD_IF, L(N, P) },
|
||
|
|
{ "l", CMD_LINE_TO_REL, R(N, N) },
|
||
|
|
{ "lineargrad", CMD_LINEAR_GRAD, L(N, N, N, N) },
|
||
|
|
{ "lineto", CMD_LINE_TO, R(N, N) },
|
||
|
|
{ "m", CMD_MOVE_TO_REL, R(N, N) },
|
||
|
|
{ "moveto", CMD_MOVE_TO, R(N, N) },
|
||
|
|
{ "newpath", CMD_NEW_PATH, NONE },
|
||
|
|
{ "preserve", CMD_PRESERVE, NONE },
|
||
|
|
{ "print", CMD_PRINT, L({ PARAM_NUMERIC_METADATA }, { PARAM_VARIADIC }) },
|
||
|
|
{ "proc", CMD_PROC_ASSIGN, L({ PARAM_PROC_NAME }, { PARAM_PROC_PARAMS }, P) },
|
||
|
|
{ "q", CMD_Q_CURVE_TO_REL, R(N, N, N, N) },
|
||
|
|
{ "radialgrad", CMD_RADIAL_GRAD, L(N, N, N, N, N, N) },
|
||
|
|
{ "rcurveto", CMD_CURVE_TO_REL, R(N, N, N, N, N, N) },
|
||
|
|
{ "rect", CMD_RECT, R(N, N, N, N) },
|
||
|
|
{ "repeat", CMD_REPEAT, L(N, P) },
|
||
|
|
{ "resetclip", CMD_RESET_CLIP, NONE },
|
||
|
|
{ "resetdash", CMD_RESET_DASH, NONE },
|
||
|
|
{ "resetmatrix", CMD_RESET_MATRIX, NONE },
|
||
|
|
{ "restore", CMD_RESTORE, NONE },
|
||
|
|
{ "rlineto", CMD_LINE_TO_REL, R(N, N) },
|
||
|
|
{ "rmoveto", CMD_MOVE_TO_REL, R(N, N) },
|
||
|
|
{ "rotate", CMD_ROTATE, L(N) },
|
||
|
|
{ "roundedrect", CMD_ROUNDEDRECT, R(N, N, N, N, N) },
|
||
|
|
{ "s", CMD_S_CURVE_TO_REL, R(N, N, N, N) },
|
||
|
|
{ "save", CMD_SAVE, NONE },
|
||
|
|
{ "scale", CMD_SCALE, L(N) },
|
||
|
|
{ "scalexy", CMD_SCALEXY, L(N, N) },
|
||
|
|
{ "setcolor", CMD_SET_COLOR, L({ PARAM_COLOR }) },
|
||
|
|
{ "setdash", CMD_SET_DASH, R(N) },
|
||
|
|
{ "setdashoffset", CMD_SET_DASH_OFFSET, R(N) },
|
||
|
|
{ "sethsla", CMD_SET_HSLA, L(N, N, N, N) },
|
||
|
|
{ "setlinecap", CMD_SET_LINE_CAP, L(C(vgs_consts_line_cap)) },
|
||
|
|
{ "setlinejoin", CMD_SET_LINE_JOIN, L(C(vgs_consts_line_join)) },
|
||
|
|
{ "setlinewidth", CMD_SET_LINE_WIDTH, L(N) },
|
||
|
|
{ "setrgba", CMD_SET_RGBA, L(N, N, N, N) },
|
||
|
|
{ "setvar", CMD_SET_VAR, L(V, N) },
|
||
|
|
{ "stroke", CMD_STROKE, NONE },
|
||
|
|
{ "t", CMD_T_CURVE_TO_REL, R(N, N) },
|
||
|
|
{ "translate", CMD_TRANSLATE, L(N, N) },
|
||
|
|
{ "v", CMD_VERT_REL, R(N) },
|
||
|
|
{ "z", CMD_CLOSE_PATH, NONE },
|
||
|
|
};
|
||
|
|
|
||
|
|
#undef C
|
||
|
|
#undef L
|
||
|
|
#undef N
|
||
|
|
#undef NONE
|
||
|
|
#undef PARAMS
|
||
|
|
#undef R
|
||
|
|
|
||
|
|
/// Comparator for `VGSCommandDecl`, to be used with `bsearch(3)`.
|
||
|
|
static int vgs_comp_command_spec(const void *cs1, const void *cs2) {
|
||
|
|
return strcmp(
|
||
|
|
((const struct VGSCommandSpec*)cs1)->name,
|
||
|
|
((const struct VGSCommandSpec*)cs2)->name
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Return the specs for the given command, or `NULL` if the name is not valid.
|
||
|
|
///
|
||
|
|
/// The implementation assumes that `vgs_commands` is sorted by `name`.
|
||
|
|
static const struct VGSCommandSpec* vgs_get_command(const char *name, size_t length) {
|
||
|
|
char bufname[64];
|
||
|
|
struct VGSCommandSpec key = { .name = bufname };
|
||
|
|
|
||
|
|
if (length >= sizeof(bufname))
|
||
|
|
return NULL;
|
||
|
|
|
||
|
|
memcpy(bufname, name, length);
|
||
|
|
bufname[length] = '\0';
|
||
|
|
|
||
|
|
return bsearch(
|
||
|
|
&key,
|
||
|
|
vgs_commands,
|
||
|
|
FF_ARRAY_ELEMS(vgs_commands),
|
||
|
|
sizeof(vgs_commands[0]),
|
||
|
|
vgs_comp_command_spec
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Return `1` if the command changes the current path in the cairo context.
|
||
|
|
static int vgs_cmd_change_path(enum VGSCommand cmd) {
|
||
|
|
switch (cmd) {
|
||
|
|
case CMD_BREAK:
|
||
|
|
case CMD_COLOR_STOP:
|
||
|
|
case CMD_DEF_HSLA:
|
||
|
|
case CMD_DEF_RGBA:
|
||
|
|
case CMD_GET_METADATA:
|
||
|
|
case CMD_IF:
|
||
|
|
case CMD_LINEAR_GRAD:
|
||
|
|
case CMD_PRINT:
|
||
|
|
case CMD_PROC_ASSIGN:
|
||
|
|
case CMD_PROC_CALL:
|
||
|
|
case CMD_RADIAL_GRAD:
|
||
|
|
case CMD_REPEAT:
|
||
|
|
case CMD_RESET_DASH:
|
||
|
|
case CMD_RESET_MATRIX:
|
||
|
|
case CMD_SET_COLOR:
|
||
|
|
case CMD_SET_DASH:
|
||
|
|
case CMD_SET_DASH_OFFSET:
|
||
|
|
case CMD_SET_HSLA:
|
||
|
|
case CMD_SET_LINE_CAP:
|
||
|
|
case CMD_SET_LINE_JOIN:
|
||
|
|
case CMD_SET_LINE_WIDTH:
|
||
|
|
case CMD_SET_RGBA:
|
||
|
|
case CMD_SET_VAR:
|
||
|
|
return 0;
|
||
|
|
|
||
|
|
default:
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* == VGS Parser ==
|
||
|
|
*
|
||
|
|
* The lexer determines the token kind by reading the first character after a
|
||
|
|
* delimiter (any of " \n\t\r,").
|
||
|
|
*
|
||
|
|
* The output of the parser is an instance of `VGSProgram`. It is a list of
|
||
|
|
* statements, and each statement is a command opcode and its arguments. This
|
||
|
|
* instance is created on filter initialization, and reused for every frame.
|
||
|
|
*
|
||
|
|
* User-variables are stored in an array initialized with a copy of
|
||
|
|
* `vgs_default_vars`.
|
||
|
|
*
|
||
|
|
* Blocks (the body for procedures, `if`, and `repeat`) are stored as nested
|
||
|
|
* `VGSProgram` instances.
|
||
|
|
*
|
||
|
|
* The source is assumed to be ASCII. If it contains multibyte chars, each
|
||
|
|
* byte is treated as an individual character. This is only relevant when the
|
||
|
|
* parser must report the location of a syntax error.
|
||
|
|
*
|
||
|
|
* There is no error recovery. The first invalid token will stop the parser.
|
||
|
|
*/
|
||
|
|
|
||
|
|
struct VGSParser {
|
||
|
|
const char* source;
|
||
|
|
size_t cursor;
|
||
|
|
|
||
|
|
const char **proc_names;
|
||
|
|
int proc_names_count;
|
||
|
|
|
||
|
|
// Store the variable names for the default ones (from `vgs_default_vars`)
|
||
|
|
// and the variables created with `setvar`.
|
||
|
|
//
|
||
|
|
// The extra slot is needed to store the `NULL` terminator expected by
|
||
|
|
// `av_expr_parse`.
|
||
|
|
const char *var_names[VAR_COUNT + 1];
|
||
|
|
};
|
||
|
|
|
||
|
|
struct VGSParserToken {
|
||
|
|
enum {
|
||
|
|
TOKEN_EOF = 1,
|
||
|
|
TOKEN_EXPR,
|
||
|
|
TOKEN_LEFT_BRACKET,
|
||
|
|
TOKEN_LITERAL,
|
||
|
|
TOKEN_RIGHT_BRACKET,
|
||
|
|
TOKEN_WORD,
|
||
|
|
} type;
|
||
|
|
|
||
|
|
const char *lexeme;
|
||
|
|
size_t position;
|
||
|
|
size_t length;
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Check if `token` is the value of `str`.
|
||
|
|
static int vgs_token_is_string(const struct VGSParserToken *token, const char *str) {
|
||
|
|
return strncmp(str, token->lexeme, token->length) == 0
|
||
|
|
&& str[token->length] == '\0';
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Compute the line/column numbers of the given token.
|
||
|
|
static void vgs_token_span(
|
||
|
|
const struct VGSParser *parser,
|
||
|
|
const struct VGSParserToken *token,
|
||
|
|
size_t *line,
|
||
|
|
size_t *column
|
||
|
|
) {
|
||
|
|
const char *source = parser->source;
|
||
|
|
|
||
|
|
*line = 1;
|
||
|
|
|
||
|
|
for (;;) {
|
||
|
|
const char *sep = strchr(source, '\n');
|
||
|
|
|
||
|
|
if (sep == NULL || (sep - parser->source) > token->position) {
|
||
|
|
*column = token->position - (source - parser->source) + 1;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
++*line;
|
||
|
|
source = sep + 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static av_printf_format(4, 5)
|
||
|
|
void vgs_log_invalid_token(
|
||
|
|
void *log_ctx,
|
||
|
|
const struct VGSParser *parser,
|
||
|
|
const struct VGSParserToken *token,
|
||
|
|
const char *extra_fmt,
|
||
|
|
...
|
||
|
|
) {
|
||
|
|
va_list ap;
|
||
|
|
char extra[256];
|
||
|
|
size_t line, column;
|
||
|
|
|
||
|
|
vgs_token_span(parser, token, &line, &column);
|
||
|
|
|
||
|
|
// Format extra message.
|
||
|
|
va_start(ap, extra_fmt);
|
||
|
|
vsnprintf(extra, sizeof(extra), extra_fmt, ap);
|
||
|
|
va_end(ap);
|
||
|
|
|
||
|
|
av_log(log_ctx, AV_LOG_ERROR,
|
||
|
|
"Invalid token '%.*s' at line %zu, column %zu: %s\n",
|
||
|
|
(int)token->length, token->lexeme, line, column, extra);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Return the next token in the source.
|
||
|
|
///
|
||
|
|
/// @param[out] token Next token.
|
||
|
|
/// @param[in] advance If true, the cursor is updated after finding a token.
|
||
|
|
///
|
||
|
|
/// @return `0` on success, and a negative `AVERROR` code on failure.
|
||
|
|
static int vgs_parser_next_token(
|
||
|
|
void *log_ctx,
|
||
|
|
struct VGSParser *parser,
|
||
|
|
struct VGSParserToken *token,
|
||
|
|
int advance
|
||
|
|
) {
|
||
|
|
|
||
|
|
#define WORD_SEPARATOR " \n\t\r,"
|
||
|
|
|
||
|
|
int level;
|
||
|
|
size_t cursor, length;
|
||
|
|
const char *source;
|
||
|
|
|
||
|
|
next_token:
|
||
|
|
|
||
|
|
source = &parser->source[parser->cursor];
|
||
|
|
|
||
|
|
cursor = strspn(source, WORD_SEPARATOR);
|
||
|
|
token->position = parser->cursor + cursor;
|
||
|
|
token->lexeme = &source[cursor];
|
||
|
|
|
||
|
|
switch (source[cursor]) {
|
||
|
|
case '\0':
|
||
|
|
token->type = TOKEN_EOF;
|
||
|
|
token->lexeme = "<EOF>";
|
||
|
|
token->length = 5;
|
||
|
|
return 0;
|
||
|
|
|
||
|
|
case '(':
|
||
|
|
// Find matching parenthesis.
|
||
|
|
level = 1;
|
||
|
|
length = 1;
|
||
|
|
|
||
|
|
while (level > 0) {
|
||
|
|
switch (source[cursor + length]) {
|
||
|
|
case '\0':
|
||
|
|
token->length = 1; // Show only the '(' in the error message.
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, token, "Unmatched parenthesis.");
|
||
|
|
return AVERROR(EINVAL);
|
||
|
|
|
||
|
|
case '(':
|
||
|
|
level++;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case ')':
|
||
|
|
level--;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
length++;
|
||
|
|
}
|
||
|
|
|
||
|
|
token->type = TOKEN_EXPR;
|
||
|
|
token->length = length;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case '{':
|
||
|
|
token->type = TOKEN_LEFT_BRACKET;
|
||
|
|
token->length = 1;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case '}':
|
||
|
|
token->type = TOKEN_RIGHT_BRACKET;
|
||
|
|
token->length = 1;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case '+':
|
||
|
|
case '-':
|
||
|
|
case '.':
|
||
|
|
case '0':
|
||
|
|
case '1':
|
||
|
|
case '2':
|
||
|
|
case '3':
|
||
|
|
case '4':
|
||
|
|
case '5':
|
||
|
|
case '6':
|
||
|
|
case '7':
|
||
|
|
case '8':
|
||
|
|
case '9':
|
||
|
|
token->type = TOKEN_LITERAL;
|
||
|
|
token->length = strcspn(token->lexeme, WORD_SEPARATOR);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case '/':
|
||
|
|
// If the next character is also '/', ignore the rest of
|
||
|
|
// the line.
|
||
|
|
//
|
||
|
|
// If it is something else, return a `TOKEN_WORD`.
|
||
|
|
if (source[cursor + 1] == '/') {
|
||
|
|
parser->cursor += cursor + strcspn(token->lexeme, "\n");
|
||
|
|
goto next_token;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* fallthrough */
|
||
|
|
|
||
|
|
default:
|
||
|
|
token->type = TOKEN_WORD;
|
||
|
|
token->length = strcspn(token->lexeme, WORD_SEPARATOR);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (advance) {
|
||
|
|
parser->cursor += cursor + token->length;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Command arguments.
|
||
|
|
struct VGSArgument {
|
||
|
|
enum {
|
||
|
|
ARG_COLOR = 1,
|
||
|
|
ARG_COLOR_VAR,
|
||
|
|
ARG_CONST,
|
||
|
|
ARG_EXPR,
|
||
|
|
ARG_LITERAL,
|
||
|
|
ARG_METADATA,
|
||
|
|
ARG_PROCEDURE_ID,
|
||
|
|
ARG_SUBPROGRAM,
|
||
|
|
ARG_VARIABLE,
|
||
|
|
} type;
|
||
|
|
|
||
|
|
union {
|
||
|
|
uint8_t color[4];
|
||
|
|
int constant;
|
||
|
|
AVExpr *expr;
|
||
|
|
double literal;
|
||
|
|
int proc_id;
|
||
|
|
struct VGSProgram *subprogram;
|
||
|
|
int variable;
|
||
|
|
};
|
||
|
|
|
||
|
|
char *metadata;
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Program statements.
|
||
|
|
struct VGSStatement {
|
||
|
|
enum VGSCommand cmd;
|
||
|
|
struct VGSArgument *args;
|
||
|
|
int args_count;
|
||
|
|
};
|
||
|
|
|
||
|
|
struct VGSProgram {
|
||
|
|
struct VGSStatement *statements;
|
||
|
|
int statements_count;
|
||
|
|
|
||
|
|
const char **proc_names;
|
||
|
|
int proc_names_count;
|
||
|
|
};
|
||
|
|
|
||
|
|
static void vgs_free(struct VGSProgram *program);
|
||
|
|
|
||
|
|
static int vgs_parse(
|
||
|
|
void *log_ctx,
|
||
|
|
struct VGSParser *parser,
|
||
|
|
struct VGSProgram *program,
|
||
|
|
int subprogram
|
||
|
|
);
|
||
|
|
|
||
|
|
static void vgs_statement_free(struct VGSStatement *stm) {
|
||
|
|
if (stm->args == NULL)
|
||
|
|
return;
|
||
|
|
|
||
|
|
for (int j = 0; j < stm->args_count; j++) {
|
||
|
|
struct VGSArgument *arg = &stm->args[j];
|
||
|
|
|
||
|
|
switch (arg->type) {
|
||
|
|
case ARG_EXPR:
|
||
|
|
av_expr_free(arg->expr);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case ARG_SUBPROGRAM:
|
||
|
|
vgs_free(arg->subprogram);
|
||
|
|
av_freep(&arg->subprogram);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
av_freep(&arg->metadata);
|
||
|
|
}
|
||
|
|
|
||
|
|
av_freep(&stm->args);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Release the memory allocated by the program.
|
||
|
|
static void vgs_free(struct VGSProgram *program) {
|
||
|
|
if (program->statements == NULL)
|
||
|
|
return;
|
||
|
|
|
||
|
|
for (int i = 0; i < program->statements_count; i++)
|
||
|
|
vgs_statement_free(&program->statements[i]);
|
||
|
|
|
||
|
|
av_freep(&program->statements);
|
||
|
|
|
||
|
|
if (program->proc_names != NULL) {
|
||
|
|
for (int i = 0; i < program->proc_names_count; i++)
|
||
|
|
av_freep(&program->proc_names[i]);
|
||
|
|
|
||
|
|
av_freep(&program->proc_names);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Consume the next argument as a numeric value, and store it in `arg`.
|
||
|
|
///
|
||
|
|
/// Return `0` on success, and a negative `AVERROR` code on failure.
|
||
|
|
static int vgs_parse_numeric_argument(
|
||
|
|
void *log_ctx,
|
||
|
|
struct VGSParser *parser,
|
||
|
|
struct VGSArgument *arg,
|
||
|
|
int metadata
|
||
|
|
) {
|
||
|
|
int ret;
|
||
|
|
char stack_buf[64];
|
||
|
|
char *lexeme, *endp;
|
||
|
|
struct VGSParserToken token;
|
||
|
|
|
||
|
|
ret = vgs_parser_next_token(log_ctx, parser, &token, 1);
|
||
|
|
if (ret != 0)
|
||
|
|
return ret;
|
||
|
|
|
||
|
|
// Convert the lexeme to a NUL-terminated string. Small lexemes are copied
|
||
|
|
// to a buffer on the stack; thus, it avoids allocating memory is most cases.
|
||
|
|
if (token.length + 1 < sizeof(stack_buf)) {
|
||
|
|
lexeme = stack_buf;
|
||
|
|
} else {
|
||
|
|
lexeme = av_malloc(token.length + 1);
|
||
|
|
|
||
|
|
if (lexeme == NULL)
|
||
|
|
return AVERROR(ENOMEM);
|
||
|
|
}
|
||
|
|
|
||
|
|
memcpy(lexeme, token.lexeme, token.length);
|
||
|
|
lexeme[token.length] = '\0';
|
||
|
|
|
||
|
|
switch (token.type) {
|
||
|
|
case TOKEN_LITERAL:
|
||
|
|
arg->type = ARG_LITERAL;
|
||
|
|
arg->literal = av_strtod(lexeme, &endp);
|
||
|
|
|
||
|
|
if (*endp != '\0') {
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token, "Expected valid number.");
|
||
|
|
ret = AVERROR(EINVAL);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case TOKEN_EXPR:
|
||
|
|
arg->type = ARG_EXPR;
|
||
|
|
ret = av_expr_parse(
|
||
|
|
&arg->expr,
|
||
|
|
lexeme,
|
||
|
|
parser->var_names,
|
||
|
|
vgs_func1_names,
|
||
|
|
vgs_func1_impls,
|
||
|
|
vgs_func2_names,
|
||
|
|
vgs_func2_impls,
|
||
|
|
0,
|
||
|
|
log_ctx
|
||
|
|
);
|
||
|
|
|
||
|
|
if (ret != 0)
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token, "Invalid expression.");
|
||
|
|
|
||
|
|
break;
|
||
|
|
|
||
|
|
case TOKEN_WORD:
|
||
|
|
ret = 1;
|
||
|
|
for (int i = 0; i < VAR_COUNT; i++) {
|
||
|
|
const char *var = parser->var_names[i];
|
||
|
|
if (var == NULL)
|
||
|
|
break;
|
||
|
|
|
||
|
|
if (vgs_token_is_string(&token, var)) {
|
||
|
|
arg->type = ARG_VARIABLE;
|
||
|
|
arg->variable = i;
|
||
|
|
ret = 0;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ret == 0)
|
||
|
|
break;
|
||
|
|
|
||
|
|
/* fallthrough */
|
||
|
|
|
||
|
|
default:
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token, "Expected numeric argument.");
|
||
|
|
ret = AVERROR(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ret == 0) {
|
||
|
|
if (metadata) {
|
||
|
|
size_t line, column;
|
||
|
|
vgs_token_span(parser, &token, &line, &column);
|
||
|
|
arg->metadata = av_asprintf("[%zu:%zu] %s", line, column, lexeme);
|
||
|
|
} else {
|
||
|
|
arg->metadata = NULL;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
memset(arg, 0, sizeof(*arg));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (lexeme != stack_buf)
|
||
|
|
av_freep(&lexeme);
|
||
|
|
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Check if the next token is a numeric value, so the last command must be
|
||
|
|
/// repeated.
|
||
|
|
static int vgs_parser_can_repeat_cmd(void *log_ctx, struct VGSParser *parser) {
|
||
|
|
struct VGSParserToken token = { 0 };
|
||
|
|
|
||
|
|
const int ret = vgs_parser_next_token(log_ctx, parser, &token, 0);
|
||
|
|
|
||
|
|
if (ret != 0)
|
||
|
|
return ret;
|
||
|
|
|
||
|
|
switch (token.type) {
|
||
|
|
case TOKEN_EXPR:
|
||
|
|
case TOKEN_LITERAL:
|
||
|
|
return 0;
|
||
|
|
|
||
|
|
case TOKEN_WORD:
|
||
|
|
// If the next token is a word, it will be considered to repeat
|
||
|
|
// the command only if it is a variable, and there is not
|
||
|
|
// known command with the same name.
|
||
|
|
|
||
|
|
if (vgs_get_command(token.lexeme, token.length) != NULL)
|
||
|
|
return 1;
|
||
|
|
|
||
|
|
for (int i = 0; i < VAR_COUNT; i++) {
|
||
|
|
const char *var = parser->var_names[i];
|
||
|
|
if (var == NULL)
|
||
|
|
return 1;
|
||
|
|
|
||
|
|
if (vgs_token_is_string(&token, var))
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
|
||
|
|
default:
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
static int vgs_is_valid_identifier(const struct VGSParserToken *token) {
|
||
|
|
// An identifier is valid if:
|
||
|
|
//
|
||
|
|
// - It starts with an alphabetic character or an underscore.
|
||
|
|
// - Everything else, alphanumeric or underscore
|
||
|
|
|
||
|
|
for (int i = 0; i < token->length; i++) {
|
||
|
|
char c = token->lexeme[i];
|
||
|
|
if (c != '_'
|
||
|
|
&& !(c >= 'a' && c <= 'z')
|
||
|
|
&& !(c >= 'A' && c <= 'Z')
|
||
|
|
&& !(i > 0 && c >= '0' && c <= '9')
|
||
|
|
) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Extract the arguments for a command, and add a new statement
|
||
|
|
/// to the program.
|
||
|
|
///
|
||
|
|
/// On success, return `0`.
|
||
|
|
static int vgs_parse_statement(
|
||
|
|
void *log_ctx,
|
||
|
|
struct VGSParser *parser,
|
||
|
|
struct VGSProgram *program,
|
||
|
|
const struct VGSCommandSpec *decl
|
||
|
|
) {
|
||
|
|
|
||
|
|
#define FAIL(err) \
|
||
|
|
do { \
|
||
|
|
vgs_statement_free(&statement); \
|
||
|
|
return AVERROR(err); \
|
||
|
|
} while(0)
|
||
|
|
|
||
|
|
struct VGSStatement statement = {
|
||
|
|
.cmd = decl->cmd,
|
||
|
|
.args = NULL,
|
||
|
|
.args_count = 0,
|
||
|
|
};
|
||
|
|
|
||
|
|
const struct VGSParameter *param = &decl->params[0];
|
||
|
|
|
||
|
|
int proc_args_count = 0;
|
||
|
|
|
||
|
|
for (;;) {
|
||
|
|
int ret;
|
||
|
|
void *r;
|
||
|
|
|
||
|
|
struct VGSParserToken token = { 0 };
|
||
|
|
struct VGSArgument arg = { 0 };
|
||
|
|
|
||
|
|
switch (param->type) {
|
||
|
|
case PARAM_VARIADIC:
|
||
|
|
// If the next token is numeric, repeat the previous parameter
|
||
|
|
// to append it to the current statement.
|
||
|
|
|
||
|
|
if (statement.args_count < MAX_COMMAND_PARAMS
|
||
|
|
&& vgs_parser_can_repeat_cmd(log_ctx, parser) == 0
|
||
|
|
) {
|
||
|
|
param--;
|
||
|
|
} else {
|
||
|
|
param++;
|
||
|
|
}
|
||
|
|
|
||
|
|
continue;
|
||
|
|
|
||
|
|
case PARAM_END:
|
||
|
|
case PARAM_MAY_REPEAT:
|
||
|
|
// Add the built statement to the program.
|
||
|
|
r = av_dynarray2_add(
|
||
|
|
(void*)&program->statements,
|
||
|
|
&program->statements_count,
|
||
|
|
sizeof(statement),
|
||
|
|
(void*)&statement
|
||
|
|
);
|
||
|
|
|
||
|
|
if (r == NULL)
|
||
|
|
FAIL(ENOMEM);
|
||
|
|
|
||
|
|
// May repeat if the next token is numeric.
|
||
|
|
if (param->type != PARAM_END
|
||
|
|
&& vgs_parser_can_repeat_cmd(log_ctx, parser) == 0
|
||
|
|
) {
|
||
|
|
param = &decl->params[0];
|
||
|
|
statement.args = NULL;
|
||
|
|
statement.args_count = 0;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
|
||
|
|
case PARAM_COLOR:
|
||
|
|
ret = vgs_parser_next_token(log_ctx, parser, &token, 1);
|
||
|
|
if (ret != 0)
|
||
|
|
FAIL(EINVAL);
|
||
|
|
|
||
|
|
arg.type = ARG_COLOR;
|
||
|
|
|
||
|
|
for (int i = VAR_U0; i < VAR_COUNT; i++) {
|
||
|
|
if (parser->var_names[i] == NULL)
|
||
|
|
break;
|
||
|
|
|
||
|
|
if (vgs_token_is_string(&token, parser->var_names[i])) {
|
||
|
|
arg.type = ARG_COLOR_VAR;
|
||
|
|
arg.variable = i;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (arg.type == ARG_COLOR_VAR)
|
||
|
|
break;
|
||
|
|
|
||
|
|
ret = av_parse_color(arg.color, token.lexeme, token.length, log_ctx);
|
||
|
|
if (ret != 0) {
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token, "Expected color.");
|
||
|
|
FAIL(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
break;
|
||
|
|
|
||
|
|
case PARAM_CONSTANT: {
|
||
|
|
int found = 0;
|
||
|
|
char expected_names[64] = { 0 };
|
||
|
|
|
||
|
|
ret = vgs_parser_next_token(log_ctx, parser, &token, 1);
|
||
|
|
if (ret != 0)
|
||
|
|
FAIL(EINVAL);
|
||
|
|
|
||
|
|
for (
|
||
|
|
const struct VGSConstant *constant = param->constants;
|
||
|
|
constant->name != NULL;
|
||
|
|
constant++
|
||
|
|
) {
|
||
|
|
if (vgs_token_is_string(&token, constant->name)) {
|
||
|
|
arg.type = ARG_CONST;
|
||
|
|
arg.constant = constant->value;
|
||
|
|
|
||
|
|
found = 1;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Collect valid names to include them in the error message, in case
|
||
|
|
// the name is not found.
|
||
|
|
av_strlcatf(expected_names, sizeof(expected_names), " '%s'", constant->name);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!found) {
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token, "Expected one of%s.", expected_names);
|
||
|
|
FAIL(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
case PARAM_PROC_ARGS:
|
||
|
|
if (vgs_parser_can_repeat_cmd(log_ctx, parser) != 0) {
|
||
|
|
// No more arguments. Jump to next parameter.
|
||
|
|
param++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (proc_args_count++ >= MAX_PROC_ARGS) {
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token,
|
||
|
|
"Too many arguments. Limit is %d", MAX_PROC_ARGS);
|
||
|
|
FAIL(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* fallthrough */
|
||
|
|
|
||
|
|
case PARAM_NUMERIC:
|
||
|
|
case PARAM_NUMERIC_METADATA:
|
||
|
|
ret = vgs_parse_numeric_argument(
|
||
|
|
log_ctx,
|
||
|
|
parser,
|
||
|
|
&arg,
|
||
|
|
param->type == PARAM_NUMERIC_METADATA
|
||
|
|
);
|
||
|
|
|
||
|
|
if (ret != 0)
|
||
|
|
FAIL(EINVAL);
|
||
|
|
|
||
|
|
break;
|
||
|
|
|
||
|
|
case PARAM_PROC_NAME: {
|
||
|
|
int proc_id;
|
||
|
|
|
||
|
|
ret = vgs_parser_next_token(log_ctx, parser, &token, 1);
|
||
|
|
if (ret != 0)
|
||
|
|
FAIL(EINVAL);
|
||
|
|
|
||
|
|
if (!vgs_is_valid_identifier(&token)) {
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token, "Invalid procedure name.");
|
||
|
|
FAIL(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use the index in the array as the identifier of the name.
|
||
|
|
|
||
|
|
for (proc_id = 0; proc_id < parser->proc_names_count; proc_id++) {
|
||
|
|
if (vgs_token_is_string(&token, parser->proc_names[proc_id]))
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (proc_id == parser->proc_names_count) {
|
||
|
|
const char *name = av_strndup(token.lexeme, token.length);
|
||
|
|
|
||
|
|
const char **r = av_dynarray2_add(
|
||
|
|
(void*)&parser->proc_names,
|
||
|
|
&parser->proc_names_count,
|
||
|
|
sizeof(name),
|
||
|
|
(void*)&name
|
||
|
|
);
|
||
|
|
|
||
|
|
if (r == NULL) {
|
||
|
|
av_freep(&name);
|
||
|
|
FAIL(ENOMEM);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
arg.type = ARG_PROCEDURE_ID;
|
||
|
|
arg.proc_id = proc_id;
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
case PARAM_RAW_IDENT:
|
||
|
|
ret = vgs_parser_next_token(log_ctx, parser, &token, 1);
|
||
|
|
if (ret != 0)
|
||
|
|
FAIL(EINVAL);
|
||
|
|
|
||
|
|
switch (token.type) {
|
||
|
|
case TOKEN_LITERAL:
|
||
|
|
case TOKEN_WORD:
|
||
|
|
arg.type = ARG_METADATA;
|
||
|
|
arg.metadata = av_strndup(token.lexeme, token.length);
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token, "Expected '{'.");
|
||
|
|
FAIL(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
break;
|
||
|
|
|
||
|
|
case PARAM_SUBPROGRAM:
|
||
|
|
ret = vgs_parser_next_token(log_ctx, parser, &token, 1);
|
||
|
|
if (ret != 0)
|
||
|
|
FAIL(EINVAL);
|
||
|
|
|
||
|
|
if (token.type != TOKEN_LEFT_BRACKET) {
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token, "Expected '{'.");
|
||
|
|
FAIL(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
arg.type = ARG_SUBPROGRAM;
|
||
|
|
arg.subprogram = av_mallocz(sizeof(struct VGSProgram));
|
||
|
|
|
||
|
|
ret = vgs_parse(log_ctx, parser, arg.subprogram, 1);
|
||
|
|
if (ret != 0) {
|
||
|
|
av_freep(&arg.subprogram);
|
||
|
|
FAIL(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
break;
|
||
|
|
|
||
|
|
case PARAM_PROC_PARAMS:
|
||
|
|
ret = vgs_parser_next_token(log_ctx, parser, &token, 0);
|
||
|
|
if (ret != 0)
|
||
|
|
FAIL(EINVAL);
|
||
|
|
|
||
|
|
if (token.type == TOKEN_WORD && proc_args_count++ >= MAX_PROC_ARGS) {
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token,
|
||
|
|
"Too many parameters. Limit is %d", MAX_PROC_ARGS);
|
||
|
|
FAIL(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (token.type != TOKEN_WORD) {
|
||
|
|
// No more variables. Jump to next parameter.
|
||
|
|
param++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* fallthrough */
|
||
|
|
|
||
|
|
case PARAM_VAR_NAME: {
|
||
|
|
int var_idx = -1;
|
||
|
|
|
||
|
|
ret = vgs_parser_next_token(log_ctx, parser, &token, 1);
|
||
|
|
if (ret != 0)
|
||
|
|
FAIL(EINVAL);
|
||
|
|
|
||
|
|
// Find the slot where the variable is allocated, or the next
|
||
|
|
// available slot if it is a new variable.
|
||
|
|
for (int i = 0; i < VAR_COUNT; i++) {
|
||
|
|
if (parser->var_names[i] == NULL
|
||
|
|
|| vgs_token_is_string(&token, parser->var_names[i])
|
||
|
|
) {
|
||
|
|
var_idx = i;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// No free slots to allocate new variables.
|
||
|
|
if (var_idx == -1) {
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token,
|
||
|
|
"Too many user variables. Can define up to %d variables.", USER_VAR_COUNT);
|
||
|
|
FAIL(E2BIG);
|
||
|
|
}
|
||
|
|
|
||
|
|
// If the index is before `VAR_U0`, the name is already taken by
|
||
|
|
// a default variable.
|
||
|
|
if (var_idx < VAR_U0) {
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token, "Reserved variable name.");
|
||
|
|
FAIL(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Need to allocate a new variable.
|
||
|
|
if (parser->var_names[var_idx] == NULL) {
|
||
|
|
if (!vgs_is_valid_identifier(&token)) {
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token, "Invalid variable name.");
|
||
|
|
FAIL(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
parser->var_names[var_idx] = av_strndup(token.lexeme, token.length);
|
||
|
|
}
|
||
|
|
|
||
|
|
arg.type = ARG_CONST;
|
||
|
|
arg.constant = var_idx;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
default:
|
||
|
|
av_assert0(0); /* unreachable */
|
||
|
|
}
|
||
|
|
|
||
|
|
r = av_dynarray2_add(
|
||
|
|
(void*)&statement.args,
|
||
|
|
&statement.args_count,
|
||
|
|
sizeof(arg),
|
||
|
|
(void*)&arg
|
||
|
|
);
|
||
|
|
|
||
|
|
if (r == NULL)
|
||
|
|
FAIL(ENOMEM);
|
||
|
|
|
||
|
|
switch (param->type) {
|
||
|
|
case PARAM_PROC_ARGS:
|
||
|
|
case PARAM_PROC_PARAMS:
|
||
|
|
// Don't update params.
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
param++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#undef FAIL
|
||
|
|
}
|
||
|
|
|
||
|
|
static void vgs_parser_init(struct VGSParser *parser, const char *source) {
|
||
|
|
parser->source = source;
|
||
|
|
parser->cursor = 0;
|
||
|
|
|
||
|
|
parser->proc_names = NULL;
|
||
|
|
parser->proc_names_count = 0;
|
||
|
|
|
||
|
|
memset(parser->var_names, 0, sizeof(parser->var_names));
|
||
|
|
for (int i = 0; i < VAR_U0; i++)
|
||
|
|
parser->var_names[i] = vgs_default_vars[i];
|
||
|
|
}
|
||
|
|
|
||
|
|
static void vgs_parser_free(struct VGSParser *parser) {
|
||
|
|
for (int i = VAR_U0; i < VAR_COUNT; i++)
|
||
|
|
if (parser->var_names[i] != NULL)
|
||
|
|
av_freep(&parser->var_names[i]);
|
||
|
|
|
||
|
|
if (parser->proc_names != NULL) {
|
||
|
|
for (int i = 0; i < parser->proc_names_count; i++)
|
||
|
|
av_freep(&parser->proc_names[i]);
|
||
|
|
|
||
|
|
av_freep(&parser->proc_names);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Build a program by parsing a script.
|
||
|
|
///
|
||
|
|
/// `subprogram` must be true when the function is called to parse the body of
|
||
|
|
/// a block (like `if` or `proc` commands).
|
||
|
|
///
|
||
|
|
/// Return `0` on success, and a negative `AVERROR` code on failure.
|
||
|
|
static int vgs_parse(
|
||
|
|
void *log_ctx,
|
||
|
|
struct VGSParser *parser,
|
||
|
|
struct VGSProgram *program,
|
||
|
|
int subprogram
|
||
|
|
) {
|
||
|
|
struct VGSParserToken token;
|
||
|
|
|
||
|
|
memset(program, 0, sizeof(*program));
|
||
|
|
|
||
|
|
for (;;) {
|
||
|
|
int ret;
|
||
|
|
const struct VGSCommandSpec *cmd;
|
||
|
|
|
||
|
|
ret = vgs_parser_next_token(log_ctx, parser, &token, 1);
|
||
|
|
if (ret != 0)
|
||
|
|
goto fail;
|
||
|
|
|
||
|
|
switch (token.type) {
|
||
|
|
case TOKEN_EOF:
|
||
|
|
if (subprogram) {
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token, "Expected '}'.");
|
||
|
|
goto fail;
|
||
|
|
} else {
|
||
|
|
// Move the proc names to the main program.
|
||
|
|
FFSWAP(const char **, program->proc_names, parser->proc_names);
|
||
|
|
FFSWAP(int, program->proc_names_count, parser->proc_names_count);
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
|
||
|
|
case TOKEN_WORD:
|
||
|
|
// The token must be a valid command.
|
||
|
|
cmd = vgs_get_command(token.lexeme, token.length);
|
||
|
|
if (cmd == NULL)
|
||
|
|
goto invalid_token;
|
||
|
|
|
||
|
|
ret = vgs_parse_statement(log_ctx, parser, program, cmd);
|
||
|
|
if (ret != 0)
|
||
|
|
goto fail;
|
||
|
|
|
||
|
|
break;
|
||
|
|
|
||
|
|
case TOKEN_RIGHT_BRACKET:
|
||
|
|
if (!subprogram)
|
||
|
|
goto invalid_token;
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
|
||
|
|
default:
|
||
|
|
goto invalid_token;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return AVERROR_BUG; /* unreachable */
|
||
|
|
|
||
|
|
invalid_token:
|
||
|
|
vgs_log_invalid_token(log_ctx, parser, &token, "Expected command.");
|
||
|
|
|
||
|
|
fail:
|
||
|
|
vgs_free(program);
|
||
|
|
return AVERROR(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* == Interpreter ==
|
||
|
|
*
|
||
|
|
* The interpreter takes the `VGSProgram` built by the parser, and translate the
|
||
|
|
* statements to calls to cairo.
|
||
|
|
*
|
||
|
|
* `VGSEvalState` tracks the state needed to execute such commands.
|
||
|
|
*/
|
||
|
|
|
||
|
|
/// Number of different states for the `randomg` function.
|
||
|
|
#define RANDOM_STATES 4
|
||
|
|
|
||
|
|
/// Block assigned to a procedure by a call to the `proc` command.
|
||
|
|
struct VGSProcedure {
|
||
|
|
const struct VGSProgram *program;
|
||
|
|
|
||
|
|
/// Number of expected arguments.
|
||
|
|
int proc_args_count;
|
||
|
|
|
||
|
|
/// Variable ids where each argument is stored.
|
||
|
|
int args[MAX_PROC_ARGS];
|
||
|
|
};
|
||
|
|
|
||
|
|
struct VGSEvalState {
|
||
|
|
void *log_ctx;
|
||
|
|
|
||
|
|
/// Current frame.
|
||
|
|
AVFrame *frame;
|
||
|
|
|
||
|
|
/// Cairo context for drawing operations.
|
||
|
|
cairo_t *cairo_ctx;
|
||
|
|
|
||
|
|
/// Pattern being built by commands like `colorstop`.
|
||
|
|
cairo_pattern_t *pattern_builder;
|
||
|
|
|
||
|
|
/// Register if `break` was called in a subprogram.
|
||
|
|
int interrupted;
|
||
|
|
|
||
|
|
/// Next call to `[eo]fill`, `[eo]clip`, or `stroke`, should use
|
||
|
|
/// the `_preserve` function.
|
||
|
|
int preserve_path;
|
||
|
|
|
||
|
|
/// Subprograms associated to each procedure identifier.
|
||
|
|
struct VGSProcedure *procedures;
|
||
|
|
|
||
|
|
/// Reference to the procedure names in the `VGSProgram`.
|
||
|
|
const char *const *proc_names;
|
||
|
|
|
||
|
|
/// Values for the variables in expressions.
|
||
|
|
///
|
||
|
|
/// Some variables (like `cx` or `cy`) are written before
|
||
|
|
/// executing each statement.
|
||
|
|
double vars[VAR_COUNT];
|
||
|
|
|
||
|
|
/// State for each index available for the `randomg` function.
|
||
|
|
FFSFC64 random_state[RANDOM_STATES];
|
||
|
|
|
||
|
|
/// Frame metadata, if any.
|
||
|
|
AVDictionary *metadata;
|
||
|
|
|
||
|
|
// Reflected Control Points. Used in T and S commands.
|
||
|
|
//
|
||
|
|
// See https://www.w3.org/TR/SVG/paths.html#ReflectedControlPoints
|
||
|
|
struct {
|
||
|
|
enum { RCP_NONE, RCP_VALID, RCP_UPDATED } status;
|
||
|
|
|
||
|
|
double cubic_x;
|
||
|
|
double cubic_y;
|
||
|
|
double quad_x;
|
||
|
|
double quad_y;
|
||
|
|
} rcp;
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Function `pathlen(n)` for `av_expr_eval`.
|
||
|
|
///
|
||
|
|
/// Compute the length of the current path in the cairo context. If `n > 0`, it
|
||
|
|
/// is the maximum number of segments to be added to the length.
|
||
|
|
static double vgs_fn_pathlen(void *data, double arg) {
|
||
|
|
if (!isfinite(arg))
|
||
|
|
return NAN;
|
||
|
|
|
||
|
|
const struct VGSEvalState *state = (struct VGSEvalState *)data;
|
||
|
|
|
||
|
|
int max_segments = (int)arg;
|
||
|
|
|
||
|
|
double lmx = NAN, lmy = NAN; // last move point
|
||
|
|
double cx = NAN, cy = NAN; // current point.
|
||
|
|
|
||
|
|
double length = 0;
|
||
|
|
cairo_path_t *path = cairo_copy_path_flat(state->cairo_ctx);
|
||
|
|
|
||
|
|
for (int i = 0; i < path->num_data; i += path->data[i].header.length) {
|
||
|
|
double x, y;
|
||
|
|
cairo_path_data_t *data = &path->data[i];
|
||
|
|
|
||
|
|
switch (data[0].header.type) {
|
||
|
|
case CAIRO_PATH_MOVE_TO:
|
||
|
|
cx = lmx = data[1].point.x;
|
||
|
|
cy = lmy = data[1].point.y;
|
||
|
|
|
||
|
|
// Don't update `length`.
|
||
|
|
continue;
|
||
|
|
|
||
|
|
case CAIRO_PATH_LINE_TO:
|
||
|
|
x = data[1].point.x;
|
||
|
|
y = data[1].point.y;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CAIRO_PATH_CLOSE_PATH:
|
||
|
|
x = lmx;
|
||
|
|
y = lmy;
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
length += hypot(cx - x, cy - y);
|
||
|
|
|
||
|
|
cx = x;
|
||
|
|
cy = y;
|
||
|
|
|
||
|
|
// If the function argument is `> 0`, use it as a limit for how
|
||
|
|
// many segments are added up.
|
||
|
|
if (--max_segments == 0)
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
cairo_path_destroy(path);
|
||
|
|
|
||
|
|
return length;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Function `randomg(n)` for `av_expr_eval`.
|
||
|
|
///
|
||
|
|
/// Compute a random value between 0 and 1. Similar to `random()`, but the
|
||
|
|
/// state is global to the VGS program.
|
||
|
|
///
|
||
|
|
/// The last 2 bits of the integer representation of the argument are used
|
||
|
|
/// as the state index. If the state is not initialized, the argument is
|
||
|
|
/// the seed for that state.
|
||
|
|
static double vgs_fn_randomg(void *data, double arg) {
|
||
|
|
if (!isfinite(arg))
|
||
|
|
return arg;
|
||
|
|
|
||
|
|
struct VGSEvalState *state = (struct VGSEvalState *)data;
|
||
|
|
|
||
|
|
const uint64_t iarg = (uint64_t)arg;
|
||
|
|
const int rng_idx = iarg % FF_ARRAY_ELEMS(state->random_state);
|
||
|
|
|
||
|
|
FFSFC64 *rng = &state->random_state[rng_idx];
|
||
|
|
|
||
|
|
if (rng->counter == 0)
|
||
|
|
ff_sfc64_init(rng, iarg, iarg, iarg, 12);
|
||
|
|
|
||
|
|
return ff_sfc64_get(rng) * (1.0 / UINT64_MAX);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Function `p(x, y)` for `av_expr_eval`.
|
||
|
|
///
|
||
|
|
/// Return the pixel color in 0xRRGGBBAA format.
|
||
|
|
///
|
||
|
|
/// The transformation matrix is applied to the given coordinates.
|
||
|
|
///
|
||
|
|
/// If the coordinates are outside the frame, return NAN.
|
||
|
|
static double vgs_fn_p(void* data, double x0, double y0) {
|
||
|
|
const struct VGSEvalState *state = (struct VGSEvalState *)data;
|
||
|
|
const AVFrame *frame = state->frame;
|
||
|
|
|
||
|
|
if (frame == NULL || !isfinite(x0) || !isfinite(y0))
|
||
|
|
return NAN;
|
||
|
|
|
||
|
|
cairo_user_to_device(state->cairo_ctx, &x0, &y0);
|
||
|
|
|
||
|
|
const int x = (int)x0;
|
||
|
|
const int y = (int)y0;
|
||
|
|
|
||
|
|
if (x < 0 || y < 0 || x >= frame->width || y >= frame->height)
|
||
|
|
return NAN;
|
||
|
|
|
||
|
|
const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(frame->format);
|
||
|
|
|
||
|
|
uint32_t color[4] = { 0, 0, 0, 255 };
|
||
|
|
|
||
|
|
for (int c = 0; c < desc->nb_components; c++) {
|
||
|
|
uint32_t pixel;
|
||
|
|
const int depth = desc->comp[c].depth;
|
||
|
|
|
||
|
|
av_read_image_line2(
|
||
|
|
&pixel,
|
||
|
|
(void*)frame->data,
|
||
|
|
frame->linesize,
|
||
|
|
desc,
|
||
|
|
x, y,
|
||
|
|
c,
|
||
|
|
1, // width
|
||
|
|
0, // read_pal_component
|
||
|
|
4 // dst_element_size
|
||
|
|
);
|
||
|
|
|
||
|
|
if (depth != 8) {
|
||
|
|
pixel = pixel * 255 / ((1 << depth) - 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
color[c] = pixel;
|
||
|
|
}
|
||
|
|
|
||
|
|
return color[0] << 24 | color[1] << 16 | color[2] << 8 | color[3];
|
||
|
|
}
|
||
|
|
|
||
|
|
static int vgs_eval_state_init(
|
||
|
|
struct VGSEvalState *state,
|
||
|
|
const struct VGSProgram *program,
|
||
|
|
void *log_ctx,
|
||
|
|
AVFrame *frame
|
||
|
|
) {
|
||
|
|
memset(state, 0, sizeof(*state));
|
||
|
|
|
||
|
|
state->log_ctx = log_ctx;
|
||
|
|
state->frame = frame;
|
||
|
|
state->rcp.status = RCP_NONE;
|
||
|
|
|
||
|
|
if (program->proc_names != NULL) {
|
||
|
|
state->procedures = av_calloc(sizeof(struct VGSProcedure), program->proc_names_count);
|
||
|
|
state->proc_names = program->proc_names;
|
||
|
|
|
||
|
|
if (state->procedures == NULL)
|
||
|
|
return AVERROR(ENOMEM);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (int i = 0; i < VAR_COUNT; i++)
|
||
|
|
state->vars[i] = NAN;
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void vgs_eval_state_free(struct VGSEvalState *state) {
|
||
|
|
if (state->pattern_builder != NULL)
|
||
|
|
cairo_pattern_destroy(state->pattern_builder);
|
||
|
|
|
||
|
|
if (state->procedures != NULL)
|
||
|
|
av_free(state->procedures);
|
||
|
|
|
||
|
|
memset(state, 0, sizeof(*state));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Draw an ellipse. `x`/`y` specifies the center, and `rx`/`ry` the radius of
|
||
|
|
/// the ellipse on the x/y axis.
|
||
|
|
///
|
||
|
|
/// Cairo does not provide a native way to create an ellipse, but it can be done
|
||
|
|
/// by scaling the Y axis with the transformation matrix.
|
||
|
|
static void draw_ellipse(cairo_t *c, double x, double y, double rx, double ry) {
|
||
|
|
cairo_save(c);
|
||
|
|
cairo_translate(c, x, y);
|
||
|
|
|
||
|
|
if (rx != ry)
|
||
|
|
cairo_scale(c, 1, ry / rx);
|
||
|
|
|
||
|
|
cairo_new_sub_path(c);
|
||
|
|
cairo_arc(c, 0, 0, rx, 0, 2 * M_PI);
|
||
|
|
cairo_close_path(c);
|
||
|
|
cairo_new_sub_path(c);
|
||
|
|
|
||
|
|
cairo_restore(c);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Draw a quadratic bezier from the current point to `x, y`, The control point
|
||
|
|
/// is specified by `x1, y1`.
|
||
|
|
///
|
||
|
|
/// If the control point is NAN, use the reflected point.
|
||
|
|
///
|
||
|
|
/// cairo only supports cubic cuvers, so control points must be adjusted to
|
||
|
|
/// simulate the behaviour in SVG.
|
||
|
|
static void draw_quad_curve_to(
|
||
|
|
struct VGSEvalState *state,
|
||
|
|
int relative,
|
||
|
|
double x1,
|
||
|
|
double y1,
|
||
|
|
double x,
|
||
|
|
double y
|
||
|
|
) {
|
||
|
|
double x0 = 0, y0 = 0; // Current point.
|
||
|
|
double xa, ya, xb, yb; // Control points for the cubic curve.
|
||
|
|
|
||
|
|
const int use_reflected = isnan(x1);
|
||
|
|
|
||
|
|
cairo_get_current_point(state->cairo_ctx, &x0, &y0);
|
||
|
|
|
||
|
|
if (relative) {
|
||
|
|
if (!use_reflected) {
|
||
|
|
x1 += x0;
|
||
|
|
y1 += y0;
|
||
|
|
}
|
||
|
|
|
||
|
|
x += x0;
|
||
|
|
y += y0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (use_reflected) {
|
||
|
|
if (state->rcp.status != RCP_NONE) {
|
||
|
|
x1 = state->rcp.quad_x;
|
||
|
|
y1 = state->rcp.quad_y;
|
||
|
|
} else {
|
||
|
|
x1 = x0;
|
||
|
|
y1 = y0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
xa = (x0 + 2 * x1) / 3;
|
||
|
|
ya = (y0 + 2 * y1) / 3;
|
||
|
|
xb = (x + 2 * x1) / 3;
|
||
|
|
yb = (y + 2 * y1) / 3;
|
||
|
|
cairo_curve_to(state->cairo_ctx, xa, ya, xb, yb, x, y);
|
||
|
|
|
||
|
|
state->rcp.status = RCP_UPDATED;
|
||
|
|
state->rcp.cubic_x = x1;
|
||
|
|
state->rcp.cubic_y = y1;
|
||
|
|
state->rcp.quad_x = 2 * x - x1;
|
||
|
|
state->rcp.quad_y = 2 * y - y1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Similar to quad_curve_to, but for cubic curves.
|
||
|
|
static void draw_cubic_curve_to(
|
||
|
|
struct VGSEvalState *state,
|
||
|
|
int relative,
|
||
|
|
double x1,
|
||
|
|
double y1,
|
||
|
|
double x2,
|
||
|
|
double y2,
|
||
|
|
double x,
|
||
|
|
double y
|
||
|
|
) {
|
||
|
|
double x0 = 0, y0 = 0; // Current point.
|
||
|
|
|
||
|
|
const int use_reflected = isnan(x1);
|
||
|
|
|
||
|
|
cairo_get_current_point(state->cairo_ctx, &x0, &y0);
|
||
|
|
|
||
|
|
if (relative) {
|
||
|
|
if (!use_reflected) {
|
||
|
|
x1 += x0;
|
||
|
|
y1 += y0;
|
||
|
|
}
|
||
|
|
|
||
|
|
x += x0;
|
||
|
|
y += y0;
|
||
|
|
x2 += x0;
|
||
|
|
y2 += y0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (use_reflected) {
|
||
|
|
if (state->rcp.status != RCP_NONE) {
|
||
|
|
x1 = state->rcp.cubic_x;
|
||
|
|
y1 = state->rcp.cubic_y;
|
||
|
|
} else {
|
||
|
|
x1 = x0;
|
||
|
|
y1 = y0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
cairo_curve_to(state->cairo_ctx, x1, y1, x2, y2, x, y);
|
||
|
|
|
||
|
|
state->rcp.status = RCP_UPDATED;
|
||
|
|
state->rcp.cubic_x = 2 * x - x2;
|
||
|
|
state->rcp.cubic_y = 2 * y - y2;
|
||
|
|
state->rcp.quad_x = x2;
|
||
|
|
state->rcp.quad_y = y2;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void draw_rounded_rect(
|
||
|
|
cairo_t *c,
|
||
|
|
double x,
|
||
|
|
double y,
|
||
|
|
double width,
|
||
|
|
double height,
|
||
|
|
double radius
|
||
|
|
) {
|
||
|
|
radius = av_clipd(radius, 0, FFMIN(height / 2, width / 2));
|
||
|
|
|
||
|
|
cairo_new_sub_path(c);
|
||
|
|
cairo_arc(c, x + radius, y + radius, radius, M_PI, 3 * M_PI / 2);
|
||
|
|
cairo_arc(c, x + width - radius, y + radius, radius, 3 * M_PI / 2, 2 * M_PI);
|
||
|
|
cairo_arc(c, x + width - radius, y + height - radius, radius, 0, M_PI / 2);
|
||
|
|
cairo_arc(c, x + radius, y + height - radius, radius, M_PI / 2, M_PI);
|
||
|
|
cairo_close_path(c);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void hsl2rgb(
|
||
|
|
double h,
|
||
|
|
double s,
|
||
|
|
double l,
|
||
|
|
double *pr,
|
||
|
|
double *pg,
|
||
|
|
double *pb
|
||
|
|
) {
|
||
|
|
// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB
|
||
|
|
|
||
|
|
double r, g, b, chroma, x, h1;
|
||
|
|
|
||
|
|
if (h < 0 || h >= 360)
|
||
|
|
h = fmod(FFMAX(h, 0), 360);
|
||
|
|
|
||
|
|
s = av_clipd(s, 0, 1);
|
||
|
|
l = av_clipd(l, 0, 1);
|
||
|
|
|
||
|
|
chroma = (1 - fabs(2 * l - 1)) * s;
|
||
|
|
h1 = h / 60;
|
||
|
|
x = chroma * (1 - fabs(fmod(h1, 2) - 1));
|
||
|
|
|
||
|
|
switch ((int)floor(h1)) {
|
||
|
|
case 0:
|
||
|
|
r = chroma;
|
||
|
|
g = x;
|
||
|
|
b = 0;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 1:
|
||
|
|
r = x;
|
||
|
|
g = chroma;
|
||
|
|
b = 0;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 2:
|
||
|
|
r = 0;
|
||
|
|
g = chroma;
|
||
|
|
b = x;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 3:
|
||
|
|
r = 0;
|
||
|
|
g = x;
|
||
|
|
b = chroma;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 4:
|
||
|
|
r = x;
|
||
|
|
g = 0;
|
||
|
|
b = chroma;
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
r = chroma;
|
||
|
|
g = 0;
|
||
|
|
b = x;
|
||
|
|
break;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
x = l - chroma / 2;
|
||
|
|
|
||
|
|
*pr = r + x;
|
||
|
|
*pg = g + x;
|
||
|
|
*pb = b + x;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Interpreter for `VGSProgram`.
|
||
|
|
///
|
||
|
|
/// Its implementation is a simple switch-based dispatch.
|
||
|
|
///
|
||
|
|
/// To evaluate blocks (like `if` or `call`), it makes a recursive call with
|
||
|
|
/// the subprogram allocated to the block.
|
||
|
|
static int vgs_eval(
|
||
|
|
struct VGSEvalState *state,
|
||
|
|
const struct VGSProgram *program
|
||
|
|
) {
|
||
|
|
|
||
|
|
#define ASSERT_ARGS(n) av_assert0(statement->args_count == n)
|
||
|
|
|
||
|
|
// When `preserve` is used, the next call to `clip`, `fill`, or `stroke`
|
||
|
|
// uses the `cairo_..._preserve` function.
|
||
|
|
#define MAY_PRESERVE(funcname) \
|
||
|
|
do { \
|
||
|
|
if (state->preserve_path) { \
|
||
|
|
state->preserve_path = 0; \
|
||
|
|
funcname##_preserve(state->cairo_ctx); \
|
||
|
|
} else { \
|
||
|
|
funcname(state->cairo_ctx); \
|
||
|
|
} \
|
||
|
|
} while(0)
|
||
|
|
|
||
|
|
double numerics[MAX_COMMAND_PARAMS];
|
||
|
|
double colors[MAX_COMMAND_PARAMS][4];
|
||
|
|
|
||
|
|
double cx, cy; // Current point.
|
||
|
|
|
||
|
|
int relative;
|
||
|
|
|
||
|
|
for (int st_number = 0; st_number < program->statements_count; st_number++) {
|
||
|
|
const struct VGSStatement *statement = &program->statements[st_number];
|
||
|
|
|
||
|
|
if (statement->args_count > FF_ARRAY_ELEMS(numerics)) {
|
||
|
|
av_log(state->log_ctx, AV_LOG_ERROR, "Too many arguments (%d).\n", statement->args_count);
|
||
|
|
return AVERROR_BUG;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (cairo_has_current_point(state->cairo_ctx)) {
|
||
|
|
cairo_get_current_point(state->cairo_ctx, &cx, &cy);
|
||
|
|
} else {
|
||
|
|
cx = NAN;
|
||
|
|
cy = NAN;
|
||
|
|
}
|
||
|
|
|
||
|
|
state->vars[VAR_CX] = cx;
|
||
|
|
state->vars[VAR_CY] = cy;
|
||
|
|
|
||
|
|
// Compute arguments.
|
||
|
|
for (int arg = 0; arg < statement->args_count; arg++) {
|
||
|
|
uint8_t color[4];
|
||
|
|
|
||
|
|
const struct VGSArgument *a = &statement->args[arg];
|
||
|
|
|
||
|
|
switch (a->type) {
|
||
|
|
case ARG_COLOR:
|
||
|
|
case ARG_COLOR_VAR:
|
||
|
|
if (a->type == ARG_COLOR) {
|
||
|
|
memcpy(color, a->color, sizeof(color));
|
||
|
|
} else {
|
||
|
|
uint32_t c = av_be2ne32((uint32_t)state->vars[a->variable]);
|
||
|
|
memcpy(color, &c, sizeof(color));
|
||
|
|
}
|
||
|
|
|
||
|
|
colors[arg][0] = (double)(color[0]) / 255.0,
|
||
|
|
colors[arg][1] = (double)(color[1]) / 255.0,
|
||
|
|
colors[arg][2] = (double)(color[2]) / 255.0,
|
||
|
|
colors[arg][3] = (double)(color[3]) / 255.0;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case ARG_EXPR:
|
||
|
|
numerics[arg] = av_expr_eval(a->expr, state->vars, state);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case ARG_LITERAL:
|
||
|
|
numerics[arg] = a->literal;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case ARG_VARIABLE:
|
||
|
|
av_assert0(a->variable < VAR_COUNT);
|
||
|
|
numerics[arg] = state->vars[a->variable];
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
numerics[arg] = NAN;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If the command uses a pending pattern (like a solid color
|
||
|
|
// or a gradient), set it to the cairo context before executing
|
||
|
|
// stroke/fill commands.
|
||
|
|
if (state->pattern_builder != NULL) {
|
||
|
|
switch (statement->cmd) {
|
||
|
|
case CMD_FILL:
|
||
|
|
case CMD_FILL_EO:
|
||
|
|
case CMD_RESTORE:
|
||
|
|
case CMD_SAVE:
|
||
|
|
case CMD_STROKE:
|
||
|
|
cairo_set_source(state->cairo_ctx, state->pattern_builder);
|
||
|
|
cairo_pattern_destroy(state->pattern_builder);
|
||
|
|
state->pattern_builder = NULL;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Execute the command.
|
||
|
|
switch (statement->cmd) {
|
||
|
|
case CMD_ARC:
|
||
|
|
ASSERT_ARGS(5);
|
||
|
|
cairo_arc(
|
||
|
|
state->cairo_ctx,
|
||
|
|
numerics[0],
|
||
|
|
numerics[1],
|
||
|
|
numerics[2],
|
||
|
|
numerics[3],
|
||
|
|
numerics[4]
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_ARC_NEG:
|
||
|
|
ASSERT_ARGS(5);
|
||
|
|
cairo_arc_negative(
|
||
|
|
state->cairo_ctx,
|
||
|
|
numerics[0],
|
||
|
|
numerics[1],
|
||
|
|
numerics[2],
|
||
|
|
numerics[3],
|
||
|
|
numerics[4]
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_CIRCLE:
|
||
|
|
ASSERT_ARGS(3);
|
||
|
|
draw_ellipse(state->cairo_ctx, numerics[0], numerics[1], numerics[2], numerics[2]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_CLIP:
|
||
|
|
case CMD_CLIP_EO:
|
||
|
|
ASSERT_ARGS(0);
|
||
|
|
cairo_set_fill_rule(
|
||
|
|
state->cairo_ctx,
|
||
|
|
statement->cmd == CMD_CLIP ?
|
||
|
|
CAIRO_FILL_RULE_WINDING :
|
||
|
|
CAIRO_FILL_RULE_EVEN_ODD
|
||
|
|
);
|
||
|
|
|
||
|
|
MAY_PRESERVE(cairo_clip);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_CLOSE_PATH:
|
||
|
|
ASSERT_ARGS(0);
|
||
|
|
cairo_close_path(state->cairo_ctx);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_COLOR_STOP:
|
||
|
|
if (state->pattern_builder == NULL) {
|
||
|
|
av_log(state->log_ctx, AV_LOG_ERROR, "colorstop with no active gradient.\n");
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
ASSERT_ARGS(2);
|
||
|
|
cairo_pattern_add_color_stop_rgba(
|
||
|
|
state->pattern_builder,
|
||
|
|
numerics[0],
|
||
|
|
colors[1][0],
|
||
|
|
colors[1][1],
|
||
|
|
colors[1][2],
|
||
|
|
colors[1][3]
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_CURVE_TO:
|
||
|
|
case CMD_CURVE_TO_REL:
|
||
|
|
ASSERT_ARGS(6);
|
||
|
|
draw_cubic_curve_to(
|
||
|
|
state,
|
||
|
|
statement->cmd == CMD_CURVE_TO_REL,
|
||
|
|
numerics[0],
|
||
|
|
numerics[1],
|
||
|
|
numerics[2],
|
||
|
|
numerics[3],
|
||
|
|
numerics[4],
|
||
|
|
numerics[5]
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_DEF_HSLA:
|
||
|
|
case CMD_DEF_RGBA: {
|
||
|
|
double r, g, b;
|
||
|
|
|
||
|
|
ASSERT_ARGS(5);
|
||
|
|
|
||
|
|
const int user_var = statement->args[0].variable;
|
||
|
|
av_assert0(user_var >= VAR_U0 && user_var < (VAR_U0 + USER_VAR_COUNT));
|
||
|
|
|
||
|
|
if (statement->cmd == CMD_DEF_HSLA) {
|
||
|
|
hsl2rgb(numerics[1], numerics[2], numerics[3], &r, &g, &b);
|
||
|
|
} else {
|
||
|
|
r = numerics[1];
|
||
|
|
g = numerics[2];
|
||
|
|
b = numerics[3];
|
||
|
|
}
|
||
|
|
|
||
|
|
#define C(v, o) ((uint32_t)(av_clipd(v, 0, 1) * 255) << o)
|
||
|
|
|
||
|
|
state->vars[user_var] = (double)(
|
||
|
|
C(r, 24)
|
||
|
|
| C(g, 16)
|
||
|
|
| C(b, 8)
|
||
|
|
| C(numerics[4], 0)
|
||
|
|
);
|
||
|
|
|
||
|
|
#undef C
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
case CMD_ELLIPSE:
|
||
|
|
ASSERT_ARGS(4);
|
||
|
|
draw_ellipse(state->cairo_ctx, numerics[0], numerics[1], numerics[2], numerics[3]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_FILL:
|
||
|
|
case CMD_FILL_EO:
|
||
|
|
ASSERT_ARGS(0);
|
||
|
|
|
||
|
|
cairo_set_fill_rule(
|
||
|
|
state->cairo_ctx,
|
||
|
|
statement->cmd == CMD_FILL ?
|
||
|
|
CAIRO_FILL_RULE_WINDING :
|
||
|
|
CAIRO_FILL_RULE_EVEN_ODD
|
||
|
|
);
|
||
|
|
|
||
|
|
MAY_PRESERVE(cairo_fill);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_GET_METADATA: {
|
||
|
|
ASSERT_ARGS(2);
|
||
|
|
|
||
|
|
double value = NAN;
|
||
|
|
|
||
|
|
const int user_var = statement->args[0].constant;
|
||
|
|
const char *key = statement->args[1].metadata;
|
||
|
|
|
||
|
|
av_assert0(user_var >= VAR_U0 && user_var < (VAR_U0 + USER_VAR_COUNT));
|
||
|
|
|
||
|
|
if (state->metadata != NULL && key != NULL) {
|
||
|
|
char *endp;
|
||
|
|
AVDictionaryEntry *entry = av_dict_get(state->metadata, key, NULL, 0);
|
||
|
|
|
||
|
|
if (entry != NULL) {
|
||
|
|
value = av_strtod(entry->value, &endp);
|
||
|
|
|
||
|
|
if (*endp != '\0')
|
||
|
|
value = NAN;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
state->vars[user_var] = value;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
case CMD_BREAK:
|
||
|
|
state->interrupted = 1;
|
||
|
|
return 0;
|
||
|
|
|
||
|
|
case CMD_IF:
|
||
|
|
ASSERT_ARGS(2);
|
||
|
|
|
||
|
|
if (isfinite(numerics[0]) && numerics[0] != 0.0) {
|
||
|
|
int ret = vgs_eval(state, statement->args[1].subprogram);
|
||
|
|
if (ret != 0 || state->interrupted != 0)
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_LINEAR_GRAD:
|
||
|
|
ASSERT_ARGS(4);
|
||
|
|
|
||
|
|
if (state->pattern_builder != NULL)
|
||
|
|
cairo_pattern_destroy(state->pattern_builder);
|
||
|
|
|
||
|
|
state->pattern_builder = cairo_pattern_create_linear(
|
||
|
|
numerics[0],
|
||
|
|
numerics[1],
|
||
|
|
numerics[2],
|
||
|
|
numerics[3]
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_LINE_TO:
|
||
|
|
ASSERT_ARGS(2);
|
||
|
|
cairo_line_to(state->cairo_ctx, numerics[0], numerics[1]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_LINE_TO_REL:
|
||
|
|
ASSERT_ARGS(2);
|
||
|
|
cairo_rel_line_to(state->cairo_ctx, numerics[0], numerics[1]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_MOVE_TO:
|
||
|
|
ASSERT_ARGS(2);
|
||
|
|
cairo_move_to(state->cairo_ctx, numerics[0], numerics[1]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_MOVE_TO_REL:
|
||
|
|
ASSERT_ARGS(2);
|
||
|
|
cairo_rel_move_to(state->cairo_ctx, numerics[0], numerics[1]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_NEW_PATH:
|
||
|
|
ASSERT_ARGS(0);
|
||
|
|
cairo_new_sub_path(state->cairo_ctx);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_PRESERVE:
|
||
|
|
ASSERT_ARGS(0);
|
||
|
|
state->preserve_path = 1;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_PRINT: {
|
||
|
|
char msg[256];
|
||
|
|
int len = 0;
|
||
|
|
|
||
|
|
for (int i = 0; i < statement->args_count; i++) {
|
||
|
|
int written;
|
||
|
|
int capacity = sizeof(msg) - len;
|
||
|
|
|
||
|
|
written = snprintf(
|
||
|
|
msg + len,
|
||
|
|
capacity,
|
||
|
|
"%s%s = %f",
|
||
|
|
i > 0 ? " | " : "",
|
||
|
|
statement->args[i].metadata,
|
||
|
|
numerics[i]
|
||
|
|
);
|
||
|
|
|
||
|
|
// If buffer is too small, discard the latest arguments.
|
||
|
|
if (written >= capacity)
|
||
|
|
break;
|
||
|
|
|
||
|
|
len += written;
|
||
|
|
}
|
||
|
|
|
||
|
|
av_log(state->log_ctx, AV_LOG_INFO, "%.*s\n", len, msg);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
case CMD_PROC_ASSIGN: {
|
||
|
|
struct VGSProcedure *proc;
|
||
|
|
|
||
|
|
const int proc_args = statement->args_count - 2;
|
||
|
|
av_assert0(proc_args >= 0 && proc_args <= MAX_PROC_ARGS);
|
||
|
|
|
||
|
|
proc = &state->procedures[statement->args[0].proc_id];
|
||
|
|
proc->program = statement->args[proc_args + 1].subprogram;
|
||
|
|
proc->proc_args_count = proc_args;
|
||
|
|
|
||
|
|
for (int i = 0; i < MAX_PROC_ARGS; i++)
|
||
|
|
proc->args[i] = i < proc_args ? statement->args[i + 1].constant : -1;
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
case CMD_PROC_CALL: {
|
||
|
|
const int proc_args = statement->args_count - 1;
|
||
|
|
av_assert0(proc_args >= 0 && proc_args <= MAX_PROC_ARGS);
|
||
|
|
|
||
|
|
const int proc_id = statement->args[0].proc_id;
|
||
|
|
|
||
|
|
const struct VGSProcedure *proc = &state->procedures[proc_id];
|
||
|
|
|
||
|
|
if (proc->proc_args_count != proc_args) {
|
||
|
|
av_log(
|
||
|
|
state->log_ctx,
|
||
|
|
AV_LOG_ERROR,
|
||
|
|
"Procedure expects %d arguments, but received %d.",
|
||
|
|
proc->proc_args_count,
|
||
|
|
proc_args
|
||
|
|
);
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (proc->program == NULL) {
|
||
|
|
const char *proc_name = state->proc_names[proc_id];
|
||
|
|
av_log(state->log_ctx, AV_LOG_ERROR,
|
||
|
|
"Missing body for procedure '%s'\n", proc_name);
|
||
|
|
} else {
|
||
|
|
int ret;
|
||
|
|
double current_vars[MAX_PROC_ARGS] = { 0 };
|
||
|
|
|
||
|
|
// Set variables for the procedure arguments
|
||
|
|
for (int i = 0; i < proc_args; i++) {
|
||
|
|
const int var = proc->args[i];
|
||
|
|
if (var != -1) {
|
||
|
|
current_vars[i] = state->vars[var];
|
||
|
|
state->vars[var] = numerics[i + 1];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ret = vgs_eval(state, proc->program);
|
||
|
|
|
||
|
|
// Restore variable values.
|
||
|
|
for (int i = 0; i < proc_args; i++) {
|
||
|
|
const int var = proc->args[i];
|
||
|
|
if (var != -1) {
|
||
|
|
state->vars[var] = current_vars[i];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ret != 0)
|
||
|
|
return ret;
|
||
|
|
|
||
|
|
// `break` interrupts the procedure, but don't stop the program.
|
||
|
|
if (state->interrupted) {
|
||
|
|
state->interrupted = 0;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
case CMD_Q_CURVE_TO:
|
||
|
|
case CMD_Q_CURVE_TO_REL:
|
||
|
|
ASSERT_ARGS(4);
|
||
|
|
relative = statement->cmd == CMD_Q_CURVE_TO_REL;
|
||
|
|
draw_quad_curve_to(
|
||
|
|
state,
|
||
|
|
relative,
|
||
|
|
numerics[0],
|
||
|
|
numerics[1],
|
||
|
|
numerics[2],
|
||
|
|
numerics[3]
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_RADIAL_GRAD:
|
||
|
|
ASSERT_ARGS(6);
|
||
|
|
|
||
|
|
if (state->pattern_builder != NULL)
|
||
|
|
cairo_pattern_destroy(state->pattern_builder);
|
||
|
|
|
||
|
|
state->pattern_builder = cairo_pattern_create_radial(
|
||
|
|
numerics[0],
|
||
|
|
numerics[1],
|
||
|
|
numerics[2],
|
||
|
|
numerics[3],
|
||
|
|
numerics[4],
|
||
|
|
numerics[5]
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_RESET_CLIP:
|
||
|
|
cairo_reset_clip(state->cairo_ctx);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_RESET_DASH:
|
||
|
|
cairo_set_dash(state->cairo_ctx, NULL, 0, 0);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_RESET_MATRIX:
|
||
|
|
cairo_identity_matrix(state->cairo_ctx);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_RECT:
|
||
|
|
ASSERT_ARGS(4);
|
||
|
|
cairo_rectangle(state->cairo_ctx, numerics[0], numerics[1], numerics[2], numerics[3]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_REPEAT: {
|
||
|
|
double var_i = state->vars[VAR_I];
|
||
|
|
|
||
|
|
ASSERT_ARGS(2);
|
||
|
|
|
||
|
|
if (!isfinite(numerics[0]))
|
||
|
|
break;
|
||
|
|
|
||
|
|
for (int i = 0, count = (int)numerics[0]; i < count; i++) {
|
||
|
|
state->vars[VAR_I] = i;
|
||
|
|
|
||
|
|
const int ret = vgs_eval(state, statement->args[1].subprogram);
|
||
|
|
if (ret != 0)
|
||
|
|
return ret;
|
||
|
|
|
||
|
|
// `break` interrupts the loop, but don't stop the program.
|
||
|
|
if (state->interrupted) {
|
||
|
|
state->interrupted = 0;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
state->vars[VAR_I] = var_i;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
case CMD_RESTORE:
|
||
|
|
ASSERT_ARGS(0);
|
||
|
|
cairo_restore(state->cairo_ctx);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_ROTATE:
|
||
|
|
ASSERT_ARGS(1);
|
||
|
|
cairo_rotate(state->cairo_ctx, numerics[0]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_ROUNDEDRECT:
|
||
|
|
ASSERT_ARGS(5);
|
||
|
|
draw_rounded_rect(
|
||
|
|
state->cairo_ctx,
|
||
|
|
numerics[0],
|
||
|
|
numerics[1],
|
||
|
|
numerics[2],
|
||
|
|
numerics[3],
|
||
|
|
numerics[4]
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_SAVE:
|
||
|
|
ASSERT_ARGS(0);
|
||
|
|
cairo_save(state->cairo_ctx);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_SCALE:
|
||
|
|
ASSERT_ARGS(1);
|
||
|
|
cairo_scale(state->cairo_ctx, numerics[0], numerics[0]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_SCALEXY:
|
||
|
|
ASSERT_ARGS(2);
|
||
|
|
cairo_scale(state->cairo_ctx, numerics[0], numerics[1]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_SET_COLOR:
|
||
|
|
ASSERT_ARGS(1);
|
||
|
|
|
||
|
|
if (state->pattern_builder != NULL)
|
||
|
|
cairo_pattern_destroy(state->pattern_builder);
|
||
|
|
|
||
|
|
state->pattern_builder = cairo_pattern_create_rgba(
|
||
|
|
colors[0][0],
|
||
|
|
colors[0][1],
|
||
|
|
colors[0][2],
|
||
|
|
colors[0][3]
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_SET_LINE_CAP:
|
||
|
|
ASSERT_ARGS(1);
|
||
|
|
cairo_set_line_cap(state->cairo_ctx, statement->args[0].constant);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_SET_LINE_JOIN:
|
||
|
|
ASSERT_ARGS(1);
|
||
|
|
cairo_set_line_join(state->cairo_ctx, statement->args[0].constant);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_SET_LINE_WIDTH:
|
||
|
|
ASSERT_ARGS(1);
|
||
|
|
cairo_set_line_width(state->cairo_ctx, numerics[0]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_SET_DASH:
|
||
|
|
case CMD_SET_DASH_OFFSET: {
|
||
|
|
int num;
|
||
|
|
double *dashes, offset, stack_buf[16];
|
||
|
|
|
||
|
|
ASSERT_ARGS(1);
|
||
|
|
|
||
|
|
num = cairo_get_dash_count(state->cairo_ctx);
|
||
|
|
|
||
|
|
if (num + 1 < FF_ARRAY_ELEMS(stack_buf)) {
|
||
|
|
dashes = stack_buf;
|
||
|
|
} else {
|
||
|
|
dashes = av_calloc(num + 1, sizeof(double));
|
||
|
|
|
||
|
|
if (dashes == NULL)
|
||
|
|
return AVERROR(ENOMEM);
|
||
|
|
}
|
||
|
|
|
||
|
|
cairo_get_dash(state->cairo_ctx, dashes, &offset);
|
||
|
|
|
||
|
|
if (statement->cmd == CMD_SET_DASH) {
|
||
|
|
dashes[num] = numerics[0];
|
||
|
|
num++;
|
||
|
|
} else {
|
||
|
|
offset = numerics[0];
|
||
|
|
}
|
||
|
|
|
||
|
|
cairo_set_dash(state->cairo_ctx, dashes, num, offset);
|
||
|
|
|
||
|
|
if (dashes != stack_buf)
|
||
|
|
av_freep(&dashes);
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
case CMD_SET_HSLA:
|
||
|
|
case CMD_SET_RGBA: {
|
||
|
|
double r, g, b;
|
||
|
|
|
||
|
|
ASSERT_ARGS(4);
|
||
|
|
|
||
|
|
if (state->pattern_builder != NULL)
|
||
|
|
cairo_pattern_destroy(state->pattern_builder);
|
||
|
|
|
||
|
|
if (statement->cmd == CMD_SET_HSLA) {
|
||
|
|
hsl2rgb(numerics[0], numerics[1], numerics[2], &r, &g, &b);
|
||
|
|
} else {
|
||
|
|
r = numerics[0];
|
||
|
|
g = numerics[1];
|
||
|
|
b = numerics[2];
|
||
|
|
}
|
||
|
|
|
||
|
|
state->pattern_builder = cairo_pattern_create_rgba(r, g, b, numerics[3]);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
case CMD_SET_VAR: {
|
||
|
|
ASSERT_ARGS(2);
|
||
|
|
|
||
|
|
const int user_var = statement->args[0].constant;
|
||
|
|
|
||
|
|
av_assert0(user_var >= VAR_U0 && user_var < (VAR_U0 + USER_VAR_COUNT));
|
||
|
|
state->vars[user_var] = numerics[1];
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
case CMD_STROKE:
|
||
|
|
ASSERT_ARGS(0);
|
||
|
|
MAY_PRESERVE(cairo_stroke);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_S_CURVE_TO:
|
||
|
|
case CMD_S_CURVE_TO_REL:
|
||
|
|
ASSERT_ARGS(4);
|
||
|
|
draw_cubic_curve_to(
|
||
|
|
state,
|
||
|
|
statement->cmd == CMD_S_CURVE_TO_REL,
|
||
|
|
NAN,
|
||
|
|
NAN,
|
||
|
|
numerics[0],
|
||
|
|
numerics[1],
|
||
|
|
numerics[2],
|
||
|
|
numerics[3]
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_TRANSLATE:
|
||
|
|
ASSERT_ARGS(2);
|
||
|
|
cairo_translate(state->cairo_ctx, numerics[0], numerics[1]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_T_CURVE_TO:
|
||
|
|
case CMD_T_CURVE_TO_REL:
|
||
|
|
ASSERT_ARGS(2);
|
||
|
|
relative = statement->cmd == CMD_T_CURVE_TO_REL;
|
||
|
|
draw_quad_curve_to(state, relative, NAN, NAN, numerics[0], numerics[1]);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case CMD_HORZ:
|
||
|
|
case CMD_HORZ_REL:
|
||
|
|
case CMD_VERT:
|
||
|
|
case CMD_VERT_REL:
|
||
|
|
ASSERT_ARGS(1);
|
||
|
|
|
||
|
|
if (cairo_has_current_point(state->cairo_ctx)) {
|
||
|
|
double d = numerics[0];
|
||
|
|
|
||
|
|
switch (statement->cmd) {
|
||
|
|
case CMD_HORZ: cx = d; break;
|
||
|
|
case CMD_VERT: cy = d; break;
|
||
|
|
case CMD_HORZ_REL: cx += d; break;
|
||
|
|
case CMD_VERT_REL: cy += d; break;
|
||
|
|
}
|
||
|
|
|
||
|
|
cairo_line_to(state->cairo_ctx, cx, cy);
|
||
|
|
}
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reflected control points will be discarded if the executed
|
||
|
|
// command did not update them, and it is a commands to
|
||
|
|
// modify the path.
|
||
|
|
if (state->rcp.status == RCP_UPDATED) {
|
||
|
|
state->rcp.status = RCP_VALID;
|
||
|
|
} else if (vgs_cmd_change_path(statement->cmd)) {
|
||
|
|
state->rcp.status = RCP_NONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for errors in cairo.
|
||
|
|
if (cairo_status(state->cairo_ctx) != CAIRO_STATUS_SUCCESS) {
|
||
|
|
av_log(
|
||
|
|
state->log_ctx,
|
||
|
|
AV_LOG_ERROR,
|
||
|
|
"Error in cairo context: %s\n",
|
||
|
|
cairo_status_to_string(cairo_status(state->cairo_ctx))
|
||
|
|
);
|
||
|
|
|
||
|
|
return AVERROR(EINVAL);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* == AVClass for drawvg ==
|
||
|
|
*
|
||
|
|
* Source is parsed on the `init` function.
|
||
|
|
*
|
||
|
|
* Cairo supports a few pixel formats, but only RGB. All compatible formats are
|
||
|
|
* listed in the `drawvg_pix_fmts` array.
|
||
|
|
*/
|
||
|
|
|
||
|
|
typedef struct DrawVGContext {
|
||
|
|
const AVClass *class;
|
||
|
|
|
||
|
|
/// Equivalent to AVPixelFormat.
|
||
|
|
cairo_format_t cairo_format;
|
||
|
|
|
||
|
|
/// Time in seconds of the first frame.
|
||
|
|
double time_start;
|
||
|
|
|
||
|
|
/// Inline source.
|
||
|
|
uint8_t *script_text;
|
||
|
|
|
||
|
|
/// File path to load the source.
|
||
|
|
uint8_t *script_file;
|
||
|
|
|
||
|
|
struct VGSProgram program;
|
||
|
|
} DrawVGContext;
|
||
|
|
|
||
|
|
#define OPT(name, field, help) \
|
||
|
|
{ \
|
||
|
|
name, \
|
||
|
|
help, \
|
||
|
|
offsetof(DrawVGContext, field), \
|
||
|
|
AV_OPT_TYPE_STRING, \
|
||
|
|
{ .str = NULL }, \
|
||
|
|
0, 0, \
|
||
|
|
AV_OPT_FLAG_FILTERING_PARAM \
|
||
|
|
| AV_OPT_FLAG_VIDEO_PARAM \
|
||
|
|
}
|
||
|
|
|
||
|
|
static const AVOption drawvg_options[]= {
|
||
|
|
OPT("script", script_text, "script source to draw the graphics"),
|
||
|
|
OPT("s", script_text, "script source to draw the graphics"),
|
||
|
|
OPT("file", script_file, "file to load the script source"),
|
||
|
|
{ NULL }
|
||
|
|
};
|
||
|
|
|
||
|
|
#undef OPT
|
||
|
|
|
||
|
|
|
||
|
|
AVFILTER_DEFINE_CLASS(drawvg);
|
||
|
|
|
||
|
|
static const enum AVPixelFormat drawvg_pix_fmts[] = {
|
||
|
|
AV_PIX_FMT_RGB32,
|
||
|
|
AV_PIX_FMT_0RGB32,
|
||
|
|
AV_PIX_FMT_RGB565,
|
||
|
|
AV_PIX_FMT_X2RGB10,
|
||
|
|
AV_PIX_FMT_NONE
|
||
|
|
};
|
||
|
|
|
||
|
|
// Return the cairo equivalent to AVPixelFormat.
|
||
|
|
static cairo_format_t cairo_format_from_pix_fmt(
|
||
|
|
DrawVGContext* ctx,
|
||
|
|
enum AVPixelFormat format
|
||
|
|
) {
|
||
|
|
// This array must have the same order of `drawvg_pix_fmts`.
|
||
|
|
const cairo_format_t format_map[] = {
|
||
|
|
CAIRO_FORMAT_ARGB32, // cairo expects pre-multiplied alpha.
|
||
|
|
CAIRO_FORMAT_RGB24,
|
||
|
|
CAIRO_FORMAT_RGB16_565,
|
||
|
|
CAIRO_FORMAT_RGB30,
|
||
|
|
CAIRO_FORMAT_INVALID,
|
||
|
|
};
|
||
|
|
|
||
|
|
for (int i = 0; i < FF_ARRAY_ELEMS(drawvg_pix_fmts); i++) {
|
||
|
|
if (drawvg_pix_fmts[i] == format)
|
||
|
|
return format_map[i];
|
||
|
|
}
|
||
|
|
|
||
|
|
const char* name = av_get_pix_fmt_name(format);
|
||
|
|
av_log(ctx, AV_LOG_ERROR, "Invalid pix_fmt: %s\n", name);
|
||
|
|
|
||
|
|
return CAIRO_FORMAT_INVALID;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int drawvg_filter_frame(AVFilterLink *inlink, AVFrame *frame) {
|
||
|
|
int ret;
|
||
|
|
double var_t;
|
||
|
|
cairo_surface_t* surface;
|
||
|
|
|
||
|
|
FilterLink *inl = ff_filter_link(inlink);
|
||
|
|
AVFilterLink *outlink = inlink->dst->outputs[0];
|
||
|
|
AVFilterContext *filter_ctx = inlink->dst;
|
||
|
|
DrawVGContext *drawvg_ctx = filter_ctx->priv;
|
||
|
|
|
||
|
|
struct VGSEvalState eval_state;
|
||
|
|
ret = vgs_eval_state_init(&eval_state, &drawvg_ctx->program, drawvg_ctx, frame);
|
||
|
|
|
||
|
|
if (ret != 0)
|
||
|
|
return ret;
|
||
|
|
|
||
|
|
// Draw directly on the frame data.
|
||
|
|
surface = cairo_image_surface_create_for_data(
|
||
|
|
frame->data[0],
|
||
|
|
drawvg_ctx->cairo_format,
|
||
|
|
frame->width,
|
||
|
|
frame->height,
|
||
|
|
frame->linesize[0]
|
||
|
|
);
|
||
|
|
|
||
|
|
if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) {
|
||
|
|
av_log(drawvg_ctx, AV_LOG_ERROR, "Failed to create cairo surface.\n");
|
||
|
|
return AVERROR_EXTERNAL;
|
||
|
|
}
|
||
|
|
|
||
|
|
eval_state.cairo_ctx = cairo_create(surface);
|
||
|
|
|
||
|
|
var_t = TS2T(frame->pts, inlink->time_base);
|
||
|
|
|
||
|
|
if (isnan(drawvg_ctx->time_start))
|
||
|
|
drawvg_ctx->time_start = var_t;
|
||
|
|
|
||
|
|
eval_state.vars[VAR_N] = inl->frame_count_out;
|
||
|
|
eval_state.vars[VAR_T] = var_t;
|
||
|
|
eval_state.vars[VAR_TS] = drawvg_ctx->time_start;
|
||
|
|
eval_state.vars[VAR_W] = inlink->w;
|
||
|
|
eval_state.vars[VAR_H] = inlink->h;
|
||
|
|
eval_state.vars[VAR_DURATION] = frame->duration * av_q2d(inlink->time_base);
|
||
|
|
|
||
|
|
eval_state.metadata = frame->metadata;
|
||
|
|
|
||
|
|
ret = vgs_eval(&eval_state, &drawvg_ctx->program);
|
||
|
|
|
||
|
|
cairo_destroy(eval_state.cairo_ctx);
|
||
|
|
cairo_surface_destroy(surface);
|
||
|
|
|
||
|
|
vgs_eval_state_free(&eval_state);
|
||
|
|
|
||
|
|
if (ret != 0)
|
||
|
|
return ret;
|
||
|
|
|
||
|
|
return ff_filter_frame(outlink, frame);
|
||
|
|
}
|
||
|
|
|
||
|
|
static int drawvg_config_props(AVFilterLink *inlink) {
|
||
|
|
AVFilterContext *filter_ctx = inlink->dst;
|
||
|
|
DrawVGContext *drawvg_ctx = filter_ctx->priv;
|
||
|
|
|
||
|
|
// Find the cairo format equivalent to the format of the frame,
|
||
|
|
// so cairo can draw directly on the memory already allocated.
|
||
|
|
|
||
|
|
drawvg_ctx->cairo_format = cairo_format_from_pix_fmt(drawvg_ctx, inlink->format);
|
||
|
|
if (drawvg_ctx->cairo_format == CAIRO_FORMAT_INVALID)
|
||
|
|
return AVERROR(EINVAL);
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
static av_cold int drawvg_init(AVFilterContext *ctx) {
|
||
|
|
int ret;
|
||
|
|
struct VGSParser parser;
|
||
|
|
DrawVGContext *drawvg = ctx->priv;
|
||
|
|
|
||
|
|
drawvg->time_start = NAN;
|
||
|
|
|
||
|
|
if ((drawvg->script_text == NULL) == (drawvg->script_file == NULL)) {
|
||
|
|
av_log(ctx, AV_LOG_ERROR,
|
||
|
|
"Either 'source' or 'file' must be provided\n");
|
||
|
|
|
||
|
|
return AVERROR(EINVAL);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (drawvg->script_file != NULL) {
|
||
|
|
ret = ff_load_textfile(
|
||
|
|
ctx,
|
||
|
|
(const char *)drawvg->script_file,
|
||
|
|
&drawvg->script_text,
|
||
|
|
NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
if (ret != 0)
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
vgs_parser_init(&parser, drawvg->script_text);
|
||
|
|
|
||
|
|
ret = vgs_parse(drawvg, &parser, &drawvg->program, 0);
|
||
|
|
|
||
|
|
vgs_parser_free(&parser);
|
||
|
|
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
static av_cold void drawvg_uninit(AVFilterContext *ctx) {
|
||
|
|
DrawVGContext *drawvg = ctx->priv;
|
||
|
|
vgs_free(&drawvg->program);
|
||
|
|
}
|
||
|
|
|
||
|
|
static const AVFilterPad drawvg_inputs[] = {
|
||
|
|
{
|
||
|
|
.name = "default",
|
||
|
|
.type = AVMEDIA_TYPE_VIDEO,
|
||
|
|
.flags = AVFILTERPAD_FLAG_NEEDS_WRITABLE,
|
||
|
|
.filter_frame = drawvg_filter_frame,
|
||
|
|
.config_props = drawvg_config_props,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const FFFilter ff_vf_drawvg = {
|
||
|
|
.p.name = "drawvg",
|
||
|
|
.p.description = NULL_IF_CONFIG_SMALL("Draw vector graphics on top of video frames."),
|
||
|
|
.p.priv_class = &drawvg_class,
|
||
|
|
.p.flags = AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC,
|
||
|
|
.priv_size = sizeof(DrawVGContext),
|
||
|
|
.init = drawvg_init,
|
||
|
|
.uninit = drawvg_uninit,
|
||
|
|
FILTER_INPUTS(drawvg_inputs),
|
||
|
|
FILTER_OUTPUTS(ff_video_default_filterpad),
|
||
|
|
FILTER_PIXFMTS_ARRAY(drawvg_pix_fmts),
|
||
|
|
};
|