1
0
mirror of https://github.com/FFmpeg/FFmpeg.git synced 2025-11-23 21:54:53 +02:00
Files
FFmpeg/libavfilter/tests/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

351 lines
9.9 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
*/
#include <cairo.h>
#include <stdarg.h>
#include <stdio.h>
#include "libavutil/log.h"
#include "libavutil/pixdesc.h"
static void mock_av_log(void *ptr, int level, const char *fmt, va_list vl) {
printf("av_log[%d]: ", level);
vprintf(fmt, vl);
}
#include "libavfilter/vf_drawvg.c"
// Mock for cairo functions.
//
// `MOCK_FN_n` macros define wrappers for functions that only receive `n`
// arguments of type `double`.
//
// `MOCK_FN_I` macro wrap a function that receives a single integer value.
struct _cairo {
double current_point_x;
double current_point_y;
};
static void update_current_point(cairo_t *cr, const char *func, double x, double y) {
// Update current point only if the function name contains `_to`.
if (strstr(func, "_to") == NULL) {
return;
}
if (strstr(func, "_rel_") == NULL) {
cr->current_point_x = x;
cr->current_point_y = y;
} else {
cr->current_point_x += x;
cr->current_point_y += y;
}
}
#define MOCK_FN_0(func) \
void func(cairo_t* cr) { \
puts(#func); \
}
#define MOCK_FN_1(func) \
void func(cairo_t* cr, double a0) { \
printf(#func " %.1f\n", a0); \
}
#define MOCK_FN_2(func) \
void func(cairo_t* cr, double a0, double a1) { \
update_current_point(cr, #func, a0, a1); \
printf(#func " %.1f %.1f\n", a0, a1); \
}
#define MOCK_FN_4(func) \
void func(cairo_t* cr, double a0, double a1, double a2, double a3) { \
printf(#func " %.1f %.1f %.1f %.1f\n", a0, a1, a2, a3); \
}
#define MOCK_FN_5(func) \
void func(cairo_t* cr, double a0, double a1, double a2, double a3, double a4) { \
printf(#func " %.1f %.1f %.1f %.1f %.1f\n", a0, a1, a2, a3, a4); \
}
#define MOCK_FN_6(func) \
void func(cairo_t* cr, double a0, double a1, double a2, double a3, double a4, double a5) { \
update_current_point(cr, #func, a4, a5); \
printf(#func " %.1f %.1f %.1f %.1f %.1f %.1f\n", a0, a1, a2, a3, a4, a5); \
}
#define MOCK_FN_I(func, type) \
void func(cairo_t* cr, type i) { \
printf(#func " %d\n", (int)i); \
}
MOCK_FN_5(cairo_arc);
MOCK_FN_0(cairo_clip);
MOCK_FN_0(cairo_clip_preserve);
MOCK_FN_0(cairo_close_path);
MOCK_FN_6(cairo_curve_to);
MOCK_FN_0(cairo_fill);
MOCK_FN_0(cairo_fill_preserve);
MOCK_FN_0(cairo_identity_matrix);
MOCK_FN_2(cairo_line_to);
MOCK_FN_2(cairo_move_to);
MOCK_FN_0(cairo_new_path);
MOCK_FN_0(cairo_new_sub_path);
MOCK_FN_4(cairo_rectangle);
MOCK_FN_6(cairo_rel_curve_to);
MOCK_FN_2(cairo_rel_line_to);
MOCK_FN_2(cairo_rel_move_to);
MOCK_FN_0(cairo_reset_clip);
MOCK_FN_0(cairo_restore);
MOCK_FN_1(cairo_rotate);
MOCK_FN_0(cairo_save);
MOCK_FN_2(cairo_scale);
MOCK_FN_I(cairo_set_fill_rule, cairo_fill_rule_t);
MOCK_FN_1(cairo_set_font_size);
MOCK_FN_I(cairo_set_line_cap, cairo_line_cap_t);
MOCK_FN_I(cairo_set_line_join, cairo_line_join_t);
MOCK_FN_1(cairo_set_line_width);
MOCK_FN_1(cairo_set_miter_limit);
MOCK_FN_4(cairo_set_source_rgba);
MOCK_FN_0(cairo_stroke);
MOCK_FN_0(cairo_stroke_preserve);
MOCK_FN_2(cairo_translate);
cairo_bool_t cairo_get_dash_count(cairo_t *cr) {
return 1;
}
cairo_status_t cairo_status(cairo_t *cr) {
return CAIRO_STATUS_SUCCESS;
}
void cairo_get_dash(cairo_t *cr, double *dashes, double *offset) {
// Return a dummy value to verify that it is included in
// the next call to `cairo_set_dash`.
*dashes = -1;
if (offset)
*offset = -2;
}
void cairo_set_dash(cairo_t *cr, const double *dashes, int num_dashes, double offset) {
printf("%s [", __func__);
for (int i = 0; i < num_dashes; i++)
printf(" %.1f", dashes[i]);
printf(" ] %.1f\n", offset);
}
cairo_bool_t cairo_has_current_point(cairo_t *cr) {
return 1;
}
void cairo_get_current_point(cairo_t *cr, double *x, double *y) {
*x = cr->current_point_x;
*y = cr->current_point_y;
}
void cairo_set_source(cairo_t *cr, cairo_pattern_t *source) {
int count;
double r, g, b, a;
double x0, y0, x1, y1, r0, r1;
printf("%s", __func__);
#define PRINT_COLOR(prefix) \
printf(prefix "#%02x%02x%02x%02x", (int)(r*255), (int)(g*255), (int)(b*255), (int)(a*255))
switch (cairo_pattern_get_type(source)) {
case CAIRO_PATTERN_TYPE_SOLID:
cairo_pattern_get_rgba(source, &r, &g, &b, &a);
PRINT_COLOR(" ");
break;
case CAIRO_PATTERN_TYPE_LINEAR:
cairo_pattern_get_linear_points(source, &x0, &y0, &x1, &y1);
printf(" lineargrad(%.1f %.1f %.1f %.1f)", x0, y0, x1, y1);
break;
case CAIRO_PATTERN_TYPE_RADIAL:
cairo_pattern_get_radial_circles(source, &x0, &y0, &r0, &x1, &y1, &r1);
printf(" radialgrad(%.1f %.1f %.1f %.1f %.1f %.1f)", x0, y0, r0, x1, y1, r1);
break;
}
if (cairo_pattern_get_color_stop_count(source, &count) == CAIRO_STATUS_SUCCESS) {
for (int i = 0; i < count; i++) {
cairo_pattern_get_color_stop_rgba(source, i, &x0, &r, &g, &b, &a);
printf(" %.1f/", x0);
PRINT_COLOR("");
}
}
printf("\n");
}
// Verify that the `vgs_commands` array is sorted, so it can
// be used with `bsearch(3)`.
static void check_sorted_cmds_array(void) {
int failures = 0;
for (int i = 0; i < FF_ARRAY_ELEMS(vgs_commands) - 1; i++) {
if (vgs_comp_command_spec(&vgs_commands[i], &vgs_commands[i]) != 0) {
printf("%s: comparator must return 0 for item %d\n", __func__, i);
failures++;
}
if (vgs_comp_command_spec(&vgs_commands[i], &vgs_commands[i + 1]) >= 0) {
printf("%s: entry for '%s' must appear after '%s', at index %d\n",
__func__, vgs_commands[i].name, vgs_commands[i + 1].name, i);
failures++;
}
}
printf("%s: %d failures\n", __func__, failures);
}
// Compile and run a script.
static void check_script(int is_file, const char* source) {
int ret;
AVDictionary *metadata = NULL;
struct VGSEvalState state;
struct VGSParser parser;
struct VGSProgram program;
struct _cairo cairo_ctx = { 0, 0 };
if (is_file) {
uint8_t *s = NULL;
printf("\n--- %s: %s\n", __func__, av_basename(source));
ret = ff_load_textfile(NULL, source, &s, NULL);
if (ret != 0) {
printf("Failed to read %s: %d\n", source, ret);
return;
}
source = s;
} else {
printf("\n--- %s: %s\n", __func__, source);
}
ret = av_dict_parse_string(&metadata, "m.a=1:m.b=2", "=", ":", 0);
av_assert0(ret == 0);
vgs_parser_init(&parser, source);
ret = vgs_parse(NULL, &parser, &program, 0);
int init_ret = vgs_eval_state_init(&state, &program, NULL, NULL);
av_assert0(init_ret == 0);
for (int i = 0; i < VAR_COUNT; i++)
state.vars[i] = 1 << i;
vgs_parser_free(&parser);
if (ret != 0) {
printf("%s: vgs_parse = %d\n", __func__, ret);
goto exit;
}
state.metadata = metadata;
state.cairo_ctx = &cairo_ctx;
ret = vgs_eval(&state, &program);
vgs_eval_state_free(&state);
if (ret != 0)
printf("%s: vgs_eval = %d\n", __func__, ret);
exit:
av_dict_free(&metadata);
if (is_file)
av_free((void*)source);
vgs_free(&program);
}
int main(int argc, const char **argv)
{
char buf[512];
av_log_set_callback(mock_av_log);
check_sorted_cmds_array();
for (int i = 1; i < argc; i++)
check_script(1, argv[i]);
// Detect unclosed expressions.
check_script(0, "M 0 (1*(t+1)");
// Invalid command.
check_script(0, "save invalid 1 2");
// Invalid constant.
check_script(0, "setlinecap unknown m 10 20");
// Missing arguments.
check_script(0, "M 0 1 2");
// Invalid variable names.
check_script(0, "setvar ba^d 0");
// Reserved names.
check_script(0, "setvar cx 0");
// Max number of user variables.
memset(buf, 0, sizeof(buf));
for (int i = 0; i < USER_VAR_COUNT; i++) {
av_strlcatf(buf, sizeof(buf), " setvar v%d %d", i, i);
}
av_strlcatf(buf, sizeof(buf), " M (v0) (v%d) 1 (unknown_var)", USER_VAR_COUNT - 1);
check_script(0, buf);
// Too many variables.
memset(buf, 0, sizeof(buf));
for (int i = 0; i < USER_VAR_COUNT + 1; i++) {
av_strlcatf(buf, sizeof(buf), " setvar v%d %d", i + 1, i);
}
check_script(0, buf);
// Invalid procedure names.
check_script(0, "call a");
check_script(0, "proc a { call b } call a");
// Invalid arguments list.
check_script(0, "proc p0 a1 a2 a3 a4 a5 a6 a7 a8 { break }");
check_script(0, "proc p0 a1 a2 { break } call p0 break");
check_script(0, "proc p0 a1 a2 { break } call p0 1 2 3");
// Long expressions.
memset(buf, 0, sizeof(buf));
strncat(buf, "M 0 (1", sizeof(buf) - 1);
for (int i = 0; i < 100; i++) {
strncat(buf, " + n", sizeof(buf) - 1);
}
strncat(buf, ")", sizeof(buf) - 1);
check_script(0, buf);
return 0;
}