]> git.sesse.net Git - ffmpeg/blobdiff - libavfilter/vf_drawtext.c
Merge commit 'c3c96deb5f8cbbdb700ba97920ceedddacb5dcb9'
[ffmpeg] / libavfilter / vf_drawtext.c
index 0d829a6d5e8b5c25472bf74ad39637f64e7e7836..b7a295f5f761a539d747690710796213dc66853d 100644 (file)
@@ -37,6 +37,7 @@
 #if HAVE_UNISTD_H
 #include <unistd.h>
 #endif
+#include <fenv.h>
 
 #if CONFIG_LIBFONTCONFIG
 #include <fontconfig/fontconfig.h>
 #include "internal.h"
 #include "video.h"
 
+#if CONFIG_LIBFRIBIDI
+#include <fribidi.h>
+#endif
+
 #include <ft2build.h>
 #include FT_FREETYPE_H
 #include FT_GLYPH_H
@@ -138,6 +143,8 @@ typedef struct DrawTextContext {
     uint8_t *fontfile;              ///< font to be used
     uint8_t *text;                  ///< text to be drawn
     AVBPrint expanded_text;         ///< used to contain the expanded text
+    uint8_t *fontcolor_expr;        ///< fontcolor expression to evaluate
+    AVBPrint expanded_fontcolor;    ///< used to contain the expanded fontcolor spec
     int ft_load_flags;              ///< flags used for loading fonts, see FT_LOAD_*
     FT_Vector *positions;           ///< positions for each element in the text
     size_t nb_positions;            ///< number of elements of positions array
@@ -182,6 +189,9 @@ typedef struct DrawTextContext {
     int tc24hmax;                   ///< 1 if timecode is wrapped to 24 hours, 0 otherwise
     int reload;                     ///< reload text file for each frame
     int start_number;               ///< starting frame number for n/frame_num var
+#if CONFIG_LIBFRIBIDI
+    int text_shaping;               ///< 1 to shape the text before drawing it
+#endif
     AVDictionary *metadata;
 } DrawTextContext;
 
@@ -193,6 +203,7 @@ static const AVOption drawtext_options[]= {
     {"text",        "set text",             OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  CHAR_MIN, CHAR_MAX, FLAGS},
     {"textfile",    "set text file",        OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  CHAR_MIN, CHAR_MAX, FLAGS},
     {"fontcolor",   "set foreground color", OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, CHAR_MIN, CHAR_MAX, FLAGS},
+    {"fontcolor_expr", "set foreground color expression", OFFSET(fontcolor_expr), AV_OPT_TYPE_STRING, {.str=""}, CHAR_MIN, CHAR_MAX, FLAGS},
     {"boxcolor",    "set box color",        OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, CHAR_MIN, CHAR_MAX, FLAGS},
     {"bordercolor", "set border color",     OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, CHAR_MIN, CHAR_MAX, FLAGS},
     {"shadowcolor", "set shadow color",     OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, CHAR_MIN, CHAR_MAX, FLAGS},
@@ -226,6 +237,10 @@ static const AVOption drawtext_options[]= {
     {"fix_bounds", "if true, check and fix text coords to avoid clipping",  OFFSET(fix_bounds), AV_OPT_TYPE_INT, {.i64=1}, 0, 1, FLAGS},
     {"start_number", "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
 
+#if CONFIG_LIBFRIBIDI
+    {"text_shaping", "attempt to shape text before drawing", OFFSET(text_shaping), AV_OPT_TYPE_INT, {.i64=1}, 0, 1, FLAGS},
+#endif
+
     /* FT_LOAD_* flags */
     { "ft_load_flags", "set font loading flags for libfreetype", OFFSET(ft_load_flags), AV_OPT_TYPE_FLAGS, { .i64 = FT_LOAD_DEFAULT }, 0, INT_MAX, FLAGS, "ft_load_flags" },
         { "default",                     NULL, 0, AV_OPT_TYPE_CONST, { .i64 = FT_LOAD_DEFAULT },                     .flags = FLAGS, .unit = "ft_load_flags" },
@@ -482,6 +497,99 @@ static int load_textfile(AVFilterContext *ctx)
     return 0;
 }
 
+static inline int is_newline(uint32_t c)
+{
+    return c == '\n' || c == '\r' || c == '\f' || c == '\v';
+}
+
+#if CONFIG_LIBFRIBIDI
+static int shape_text(AVFilterContext *ctx)
+{
+    DrawTextContext *s = ctx->priv;
+    uint8_t *tmp;
+    int ret = AVERROR(ENOMEM);
+    static const FriBidiFlags flags = FRIBIDI_FLAGS_DEFAULT |
+                                      FRIBIDI_FLAGS_ARABIC;
+    FriBidiChar *unicodestr = NULL;
+    FriBidiStrIndex len;
+    FriBidiParType direction = FRIBIDI_PAR_LTR;
+    FriBidiStrIndex line_start = 0;
+    FriBidiStrIndex line_end = 0;
+    FriBidiLevel *embedding_levels = NULL;
+    FriBidiArabicProp *ar_props = NULL;
+    FriBidiCharType *bidi_types = NULL;
+    FriBidiStrIndex i,j;
+
+    len = strlen(s->text);
+    if (!(unicodestr = av_malloc_array(len, sizeof(*unicodestr)))) {
+        goto out;
+    }
+    len = fribidi_charset_to_unicode(FRIBIDI_CHAR_SET_UTF8,
+                                     s->text, len, unicodestr);
+
+    bidi_types = av_malloc_array(len, sizeof(*bidi_types));
+    if (!bidi_types) {
+        goto out;
+    }
+
+    fribidi_get_bidi_types(unicodestr, len, bidi_types);
+
+    embedding_levels = av_malloc_array(len, sizeof(*embedding_levels));
+    if (!embedding_levels) {
+        goto out;
+    }
+
+    if (!fribidi_get_par_embedding_levels(bidi_types, len, &direction,
+                                          embedding_levels)) {
+        goto out;
+    }
+
+    ar_props = av_malloc_array(len, sizeof(*ar_props));
+    if (!ar_props) {
+        goto out;
+    }
+
+    fribidi_get_joining_types(unicodestr, len, ar_props);
+    fribidi_join_arabic(bidi_types, len, embedding_levels, ar_props);
+    fribidi_shape(flags, embedding_levels, len, ar_props, unicodestr);
+
+    for (line_end = 0, line_start = 0; line_end < len; line_end++) {
+        if (is_newline(unicodestr[line_end]) || line_end == len - 1) {
+            if (!fribidi_reorder_line(flags, bidi_types,
+                                      line_end - line_start + 1, line_start,
+                                      direction, embedding_levels, unicodestr,
+                                      NULL)) {
+                goto out;
+            }
+            line_start = line_end + 1;
+        }
+    }
+
+    /* Remove zero-width fill chars put in by libfribidi */
+    for (i = 0, j = 0; i < len; i++)
+        if (unicodestr[i] != FRIBIDI_CHAR_FILL)
+            unicodestr[j++] = unicodestr[i];
+    len = j;
+
+    if (!(tmp = av_realloc(s->text, (len * 4 + 1) * sizeof(*s->text)))) {
+        /* Use len * 4, as a unicode character can be up to 4 bytes in UTF-8 */
+        goto out;
+    }
+
+    s->text = tmp;
+    len = fribidi_unicode_to_charset(FRIBIDI_CHAR_SET_UTF8,
+                                     unicodestr, len, s->text);
+    ret = 0;
+
+out:
+    av_free(unicodestr);
+    av_free(embedding_levels);
+    av_free(ar_props);
+    av_free(bidi_types);
+    return ret;
+}
+#endif
+
 static av_cold int init(AVFilterContext *ctx)
 {
     int err;
@@ -509,6 +617,12 @@ static av_cold int init(AVFilterContext *ctx)
             return err;
     }
 
+#if CONFIG_LIBFRIBIDI
+    if (s->text_shaping)
+        if ((err = shape_text(ctx)) < 0)
+            return err;
+#endif
+
     if (s->reload && !s->textfile)
         av_log(ctx, AV_LOG_WARNING, "No file to reload\n");
 
@@ -572,6 +686,7 @@ static av_cold int init(AVFilterContext *ctx)
         av_log(ctx, AV_LOG_WARNING, "expansion=strftime is deprecated.\n");
 
     av_bprint_init(&s->expanded_text, 0, AV_BPRINT_SIZE_UNLIMITED);
+    av_bprint_init(&s->expanded_fontcolor, 0, AV_BPRINT_SIZE_UNLIMITED);
 
     return 0;
 }
@@ -615,11 +730,7 @@ static av_cold void uninit(AVFilterContext *ctx)
     FT_Done_FreeType(s->library);
 
     av_bprint_finalize(&s->expanded_text, NULL);
-}
-
-static inline int is_newline(uint32_t c)
-{
-    return c == '\n' || c == '\r' || c == '\f' || c == '\v';
+    av_bprint_finalize(&s->expanded_fontcolor, NULL);
 }
 
 static int config_input(AVFilterLink *inlink)
@@ -803,6 +914,66 @@ static int func_eval_expr(AVFilterContext *ctx, AVBPrint *bp,
     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 ((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);
+    }
+
+    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;
@@ -811,6 +982,8 @@ static const struct drawtext_function {
 } 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, 2, 0,   func_pts      },
     { "gmtime",    0, 1, 'G', func_strftime },
@@ -887,11 +1060,8 @@ end:
     return ret;
 }
 
