[FFmpeg-devel] [PATCH 4/4] lavfi/drawtext: implement more generic expansion.

Nicolas George nicolas.george at normalesup.org
Thu Nov 15 17:13:58 CET 2012


The new expansion mechanism uses the %{...} notation.
For compatibility reasons, it must be enabled explicitly,
but a warning is printed if a change is likely to happen.

TODO micro bump

Signed-off-by: Nicolas George <nicolas.george at normalesup.org>
---
 Changelog                 |    1 +
 doc/filters.texi          |   55 ++++++++++++++-
 libavfilter/vf_drawtext.c |  167 +++++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 214 insertions(+), 9 deletions(-)


Implement Clément and Stefano's comments.


diff --git a/Changelog b/Changelog
index 9ef12b8..8fe99af 100644
--- a/Changelog
+++ b/Changelog
@@ -23,6 +23,7 @@ version <next>:
 - field filter ported from libmpcodecs
 - AVR demuxer
 - geq filter ported from libmpcodecs
+- new expansion syntax for drawtext
 
 
 version 1.0:
diff --git a/doc/filters.texi b/doc/filters.texi
index b86b146..e75cd15 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -1844,8 +1844,7 @@ libfreetype library.
 To enable compilation of this filter you need to configure FFmpeg with
 @code{--enable-libfreetype}.
 
-The filter also recognizes strftime() sequences in the provided text
-and expands them accordingly. Check the documentation of strftime().
+ at subsection Syntax
 
 The filter accepts parameters as a list of @var{key}=@var{value} pairs,
 separated by ":".
@@ -1875,6 +1874,12 @@ Default value is "1".
 
 See below for the list of accepted constants and functions.
 
+ at item expand
+Select how the @var{text} is expanded. Can be either @code{none},
+ at code{strftime} (default for compatibity reasons but deprecated) or
+ at code{expand}. See the @ref{drawtext_expansion, Text expansion} section
+below for details.
+
 @item fix_bounds
 If true, check and fix text coords to avoid clipping.
 
@@ -2039,6 +2044,46 @@ each other, so you can for example specify @code{y=x/dar}.
 If libavfilter was built with @code{--enable-fontconfig}, then
 @option{fontfile} can be a fontconfig pattern or omitted.
 
+ at anchor{drawtext_expansion}
+ at subsection Text expansion
+
+If @option{expand} is set to @code{strftime} (which is the default for now),
+the filter recognizes strftime() sequences in the provided text and expands
+them accordingly. Check the documentation of strftime(). This feature is
+deprecated.
+
+If @option{expand} is set to @code{none}, the text is printed verbatim.
+
+If @option{expand} is set to @code{expand} (which will be the default), the
+following expansion mechanism is used.
+
+The backslash character '\', followed by any character, always expand to the
+second character.
+
+Sequence of the form @code{%@{...@}} are expanded. 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. Note that they probably must also be escaped as the
+value for the @option{text} option in the filter argument string and as the
+filter argument in the filter graph description, and possibly also for the
+shell, that makes up to four levels of escaping; using a text file avoids
+these problems.
+
+The following functions are available:
+
+ at table @command
+
+ at item localtime
+The time at which the filter is running, expressed in the local time zone.
+It can accept an argument: a strftime() format string.
+
+ at item pts
+The timestamp of the current frame, in seconds, with microsecond accuracy.
+
+ at end table
+
+ at subsection Examples
+
 Some examples follow.
 
 @itemize
@@ -2104,6 +2149,12 @@ Use fontconfig to set the font. Note that the colons need to be escaped.
 drawtext='fontfile=Linux Libertine O-40\:style=Semibold:text=FFmpeg'
 @end example
 
+ at item
+Print the date of a real-time encoding (see strftime(3)):
+ at example
+drawtext='fontfile=FreeSans.ttf:expand=expand:text=%@{localtime:%a %b %d %Y@}'
+ at end example
+
 @end itemize
 
 For more information about libfreetype, check:
diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c
index f9c69ae..54c303c 100644
--- a/libavfilter/vf_drawtext.c
+++ b/libavfilter/vf_drawtext.c
@@ -113,8 +113,15 @@ enum var_name {
     VAR_VARS_NB
 };
 
