summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefano Sabatini <stefasab@gmail.com>2023-11-20 01:13:17 +0100
committerStefano Sabatini <stefasab@gmail.com>2024-01-01 20:12:52 +0100
commit732fb122e66cf4d0d9cec2eed00e088ea6a3b97d (patch)
tree43a1d209b626fe95cd1d2ec2b11b92c397061bac
parentc2ab41b7e0b9bc700f6a4814f629ac52962e0025 (diff)
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.
-rw-r--r--libavfilter/Makefile2
-rw-r--r--libavfilter/textutils.c382
-rw-r--r--libavfilter/textutils.h229
-rw-r--r--libavfilter/vf_drawtext.c533
4 files changed, 743 insertions, 403 deletions
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index afc7bc1566..badaa43859 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -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
diff --git a/libavfilter/textutils.c b/libavfilter/textutils.c
new file mode 100644
index 0000000000..ef658d04a2
--- /dev/null
+++ b/libavfilter/textutils.c
@@ -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;
+}
+
diff --git a/libavfilter/textutils.h b/libavfilter/textutils.h
new file mode 100644
index 0000000000..7fa856c681
--- /dev/null
+++ b/libavfilter/textutils.h
@@ -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 */
diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c
index c5477cbff1..c1ea5b90b3 100644
--- a/libavfilter/vf_drawtext.c
+++ b/libavfilter/vf_drawtext.c
@@ -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;
}