diff mbox series

[FFmpeg-devel,1/2] lavfi: introduce textutils

Message ID 20231130004914.329717-2-stefasab@gmail.com
State New
Headers show
Series [FFmpeg-devel,1/2] lavfi: introduce textutils | expand

Checks

Context Check Description
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Stefano Sabatini Nov. 30, 2023, 12:49 a.m. UTC
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   | 379 +++++++++++++++++++++++++++
 libavfilter/textutils.h   | 182 +++++++++++++
 libavfilter/vf_drawtext.c | 533 ++++++++++----------------------------
 4 files changed, 693 insertions(+), 403 deletions(-)
 create mode 100644 libavfilter/textutils.c
 create mode 100644 libavfilter/textutils.h

Comments

Stefano Sabatini Dec. 3, 2023, 3:05 p.m. UTC | #1
On date Thursday 2023-11-30 01:49:13 +0100, Stefano Sabatini wrote:
> 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   | 379 +++++++++++++++++++++++++++
>  libavfilter/textutils.h   | 182 +++++++++++++
>  libavfilter/vf_drawtext.c | 533 ++++++++++----------------------------
>  4 files changed, 693 insertions(+), 403 deletions(-)
>  create mode 100644 libavfilter/textutils.c
>  create mode 100644 libavfilter/textutils.h

Updated (with more doc and a more genric ff_print_formatted_eval_expr
function).
Nicolas George Dec. 6, 2023, 8:20 p.m. UTC | #2
Stefano Sabatini (12023-11-30):
> 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   | 379 +++++++++++++++++++++++++++
>  libavfilter/textutils.h   | 182 +++++++++++++
>  libavfilter/vf_drawtext.c | 533 ++++++++++----------------------------
>  4 files changed, 693 insertions(+), 403 deletions(-)
>  create mode 100644 libavfilter/textutils.c
>  create mode 100644 libavfilter/textutils.h

Hi.

I had kept this mail to point that this would greatly benefit from using
AVWriter instead of BPrint, since it is a much more elegant API with the
early design mistakes fixed, and to propose to work together to make it
happen.

But right now, anything feels more appealing than working on FFmpeg.

Regards,
Stefano Sabatini Dec. 9, 2023, 6:12 p.m. UTC | #3
On date Sunday 2023-12-03 16:05:40 +0100, Stefano Sabatini wrote:
> On date Thursday 2023-11-30 01:49:13 +0100, Stefano Sabatini wrote:
> > 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   | 379 +++++++++++++++++++++++++++
> >  libavfilter/textutils.h   | 182 +++++++++++++
> >  libavfilter/vf_drawtext.c | 533 ++++++++++----------------------------
> >  4 files changed, 693 insertions(+), 403 deletions(-)
> >  create mode 100644 libavfilter/textutils.c
> >  create mode 100644 libavfilter/textutils.h
> 
> Updated (with more doc and a more genric ff_print_formatted_eval_expr
> function).

Rev 3.
Stefano Sabatini Dec. 16, 2023, 3:35 p.m. UTC | #4
On date Saturday 2023-12-09 19:12:29 +0100, Stefano Sabatini wrote:
> On date Sunday 2023-12-03 16:05:40 +0100, Stefano Sabatini wrote:
> > On date Thursday 2023-11-30 01:49:13 +0100, Stefano Sabatini wrote:
> > > 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   | 379 +++++++++++++++++++++++++++
> > >  libavfilter/textutils.h   | 182 +++++++++++++
> > >  libavfilter/vf_drawtext.c | 533 ++++++++++----------------------------
> > >  4 files changed, 693 insertions(+), 403 deletions(-)
> > >  create mode 100644 libavfilter/textutils.c
> > >  create mode 100644 libavfilter/textutils.h
> > 
> > Updated (with more doc and a more genric ff_print_formatted_eval_expr
> > function).
> 
> Rev 3.

> From d9d033dcb8c3a8a473fd08db209f782f99a65780 Mon Sep 17 00:00:00 2001
> From: Stefano Sabatini <stefasab@gmail.com>
> Date: Mon, 20 Nov 2023 01:13:17 +0100
> Subject: [PATCH 1/2] 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

Ping, if there are no objections or comments I plan to push this in a
week or something.
Timo Rothenpieler Jan. 3, 2024, 12:58 p.m. UTC | #5
On 16.12.2023 16:35, Stefano Sabatini wrote:
> Ping, if there are no objections or comments I plan to push this in a
> week or something.

