lavfi: introduce textutils

Generalize drawtext utilities to make them usable in other filters.
This will be needed to introduce the QR code source and filter without
duplicating functionality.
This commit is contained in:
Stefano Sabatini 2023-11-20 01:13:17 +01:00
parent c2ab41b7e0
commit 732fb122e6
4 changed files with 743 additions and 403 deletions

View File

@ -291,7 +291,7 @@ OBJS-$(CONFIG_DOUBLEWEAVE_FILTER) += vf_weave.o
OBJS-$(CONFIG_DRAWBOX_FILTER) += vf_drawbox.o
OBJS-$(CONFIG_DRAWGRAPH_FILTER) += f_drawgraph.o
OBJS-$(CONFIG_DRAWGRID_FILTER) += vf_drawbox.o
OBJS-$(CONFIG_DRAWTEXT_FILTER) += vf_drawtext.o
OBJS-$(CONFIG_DRAWTEXT_FILTER) += vf_drawtext.o textutils.o
OBJS-$(CONFIG_EDGEDETECT_FILTER) += vf_edgedetect.o edge_common.o
OBJS-$(CONFIG_ELBG_FILTER) += vf_elbg.o
OBJS-$(CONFIG_ENTROPY_FILTER) += vf_entropy.o

382
libavfilter/textutils.c Normal file
View File