-static int expand_text(AVFilterContext *ctx)
+static int expand_text(AVFilterContext *ctx, char *text, AVBPrint *bp)
 {
-    DrawTextContext *s = ctx->priv;
-    char *text = s->text;
-    AVBPrint *bp = &s->expanded_text;
     int ret;
 
     av_bprint_clear(bp);
@@ -987,7 +1157,7 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame,
         av_bprintf(bp, "%s", s->text);
         break;
     case EXP_NORMAL:
-        if ((ret = expand_text(ctx)) < 0)
+        if ((ret = expand_text(ctx, s->text, &s->expanded_text)) < 0)
             return ret;
         break;
     case EXP_STRFTIME:
@@ -1013,6 +1183,20 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame,
         s->nb_positions = len;
     }
 
+    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)
+            return ret;
+        if (!av_bprint_is_complete(&s->expanded_fontcolor))
+            return AVERROR(ENOMEM);
+        av_log(s, AV_LOG_DEBUG, "Evaluated fontcolor is '%s'\n", s->expanded_fontcolor.str);
+        ret = av_parse_color(s->fontcolor.rgba, s->expanded_fontcolor.str, -1, s);
+        if (ret)
+            return ret;
+        ff_draw_color(&s->dc, &s->fontcolor, s->fontcolor.rgba);
+    }
+
     x = 0;
     y = 0;
 
@@ -1132,9 +1316,15 @@ static int filter_frame(AVFilterLink *inlink, AVFrame *frame)
     DrawTextContext *s = ctx->priv;
     int ret;
 
-    if (s->reload)
+    if (s->reload) {
         if ((ret = load_textfile(ctx)) < 0)
             return ret;
+#if CONFIG_LIBFRIBIDI
+        if (s->text_shaping)
+            if ((ret = shape_text(ctx)) < 0)
+                return ret;
+#endif
+    }
 
     s->var_values[VAR_N] = inlink->frame_count+s->start_number;
     s->var_values[VAR_T] = frame->pts == AV_NOPTS_VALUE ?