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;
         }