@ -0,0 +1,382 @@
/*
* 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
* text expansion utilities
*/
#include <fenv.h>
#include <math.h>
#include <string.h>
#include "textutils.h"
#include "libavutil/avutil.h"
#include "libavutil/error.h"
#include "libavutil/file.h"
#include "libavutil/time.h"
static int ff_expand_text_function_internal(FFExpandTextContext *expand_text, AVBPrint *bp,
char *name, unsigned argc, char **argv)
{
void *log_ctx = expand_text->log_ctx;
FFExpandTextFunction *functions = expand_text->functions;
unsigned i;
for (i = 0; i < expand_text->functions_nb; i++) {
if (strcmp(name, functions[i].name))
continue;
if (argc < functions[i].argc_min) {
av_log(log_ctx, AV_LOG_ERROR, "%%{%s} requires at least %d arguments\n",
name, functions[i].argc_min);
return AVERROR(EINVAL);
}
if (argc > functions[i].argc_max) {
av_log(log_ctx, AV_LOG_ERROR, "%%{%s} requires at most %d arguments\n",
name, functions[i].argc_max);
return AVERROR(EINVAL);
}
break;
}
if (i >= expand_text->functions_nb) {
av_log(log_ctx, AV_LOG_ERROR, "%%{%s} is not known\n", name);
return AVERROR(EINVAL);
}
return functions[i].func(log_ctx, bp, name, argc, argv);
}
/**
* Expand text template pointed to by *rtext.
*
* Expand text template defined in text using the logic defined in a text
* expander object.
*
* This function expects the text to be in the format %{FUNCTION_NAME[:PARAMS]},
* where PARAMS is a sequence of strings separated by : and represents the function
* arguments to use for the function evaluation.
*
* @param text_expander TextExpander object used to expand the text
* @param bp BPrint object where the expanded text is written to
* @param rtext pointer to pointer to the text to expand, it is updated to point
* to the next part of the template to process
* @return negative value corresponding to an AVERROR error code in case of
* errors, a non-negative value otherwise
*/
static int ff_expand_text_function(FFExpandTextContext *expand_text, AVBPrint *bp, char **rtext)
{
void *log_ctx = expand_text->log_ctx;
const char *text = *rtext;
char *argv[16] = { NULL };
unsigned argc = 0, i;
int ret;
if (*text != '{') {
av_log(log_ctx, AV_LOG_ERROR, "Stray %% near '%s'\n", text);
return AVERROR(EINVAL);
}
text++;
while (1) {
if (!(argv[argc++] = av_get_token(&text, ":}"))) {
ret = AVERROR(ENOMEM);
goto end;
}
if (!*text) {
av_log(log_ctx, AV_LOG_ERROR, "Unterminated %%{} near '%s'\n", *rtext);
ret = AVERROR(EINVAL);
goto end;
}
if (argc == FF_ARRAY_ELEMS(argv))
av_freep(&argv[--argc]); /* error will be caught later */
if (*text == '}')
break;
text++;
}
if ((ret = ff_expand_text_function_internal(expand_text, bp, argv[0], argc - 1, argv + 1)) < 0)
goto end;
ret = 0;
*rtext = (char *)text + 1;
end:
for (i = 0; i < argc; i++)
av_freep(&argv[i]);
return ret;
}
int ff_expand_text(FFExpandTextContext *expand_text, char *text, AVBPrint *bp)
{
int ret;
av_bprint_clear(bp);
if (!text)
return 0;
while (*text) {
if (*text == '\\' && text[1]) {
av_bprint_chars(bp, text[1], 1);
text += 2;
} else if (*text == '%') {
text++;
if ((ret = ff_expand_text_function(expand_text, bp, &text)) < 0)
return ret;
} else {
av_bprint_chars(bp, *text, 1);
text++;
}
}
if (!av_bprint_is_complete(bp))
return AVERROR(ENOMEM);
return 0;
}
int ff_print_pts(void *log_ctx, AVBPrint *bp, double pts, const char *delta,
const char *fmt, const char *strftime_fmt)
{
int ret;
if (delta) {
int64_t delta_i;
if ((ret = av_parse_time(&delta_i, delta, 1)) < 0) {
av_log(log_ctx, AV_LOG_ERROR, "Invalid delta '%s'\n", delta);
return ret;
}
pts += (double)delta_i / AV_TIME_BASE;
}
if (!strcmp(fmt, "flt")) {
av_bprintf(bp, "%.6f", pts);
} else if (!strcmp(fmt, "hms") ||
!strcmp(fmt, "hms24hh")) {
if (isnan(pts)) {
av_bprintf(bp, " ??:??:??.???");
} else {
int64_t ms = llrint(pts * 1000);
char sign = ' ';
if (ms < 0) {
sign = '-';
ms = -ms;
}
if (!strcmp(fmt, "hms24hh")) {
/* wrap around 24 hours */
ms %= 24 * 60 * 60 * 1000;
}
av_bprintf(bp, "%c%02d:%02d:%02d.%03d", sign,
(int)(ms / (60 * 60 * 1000)),
(int)(ms / (60 * 1000)) % 60,
(int)(ms / 1000) % 60,
(int)(ms % 1000));
}
} else if (!strcmp(fmt, "localtime") ||
!strcmp(fmt, "gmtime")) {
struct tm tm;
time_t ms = (time_t)pts;
if (!strcmp(fmt, "localtime"))
localtime_r(&ms, &tm);
else
gmtime_r(&ms, &tm);
av_bprint_strftime(bp, av_x_if_null(strftime_fmt, "%Y-%m-%d %H:%M:%S"), &tm);
} else {
av_log(log_ctx, AV_LOG_ERROR, "Invalid format '%s'\n", fmt);
return AVERROR(EINVAL);
}
return 0;
}
int ff_print_time(void *log_ctx, AVBPrint *bp,
const char *strftime_fmt, char localtime)
{
const char *fmt = av_x_if_null(strftime_fmt, "%Y-%m-%d %H:%M:%S");
const char *fmt_begin = fmt;
int64_t unow;
time_t now;
struct tm tm;
const char *begin;
const char *tmp;
int len;
int div;
AVBPrint fmt_bp;
av_bprint_init(&fmt_bp, 0, AV_BPRINT_SIZE_UNLIMITED);
unow = av_gettime();
now = unow / 1000000;
if (localtime)
localtime_r(&now, &tm);
else
tm = *gmtime_r(&now, &tm);
// manually parse format for %N (fractional seconds)
begin = fmt;
while ((begin = strchr(begin, '%'))) {
tmp = begin + 1;
len = 0;
// skip escaped "%%"
if (*tmp == '%') {
begin = tmp + 1;
continue;
}
// count digits between % and possible N
while (*tmp != '\0' && av_isdigit((int)*tmp)) {
len++;
tmp++;
}
// N encountered, insert time
if (*tmp == 'N') {
int num_digits = 3; // default show millisecond [1,6]
// if digit given, expect [1,6], warn & clamp otherwise
if (len == 1) {
num_digits = av_clip(*(begin + 1) - '0', 1, 6);
} else if (len > 1) {
av_log(log_ctx, AV_LOG_WARNING, "Invalid number of decimals for %%N, using default of %i\n", num_digits);
}
len += 2; // add % and N to get length of string part
div = pow(10, 6 - num_digits);
av_bprintf(&fmt_bp, "%.*s%0*d", (int)(begin - fmt_begin), fmt_begin, num_digits, (int)(unow % 1000000) / div);
begin += len;
fmt_begin = begin;
continue;
}
begin = tmp;
}
av_bprintf(&fmt_bp, "%s", fmt_begin);
if (!av_bprint_is_complete(&fmt_bp)) {
av_log(log_ctx, AV_LOG_WARNING, "Format string truncated at %u/%u.", fmt_bp.size, fmt_bp.len);
}
av_bprint_strftime(bp, fmt_bp.str, &tm);
av_bprint_finalize(&fmt_bp, NULL);
return 0;
}
int ff_print_eval_expr(void *log_ctx, AVBPrint *bp,
const char *expr,
const char * const *fun_names, const ff_eval_func2 *fun_values,
const char * const *var_names, const double *var_values,
void *eval_ctx)
{
double res;
int ret;
ret = av_expr_parse_and_eval(&res, expr, var_names, var_values,
NULL, NULL, fun_names, fun_values,
eval_ctx, 0, log_ctx);
if (ret < 0)
av_log(log_ctx, AV_LOG_ERROR,
"Text expansion expression '%s' is not valid\n",
expr);
else
av_bprintf(bp, "%f", res);
return ret;
}
int ff_print_formatted_eval_expr(void *log_ctx, AVBPrint *bp,
const char *expr,
const char * const *fun_names, const ff_eval_func2 *fun_values,
const char * const *var_names, const double *var_values,
void *eval_ctx,
const char format, int positions)
{
double res;
int intval;
int ret;
char fmt_str[30] = "%";
ret = av_expr_parse_and_eval(&res, expr, var_names, var_values,
NULL, NULL, fun_names, fun_values,
eval_ctx, 0, log_ctx);
if (ret < 0) {
av_log(log_ctx, AV_LOG_ERROR,
"Text expansion expression '%s' is not valid\n",
expr);
return ret;
}
if (!strchr("xXdu", format)) {
av_log(log_ctx, AV_LOG_ERROR, "Invalid format '%c' specified,"
" allowed values: 'x', 'X', 'd', 'u'\n", format);
return AVERROR(EINVAL);
}
feclearexcept(FE_ALL_EXCEPT);
intval = res;
#if defined(FE_INVALID) && defined(FE_OVERFLOW) && defined(FE_UNDERFLOW)
if ((ret = fetestexcept(FE_INVALID|FE_OVERFLOW|FE_UNDERFLOW))) {
av_log(log_ctx, AV_LOG_ERROR, "Conversion of floating-point result to int failed. Control register: 0x%08x. Conversion result: %d\n", ret, intval);
return AVERROR(EINVAL);
}
#endif
if (positions >= 0)
av_strlcatf(fmt_str, sizeof(fmt_str), "0%u", positions);
av_strlcatf(fmt_str, sizeof(fmt_str), "%c", format);
av_log(log_ctx, AV_LOG_DEBUG, "Formatting value %f (expr '%s') with spec '%s'\n",
res, expr, fmt_str);
av_bprintf(bp, fmt_str, intval);
return 0;
}
int ff_load_textfile(void *log_ctx, const char *textfile,
unsigned char **text, size_t *text_size)
{
int err;
uint8_t *textbuf;
uint8_t *tmp;
size_t textbuf_size;
if ((err = av_file_map(textfile, &textbuf, &textbuf_size, 0, log_ctx)) < 0) {
av_log(log_ctx, AV_LOG_ERROR,
"The text file '%s' could not be read or is empty\n",
textfile);
return err;
}
if (textbuf_size > 0 && ff_is_newline(textbuf[textbuf_size - 1]))
textbuf_size--;
if (textbuf_size > SIZE_MAX - 1 || !(tmp = av_realloc(*text, textbuf_size + 1))) {
av_file_unmap(textbuf, textbuf_size);
return AVERROR(ENOMEM);
}
*text = tmp;
memcpy(*text, textbuf, textbuf_size);
(*text)[textbuf_size] = 0;
if (text_size)
*text_size = textbuf_size;
av_file_unmap(textbuf, textbuf_size);
return 0;
}

