From 732fb122e66cf4d0d9cec2eed00e088ea6a3b97d Mon Sep 17 00:00:00 2001 From: Stefano Sabatini Date: Mon, 20 Nov 2023 01:13:17 +0100 Subject: [PATCH] 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. --- libavfilter/Makefile | 2 +- libavfilter/textutils.c | 382 +++++++++++++++++++++++++++ libavfilter/textutils.h | 229 ++++++++++++++++ libavfilter/vf_drawtext.c | 533 ++++++++++---------------------------- 4 files changed, 743 insertions(+), 403 deletions(-) create mode 100644 libavfilter/textutils.c create mode 100644 libavfilter/textutils.h 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 +#include +#include + +#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; }