This broke the build:

> libavfilter/vf_drawtext.c: In function 'shape_text':
> libavfilter/vf_drawtext.c:687:13: error: implicit declaration of function 'is_newline'; did you mean 'ff_is_newline'? [-Werror=implicit-function-declaration]
>   687 |         if (is_newline(unicodestr[line_end]) || line_end == len - 1) {
>       |             ^~~~~~~~~~
>       |             ff_is_newline

Looks like one instance of is_newline was forgotten?
Stefano Sabatini Jan. 3, 2024, 3:50 p.m. UTC | #6
On date Wednesday 2024-01-03 13:58:14 +0100, Timo Rothenpieler wrote:
> On 16.12.2023 16:35, Stefano Sabatini wrote:
> > Ping, if there are no objections or comments I plan to push this in a
> > week or something.
> 
> This broke the build:
> 
> > libavfilter/vf_drawtext.c: In function 'shape_text':
> > libavfilter/vf_drawtext.c:687:13: error: implicit declaration of function 'is_newline'; did you mean 'ff_is_newline'? [-Werror=implicit-function-declaration]
> >   687 |         if (is_newline(unicodestr[line_end]) || line_end == len - 1) {
> >       |             ^~~~~~~~~~
> >       |             ff_is_newline
> 
> Looks like one instance of is_newline was forgotten?

Thanks, fixed in master.
diff mbox series

Patch

diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index de51c2a403..e49be354bb 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -290,7 +290,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..4598861161
--- /dev/null
+++ b/libavfilter/textutils.c
@@ -0,0 +1,379 @@ 
+/*
+ * 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(TextExpander *text_expander, AVBPrint *bp,
+                                            char *name, unsigned argc, char **argv)
+{
+    void *ctx = text_expander->ctx;
+    TextExpanderFunction *functions = text_expander->functions;
+    unsigned i;
+
+    for (i = 0; i < text_expander->functions_nb; i++) {
+        if (strcmp(name, functions[i].name))
+            continue;
+        if (argc < functions[i].argc_min) {
+            av_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(ctx, AV_LOG_ERROR, "%%{%s} requires at most %d arguments\n",
+                   name, functions[i].argc_max);
+            return AVERROR(EINVAL);
+        }
+        break;
+    }
+    if (i >= text_expander->functions_nb) {
+        av_log(ctx, AV_LOG_ERROR, "%%{%s} is not known\n", name);
+        return AVERROR(EINVAL);
+    }
+
+    return functions[i].func(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(TextExpander *text_expander, AVBPrint *bp, char **rtext)
+{
+    void *ctx = text_expander->ctx;
+    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 = ff_expand_text_function_internal(text_expander, 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(TextExpander *text_expander, 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 = ff_expand_text_function(text_expander, 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_eval_expr_int_format(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..8dd8255eec
--- /dev/null
+++ b/libavfilter/textutils.h
@@ -0,0 +1,182 @@ 
+/*
+ * 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 TextExpanderFunction {
+    /**
+     * 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);
+} TextExpanderFunction;
+
+/**
+ * Text expander object, 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 TextExpander {
+    /**
+     * context to pass to the function, used for logging and for accessing
+     * the context for the function
+     */
+    void *ctx;
+
+    /**
+     * list of functions to use to expand sequences in the format
+     * FUNCTION_NAME{PARAMS}
+     */
+    TextExpanderFunction *functions;
+
+    /**
+     * number of functions
+     */
+    unsigned int functions_nb;
+} TextExpander;
+
+/**
+ * Expand text template.
+ *
+ * Expand text template defined in text using the logic defined in a text
+ * expander object.
+ *
+ * @param text_expander TextExpander object 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(TextExpander *text_expander, 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);
+
+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);
+
+int ff_print_eval_expr_int_format(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..bc387c1a36 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
+    TextExpander text_expander;     ///< text expander 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_eval_expr_int_format(ctx, bp, argv[0],
+                                         fun2_names, fun2,
+                                         var_names, s->var_values,
+                                         &s->prng,
+                                         argv[1][0], positions);
+}
+
+static TextExpanderFunction text_expander_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->text_expander = (TextExpander) {
+        .ctx = ctx,
+        .functions = text_expander_functions,
+        .functions_nb = FF_ARRAY_ELEMS(text_expander_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->text_expander, 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->text_expander, 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;
         }