229
libavfilter/textutils.h Normal file
View File

@ -0,0 +1,229 @@
/*
* 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
* text utilities
*/
#ifndef AVFILTER_TEXTUTILS_H
#define AVFILTER_TEXTUTILS_H
#include "libavutil/bprint.h"
#include "libavutil/eval.h"
#include "libavutil/log.h"
#include "libavutil/parseutils.h"
/**
* Function used to expand a template sequence in the format
* %{FUNCTION_NAME[:PARAMS]}, defined in the TextExpander object.
*/
typedef struct FFExpandTextFunction {
/**
* name of the function
*/
const char *name;
/**
* minimum and maximum number of arguments accepted by the
* function in the PARAMS
*/
unsigned argc_min, argc_max;
/**
* actual function used to perform the expansion
*/
int (*func)(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **args);
} FFExpandTextFunction;
/**
* Text expander context, used to encapsulate the logic to expand a
* given text template.
*
* A backslash character @samp{\} in a text template, followed by any
* character, always expands to the second character.
* Sequences of the form %{FUNCTION_NAME[:PARAMS]} are expanded using a
* function defined in the object. The text between the braces is a
* function name, possibly followed by arguments separated by ':'. If
* the arguments contain special characters or delimiters (':' or
* '}'), they should be escaped.
*/
typedef struct FFExpandTextContext {
/**
* log context to pass to the function, used for logging and for
* accessing the context for the function
*/
void *log_ctx;
/**
* list of functions to use to expand sequences in the format
* FUNCTION_NAME{PARAMS}
*/
FFExpandTextFunction *functions;
/**
* number of functions
*/
unsigned int functions_nb;
} FFExpandTextContext;
/**
* Expand text template.
*
* Expand text template defined in text using the logic defined in a text
* expander object.
*
* @param expand_text text expansion context used to expand the text
* @param text template text to expand
* @param bp BPrint object where the expanded text is written to
* @return negative value corresponding to an AVERROR error code in case of
* errors, a non-negative value otherwise
*/
int ff_expand_text(FFExpandTextContext *expand_text, char *text, AVBPrint *bp);
/**
* Print PTS representation to an AVBPrint object.
*
* @param log_ctx pointer to av_log object
* @param bp AVBPrint object where the PTS textual representation is written to
* @param pts PTS value expressed as a double to represent
* @param delta delta time parsed by av_parse_time(), added to the PTS
* @param fmt string representing the format to use for printing, can be
* "flt" - use a float representation with 6 decimal digits,
* "hms" - use HH:MM:SS.MMM format,
* "hms24hh" - same as "hms" but wraps the hours in 24hh format
* (so that it is expressed in the range 00-23),
* "localtime" or "gmtime" - expand the PTS according to the
* @code{strftime()} function rules, using either the corresponding
* @code{localtime()} or @code{gmtime()} time
* @param strftime_fmt: @code{strftime()} format to use to represent the PTS in
* case the format "localtime" or "gmtime" was selected, if not specified
* defaults to "%Y-%m-%d %H:%M:%S"
* @return negative value corresponding to an AVERROR error code in case of
* errors, a non-negative value otherwise
*/
int ff_print_pts(void *log_ctx, AVBPrint *bp, double pts, const char *delta,
const char *fmt, const char *strftime_fmt);
/**
* Print time representation to an AVBPrint object.
*
* @param log_ctx pointer to av_log object
* @param bp AVBPrint object where the time textual representation is written to
* @param strftime_fmt: strftime() format to use to represent the time in case
* if not specified defaults to "%Y-%m-%d %H:%M:%S". The format string is
* extended to support the %[1-6]N after %S which prints fractions of the
* second with optionally specified number of digits, if not specified
* defaults to 3.
* @param localtime use local time to compute the time if non-zero, otherwise
* use UTC
* @return negative value corresponding to an AVERROR error code in case of
* errors, a non-negative value otherwise
*/
int ff_print_time(void *log_ctx, AVBPrint *bp, const char *strftime_fmt, char localtime);
typedef double (*ff_eval_func2)(void *, double a, double b);
/**
* Evaluate and print expression to an AVBprint object.
* The output is written as a double representation.
*
* This is a wrapper around av_expr_parse_and_eval() and following the
* same rules.
*
* @param log_ctx pointer to av_log object
* @param bp AVBPrint object where the evaluated expression is written to
* @param expr the expression to be evaluated
* @param fun_names names of the ff_eval_func2 functions used to evaluate the expression
* @param fun_values values of the ff_eval_func2 functions used to evaluate the expression
* @param var_names names of the variables used in the expression
* @param var_values values of the variables used in the expression
* @param eval_ctx evaluation context to be passed to some functions
*
* @return negative value corresponding to an AVERROR error code in case of
* errors, a non-negative value otherwise
*/
int ff_print_eval_expr(void *log_ctx, AVBPrint *bp,
const char *expr,
const char * const *fun_names, const ff_eval_func2 *fun_values,
const char * const *var_names, const double *var_values,
void *eval_ctx);
/**
* Evaluate and print expression to an AVBprint object, using the
* specified format.
*
* This is a wrapper around av_expr_parse_and_eval() and following the
* same rules.
*
* The format is specified as a printf format character, optionally
* preceded by the positions numbers for zero-padding.
*
* The following formats are accepted:
* - x: use lowercase hexadecimal representation
* - X: use uppercase hexadecimal representation
* - d: use decimal representation
* - u: use unsigned decimal representation
*
* @param log_ctx pointer to av_log object
* @param bp AVBPrint object where the evaluated expression is written to
* @param expr the expression to be evaluated
* @param fun_names names of the ff_eval_func2 functions used to evaluate the expression
* @param fun_values values of the ff_eval_func2 functions used to evaluate the expression
* @param var_names names of the variables used in the expression
* @param var_values values of the variables used in the expression
* @param eval_ctx evaluation context to be passed to some functions
* @param format a character representing the format, to be chosen in xXdu
* @param positions final size of the value representation with 0-padding
* @return negative value corresponding to an AVERROR error code in case of
* errors, a non-negative value otherwise
*/
int ff_print_formatted_eval_expr(void *log_ctx, AVBPrint *bp,
const char *expr,
const char * const *fun_names, const ff_eval_func2 *fun_values,
const char * const *var_names, const double *var_values,
void *eval_ctx,
const char format, int positions);
/**
* Check if the character is a newline.
*
* @param c character to check
* @return non-negative value in case c is a newline, 0 otherwise
*/
static inline int ff_is_newline(uint32_t c)
{
return c == '\n' || c == '\r' || c == '\f' || c == '\v';
}
/**
* Load text file into the buffer pointed by text.
*
* @param log_ctx pointer to av_log object
* @param textfile filename containing the text to load
* @param text pointer to the text buffer where the loaded text will be
* loaded
* @param text_size pointer to the value to set with the loaded text data,
* including the terminating 0 character
* @return negative value corresponding to an AVERROR error code in case of
* errors, a non-negative value otherwise
*/
int ff_load_textfile(void *log_ctx, const char *textfile,
unsigned char **text, size_t *text_size);
#endif /* AVFILTER_TEXTUTILS__H */

