Files
ffmpeg/libavfilter/vf_drawvg.c
Kacper Michajłow a75b15a4ab avfilter/vf_drawvg: round color values to avoid differences on some platforms
This ensures consistent color conversion between double and u8 and
guarantees that values remain consistent across different platforms,
especially when x87 math is used.

Note that libcairo also performs rounding internally when converting
doubles to integers, see _cairo_color_double_to_short().

Fixes: fate-filter-drawvg-interpreter
Signed-off-by: Kacper Michajłow <kasper93@gmail.com>
2025-11-25 22:32:50 +01: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)lround(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),
};