+enum expansion_mode {
+    EXP_NONE,
+    EXP_EXPAND,
+    EXP_STRFTIME,
+};
+
 typedef struct {
     const AVClass *class;
+    enum expansion_mode exp_mode;   ///< use old strftime-style expand
     int reinit;                     ///< tells if the filter is being reinited
     uint8_t *fontfile;              ///< font to be used
     uint8_t *text;                  ///< text to be drawn
@@ -181,6 +188,10 @@ static const AVOption drawtext_options[]= {
 {"tabsize",  "set tab size",         OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0,        INT_MAX , FLAGS},
 {"basetime", "set base time",        OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX , FLAGS},
 {"draw",     "if false do not draw", OFFSET(draw_expr),          AV_OPT_TYPE_STRING, {.str="1"},   CHAR_MIN, CHAR_MAX, FLAGS},
+{"expand",   "set the expansion mode", OFFSET(exp_mode),         AV_OPT_TYPE_INT,    {.i64=EXP_STRFTIME}, 0,        2, FLAGS, "expand"},
+{"none",     "set no expansion",     OFFSET(exp_mode),           AV_OPT_TYPE_CONST,  {.i64=EXP_NONE},     0,        0, FLAGS, "expand"},
+{"expand",   "set normal expansion", OFFSET(exp_mode),           AV_OPT_TYPE_CONST,  {.i64=EXP_EXPAND},   0,        0, FLAGS, "expand"},
+{"strftime", "set strftime expansion (deprecated)", OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_STRFTIME}, 0,    0, FLAGS, "expand"},
 {"timecode", "set initial timecode", OFFSET(tc_opt_string),      AV_OPT_TYPE_STRING, {.str=NULL},  CHAR_MIN, CHAR_MAX, FLAGS},
 {"tc24hmax", "set 24 hours max (timecode only)", OFFSET(tc24hmax), AV_OPT_TYPE_INT,  {.i64=0},            0,        1, FLAGS},
 {"timecode_rate", "set rate (timecode only)", OFFSET(tc_rate),   AV_OPT_TYPE_RATIONAL, {.dbl=0},          0,  INT_MAX, FLAGS},
@@ -484,6 +495,10 @@ static av_cold int init(AVFilterContext *ctx, const char *args)
     }
     dtext->tabsize *= glyph->advance;
 
+    if (dtext->exp_mode == EXP_STRFTIME &&
+        (strchr(dtext->text, '%') || strchr(dtext->text, '\\')))
+        av_log(ctx, AV_LOG_WARNING, "expand=strftime is deprecated.\n");
+
     av_bprint_init(&dtext->expanded_text, 0, AV_BPRINT_SIZE_UNLIMITED);
 
     return 0;
@@ -585,6 +600,138 @@ static int command(AVFilterContext *ctx, const char *cmd, const char *arg, char
     return AVERROR(ENOSYS);
 }
 
+static int func_pts(AVFilterContext *ctx, AVBPrint *bp,
+                    char *fct, unsigned argc, char **argv, int tag)
+{
+    DrawTextContext *dtext = ctx->priv;
+
+    av_bprintf(bp, "%.6f", dtext->var_values[VAR_T]);
+    return 0;
+}
+
+#if !HAVE_LOCALTIME_R
+static void localtime_r(const time_t *t, struct tm *tm)
+{
+    *tm = localtime(t);
+}
+#endif
+
+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";
+    time_t now;
+    struct tm tm;
+
+    time(&now);
+    localtime_r(&now, &tm);
+    av_bprint_strftime(bp, fmt, &tm);
+    return 0;
+}
+
+static const struct drawtext_function {
+    const char *name;
+    unsigned argc_min, argc_max;
+    int tag;
+    int (*func)(AVFilterContext *, AVBPrint *, char *, unsigned, char **, int);
+} functions[] = {
+    { "pts",       0, 0, 0,   func_pts      },
+    { "localtime", 0, 1, 'L', func_strftime },
+};
+
+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)
+{
+    DrawTextContext *dtext = ctx->priv;
+    char *text = dtext->text;
+    AVBPrint *bp = &dtext->expanded_text;
+    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 int draw_glyphs(DrawTextContext *dtext, AVFilterBufferRef *picref,
                        int width, int height, const uint8_t rgbcolor[4], FFDrawColor *color, int x, int y)
 {
@@ -648,13 +795,19 @@ static int draw_text(AVFilterContext *ctx, AVFilterBufferRef *picref,
     if(dtext->basetime != AV_NOPTS_VALUE)
         now= picref->pts*av_q2d(ctx->inputs[0]->time_base) + dtext->basetime/1000000;
 
-#if HAVE_LOCALTIME_R
-    localtime_r(&now, &ltime);
-#else
-    if(strchr(dtext->text, '%'))
-        ltime= *localtime(&now);
-#endif
-    av_bprint_strftime(bp, dtext->text, &ltime);
+    switch (dtext->exp_mode) {
+    case EXP_NONE:
+        av_bprintf(bp, "%s", dtext->text);
+        break;
+    case EXP_EXPAND:
+        if ((ret = expand_text(ctx)) < 0)
+            return ret;
+        break;
+    case EXP_STRFTIME:
+        localtime_r(&now, &ltime);
+        av_bprint_strftime(bp, dtext->text, &ltime);
+        break;
+    }
 
     if (dtext->tc_opt_string) {
         char tcbuf[AV_TIMECODE_STR_SIZE];
-- 
1.7.10.4



More information about the ffmpeg-devel mailing list