View File

@ -47,7 +47,6 @@
#include "libavutil/avstring.h"
#include "libavutil/bprint.h"
#include "libavutil/common.h"
#include "libavutil/file.h"
#include "libavutil/eval.h"
#include "libavutil/opt.h"
#include "libavutil/random_seed.h"
@ -62,6 +61,7 @@
#include "drawutils.h"
#include "formats.h"
#include "internal.h"
#include "textutils.h"
#include "video.h"
#if CONFIG_LIBFRIBIDI
@ -253,6 +253,7 @@ typedef struct TextMetrics {
typedef struct DrawTextContext {
const AVClass *class;
int exp_mode; ///< expansion mode to use for the text
FFExpandTextContext expand_text; ///< expand text in case exp_mode == NORMAL
int reinit; ///< tells if the filter is being reinited
#if CONFIG_LIBFONTCONFIG
uint8_t *font; ///< font to be used
@ -631,40 +632,6 @@ static int load_font(AVFilterContext *ctx)
return err;
}
static inline int is_newline(uint32_t c)
{
return c == '\n' || c == '\r' || c == '\f' || c == '\v';
}
static int load_textfile(AVFilterContext *ctx)
{
DrawTextContext *s = ctx->priv;
int err;
uint8_t *textbuf;
uint8_t *tmp;
size_t textbuf_size;
if ((err = av_file_map(s->textfile, &textbuf, &textbuf_size, 0, ctx)) < 0) {
av_log(ctx, AV_LOG_ERROR,
"The text file '%s' could not be read or is empty\n",
s->textfile);
return err;
}
if (textbuf_size > 0 && is_newline(textbuf[textbuf_size - 1]))
textbuf_size--;
if (textbuf_size > SIZE_MAX - 1 || !(tmp = av_realloc(s->text, textbuf_size + 1))) {
av_file_unmap(textbuf, textbuf_size);
return AVERROR(ENOMEM);
}
s->text = tmp;
memcpy(s->text, textbuf, textbuf_size);
s->text[textbuf_size] = 0;
av_file_unmap(textbuf, textbuf_size);
return 0;
}
#if CONFIG_LIBFRIBIDI
static int shape_text(AVFilterContext *ctx)
{
@ -885,6 +852,123 @@ static int string_to_array(const char *source, int *result, int result_size)
return counter;
}
static int func_pict_type(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv)
{
DrawTextContext *s = ((AVFilterContext *)ctx)->priv;
av_bprintf(bp, "%c", av_get_picture_type_char(s->var_values[VAR_PICT_TYPE]));
return 0;
}
static int func_pts(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv)
{
DrawTextContext *s = ((AVFilterContext *)ctx)->priv;
const char *fmt;
const char *strftime_fmt = NULL;
const char *delta = NULL;
double pts = s->var_values[VAR_T];
// argv: pts, FMT, [DELTA, 24HH | strftime_fmt]
fmt = argc >= 1 ? argv[0] : "flt";
if (argc >= 2) {
delta = argv[1];
}
if (argc >= 3) {
if (!strcmp(fmt, "hms")) {
if (!strcmp(argv[2], "24HH")) {
av_log(ctx, AV_LOG_WARNING, "pts third argument 24HH is deprected, use pts:hms24hh instead\n");
fmt = "hms24";
} else {
av_log(ctx, AV_LOG_ERROR, "Invalid argument '%s', '24HH' was expected\n", argv[2]);
return AVERROR(EINVAL);
}
} else {
strftime_fmt = argv[2];
}
}
return ff_print_pts(ctx, bp, pts, delta, fmt, strftime_fmt);
}
static int func_frame_num(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv)
{
DrawTextContext *s = ((AVFilterContext *)ctx)->priv;
av_bprintf(bp, "%d", (int)s->var_values[VAR_N]);
return 0;
}
static int func_metadata(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv)
{
DrawTextContext *s = ((AVFilterContext *)ctx)->priv;
AVDictionaryEntry *e = av_dict_get(s->metadata, argv[0], NULL, 0);
if (e && e->value)
av_bprintf(bp, "%s", e->value);
else if (argc >= 2)
av_bprintf(bp, "%s", argv[1]);
return 0;
}
static int func_strftime(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv)
{
const char *strftime_fmt = argc ? argv[0] : NULL;
return ff_print_time(ctx, bp, strftime_fmt, !strcmp(function_name, "localtime"));
}
static int func_eval_expr(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv)
{
DrawTextContext *s = ((AVFilterContext *)ctx)->priv;
return ff_print_eval_expr(ctx, bp, argv[0],
fun2_names, fun2,
var_names, s->var_values, &s->prng);
}
static int func_eval_expr_int_format(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv)
{
DrawTextContext *s = ((AVFilterContext *)ctx)->priv;
int ret;
int positions = -1;
/*
* argv[0] expression to be converted to `int`
* argv[1] format: 'x', 'X', 'd' or 'u'
* argv[2] positions printed (optional)
*/
if (argc == 3) {
ret = sscanf(argv[2], "%u", &positions);
if (ret != 1) {
av_log(ctx, AV_LOG_ERROR, "expr_int_format(): Invalid number of positions"
" to print: '%s'\n", argv[2]);
return AVERROR(EINVAL);
}
}
return ff_print_formatted_eval_expr(ctx, bp, argv[0],
fun2_names, fun2,
var_names, s->var_values,
&s->prng,
argv[1][0], positions);
}
static FFExpandTextFunction expand_text_functions[] = {
{ "e", 1, 1, func_eval_expr },
{ "eif", 2, 3, func_eval_expr_int_format },
{ "expr", 1, 1, func_eval_expr },
{ "expr_int_format", 2, 3, func_eval_expr_int_format },
{ "frame_num", 0, 0, func_frame_num },
{ "gmtime", 0, 1, func_strftime },
{ "localtime", 0, 1, func_strftime },
{ "metadata", 1, 2, func_metadata },
{ "n", 0, 0, func_frame_num },
{ "pict_type", 0, 0, func_pict_type },
{ "pts", 0, 3, func_pts }
};
static av_cold int init(AVFilterContext *ctx)
{
int err;
@ -907,7 +991,7 @@ static av_cold int init(AVFilterContext *ctx)
"Both text and text file provided. Please provide only one\n");
return AVERROR(EINVAL);
}
if ((err = load_textfile(ctx)) < 0)
if ((err = ff_load_textfile(ctx, (const char *)s->textfile, &s->text, NULL)) < 0)
return err;
}
@ -950,6 +1034,12 @@ static av_cold int init(AVFilterContext *ctx)
return AVERROR(EINVAL);
}
s->expand_text = (FFExpandTextContext) {
.log_ctx = ctx,
.functions = expand_text_functions,
.functions_nb = FF_ARRAY_ELEMS(expand_text_functions)
};
#if CONFIG_LIBFRIBIDI
if (s->text_shaping)
if ((err = shape_text(ctx)) < 0)
@ -1160,367 +1250,6 @@ fail:
return ret;
}
static int func_pict_type(AVFilterContext *ctx, AVBPrint *bp,
char *fct, unsigned argc, char **argv, int tag)
{
DrawTextContext *s = ctx->priv;
av_bprintf(bp, "%c", av_get_picture_type_char(s->var_values[VAR_PICT_TYPE]));
return 0;
}
static int func_pts(AVFilterContext *ctx, AVBPrint *bp,
char *fct, unsigned argc, char **argv, int tag)
{
DrawTextContext *s = ctx->priv;
const char *fmt;
double pts = s->var_values[VAR_T];
int ret;
fmt = argc >= 1 ? argv[0] : "flt";
if (argc >= 2) {
int64_t delta;
if ((ret = av_parse_time(&delta, argv[1], 1)) < 0) {
av_log(ctx, AV_LOG_ERROR, "Invalid delta '%s'\n", argv[1]);
return ret;
}
pts += (double)delta / AV_TIME_BASE;
}
if (!strcmp(fmt, "flt")) {
av_bprintf(bp, "%.6f", pts);
} else if (!strcmp(fmt, "hms")) {
if (isnan(pts)) {
av_bprintf(bp, " ??:??:??.???");
} else {
int64_t ms = llrint(pts * 1000);
char sign = ' ';
if (ms < 0) {
sign = '-';
ms = -ms;
}
if (argc >= 3) {
if (!strcmp(argv[2], "24HH")) {
ms %= 24 * 60 * 60 * 1000;
} else {
av_log(ctx, AV_LOG_ERROR, "Invalid argument '%s'\n", argv[2]);
return AVERROR(EINVAL);
}
}
av_bprintf(bp, "%c%02d:%02d:%02d.%03d", sign,
(int)(ms / (60 * 60 * 1000)),
(int)(ms / (60 * 1000)) % 60,
(int)(ms / 1000) % 60,
(int)(ms % 1000));
}
} else if (!strcmp(fmt, "localtime") ||
!strcmp(fmt, "gmtime")) {
struct tm tm;
time_t ms = (time_t)pts;
const char *timefmt = argc >= 3 ? argv[2] : "%Y-%m-%d %H:%M:%S";
if (!strcmp(fmt, "localtime"))
localtime_r(&ms, &tm);
else
gmtime_r(&ms, &tm);
av_bprint_strftime(bp, timefmt, &tm);
} else {
av_log(ctx, AV_LOG_ERROR, "Invalid format '%s'\n", fmt);
return AVERROR(EINVAL);
}
return 0;
}
static int func_frame_num(AVFilterContext *ctx, AVBPrint *bp,
char *fct, unsigned argc, char **argv, int tag)
{
DrawTextContext *s = ctx->priv;
av_bprintf(bp, "%d", (int)s->var_values[VAR_N]);
return 0;
}
static int func_metadata(AVFilterContext *ctx, AVBPrint *bp,
char *fct, unsigned argc, char **argv, int tag)
{
DrawTextContext *s = ctx->priv;
AVDictionaryEntry *e = av_dict_get(s->metadata, argv[0], NULL, 0);
if (e && e->value)
av_bprintf(bp, "%s", e->value);
else if (argc >= 2)
av_bprintf(bp, "%s", argv[1]);
return 0;
}
static int func_strftime(AVFilterContext *ctx, AVBPrint *bp,
char *fct, unsigned argc, char **argv, int tag)
{
const char *fmt = argc ? argv[0] : "%Y-%m-%d %H:%M:%S";
const char *fmt_begin = fmt;
int64_t unow;
time_t now;
struct tm tm;
const char *begin;
const char *tmp;
int len;
int div;
AVBPrint fmt_bp;
av_bprint_init(&fmt_bp, 0, AV_BPRINT_SIZE_UNLIMITED);
unow = av_gettime();
now = unow / 1000000;
if (tag == 'L' || tag == 'm')
localtime_r(&now, &tm);
else
tm = *gmtime_r(&now, &tm);
// manually parse format for %N (fractional seconds)
begin = fmt;
while ((begin = strchr(begin, '%'))) {
tmp = begin + 1;
len = 0;
// skip escaped "%%"
if (*tmp == '%') {
begin = tmp + 1;
continue;
}
// count digits between % and possible N
while (*tmp != '\0' && av_isdigit((int)*tmp)) {
len++;
tmp++;
}
// N encountered, insert time
if (*tmp == 'N') {
int num_digits = 3; // default show millisecond [1,6]
// if digit given, expect [1,6], warn & clamp otherwise
if (len == 1) {
num_digits = av_clip(*(begin + 1) - '0', 1, 6);
} else if (len > 1) {
av_log(ctx, AV_LOG_WARNING, "Invalid number of decimals for %%N, using default of %i\n", num_digits);
}
len += 2; // add % and N to get length of string part
div = pow(10, 6 - num_digits);
av_bprintf(&fmt_bp, "%.*s%0*d", (int)(begin - fmt_begin), fmt_begin, num_digits, (int)(unow % 1000000) / div);
begin += len;
fmt_begin = begin;
continue;
}
begin = tmp;
}
av_bprintf(&fmt_bp, "%s", fmt_begin);
if (!av_bprint_is_complete(&fmt_bp)) {
av_log(ctx, AV_LOG_WARNING, "Format string truncated at %u/%u.", fmt_bp.size, fmt_bp.len);
}
av_bprint_strftime(bp, fmt_bp.str, &tm);
av_bprint_finalize(&fmt_bp, NULL);
return 0;
}
static int func_eval_expr(AVFilterContext *ctx, AVBPrint *bp,
char *fct, unsigned argc, char **argv, int tag)
{
DrawTextContext *s = ctx->priv;
double res;
int ret;
ret = av_expr_parse_and_eval(&res, argv[0], var_names, s->var_values,
NULL, NULL, fun2_names, fun2,
&s->prng, 0, ctx);
if (ret < 0)
av_log(ctx, AV_LOG_ERROR,
"Expression '%s' for the expr text expansion function is not valid\n",
argv[0]);
else
av_bprintf(bp, "%f", res);
return ret;
}
static int func_eval_expr_int_format(AVFilterContext *ctx, AVBPrint *bp,
char *fct, unsigned argc, char **argv, int tag)
{
DrawTextContext *s = ctx->priv;
double res;
int intval;
int ret;
unsigned int positions = 0;
char fmt_str[30] = "%";
/*
* argv[0] expression to be converted to `int`
* argv[1] format: 'x', 'X', 'd' or 'u'
* argv[2] positions printed (optional)
*/
ret = av_expr_parse_and_eval(&res, argv[0], var_names, s->var_values,
NULL, NULL, fun2_names, fun2,
&s->prng, 0, ctx);
if (ret < 0) {
av_log(ctx, AV_LOG_ERROR,
"Expression '%s' for the expr text expansion function is not valid\n",
argv[0]);
return ret;
}
if (!strchr("xXdu", argv[1][0])) {
av_log(ctx, AV_LOG_ERROR, "Invalid format '%c' specified,"
" allowed values: 'x', 'X', 'd', 'u'\n", argv[1][0]);
return AVERROR(EINVAL);
}
if (argc == 3) {
ret = sscanf(argv[2], "%u", &positions);
if (ret != 1) {
av_log(ctx, AV_LOG_ERROR, "expr_int_format(): Invalid number of positions"
" to print: '%s'\n", argv[2]);
return AVERROR(EINVAL);
}
}
feclearexcept(FE_ALL_EXCEPT);
intval = res;
#if defined(FE_INVALID) && defined(FE_OVERFLOW) && defined(FE_UNDERFLOW)
if ((ret = fetestexcept(FE_INVALID|FE_OVERFLOW|FE_UNDERFLOW))) {
av_log(ctx, AV_LOG_ERROR, "Conversion of floating-point result to int failed. Control register: 0x%08x. Conversion result: %d\n", ret, intval);
return AVERROR(EINVAL);
}
#endif
if (argc == 3)
av_strlcatf(fmt_str, sizeof(fmt_str), "0%u", positions);
av_strlcatf(fmt_str, sizeof(fmt_str), "%c", argv[1][0]);
av_log(ctx, AV_LOG_DEBUG, "Formatting value %f (expr '%s') with spec '%s'\n",
res, argv[0], fmt_str);
av_bprintf(bp, fmt_str, intval);
return 0;
}
static const struct drawtext_function {
const char *name;
unsigned argc_min, argc_max;
int tag; /**< opaque argument to func */
int (*func)(AVFilterContext *, AVBPrint *, char *, unsigned, char **, int);
} functions[] = {
{ "expr", 1, 1, 0, func_eval_expr },
{ "e", 1, 1, 0, func_eval_expr },
{ "expr_int_format", 2, 3, 0, func_eval_expr_int_format },
{ "eif", 2, 3, 0, func_eval_expr_int_format },
{ "pict_type", 0, 0, 0, func_pict_type },
{ "pts", 0, 3, 0, func_pts },
{ "gmtime", 0, 1, 'G', func_strftime },
{ "localtime", 0, 1, 'L', func_strftime },
{ "frame_num", 0, 0, 0, func_frame_num },
{ "n", 0, 0, 0, func_frame_num },
{ "metadata", 1, 2, 0, func_metadata },
};
static int eval_function(AVFilterContext *ctx, AVBPrint *bp, char *fct,
unsigned argc, char **argv)
{
unsigned i;
for (i = 0; i < FF_ARRAY_ELEMS(functions); i++) {
if (strcmp(fct, functions[i].name))
continue;
if (argc < functions[i].argc_min) {
av_log(ctx, AV_LOG_ERROR, "%%{%s} requires at least %d arguments\n",
fct, functions[i].argc_min);
return AVERROR(EINVAL);
}
if (argc > functions[i].argc_max) {
av_log(ctx, AV_LOG_ERROR, "%%{%s} requires at most %d arguments\n",
fct, functions[i].argc_max);
return AVERROR(EINVAL);
}
break;
}
if (i >= FF_ARRAY_ELEMS(functions)) {
av_log(ctx, AV_LOG_ERROR, "%%{%s} is not known\n", fct);
return AVERROR(EINVAL);
}
return functions[i].func(ctx, bp, fct, argc, argv, functions[i].tag);
}
static int expand_function(AVFilterContext *ctx, AVBPrint *bp, char **rtext)
{
const char *text = *rtext;
char *argv[16] = { NULL };
unsigned argc = 0, i;
int ret;
if (*text != '{') {
av_log(ctx, AV_LOG_ERROR, "Stray %% near '%s'\n", text);
return AVERROR(EINVAL);
}
text++;
while (1) {
if (!(argv[argc++] = av_get_token(&text, ":}"))) {
ret = AVERROR(ENOMEM);
goto end;
}
if (!*text) {
av_log(ctx, AV_LOG_ERROR, "Unterminated %%{} near '%s'\n", *rtext);
ret = AVERROR(EINVAL);
goto end;
}
if (argc == FF_ARRAY_ELEMS(argv))
av_freep(&argv[--argc]); /* error will be caught later */
if (*text == '}')
break;
text++;
}
if ((ret = eval_function(ctx, bp, argv[0], argc - 1, argv + 1)) < 0)
goto end;
ret = 0;
*rtext = (char *)text + 1;
end:
for (i = 0; i < argc; i++)
av_freep(&argv[i]);
return ret;
}
static int expand_text(AVFilterContext *ctx, char *text, AVBPrint *bp)
{
int ret;
av_bprint_clear(bp);
while (*text) {
if (*text == '\\' && text[1]) {
av_bprint_chars(bp, text[1], 1);
text += 2;
} else if (*text == '%') {
text++;
if ((ret = expand_function(ctx, bp, &text)) < 0)
return ret;
} else {
av_bprint_chars(bp, *text, 1);
text++;
}
}
if (!av_bprint_is_complete(bp))
return AVERROR(ENOMEM);
return 0;
}
static void update_color_with_alpha(DrawTextContext *s, FFDrawColor *color, const FFDrawColor incolor)
{
*color = incolor;
@ -1688,7 +1417,7 @@ static int measure_text(AVFilterContext *ctx, TextMetrics *metrics)
for (i = 0, p = text; 1; i++) {
GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_failed;);
continue_on_failed:
if (is_newline(code) || code == 0) {
if (ff_is_newline(code) || code == 0) {
++line_count;
if (code == 0) {
break;
@ -1729,7 +1458,7 @@ continue_on_failed:
}
GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_failed2;);
continue_on_failed2:
if (is_newline(code) || code == 0) {
if (ff_is_newline(code) || code == 0) {
TextLine *cur_line = &s->lines[line_count];
HarfbuzzData *hb = &cur_line->hb_data;
cur_line->cluster_offset = line_offset;
@ -1861,7 +1590,7 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame)
av_bprintf(bp, "%s", s->text);
break;
case EXP_NORMAL:
if ((ret = expand_text(ctx, s->text, &s->expanded_text)) < 0)
if ((ret = ff_expand_text(&s->expand_text, s->text, &s->expanded_text)) < 0)
return ret;
break;
case EXP_STRFTIME:
@ -1883,7 +1612,7 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame)
if (s->fontcolor_expr[0]) {
/* If expression is set, evaluate and replace the static value */
av_bprint_clear(&s->expanded_fontcolor);
if ((ret = expand_text(ctx, s->fontcolor_expr, &s->expanded_fontcolor)) < 0)
if ((ret = ff_expand_text(&s->expand_text, s->fontcolor_expr, &s->expanded_fontcolor)) < 0)
return ret;
if (!av_bprint_is_complete(&s->expanded_fontcolor))
return AVERROR(ENOMEM);
@ -2125,7 +1854,7 @@ static int filter_frame(AVFilterLink *inlink, AVFrame *frame)
}
if (s->reload && !(inlink->frame_count_out % s->reload)) {
if ((ret = load_textfile(ctx)) < 0) {
if ((ret = ff_load_textfile(ctx, (const char *)s->textfile, &s->text, NULL)) < 0) {
av_frame_free(&frame);
return ret;
}