1
0
mirror of https://github.com/FFmpeg/FFmpeg.git synced 2025-11-23 21:54:53 +02:00
Files
FFmpeg/libavfilter/vf_drawvg.c
Ayose 016d767c8e lavfi: add drawvg video filter.
The drawvg filter can draw vector graphics on top of a video, using libcairo. It
is enabled if FFmpeg is configured with `--enable-cairo`.

The language for drawvg scripts is documented in `doc/drawvg-reference.texi`.

There are two new tests:

- `fate-filter-drawvg-interpreter` launch a script with most commands, and
  verify which libcairo functions are executed.
- `fate-filter-drawvg-video` render a very simple image, just to verify that
  libcairo is working as expected.

Signed-off-by: Ayose <ayosec@gmail.com>
2025-10-25 13:21:50 +00:00

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),
};