* [FFmpeg-devel] [POC][PATCHSET] Add qrencodesrc source @ 2023-11-30 0:49 Stefano Sabatini 2023-11-30 0:49 ` [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils Stefano Sabatini ` (2 more replies) 0 siblings, 3 replies; 22+ messages in thread From: Stefano Sabatini @ 2023-11-30 0:49 UTC (permalink / raw) To: FFmpeg development discussions and patches This is meant to introduce functionality to handle QR codes. This is still in early development stage (missing docs, padding functionality to avoid to insert a pad filter?), plus a filter will be added for adding a QR code on the input reading from metadata, and possibly at some point also a quirc-based decoder, but comments are welcome. QR codes are robust to lossy coding, therefore it should be possible to use them to compare a generated video with a reference one (in case they cannot be compared frame-by-frame). [PATCH 1/2] lavfi: introduce textutils [PATCH 2/2] lavfi: add qrencodesrc source _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils 2023-11-30 0:49 [FFmpeg-devel] [POC][PATCHSET] Add qrencodesrc source Stefano Sabatini @ 2023-11-30 0:49 ` Stefano Sabatini 2023-12-03 15:05 ` Stefano Sabatini 2023-12-06 20:20 ` Nicolas George 2023-11-30 0:49 ` [FFmpeg-devel] [PATCH 2/2] lavfi: add qrencodesrc source Stefano Sabatini 2023-11-30 11:07 ` [FFmpeg-devel] [POC][PATCHSET] Add " Tomas Härdin 2 siblings, 2 replies; 22+ messages in thread From: Stefano Sabatini @ 2023-11-30 0:49 UTC (permalink / raw) To: FFmpeg development discussions and patches; +Cc: Stefano Sabatini 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 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; } -- 2.34.1 _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils 2023-11-30 0:49 ` [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils Stefano Sabatini @ 2023-12-03 15:05 ` Stefano Sabatini 2023-12-09 18:12 ` Stefano Sabatini 2023-12-06 20:20 ` Nicolas George 1 sibling, 1 reply; 22+ messages in thread From: Stefano Sabatini @ 2023-12-03 15:05 UTC (permalink / raw) To: FFmpeg development discussions and patches [-- Attachment #1: Type: text/plain, Size: 700 bytes --] 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). [-- Attachment #2: 0001-lavfi-introduce-textutils.patch --] [-- Type: text/x-diff, Size: 44183 bytes --] From 09a6e80ef578f080dda4c5e57caef6feca2ba473 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/3] 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 | 379 +++++++++++++++++++++++++++ libavfilter/textutils.h | 229 ++++++++++++++++ libavfilter/vf_drawtext.c | 533 ++++++++++---------------------------- 4 files changed, 740 insertions(+), 403 deletions(-) create mode 100644 libavfilter/textutils.c create mode 100644 libavfilter/textutils.h diff --git a/libavfilter/Makefile b/libavfilter/Makefile index 63725f91b4..1f9bbcc1af 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -291,7 +291,7 @@ OBJS-$(CONFIG_DOUBLEWEAVE_FILTER) += vf_weave.o OBJS-$(CONFIG_DRAWBOX_FILTER) += vf_drawbox.o OBJS-$(CONFIG_DRAWGRAPH_FILTER) += f_drawgraph.o OBJS-$(CONFIG_DRAWGRID_FILTER) += vf_drawbox.o -OBJS-$(CONFIG_DRAWTEXT_FILTER) += vf_drawtext.o +OBJS-$(CONFIG_DRAWTEXT_FILTER) += vf_drawtext.o textutils.o OBJS-$(CONFIG_EDGEDETECT_FILTER) += vf_edgedetect.o edge_common.o OBJS-$(CONFIG_ELBG_FILTER) += vf_elbg.o OBJS-$(CONFIG_ENTROPY_FILTER) += vf_entropy.o diff --git a/libavfilter/textutils.c b/libavfilter/textutils.c new file mode 100644 index 0000000000..696363190c --- /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_formatted_eval_expr(void *log_ctx, AVBPrint *bp, + const char *expr, + const char * const *fun_names, const ff_eval_func2 *fun_values, + const char * const *var_names, const double *var_values, + void *eval_ctx, + const char format, int positions) +{ + double res; + int intval; + int ret; + char fmt_str[30] = "%"; + + ret = av_expr_parse_and_eval(&res, expr, var_names, var_values, + NULL, NULL, fun_names, fun_values, + eval_ctx, 0, log_ctx); + if (ret < 0) { + av_log(log_ctx, AV_LOG_ERROR, + "Text expansion expression '%s' is not valid\n", + expr); + return ret; + } + + if (!strchr("xXdu", format)) { + av_log(log_ctx, AV_LOG_ERROR, "Invalid format '%c' specified," + " allowed values: 'x', 'X', 'd', 'u'\n", format); + return AVERROR(EINVAL); + } + + feclearexcept(FE_ALL_EXCEPT); + intval = res; +#if defined(FE_INVALID) && defined(FE_OVERFLOW) && defined(FE_UNDERFLOW) + if ((ret = fetestexcept(FE_INVALID|FE_OVERFLOW|FE_UNDERFLOW))) { + av_log(log_ctx, AV_LOG_ERROR, "Conversion of floating-point result to int failed. Control register: 0x%08x. Conversion result: %d\n", ret, intval); + return AVERROR(EINVAL); + } +#endif + + if (positions >= 0) + av_strlcatf(fmt_str, sizeof(fmt_str), "0%u", positions); + av_strlcatf(fmt_str, sizeof(fmt_str), "%c", format); + + av_log(log_ctx, AV_LOG_DEBUG, "Formatting value %f (expr '%s') with spec '%s'\n", + res, expr, fmt_str); + + av_bprintf(bp, fmt_str, intval); + + return 0; +} + + +int ff_load_textfile(void *log_ctx, const char *textfile, + unsigned char **text, size_t *text_size) +{ + int err; + uint8_t *textbuf; + uint8_t *tmp; + size_t textbuf_size; + + if ((err = av_file_map(textfile, &textbuf, &textbuf_size, 0, log_ctx)) < 0) { + av_log(log_ctx, AV_LOG_ERROR, + "The text file '%s' could not be read or is empty\n", + textfile); + return err; + } + + if (textbuf_size > 0 && ff_is_newline(textbuf[textbuf_size - 1])) + textbuf_size--; + if (textbuf_size > SIZE_MAX - 1 || !(tmp = av_realloc(*text, textbuf_size + 1))) { + av_file_unmap(textbuf, textbuf_size); + return AVERROR(ENOMEM); + } + *text = tmp; + memcpy(*text, textbuf, textbuf_size); + (*text)[textbuf_size] = 0; + if (text_size) + *text_size = textbuf_size; + av_file_unmap(textbuf, textbuf_size); + + return 0; +} + diff --git a/libavfilter/textutils.h b/libavfilter/textutils.h new file mode 100644 index 0000000000..e0fe3ca8fe --- /dev/null +++ b/libavfilter/textutils.h @@ -0,0 +1,229 @@ +/* + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * text utilities + */ + +#ifndef AVFILTER_TEXTUTILS_H +#define AVFILTER_TEXTUTILS_H + +#include "libavutil/bprint.h" +#include "libavutil/eval.h" +#include "libavutil/log.h" +#include "libavutil/parseutils.h" + +/** + * Function used to expand a template sequence in the format + * %{FUNCTION_NAME[:PARAMS]}, defined in the TextExpander object. + */ +typedef struct 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); + +/** + * Evaluate and print expression to an AVBprint object. + * The output is written as a double representation. + * + * This is a wrapper around av_expr_parse_and_eval() and following the + * same rules. + * + * @param log_ctx pointer to av_log object + * @param bp AVBPrint object where the evaluated expression is written to + * @param expr the expression to be evaluated + * @param fun_names names of the ff_eval_func2 functions used to evaluate the expression + * @param fun_values values of the ff_eval_func2 functions used to evaluate the expression + * @param var_names names of the variables used in the expression + * @param var_values values of the variables used in the expression + * @param eval_ctx evaluation context to be passed to some functions + * + * @return negative value corresponding to an AVERROR error code in case of + * errors, a non-negative value otherwise + */ +int ff_print_eval_expr(void *log_ctx, AVBPrint *bp, + const char *expr, + const char * const *fun_names, const ff_eval_func2 *fun_values, + const char * const *var_names, const double *var_values, + void *eval_ctx); + +/** + * Evaluate and print expression to an AVBprint object, using the + * specified format. + * + * This is a wrapper around av_expr_parse_and_eval() and following the + * same rules. + * + * The format is specified as a printf format character, optionally + * preceded by the positions numbers for zero-padding. + * + * The following formats are accepted: + * - x: use lowercase hexadecimal representation + * - X: use uppercase hexadecimal representation + * - d: use decimal representation + * - u: use unsigned decimal representation + * + * @param log_ctx pointer to av_log object + * @param bp AVBPrint object where the evaluated expression is written to + * @param expr the expression to be evaluated + * @param fun_names names of the ff_eval_func2 functions used to evaluate the expression + * @param fun_values values of the ff_eval_func2 functions used to evaluate the expression + * @param var_names names of the variables used in the expression + * @param var_values values of the variables used in the expression + * @param eval_ctx evaluation context to be passed to some functions + * @param format a character representing the format, to be chosen in xXdu + * @param positions final size of the value representation with 0-padding + * @return negative value corresponding to an AVERROR error code in case of + * errors, a non-negative value otherwise + */ +int ff_print_formatted_eval_expr(void *log_ctx, AVBPrint *bp, + const char *expr, + const char * const *fun_names, const ff_eval_func2 *fun_values, + const char * const *var_names, const double *var_values, + void *eval_ctx, + const char format, int positions); + +/** + * Check if the character is a newline. + * + * @param c character to check + * @return non-negative value in case c is a newline, 0 otherwise + */ +static inline int ff_is_newline(uint32_t c) +{ + return c == '\n' || c == '\r' || c == '\f' || c == '\v'; +} + +/** + * Load text file into the buffer pointed by text. + * + * @param log_ctx pointer to av_log object + * @param textfile filename containing the text to load + * @param text pointer to the text buffer where the loaded text will be + * loaded + * @param text_size pointer to the value to set with the loaded text data, + * including the terminating 0 character + * @return negative value corresponding to an AVERROR error code in case of + * errors, a non-negative value otherwise + */ +int ff_load_textfile(void *log_ctx, const char *textfile, + unsigned char **text, size_t *text_size); + +#endif /* AVFILTER_TEXTUTILS__H */ diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c index c5477cbff1..f4635eedad 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_formatted_eval_expr(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; } -- 2.34.1 [-- Attachment #3: Type: text/plain, Size: 251 bytes --] _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils 2023-12-03 15:05 ` Stefano Sabatini @ 2023-12-09 18:12 ` Stefano Sabatini 2023-12-16 15:35 ` Stefano Sabatini 0 siblings, 1 reply; 22+ messages in thread From: Stefano Sabatini @ 2023-12-09 18:12 UTC (permalink / raw) To: FFmpeg development discussions and patches [-- Attachment #1: Type: text/plain, Size: 804 bytes --] 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. [-- Attachment #2: 0001-lavfi-introduce-textutils.patch --] [-- Type: text/x-diff, Size: 44281 bytes --] 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 diff --git a/libavfilter/Makefile b/libavfilter/Makefile index 63725f91b4..1f9bbcc1af 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -291,7 +291,7 @@ OBJS-$(CONFIG_DOUBLEWEAVE_FILTER) += vf_weave.o OBJS-$(CONFIG_DRAWBOX_FILTER) += vf_drawbox.o OBJS-$(CONFIG_DRAWGRAPH_FILTER) += f_drawgraph.o OBJS-$(CONFIG_DRAWGRID_FILTER) += vf_drawbox.o -OBJS-$(CONFIG_DRAWTEXT_FILTER) += vf_drawtext.o +OBJS-$(CONFIG_DRAWTEXT_FILTER) += vf_drawtext.o textutils.o OBJS-$(CONFIG_EDGEDETECT_FILTER) += vf_edgedetect.o edge_common.o OBJS-$(CONFIG_ELBG_FILTER) += vf_elbg.o OBJS-$(CONFIG_ENTROPY_FILTER) += vf_entropy.o diff --git a/libavfilter/textutils.c b/libavfilter/textutils.c new file mode 100644 index 0000000000..ef658d04a2 --- /dev/null +++ b/libavfilter/textutils.c @@ -0,0 +1,382 @@ +/* + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * text expansion utilities + */ + +#include <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(FFExpandTextContext *expand_text, AVBPrint *bp, + char *name, unsigned argc, char **argv) +{ + void *log_ctx = expand_text->log_ctx; + FFExpandTextFunction *functions = expand_text->functions; + unsigned i; + + for (i = 0; i < expand_text->functions_nb; i++) { + if (strcmp(name, functions[i].name)) + continue; + if (argc < functions[i].argc_min) { + av_log(log_ctx, AV_LOG_ERROR, "%%{%s} requires at least %d arguments\n", + name, functions[i].argc_min); + return AVERROR(EINVAL); + } + if (argc > functions[i].argc_max) { + av_log(log_ctx, AV_LOG_ERROR, "%%{%s} requires at most %d arguments\n", + name, functions[i].argc_max); + return AVERROR(EINVAL); + } + break; + } + if (i >= expand_text->functions_nb) { + av_log(log_ctx, AV_LOG_ERROR, "%%{%s} is not known\n", name); + return AVERROR(EINVAL); + } + + return functions[i].func(log_ctx, bp, name, argc, argv); +} + +/** + * Expand text template pointed to by *rtext. + * + * Expand text template defined in text using the logic defined in a text + * expander object. + * + * This function expects the text to be in the format %{FUNCTION_NAME[:PARAMS]}, + * where PARAMS is a sequence of strings separated by : and represents the function + * arguments to use for the function evaluation. + * + * @param text_expander TextExpander object used to expand the text + * @param bp BPrint object where the expanded text is written to + * @param rtext pointer to pointer to the text to expand, it is updated to point + * to the next part of the template to process + * @return negative value corresponding to an AVERROR error code in case of + * errors, a non-negative value otherwise + */ +static int ff_expand_text_function(FFExpandTextContext *expand_text, AVBPrint *bp, char **rtext) +{ + void *log_ctx = expand_text->log_ctx; + const char *text = *rtext; + char *argv[16] = { NULL }; + unsigned argc = 0, i; + int ret; + + if (*text != '{') { + av_log(log_ctx, AV_LOG_ERROR, "Stray %% near '%s'\n", text); + return AVERROR(EINVAL); + } + text++; + while (1) { + if (!(argv[argc++] = av_get_token(&text, ":}"))) { + ret = AVERROR(ENOMEM); + goto end; + } + if (!*text) { + av_log(log_ctx, AV_LOG_ERROR, "Unterminated %%{} near '%s'\n", *rtext); + ret = AVERROR(EINVAL); + goto end; + } + if (argc == FF_ARRAY_ELEMS(argv)) + av_freep(&argv[--argc]); /* error will be caught later */ + if (*text == '}') + break; + text++; + } + + if ((ret = ff_expand_text_function_internal(expand_text, bp, argv[0], argc - 1, argv + 1)) < 0) + goto end; + ret = 0; + *rtext = (char *)text + 1; + +end: + for (i = 0; i < argc; i++) + av_freep(&argv[i]); + return ret; +} + +int ff_expand_text(FFExpandTextContext *expand_text, char *text, AVBPrint *bp) +{ + int ret; + + av_bprint_clear(bp); + if (!text) + return 0; + + while (*text) { + if (*text == '\\' && text[1]) { + av_bprint_chars(bp, text[1], 1); + text += 2; + } else if (*text == '%') { + text++; + if ((ret = ff_expand_text_function(expand_text, bp, &text)) < 0) + return ret; + } else { + av_bprint_chars(bp, *text, 1); + text++; + } + } + if (!av_bprint_is_complete(bp)) + return AVERROR(ENOMEM); + return 0; +} + +int ff_print_pts(void *log_ctx, AVBPrint *bp, double pts, const char *delta, + const char *fmt, const char *strftime_fmt) +{ + int ret; + + if (delta) { + int64_t delta_i; + if ((ret = av_parse_time(&delta_i, delta, 1)) < 0) { + av_log(log_ctx, AV_LOG_ERROR, "Invalid delta '%s'\n", delta); + return ret; + } + pts += (double)delta_i / AV_TIME_BASE; + } + + if (!strcmp(fmt, "flt")) { + av_bprintf(bp, "%.6f", pts); + } else if (!strcmp(fmt, "hms") || + !strcmp(fmt, "hms24hh")) { + if (isnan(pts)) { + av_bprintf(bp, " ??:??:??.???"); + } else { + int64_t ms = llrint(pts * 1000); + char sign = ' '; + if (ms < 0) { + sign = '-'; + ms = -ms; + } + if (!strcmp(fmt, "hms24hh")) { + /* wrap around 24 hours */ + ms %= 24 * 60 * 60 * 1000; + } + av_bprintf(bp, "%c%02d:%02d:%02d.%03d", sign, + (int)(ms / (60 * 60 * 1000)), + (int)(ms / (60 * 1000)) % 60, + (int)(ms / 1000) % 60, + (int)(ms % 1000)); + } + } else if (!strcmp(fmt, "localtime") || + !strcmp(fmt, "gmtime")) { + struct tm tm; + time_t ms = (time_t)pts; + if (!strcmp(fmt, "localtime")) + localtime_r(&ms, &tm); + else + gmtime_r(&ms, &tm); + av_bprint_strftime(bp, av_x_if_null(strftime_fmt, "%Y-%m-%d %H:%M:%S"), &tm); + } else { + av_log(log_ctx, AV_LOG_ERROR, "Invalid format '%s'\n", fmt); + return AVERROR(EINVAL); + } + return 0; +} + +int ff_print_time(void *log_ctx, AVBPrint *bp, + const char *strftime_fmt, char localtime) +{ + const char *fmt = av_x_if_null(strftime_fmt, "%Y-%m-%d %H:%M:%S"); + const char *fmt_begin = fmt; + int64_t unow; + time_t now; + struct tm tm; + const char *begin; + const char *tmp; + int len; + int div; + AVBPrint fmt_bp; + + av_bprint_init(&fmt_bp, 0, AV_BPRINT_SIZE_UNLIMITED); + + unow = av_gettime(); + now = unow / 1000000; + if (localtime) + localtime_r(&now, &tm); + else + tm = *gmtime_r(&now, &tm); + + // manually parse format for %N (fractional seconds) + begin = fmt; + while ((begin = strchr(begin, '%'))) { + tmp = begin + 1; + len = 0; + + // skip escaped "%%" + if (*tmp == '%') { + begin = tmp + 1; + continue; + } + + // count digits between % and possible N + while (*tmp != '\0' && av_isdigit((int)*tmp)) { + len++; + tmp++; + } + + // N encountered, insert time + if (*tmp == 'N') { + int num_digits = 3; // default show millisecond [1,6] + + // if digit given, expect [1,6], warn & clamp otherwise + if (len == 1) { + num_digits = av_clip(*(begin + 1) - '0', 1, 6); + } else if (len > 1) { + av_log(log_ctx, AV_LOG_WARNING, "Invalid number of decimals for %%N, using default of %i\n", num_digits); + } + + len += 2; // add % and N to get length of string part + + div = pow(10, 6 - num_digits); + + av_bprintf(&fmt_bp, "%.*s%0*d", (int)(begin - fmt_begin), fmt_begin, num_digits, (int)(unow % 1000000) / div); + + begin += len; + fmt_begin = begin; + + continue; + } + + begin = tmp; + } + + av_bprintf(&fmt_bp, "%s", fmt_begin); + if (!av_bprint_is_complete(&fmt_bp)) { + av_log(log_ctx, AV_LOG_WARNING, "Format string truncated at %u/%u.", fmt_bp.size, fmt_bp.len); + } + + av_bprint_strftime(bp, fmt_bp.str, &tm); + + av_bprint_finalize(&fmt_bp, NULL); + + return 0; +} + +int ff_print_eval_expr(void *log_ctx, AVBPrint *bp, + const char *expr, + const char * const *fun_names, const ff_eval_func2 *fun_values, + const char * const *var_names, const double *var_values, + void *eval_ctx) +{ + double res; + int ret; + + ret = av_expr_parse_and_eval(&res, expr, var_names, var_values, + NULL, NULL, fun_names, fun_values, + eval_ctx, 0, log_ctx); + if (ret < 0) + av_log(log_ctx, AV_LOG_ERROR, + "Text expansion expression '%s' is not valid\n", + expr); + else + av_bprintf(bp, "%f", res); + + return ret; +} + +int ff_print_formatted_eval_expr(void *log_ctx, AVBPrint *bp, + const char *expr, + const char * const *fun_names, const ff_eval_func2 *fun_values, + const char * const *var_names, const double *var_values, + void *eval_ctx, + const char format, int positions) +{ + double res; + int intval; + int ret; + char fmt_str[30] = "%"; + + ret = av_expr_parse_and_eval(&res, expr, var_names, var_values, + NULL, NULL, fun_names, fun_values, + eval_ctx, 0, log_ctx); + if (ret < 0) { + av_log(log_ctx, AV_LOG_ERROR, + "Text expansion expression '%s' is not valid\n", + expr); + return ret; + } + + if (!strchr("xXdu", format)) { + av_log(log_ctx, AV_LOG_ERROR, "Invalid format '%c' specified," + " allowed values: 'x', 'X', 'd', 'u'\n", format); + return AVERROR(EINVAL); + } + + feclearexcept(FE_ALL_EXCEPT); + intval = res; +#if defined(FE_INVALID) && defined(FE_OVERFLOW) && defined(FE_UNDERFLOW) + if ((ret = fetestexcept(FE_INVALID|FE_OVERFLOW|FE_UNDERFLOW))) { + av_log(log_ctx, AV_LOG_ERROR, "Conversion of floating-point result to int failed. Control register: 0x%08x. Conversion result: %d\n", ret, intval); + return AVERROR(EINVAL); + } +#endif + + if (positions >= 0) + av_strlcatf(fmt_str, sizeof(fmt_str), "0%u", positions); + av_strlcatf(fmt_str, sizeof(fmt_str), "%c", format); + + av_log(log_ctx, AV_LOG_DEBUG, "Formatting value %f (expr '%s') with spec '%s'\n", + res, expr, fmt_str); + + av_bprintf(bp, fmt_str, intval); + + return 0; +} + + +int ff_load_textfile(void *log_ctx, const char *textfile, + unsigned char **text, size_t *text_size) +{ + int err; + uint8_t *textbuf; + uint8_t *tmp; + size_t textbuf_size; + + if ((err = av_file_map(textfile, &textbuf, &textbuf_size, 0, log_ctx)) < 0) { + av_log(log_ctx, AV_LOG_ERROR, + "The text file '%s' could not be read or is empty\n", + textfile); + return err; + } + + if (textbuf_size > 0 && ff_is_newline(textbuf[textbuf_size - 1])) + textbuf_size--; + if (textbuf_size > SIZE_MAX - 1 || !(tmp = av_realloc(*text, textbuf_size + 1))) { + av_file_unmap(textbuf, textbuf_size); + return AVERROR(ENOMEM); + } + *text = tmp; + memcpy(*text, textbuf, textbuf_size); + (*text)[textbuf_size] = 0; + if (text_size) + *text_size = textbuf_size; + av_file_unmap(textbuf, textbuf_size); + + return 0; +} + diff --git a/libavfilter/textutils.h b/libavfilter/textutils.h new file mode 100644 index 0000000000..7fa856c681 --- /dev/null +++ b/libavfilter/textutils.h @@ -0,0 +1,229 @@ +/* + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * text utilities + */ + +#ifndef AVFILTER_TEXTUTILS_H +#define AVFILTER_TEXTUTILS_H + +#include "libavutil/bprint.h" +#include "libavutil/eval.h" +#include "libavutil/log.h" +#include "libavutil/parseutils.h" + +/** + * Function used to expand a template sequence in the format + * %{FUNCTION_NAME[:PARAMS]}, defined in the TextExpander object. + */ +typedef struct FFExpandTextFunction { + /** + * name of the function + */ + const char *name; + + /** + * minimum and maximum number of arguments accepted by the + * function in the PARAMS + */ + unsigned argc_min, argc_max; + + /** + * actual function used to perform the expansion + */ + int (*func)(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **args); +} FFExpandTextFunction; + +/** + * Text expander context, used to encapsulate the logic to expand a + * given text template. + * + * A backslash character @samp{\} in a text template, followed by any + * character, always expands to the second character. + * Sequences of the form %{FUNCTION_NAME[:PARAMS]} are expanded using a + * function defined in the object. The text between the braces is a + * function name, possibly followed by arguments separated by ':'. If + * the arguments contain special characters or delimiters (':' or + * '}'), they should be escaped. + */ +typedef struct FFExpandTextContext { + /** + * log context to pass to the function, used for logging and for + * accessing the context for the function + */ + void *log_ctx; + + /** + * list of functions to use to expand sequences in the format + * FUNCTION_NAME{PARAMS} + */ + FFExpandTextFunction *functions; + + /** + * number of functions + */ + unsigned int functions_nb; +} FFExpandTextContext; + +/** + * Expand text template. + * + * Expand text template defined in text using the logic defined in a text + * expander object. + * + * @param expand_text text expansion context used to expand the text + * @param text template text to expand + * @param bp BPrint object where the expanded text is written to + * @return negative value corresponding to an AVERROR error code in case of + * errors, a non-negative value otherwise + */ +int ff_expand_text(FFExpandTextContext *expand_text, char *text, AVBPrint *bp); + +/** + * Print PTS representation to an AVBPrint object. + * + * @param log_ctx pointer to av_log object + * @param bp AVBPrint object where the PTS textual representation is written to + * @param pts PTS value expressed as a double to represent + * @param delta delta time parsed by av_parse_time(), added to the PTS + * @param fmt string representing the format to use for printing, can be + * "flt" - use a float representation with 6 decimal digits, + * "hms" - use HH:MM:SS.MMM format, + * "hms24hh" - same as "hms" but wraps the hours in 24hh format + * (so that it is expressed in the range 00-23), + * "localtime" or "gmtime" - expand the PTS according to the + * @code{strftime()} function rules, using either the corresponding + * @code{localtime()} or @code{gmtime()} time + * @param strftime_fmt: @code{strftime()} format to use to represent the PTS in + * case the format "localtime" or "gmtime" was selected, if not specified + * defaults to "%Y-%m-%d %H:%M:%S" + * @return negative value corresponding to an AVERROR error code in case of + * errors, a non-negative value otherwise + */ +int ff_print_pts(void *log_ctx, AVBPrint *bp, double pts, const char *delta, + const char *fmt, const char *strftime_fmt); + +/** + * Print time representation to an AVBPrint object. + * + * @param log_ctx pointer to av_log object + * @param bp AVBPrint object where the time textual representation is written to + * @param strftime_fmt: strftime() format to use to represent the time in case + * if not specified defaults to "%Y-%m-%d %H:%M:%S". The format string is + * extended to support the %[1-6]N after %S which prints fractions of the + * second with optionally specified number of digits, if not specified + * defaults to 3. + * @param localtime use local time to compute the time if non-zero, otherwise + * use UTC + * @return negative value corresponding to an AVERROR error code in case of + * errors, a non-negative value otherwise + */ +int ff_print_time(void *log_ctx, AVBPrint *bp, const char *strftime_fmt, char localtime); + +typedef double (*ff_eval_func2)(void *, double a, double b); + +/** + * Evaluate and print expression to an AVBprint object. + * The output is written as a double representation. + * + * This is a wrapper around av_expr_parse_and_eval() and following the + * same rules. + * + * @param log_ctx pointer to av_log object + * @param bp AVBPrint object where the evaluated expression is written to + * @param expr the expression to be evaluated + * @param fun_names names of the ff_eval_func2 functions used to evaluate the expression + * @param fun_values values of the ff_eval_func2 functions used to evaluate the expression + * @param var_names names of the variables used in the expression + * @param var_values values of the variables used in the expression + * @param eval_ctx evaluation context to be passed to some functions + * + * @return negative value corresponding to an AVERROR error code in case of + * errors, a non-negative value otherwise + */ +int ff_print_eval_expr(void *log_ctx, AVBPrint *bp, + const char *expr, + const char * const *fun_names, const ff_eval_func2 *fun_values, + const char * const *var_names, const double *var_values, + void *eval_ctx); + +/** + * Evaluate and print expression to an AVBprint object, using the + * specified format. + * + * This is a wrapper around av_expr_parse_and_eval() and following the + * same rules. + * + * The format is specified as a printf format character, optionally + * preceded by the positions numbers for zero-padding. + * + * The following formats are accepted: + * - x: use lowercase hexadecimal representation + * - X: use uppercase hexadecimal representation + * - d: use decimal representation + * - u: use unsigned decimal representation + * + * @param log_ctx pointer to av_log object + * @param bp AVBPrint object where the evaluated expression is written to + * @param expr the expression to be evaluated + * @param fun_names names of the ff_eval_func2 functions used to evaluate the expression + * @param fun_values values of the ff_eval_func2 functions used to evaluate the expression + * @param var_names names of the variables used in the expression + * @param var_values values of the variables used in the expression + * @param eval_ctx evaluation context to be passed to some functions + * @param format a character representing the format, to be chosen in xXdu + * @param positions final size of the value representation with 0-padding + * @return negative value corresponding to an AVERROR error code in case of + * errors, a non-negative value otherwise + */ +int ff_print_formatted_eval_expr(void *log_ctx, AVBPrint *bp, + const char *expr, + const char * const *fun_names, const ff_eval_func2 *fun_values, + const char * const *var_names, const double *var_values, + void *eval_ctx, + const char format, int positions); + +/** + * Check if the character is a newline. + * + * @param c character to check + * @return non-negative value in case c is a newline, 0 otherwise + */ +static inline int ff_is_newline(uint32_t c) +{ + return c == '\n' || c == '\r' || c == '\f' || c == '\v'; +} + +/** + * Load text file into the buffer pointed by text. + * + * @param log_ctx pointer to av_log object + * @param textfile filename containing the text to load + * @param text pointer to the text buffer where the loaded text will be + * loaded + * @param text_size pointer to the value to set with the loaded text data, + * including the terminating 0 character + * @return negative value corresponding to an AVERROR error code in case of + * errors, a non-negative value otherwise + */ +int ff_load_textfile(void *log_ctx, const char *textfile, + unsigned char **text, size_t *text_size); + +#endif /* AVFILTER_TEXTUTILS__H */ diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c index c5477cbff1..c1ea5b90b3 100644 --- a/libavfilter/vf_drawtext.c +++ b/libavfilter/vf_drawtext.c @@ -47,7 +47,6 @@ #include "libavutil/avstring.h" #include "libavutil/bprint.h" #include "libavutil/common.h" -#include "libavutil/file.h" #include "libavutil/eval.h" #include "libavutil/opt.h" #include "libavutil/random_seed.h" @@ -62,6 +61,7 @@ #include "drawutils.h" #include "formats.h" #include "internal.h" +#include "textutils.h" #include "video.h" #if CONFIG_LIBFRIBIDI @@ -253,6 +253,7 @@ typedef struct TextMetrics { typedef struct DrawTextContext { const AVClass *class; int exp_mode; ///< expansion mode to use for the text + FFExpandTextContext expand_text; ///< expand text in case exp_mode == NORMAL int reinit; ///< tells if the filter is being reinited #if CONFIG_LIBFONTCONFIG uint8_t *font; ///< font to be used @@ -631,40 +632,6 @@ static int load_font(AVFilterContext *ctx) return err; } -static inline int is_newline(uint32_t c) -{ - return c == '\n' || c == '\r' || c == '\f' || c == '\v'; -} - -static int load_textfile(AVFilterContext *ctx) -{ - DrawTextContext *s = ctx->priv; - int err; - uint8_t *textbuf; - uint8_t *tmp; - size_t textbuf_size; - - if ((err = av_file_map(s->textfile, &textbuf, &textbuf_size, 0, ctx)) < 0) { - av_log(ctx, AV_LOG_ERROR, - "The text file '%s' could not be read or is empty\n", - s->textfile); - return err; - } - - if (textbuf_size > 0 && is_newline(textbuf[textbuf_size - 1])) - textbuf_size--; - if (textbuf_size > SIZE_MAX - 1 || !(tmp = av_realloc(s->text, textbuf_size + 1))) { - av_file_unmap(textbuf, textbuf_size); - return AVERROR(ENOMEM); - } - s->text = tmp; - memcpy(s->text, textbuf, textbuf_size); - s->text[textbuf_size] = 0; - av_file_unmap(textbuf, textbuf_size); - - return 0; -} - #if CONFIG_LIBFRIBIDI static int shape_text(AVFilterContext *ctx) { @@ -885,6 +852,123 @@ static int string_to_array(const char *source, int *result, int result_size) return counter; } +static int func_pict_type(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv) +{ + DrawTextContext *s = ((AVFilterContext *)ctx)->priv; + + av_bprintf(bp, "%c", av_get_picture_type_char(s->var_values[VAR_PICT_TYPE])); + return 0; +} + +static int func_pts(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv) +{ + DrawTextContext *s = ((AVFilterContext *)ctx)->priv; + const char *fmt; + const char *strftime_fmt = NULL; + const char *delta = NULL; + double pts = s->var_values[VAR_T]; + + // argv: pts, FMT, [DELTA, 24HH | strftime_fmt] + + fmt = argc >= 1 ? argv[0] : "flt"; + if (argc >= 2) { + delta = argv[1]; + } + if (argc >= 3) { + if (!strcmp(fmt, "hms")) { + if (!strcmp(argv[2], "24HH")) { + av_log(ctx, AV_LOG_WARNING, "pts third argument 24HH is deprected, use pts:hms24hh instead\n"); + fmt = "hms24"; + } else { + av_log(ctx, AV_LOG_ERROR, "Invalid argument '%s', '24HH' was expected\n", argv[2]); + return AVERROR(EINVAL); + } + } else { + strftime_fmt = argv[2]; + } + } + + return ff_print_pts(ctx, bp, pts, delta, fmt, strftime_fmt); +} + +static int func_frame_num(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv) +{ + DrawTextContext *s = ((AVFilterContext *)ctx)->priv; + + av_bprintf(bp, "%d", (int)s->var_values[VAR_N]); + return 0; +} + +static int func_metadata(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv) +{ + DrawTextContext *s = ((AVFilterContext *)ctx)->priv; + AVDictionaryEntry *e = av_dict_get(s->metadata, argv[0], NULL, 0); + + if (e && e->value) + av_bprintf(bp, "%s", e->value); + else if (argc >= 2) + av_bprintf(bp, "%s", argv[1]); + return 0; +} + +static int func_strftime(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv) +{ + const char *strftime_fmt = argc ? argv[0] : NULL; + + return ff_print_time(ctx, bp, strftime_fmt, !strcmp(function_name, "localtime")); +} + +static int func_eval_expr(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv) +{ + DrawTextContext *s = ((AVFilterContext *)ctx)->priv; + + return ff_print_eval_expr(ctx, bp, argv[0], + fun2_names, fun2, + var_names, s->var_values, &s->prng); +} + +static int func_eval_expr_int_format(void *ctx, AVBPrint *bp, const char *function_name, unsigned argc, char **argv) +{ + DrawTextContext *s = ((AVFilterContext *)ctx)->priv; + int ret; + int positions = -1; + + /* + * argv[0] expression to be converted to `int` + * argv[1] format: 'x', 'X', 'd' or 'u' + * argv[2] positions printed (optional) + */ + + if (argc == 3) { + ret = sscanf(argv[2], "%u", &positions); + if (ret != 1) { + av_log(ctx, AV_LOG_ERROR, "expr_int_format(): Invalid number of positions" + " to print: '%s'\n", argv[2]); + return AVERROR(EINVAL); + } + } + + return ff_print_formatted_eval_expr(ctx, bp, argv[0], + fun2_names, fun2, + var_names, s->var_values, + &s->prng, + argv[1][0], positions); +} + +static FFExpandTextFunction expand_text_functions[] = { + { "e", 1, 1, func_eval_expr }, + { "eif", 2, 3, func_eval_expr_int_format }, + { "expr", 1, 1, func_eval_expr }, + { "expr_int_format", 2, 3, func_eval_expr_int_format }, + { "frame_num", 0, 0, func_frame_num }, + { "gmtime", 0, 1, func_strftime }, + { "localtime", 0, 1, func_strftime }, + { "metadata", 1, 2, func_metadata }, + { "n", 0, 0, func_frame_num }, + { "pict_type", 0, 0, func_pict_type }, + { "pts", 0, 3, func_pts } +}; + static av_cold int init(AVFilterContext *ctx) { int err; @@ -907,7 +991,7 @@ static av_cold int init(AVFilterContext *ctx) "Both text and text file provided. Please provide only one\n"); return AVERROR(EINVAL); } - if ((err = load_textfile(ctx)) < 0) + if ((err = ff_load_textfile(ctx, (const char *)s->textfile, &s->text, NULL)) < 0) return err; } @@ -950,6 +1034,12 @@ static av_cold int init(AVFilterContext *ctx) return AVERROR(EINVAL); } + s->expand_text = (FFExpandTextContext) { + .log_ctx = ctx, + .functions = expand_text_functions, + .functions_nb = FF_ARRAY_ELEMS(expand_text_functions) + }; + #if CONFIG_LIBFRIBIDI if (s->text_shaping) if ((err = shape_text(ctx)) < 0) @@ -1160,367 +1250,6 @@ fail: return ret; } -static int func_pict_type(AVFilterContext *ctx, AVBPrint *bp, - char *fct, unsigned argc, char **argv, int tag) -{ - DrawTextContext *s = ctx->priv; - - av_bprintf(bp, "%c", av_get_picture_type_char(s->var_values[VAR_PICT_TYPE])); - return 0; -} - -static int func_pts(AVFilterContext *ctx, AVBPrint *bp, - char *fct, unsigned argc, char **argv, int tag) -{ - DrawTextContext *s = ctx->priv; - const char *fmt; - double pts = s->var_values[VAR_T]; - int ret; - - fmt = argc >= 1 ? argv[0] : "flt"; - if (argc >= 2) { - int64_t delta; - if ((ret = av_parse_time(&delta, argv[1], 1)) < 0) { - av_log(ctx, AV_LOG_ERROR, "Invalid delta '%s'\n", argv[1]); - return ret; - } - pts += (double)delta / AV_TIME_BASE; - } - if (!strcmp(fmt, "flt")) { - av_bprintf(bp, "%.6f", pts); - } else if (!strcmp(fmt, "hms")) { - if (isnan(pts)) { - av_bprintf(bp, " ??:??:??.???"); - } else { - int64_t ms = llrint(pts * 1000); - char sign = ' '; - if (ms < 0) { - sign = '-'; - ms = -ms; - } - if (argc >= 3) { - if (!strcmp(argv[2], "24HH")) { - ms %= 24 * 60 * 60 * 1000; - } else { - av_log(ctx, AV_LOG_ERROR, "Invalid argument '%s'\n", argv[2]); - return AVERROR(EINVAL); - } - } - av_bprintf(bp, "%c%02d:%02d:%02d.%03d", sign, - (int)(ms / (60 * 60 * 1000)), - (int)(ms / (60 * 1000)) % 60, - (int)(ms / 1000) % 60, - (int)(ms % 1000)); - } - } else if (!strcmp(fmt, "localtime") || - !strcmp(fmt, "gmtime")) { - struct tm tm; - time_t ms = (time_t)pts; - const char *timefmt = argc >= 3 ? argv[2] : "%Y-%m-%d %H:%M:%S"; - if (!strcmp(fmt, "localtime")) - localtime_r(&ms, &tm); - else - gmtime_r(&ms, &tm); - av_bprint_strftime(bp, timefmt, &tm); - } else { - av_log(ctx, AV_LOG_ERROR, "Invalid format '%s'\n", fmt); - return AVERROR(EINVAL); - } - return 0; -} - -static int func_frame_num(AVFilterContext *ctx, AVBPrint *bp, - char *fct, unsigned argc, char **argv, int tag) -{ - DrawTextContext *s = ctx->priv; - - av_bprintf(bp, "%d", (int)s->var_values[VAR_N]); - return 0; -} - -static int func_metadata(AVFilterContext *ctx, AVBPrint *bp, - char *fct, unsigned argc, char **argv, int tag) -{ - DrawTextContext *s = ctx->priv; - AVDictionaryEntry *e = av_dict_get(s->metadata, argv[0], NULL, 0); - - if (e && e->value) - av_bprintf(bp, "%s", e->value); - else if (argc >= 2) - av_bprintf(bp, "%s", argv[1]); - return 0; -} - -static int func_strftime(AVFilterContext *ctx, AVBPrint *bp, - char *fct, unsigned argc, char **argv, int tag) -{ - const char *fmt = argc ? argv[0] : "%Y-%m-%d %H:%M:%S"; - const char *fmt_begin = fmt; - int64_t unow; - time_t now; - struct tm tm; - const char *begin; - const char *tmp; - int len; - int div; - AVBPrint fmt_bp; - - av_bprint_init(&fmt_bp, 0, AV_BPRINT_SIZE_UNLIMITED); - - unow = av_gettime(); - now = unow / 1000000; - if (tag == 'L' || tag == 'm') - localtime_r(&now, &tm); - else - tm = *gmtime_r(&now, &tm); - - // manually parse format for %N (fractional seconds) - begin = fmt; - while ((begin = strchr(begin, '%'))) { - tmp = begin + 1; - len = 0; - - // skip escaped "%%" - if (*tmp == '%') { - begin = tmp + 1; - continue; - } - - // count digits between % and possible N - while (*tmp != '\0' && av_isdigit((int)*tmp)) { - len++; - tmp++; - } - - // N encountered, insert time - if (*tmp == 'N') { - int num_digits = 3; // default show millisecond [1,6] - - // if digit given, expect [1,6], warn & clamp otherwise - if (len == 1) { - num_digits = av_clip(*(begin + 1) - '0', 1, 6); - } else if (len > 1) { - av_log(ctx, AV_LOG_WARNING, "Invalid number of decimals for %%N, using default of %i\n", num_digits); - } - - len += 2; // add % and N to get length of string part - - div = pow(10, 6 - num_digits); - - av_bprintf(&fmt_bp, "%.*s%0*d", (int)(begin - fmt_begin), fmt_begin, num_digits, (int)(unow % 1000000) / div); - - begin += len; - fmt_begin = begin; - - continue; - } - - begin = tmp; - } - - av_bprintf(&fmt_bp, "%s", fmt_begin); - if (!av_bprint_is_complete(&fmt_bp)) { - av_log(ctx, AV_LOG_WARNING, "Format string truncated at %u/%u.", fmt_bp.size, fmt_bp.len); - } - - av_bprint_strftime(bp, fmt_bp.str, &tm); - - av_bprint_finalize(&fmt_bp, NULL); - - return 0; -} - -static int func_eval_expr(AVFilterContext *ctx, AVBPrint *bp, - char *fct, unsigned argc, char **argv, int tag) -{ - DrawTextContext *s = ctx->priv; - double res; - int ret; - - ret = av_expr_parse_and_eval(&res, argv[0], var_names, s->var_values, - NULL, NULL, fun2_names, fun2, - &s->prng, 0, ctx); - if (ret < 0) - av_log(ctx, AV_LOG_ERROR, - "Expression '%s' for the expr text expansion function is not valid\n", - argv[0]); - else - av_bprintf(bp, "%f", res); - - return ret; -} - -static int func_eval_expr_int_format(AVFilterContext *ctx, AVBPrint *bp, - char *fct, unsigned argc, char **argv, int tag) -{ - DrawTextContext *s = ctx->priv; - double res; - int intval; - int ret; - unsigned int positions = 0; - char fmt_str[30] = "%"; - - /* - * argv[0] expression to be converted to `int` - * argv[1] format: 'x', 'X', 'd' or 'u' - * argv[2] positions printed (optional) - */ - - ret = av_expr_parse_and_eval(&res, argv[0], var_names, s->var_values, - NULL, NULL, fun2_names, fun2, - &s->prng, 0, ctx); - if (ret < 0) { - av_log(ctx, AV_LOG_ERROR, - "Expression '%s' for the expr text expansion function is not valid\n", - argv[0]); - return ret; - } - - if (!strchr("xXdu", argv[1][0])) { - av_log(ctx, AV_LOG_ERROR, "Invalid format '%c' specified," - " allowed values: 'x', 'X', 'd', 'u'\n", argv[1][0]); - return AVERROR(EINVAL); - } - - if (argc == 3) { - ret = sscanf(argv[2], "%u", &positions); - if (ret != 1) { - av_log(ctx, AV_LOG_ERROR, "expr_int_format(): Invalid number of positions" - " to print: '%s'\n", argv[2]); - return AVERROR(EINVAL); - } - } - - feclearexcept(FE_ALL_EXCEPT); - intval = res; -#if defined(FE_INVALID) && defined(FE_OVERFLOW) && defined(FE_UNDERFLOW) - if ((ret = fetestexcept(FE_INVALID|FE_OVERFLOW|FE_UNDERFLOW))) { - av_log(ctx, AV_LOG_ERROR, "Conversion of floating-point result to int failed. Control register: 0x%08x. Conversion result: %d\n", ret, intval); - return AVERROR(EINVAL); - } -#endif - - if (argc == 3) - av_strlcatf(fmt_str, sizeof(fmt_str), "0%u", positions); - av_strlcatf(fmt_str, sizeof(fmt_str), "%c", argv[1][0]); - - av_log(ctx, AV_LOG_DEBUG, "Formatting value %f (expr '%s') with spec '%s'\n", - res, argv[0], fmt_str); - - av_bprintf(bp, fmt_str, intval); - - return 0; -} - -static const struct drawtext_function { - const char *name; - unsigned argc_min, argc_max; - int tag; /**< opaque argument to func */ - int (*func)(AVFilterContext *, AVBPrint *, char *, unsigned, char **, int); -} functions[] = { - { "expr", 1, 1, 0, func_eval_expr }, - { "e", 1, 1, 0, func_eval_expr }, - { "expr_int_format", 2, 3, 0, func_eval_expr_int_format }, - { "eif", 2, 3, 0, func_eval_expr_int_format }, - { "pict_type", 0, 0, 0, func_pict_type }, - { "pts", 0, 3, 0, func_pts }, - { "gmtime", 0, 1, 'G', func_strftime }, - { "localtime", 0, 1, 'L', func_strftime }, - { "frame_num", 0, 0, 0, func_frame_num }, - { "n", 0, 0, 0, func_frame_num }, - { "metadata", 1, 2, 0, func_metadata }, -}; - -static int eval_function(AVFilterContext *ctx, AVBPrint *bp, char *fct, - unsigned argc, char **argv) -{ - unsigned i; - - for (i = 0; i < FF_ARRAY_ELEMS(functions); i++) { - if (strcmp(fct, functions[i].name)) - continue; - if (argc < functions[i].argc_min) { - av_log(ctx, AV_LOG_ERROR, "%%{%s} requires at least %d arguments\n", - fct, functions[i].argc_min); - return AVERROR(EINVAL); - } - if (argc > functions[i].argc_max) { - av_log(ctx, AV_LOG_ERROR, "%%{%s} requires at most %d arguments\n", - fct, functions[i].argc_max); - return AVERROR(EINVAL); - } - break; - } - if (i >= FF_ARRAY_ELEMS(functions)) { - av_log(ctx, AV_LOG_ERROR, "%%{%s} is not known\n", fct); - return AVERROR(EINVAL); - } - return functions[i].func(ctx, bp, fct, argc, argv, functions[i].tag); -} - -static int expand_function(AVFilterContext *ctx, AVBPrint *bp, char **rtext) -{ - const char *text = *rtext; - char *argv[16] = { NULL }; - unsigned argc = 0, i; - int ret; - - if (*text != '{') { - av_log(ctx, AV_LOG_ERROR, "Stray %% near '%s'\n", text); - return AVERROR(EINVAL); - } - text++; - while (1) { - if (!(argv[argc++] = av_get_token(&text, ":}"))) { - ret = AVERROR(ENOMEM); - goto end; - } - if (!*text) { - av_log(ctx, AV_LOG_ERROR, "Unterminated %%{} near '%s'\n", *rtext); - ret = AVERROR(EINVAL); - goto end; - } - if (argc == FF_ARRAY_ELEMS(argv)) - av_freep(&argv[--argc]); /* error will be caught later */ - if (*text == '}') - break; - text++; - } - - if ((ret = eval_function(ctx, bp, argv[0], argc - 1, argv + 1)) < 0) - goto end; - ret = 0; - *rtext = (char *)text + 1; - -end: - for (i = 0; i < argc; i++) - av_freep(&argv[i]); - return ret; -} - -static int expand_text(AVFilterContext *ctx, char *text, AVBPrint *bp) -{ - int ret; - - av_bprint_clear(bp); - while (*text) { - if (*text == '\\' && text[1]) { - av_bprint_chars(bp, text[1], 1); - text += 2; - } else if (*text == '%') { - text++; - if ((ret = expand_function(ctx, bp, &text)) < 0) - return ret; - } else { - av_bprint_chars(bp, *text, 1); - text++; - } - } - if (!av_bprint_is_complete(bp)) - return AVERROR(ENOMEM); - return 0; -} - static void update_color_with_alpha(DrawTextContext *s, FFDrawColor *color, const FFDrawColor incolor) { *color = incolor; @@ -1688,7 +1417,7 @@ static int measure_text(AVFilterContext *ctx, TextMetrics *metrics) for (i = 0, p = text; 1; i++) { GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_failed;); continue_on_failed: - if (is_newline(code) || code == 0) { + if (ff_is_newline(code) || code == 0) { ++line_count; if (code == 0) { break; @@ -1729,7 +1458,7 @@ continue_on_failed: } GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_failed2;); continue_on_failed2: - if (is_newline(code) || code == 0) { + if (ff_is_newline(code) || code == 0) { TextLine *cur_line = &s->lines[line_count]; HarfbuzzData *hb = &cur_line->hb_data; cur_line->cluster_offset = line_offset; @@ -1861,7 +1590,7 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame) av_bprintf(bp, "%s", s->text); break; case EXP_NORMAL: - if ((ret = expand_text(ctx, s->text, &s->expanded_text)) < 0) + if ((ret = ff_expand_text(&s->expand_text, s->text, &s->expanded_text)) < 0) return ret; break; case EXP_STRFTIME: @@ -1883,7 +1612,7 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame) if (s->fontcolor_expr[0]) { /* If expression is set, evaluate and replace the static value */ av_bprint_clear(&s->expanded_fontcolor); - if ((ret = expand_text(ctx, s->fontcolor_expr, &s->expanded_fontcolor)) < 0) + if ((ret = ff_expand_text(&s->expand_text, s->fontcolor_expr, &s->expanded_fontcolor)) < 0) return ret; if (!av_bprint_is_complete(&s->expanded_fontcolor)) return AVERROR(ENOMEM); @@ -2125,7 +1854,7 @@ static int filter_frame(AVFilterLink *inlink, AVFrame *frame) } if (s->reload && !(inlink->frame_count_out % s->reload)) { - if ((ret = load_textfile(ctx)) < 0) { + if ((ret = ff_load_textfile(ctx, (const char *)s->textfile, &s->text, NULL)) < 0) { av_frame_free(&frame); return ret; } -- 2.34.1 [-- Attachment #3: Type: text/plain, Size: 251 bytes --] _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils 2023-12-09 18:12 ` Stefano Sabatini @ 2023-12-16 15:35 ` Stefano Sabatini 2024-01-03 12:58 ` Timo Rothenpieler 0 siblings, 1 reply; 22+ messages in thread From: Stefano Sabatini @ 2023-12-16 15:35 UTC (permalink / raw) To: FFmpeg development discussions and patches 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. _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils 2023-12-16 15:35 ` Stefano Sabatini @ 2024-01-03 12:58 ` Timo Rothenpieler 2024-01-03 15:50 ` Stefano Sabatini 0 siblings, 1 reply; 22+ messages in thread From: Timo Rothenpieler @ 2024-01-03 12:58 UTC (permalink / raw) To: ffmpeg-devel, Stefano Sabatini 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? _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils 2024-01-03 12:58 ` Timo Rothenpieler @ 2024-01-03 15:50 ` Stefano Sabatini 0 siblings, 0 replies; 22+ messages in thread From: Stefano Sabatini @ 2024-01-03 15:50 UTC (permalink / raw) To: Timo Rothenpieler; +Cc: ffmpeg-devel 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. _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils 2023-11-30 0:49 ` [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils Stefano Sabatini 2023-12-03 15:05 ` Stefano Sabatini @ 2023-12-06 20:20 ` Nicolas George 1 sibling, 0 replies; 22+ messages in thread From: Nicolas George @ 2023-12-06 20:20 UTC (permalink / raw) To: FFmpeg development discussions and patches; +Cc: Stefano Sabatini [-- Attachment #1.1: Type: text/plain, Size: 942 bytes --] 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, -- Nicolas George [-- Attachment #1.2: signature.asc --] [-- Type: application/pgp-signature, Size: 833 bytes --] [-- Attachment #2: Type: text/plain, Size: 251 bytes --] _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* [FFmpeg-devel] [PATCH 2/2] lavfi: add qrencodesrc source 2023-11-30 0:49 [FFmpeg-devel] [POC][PATCHSET] Add qrencodesrc source Stefano Sabatini 2023-11-30 0:49 ` [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils Stefano Sabatini @ 2023-11-30 0:49 ` Stefano Sabatini 2023-12-09 18:13 ` Stefano Sabatini 2023-11-30 11:07 ` [FFmpeg-devel] [POC][PATCHSET] Add " Tomas Härdin 2 siblings, 1 reply; 22+ messages in thread From: Stefano Sabatini @ 2023-11-30 0:49 UTC (permalink / raw) To: FFmpeg development discussions and patches; +Cc: Stefano Sabatini --- configure | 4 + libavfilter/Makefile | 1 + libavfilter/allfilters.c | 1 + libavfilter/vsrc_qrencode.c | 435 ++++++++++++++++++++++++++++++++++++ 4 files changed, 441 insertions(+) create mode 100644 libavfilter/vsrc_qrencode.c diff --git a/configure b/configure index d6e4a1e7df..f197f499dd 100755 --- a/configure +++ b/configure @@ -256,6 +256,7 @@ External library support: --enable-libopus enable Opus de/encoding via libopus [no] --enable-libplacebo enable libplacebo library [no] --enable-libpulse enable Pulseaudio input via libpulse [no] + --enable-libqrencode enable QR encode generation via libqrencode [no] --enable-librabbitmq enable RabbitMQ library [no] --enable-librav1e enable AV1 encoding via rav1e [no] --enable-librist enable RIST via librist [no] @@ -1877,6 +1878,7 @@ EXTERNAL_LIBRARY_LIST=" libopus libplacebo libpulse + libqrencode librabbitmq librav1e librist @@ -3763,6 +3765,7 @@ nnedi_filter_deps="gpl" ocr_filter_deps="libtesseract" ocv_filter_deps="libopencv" openclsrc_filter_deps="opencl" +qrencodesrc_filter_deps="libqrencode" overlay_opencl_filter_deps="opencl" overlay_qsv_filter_deps="libmfx" overlay_qsv_filter_select="qsvvpp" @@ -6803,6 +6806,7 @@ enabled libopus && { } enabled libplacebo && require_pkg_config libplacebo "libplacebo >= 4.192.0" libplacebo/vulkan.h pl_vulkan_create enabled libpulse && require_pkg_config libpulse libpulse pulse/pulseaudio.h pa_context_new +enabled libqrencode && require_pkg_config libqrencode libqrencode qrencode.h QRcode_encodeString enabled librabbitmq && require_pkg_config librabbitmq "librabbitmq >= 0.7.1" amqp.h amqp_new_connection enabled librav1e && require_pkg_config librav1e "rav1e >= 0.5.0" rav1e.h rav1e_context_new enabled librist && require_pkg_config librist "librist >= 0.2.7" librist/librist.h rist_receiver_create diff --git a/libavfilter/Makefile b/libavfilter/Makefile index e49be354bb..3eee1ba085 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -597,6 +597,7 @@ OBJS-$(CONFIG_NULLSRC_FILTER) += vsrc_testsrc.o OBJS-$(CONFIG_OPENCLSRC_FILTER) += vf_program_opencl.o opencl.o OBJS-$(CONFIG_PAL75BARS_FILTER) += vsrc_testsrc.o OBJS-$(CONFIG_PAL100BARS_FILTER) += vsrc_testsrc.o +OBJS-$(CONFIG_QRENCODESRC_FILTER) += vsrc_qrencode.o textutils.o OBJS-$(CONFIG_RGBTESTSRC_FILTER) += vsrc_testsrc.o OBJS-$(CONFIG_SIERPINSKI_FILTER) += vsrc_sierpinski.o OBJS-$(CONFIG_SMPTEBARS_FILTER) += vsrc_testsrc.o diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c index aa49703c6e..3d8c454ab0 100644 --- a/libavfilter/allfilters.c +++ b/libavfilter/allfilters.c @@ -559,6 +559,7 @@ extern const AVFilter ff_vsrc_mandelbrot; extern const AVFilter ff_vsrc_mptestsrc; extern const AVFilter ff_vsrc_nullsrc; extern const AVFilter ff_vsrc_openclsrc; +extern const AVFilter ff_vsrc_qrencodesrc; extern const AVFilter ff_vsrc_pal75bars; extern const AVFilter ff_vsrc_pal100bars; extern const AVFilter ff_vsrc_rgbtestsrc; diff --git a/libavfilter/vsrc_qrencode.c b/libavfilter/vsrc_qrencode.c new file mode 100644 index 0000000000..76ebbda999 --- /dev/null +++ b/libavfilter/vsrc_qrencode.c @@ -0,0 +1,435 @@ +/* + * 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 QR encoder source. + * + * A QR code (quick-response code) is a type of two-dimensional matrix + * barcode, invented in 1994, by Japanese company Denso Wave for + * labelling automobile parts. + * + * This source uses the libqrencode library to generate QR code: + * https://fukuchi.org/works/qrencode/ + */ + +// #define DEBUG + +#include "libavutil/internal.h" +#include "libavutil/imgutils.h" +#include "libavutil/opt.h" +#include "libavutil/lfg.h" +#include "libavutil/random_seed.h" + +#include "avfilter.h" +#include "internal.h" +#include "formats.h" +#include "textutils.h" +#include "video.h" +#include "libswscale/swscale.h" + +#include <qrencode.h> + +enum var_name { + VAR_N, + VAR_QW, + VAR_T, + VAR_W, + VAR_VARS_NB +}; + +static const char *const var_names[] = { + "n", ///< number of frame + "qw", ///< width of the rendered QR code + "t", ///< timestamp expressed in seconds + "w", ///< width of the frame + NULL +}; + +enum Expansion { + EXPANSION_NONE, + EXPANSION_NORMAL +}; + +typedef struct QREncodeContext { + const AVClass *class; + int width; + unsigned char *text; + AVBPrint expanded_text; ///< used to contain the expanded text + char *textfile; + uint64_t pts; + + int level; + char case_sensitive; + + uint8_t foreground_color[4]; + uint8_t background_color[4]; + + uint8_t *qrcode_data[4]; + int qrcode_linesize[4]; + uint8_t qrcode_width; + AVRational frame_rate; + + int expansion; ///< expansion mode to use for the text + TextExpander text_expander; ///< text expander in case exp_mode == NORMAL + double var_values[VAR_VARS_NB]; + AVLFG prng; ///< random generator +} QREncodeContext; + +#define OFFSET(x) offsetof(QREncodeContext, x) +#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM + +static const AVOption qrencode_options[] = { + { "rate", "set video rate", OFFSET(frame_rate), AV_OPT_TYPE_VIDEO_RATE, {.str = "25"}, 0, INT_MAX, FLAGS }, + { "r", "set video rate", OFFSET(frame_rate), AV_OPT_TYPE_VIDEO_RATE, {.str = "25"}, 0, INT_MAX, FLAGS }, + { "width", "set video width", OFFSET(width), AV_OPT_TYPE_INT, {.i64 = 64}, 16, INT_MAX, FLAGS }, + { "w", "set video width", OFFSET(width), AV_OPT_TYPE_INT, {.i64 = 64}, 16, INT_MAX, FLAGS }, + { "case_sensitive", "generate code which is case sensitive", OFFSET(case_sensitive), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS }, + { "cs", "generate code which is case sensitive", OFFSET(case_sensitive), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS }, + { "level", "error correction level, lowest is L", OFFSET(level), AV_OPT_TYPE_INT, { .i64 = AVCOL_SPC_UNSPECIFIED }, 0, QR_ECLEVEL_H, .flags = FLAGS, "level"}, + { "L", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = QR_ECLEVEL_L }, 0, 0, FLAGS, "level" }, + { "M", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = QR_ECLEVEL_M }, 0, 0, FLAGS, "level" }, + { "Q", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = QR_ECLEVEL_Q }, 0, 0, FLAGS, "level" }, + { "H", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = QR_ECLEVEL_H }, 0, 0, FLAGS, "level" }, + {"expansion", "set the expansion mode", OFFSET(expansion), AV_OPT_TYPE_INT, {.i64=EXPANSION_NORMAL}, 0, 2, FLAGS, "expansion"}, + {"none", "set no expansion", OFFSET(expansion), AV_OPT_TYPE_CONST, {.i64=EXPANSION_NONE}, 0, 0, FLAGS, "expansion"}, + {"normal", "set normal expansion", OFFSET(expansion), AV_OPT_TYPE_CONST, {.i64=EXPANSION_NORMAL}, 0, 0, FLAGS, "expansion"}, + { "foreground_color", "set QR foreground color", OFFSET(foreground_color), AV_OPT_TYPE_COLOR, {.str="black"}, 0, 0, FLAGS }, + { "fc", "set QR foreground color", OFFSET(foreground_color), AV_OPT_TYPE_COLOR, {.str="black"}, 0, 0, FLAGS }, + { "background_color", "set QR background color", OFFSET(background_color), AV_OPT_TYPE_COLOR, {.str="white"}, 0, 0, FLAGS }, + { "bc", "set QR background color", OFFSET(background_color), AV_OPT_TYPE_COLOR, {.str="white"}, 0, 0, FLAGS }, + {"text", "set text to encode", OFFSET(text), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS}, + {"textfile", "set text file to encode", OFFSET(textfile), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS}, + { NULL } +}; + +AVFILTER_DEFINE_CLASS(qrencode); + +static const char *const fun2_names[] = { + "rand" +}; + +static double drand(void *opaque, double min, double max) +{ + return min + (max-min) / UINT_MAX * av_lfg_get(opaque); +} + +static const ff_eval_func2 fun2[] = { + drand, + NULL +}; + +static int func_pts(void *ctx, AVBPrint *bp, const char *function_name, + unsigned argc, char **argv) +{ + QREncodeContext *qr = ((AVFilterContext *)ctx)->priv; + const char *fmt; + const char *strftime_fmt = NULL; + const char *delta = NULL; + double pts = qr->var_values[VAR_T]; + + // argv: pts, FMT, [DELTA, strftime_fmt] + + fmt = argc >= 1 ? argv[0] : "flt"; + if (argc >= 2) { + delta = argv[1]; + } + if (argc >= 3) { + 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) +{ + QREncodeContext *qr = ((AVFilterContext *)ctx)->priv; + + av_bprintf(bp, "%d", (int)qr->var_values[VAR_N]); + 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) +{ + QREncodeContext *qr = ((AVFilterContext *)ctx)->priv; + + return ff_print_eval_expr(ctx, bp, argv[0], + fun2_names, fun2, + var_names, qr->var_values, &qr->prng); +} + +static int func_eval_expr_int_format(void *ctx, AVBPrint *bp, const char *function_name, + unsigned argc, char **argv) +{ + QREncodeContext *qr = ((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, qr->var_values, + &qr->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 }, + { "n", 0, 0, func_frame_num }, + { "pts", 0, 3, func_pts } +}; + +static av_cold int init(AVFilterContext *ctx) +{ + QREncodeContext *qr = ctx->priv; + int err; + + qr->qrcode_width = -1; + + if (qr->textfile) { + if (qr->text) { + av_log(ctx, AV_LOG_ERROR, + "Both text and text file provided. Please provide only one\n"); + return AVERROR(EINVAL); + } + if ((err = ff_load_textfile(ctx, (const char *)qr->textfile, &(qr->text), NULL)) < 0) + return err; + } + + av_log(ctx, AV_LOG_VERBOSE, + "w:%"PRId64" case_sensitive:%d level:%d\n", + qr->width, qr->case_sensitive, qr->level); + + av_lfg_init(&qr->prng, av_get_random_seed()); + + qr->text_expander = (TextExpander) { + .ctx = ctx, + .functions = text_expander_functions, + .functions_nb = FF_ARRAY_ELEMS(text_expander_functions) + }; + + av_bprint_init(&qr->expanded_text, 0, AV_BPRINT_SIZE_UNLIMITED); + + return 0; +} + +static av_cold void uninit(AVFilterContext *ctx) +{ + QREncodeContext *qr = ctx->priv; + + av_bprint_finalize(&qr->expanded_text, NULL); + av_freep(&qr->qrcode_data[0]); +} + +static int config_props(AVFilterLink *outlink) +{ + QREncodeContext *qr = outlink->src->priv; + + qr->var_values[VAR_W] = outlink->w = qr->width; + outlink->h = qr->width; + outlink->time_base = av_inv_q(qr->frame_rate); + outlink->frame_rate = qr->frame_rate; + + return 0; +} + +#ifdef DEBUG +static void show_qrcode(AVFilterContext *ctx, const QRcode *qrcode) +{ + int i, j; + char *line = av_malloc(qrcode->width + 1); + const char *p = qrcode->data; + + if (!line) + return; + for (i = 0; i < qrcode->width; i++) { + for (j = 0; j < qrcode->width; j++) + line[j] = (*p++)&1 ? '@' : ' '; + line[j] = 0; + av_log(ctx, AV_LOG_DEBUG, "%3d: %s\n", i, line); + } + av_free(line); +} +#endif + +static int request_frame(AVFilterLink *outlink) +{ + AVFilterContext *ctx = (AVFilterContext *)outlink->src; + QREncodeContext *qr = ctx->priv; + AVFrame *picref = ff_get_video_buffer(outlink, qr->width, qr->width); + struct SwsContext *sws = NULL; + QRcode *qrcode = NULL; + int i, j; + int ret; + uint8_t *srcp; + uint8_t *dstp0, *dstp; + + if (!picref) + return AVERROR(ENOMEM); + picref->sample_aspect_ratio = (AVRational) {1, 1}; + qr->var_values[VAR_N] = picref->pts = qr->pts++; + qr->var_values[VAR_T] = qr->pts * av_q2d(outlink->time_base); + + av_inv_q(qr->frame_rate); + + switch (qr->expansion) { + case EXPANSION_NONE: + av_bprintf(&qr->expanded_text, "%s", qr->text); + break; + case EXPANSION_NORMAL: + if ((ret = ff_expand_text(&qr->text_expander, qr->text, &qr->expanded_text)) < 0) + return ret; + break; + } + + qrcode = QRcode_encodeString(qr->expanded_text.str, 1, qr->level, QR_MODE_8, + qr->case_sensitive); + if (!qrcode) { + ret = AVERROR(errno); + av_log(ctx, AV_LOG_ERROR, + "Failed to encode string with error \'%s\'\n", av_err2str(ret)); + goto error; + } + + qr->var_values[VAR_QW] = qrcode->width; + av_log(ctx, AV_LOG_DEBUG, + "Encoded QR with width:%d version:%d\n", qrcode->width, qrcode->version); + +#ifdef DEBUG + show_qrcode(ctx, (const QRcode *)qrcode); +#endif + + if (qrcode->width != qr->qrcode_width) { + qr->qrcode_width = qrcode->width; + ret = av_image_alloc(qr->qrcode_data, qr->qrcode_linesize, + qrcode->width, qrcode->width, + AV_PIX_FMT_RGBA, 16); + if (ret < 0) { + av_log(ctx, AV_LOG_ERROR, "Failed to allocate image for QR code with width %d\n", qrcode->width); + goto error; + } + } + + dstp0 = qr->qrcode_data[0]; + srcp = qrcode->data; + + for (i = 0; i < qrcode->width; i++) { + dstp = dstp0; + + for (j = 0; j < qrcode->width; j++) { + if ((*srcp++)&1) { + *dstp++ = qr->foreground_color[0]; + *dstp++ = qr->foreground_color[1]; + *dstp++ = qr->foreground_color[2]; + *dstp++ = qr->foreground_color[3]; + } else { + *dstp++ = qr->background_color[0]; + *dstp++ = qr->background_color[1]; + *dstp++ = qr->background_color[2]; + *dstp++ = qr->background_color[3]; + } + } + dstp0 += qr->qrcode_linesize[0]; + } + + sws = sws_alloc_context(); + if (!sws) { + ret = AVERROR(ENOMEM); + goto error; + } + + av_opt_set_int(sws, "srcw", qr->qrcode_width, 0); + av_opt_set_int(sws, "srch", qr->qrcode_width, 0); + av_opt_set_int(sws, "src_format", AV_PIX_FMT_RGBA, 0); + av_opt_set_int(sws, "dstw", qr->width, 0); + av_opt_set_int(sws, "dsth", qr->width, 0); + av_opt_set_int(sws, "dst_format", outlink->format, 0); + av_opt_set_int(sws, "sws_flags", SWS_POINT, 0); + + if ((ret = sws_init_context(sws, NULL, NULL)) < 0) + goto error; + + sws_scale(sws, + (const uint8_t *const *)&qr->qrcode_data, qr->qrcode_linesize, + 0, qrcode->width, + picref->data, picref->linesize); + ret = ff_filter_frame(outlink, picref); + +error: + sws_freeContext(sws); + QRcode_free(qrcode); + + return ret; +} + +static int query_formats(AVFilterContext *ctx) +{ + enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_RGBA, AV_PIX_FMT_NONE }; + + return ff_set_common_formats_from_list(ctx, pix_fmts); +} + +static const AVFilterPad qrencode_outputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_VIDEO, + .request_frame = request_frame, + .config_props = config_props, + } +}; + +const AVFilter ff_vsrc_qrencodesrc = { + .name = "qrencode", + .description = NULL_IF_CONFIG_SMALL("Generate a QR code."), + .priv_size = sizeof(QREncodeContext), + .priv_class = &qrencode_class, + .init = init, + .uninit = uninit, + .inputs = NULL, + FILTER_OUTPUTS(qrencode_outputs), + FILTER_QUERY_FUNC(query_formats), +}; -- 2.34.1 _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [PATCH 2/2] lavfi: add qrencodesrc source 2023-11-30 0:49 ` [FFmpeg-devel] [PATCH 2/2] lavfi: add qrencodesrc source Stefano Sabatini @ 2023-12-09 18:13 ` Stefano Sabatini 2023-12-11 20:59 ` Kyle Swanson 2023-12-17 18:00 ` Stefano Sabatini 0 siblings, 2 replies; 22+ messages in thread From: Stefano Sabatini @ 2023-12-09 18:13 UTC (permalink / raw) To: FFmpeg development discussions and patches [-- Attachment #1: Type: text/plain, Size: 381 bytes --] On date Thursday 2023-11-30 01:49:14 +0100, Stefano Sabatini wrote: > --- > configure | 4 + > libavfilter/Makefile | 1 + > libavfilter/allfilters.c | 1 + > libavfilter/vsrc_qrencode.c | 435 ++++++++++++++++++++++++++++++++++++ > 4 files changed, 441 insertions(+) > create mode 100644 libavfilter/vsrc_qrencode.c Rev2 with padding and doc. [-- Attachment #2: 0002-lavfi-add-qrencodesrc-source.patch --] [-- Type: text/x-diff, Size: 31359 bytes --] From b16201db38d3e492635a9d202b0e654811a52ec5 Mon Sep 17 00:00:00 2001 From: Stefano Sabatini <stefasab@gmail.com> Date: Tue, 28 Nov 2023 23:58:15 +0100 Subject: [PATCH 2/2] lavfi: add qrencodesrc source Introduce qrencodesrc source. --- configure | 4 + doc/filters.texi | 219 +++++++++++++++ libavfilter/Makefile | 1 + libavfilter/allfilters.c | 1 + libavfilter/vsrc_qrencode.c | 547 ++++++++++++++++++++++++++++++++++++ 5 files changed, 772 insertions(+) create mode 100644 libavfilter/vsrc_qrencode.c diff --git a/configure b/configure index 838e627084..a2e9026c49 100755 --- a/configure +++ b/configure @@ -256,6 +256,7 @@ External library support: --enable-libopus enable Opus de/encoding via libopus [no] --enable-libplacebo enable libplacebo library [no] --enable-libpulse enable Pulseaudio input via libpulse [no] + --enable-libqrencode enable QR encode generation via libqrencode [no] --enable-librabbitmq enable RabbitMQ library [no] --enable-librav1e enable AV1 encoding via rav1e [no] --enable-librist enable RIST via librist [no] @@ -1879,6 +1880,7 @@ EXTERNAL_LIBRARY_LIST=" libopus libplacebo libpulse + libqrencode librabbitmq librav1e librist @@ -3769,6 +3771,7 @@ nnedi_filter_deps="gpl" ocr_filter_deps="libtesseract" ocv_filter_deps="libopencv" openclsrc_filter_deps="opencl" +qrencodesrc_filter_deps="libqrencode" overlay_opencl_filter_deps="opencl" overlay_qsv_filter_deps="libmfx" overlay_qsv_filter_select="qsvvpp" @@ -6812,6 +6815,7 @@ enabled libopus && { } enabled libplacebo && require_pkg_config libplacebo "libplacebo >= 4.192.0" libplacebo/vulkan.h pl_vulkan_create enabled libpulse && require_pkg_config libpulse libpulse pulse/pulseaudio.h pa_context_new +enabled libqrencode && require_pkg_config libqrencode libqrencode qrencode.h QRcode_encodeString enabled librabbitmq && require_pkg_config librabbitmq "librabbitmq >= 0.7.1" amqp.h amqp_new_connection enabled librav1e && require_pkg_config librav1e "rav1e >= 0.5.0" rav1e.h rav1e_context_new enabled librist && require_pkg_config librist "librist >= 0.2.7" librist/librist.h rist_receiver_create diff --git a/doc/filters.texi b/doc/filters.texi index de19d130cc..09aee86a46 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -28660,6 +28660,225 @@ ffplay -f lavfi life=s=300x200:mold=10:r=60:ratio=0.1:death_color=#C83232:life_c @end example @end itemize +@section qrencodesrc + +Generate a QR code using the libqrencode library (see +@url{https://fukuchi.org/works/qrencode/}). + +To enable the compilation of this source, you need to configure FFmpeg with with +@code{--enable-libqrencode}. + +The QR code is generated from the provided text or text pattern. The +corresponding QR code is scaled and put in the video output according to the +specified output size options. + +In case no text is specified, the QR code is not generated, but an empty colored +output is returned instead. + +This source accepts the following options: + +@table @option + +@item qrcode_width, w +@item padded_qrcode_width, W +Specify an expression for the width of the rendered QR code, with and without +padding. The @var{qrcode_width} expression can reference the value set by the +@var{padded_qrcode_width} expression, and vice versa. +By default @var{padded_qrcode_width} is set to @var{qrcode_width}, meaning that +there is no padding. + +@item rate, r +Specify the frame rate of the sourced video, as the number of frames +generated per second. It has to be a string in the format +@var{frame_rate_num}/@var{frame_rate_den}, an integer number, a floating point +number or a valid video frame rate abbreviation. The default value is +"25". + +@item case_sensitive, cs +Instruct libqrencode to use case sensitive encoding. This is enabled by +default. This can be disabled to reduce the QR encoding size. + +@item level, l +Specify the QR encoding error correction level. With an higher correction level, +the encoding size will increase but the code will be more robust to corruption. +Lower level is @var{L}. + +It accepts the following values: +@table @samp +@item L +@item M +@item Q +@item H +@end table + +@item expansion +Select how the input text is expanded. Can be either @code{none}, or +@code{normal} (default). See the @ref{qrencodesrc_expansion, Text expansion} +section below for details. + +@item text +@item textfile +Define the text to be rendered. In case neither is specified, no QR is encoded +(just an empty colored frame). + +In case expansion is enabled, the text is treated as a text template, using the +drawtext expansion mechanism. See section below. + +@item background_color, bc +@item foreground_color, fc +Set the QR code and background color. The default value of +@var{foreground_color} is "black", the default value of @var{background_color} +is "white". + +For the syntax of the color options, check the @ref{color syntax,,"Color" +section in the ffmpeg-utils manual,ffmpeg-utils}. +@end table + +@anchor{qrencodesrc_expansion} +@subsection Text expansion + +If @option{expansion} is set to @code{none}, the text is printed verbatim. + +If @option{expansion} is set to @code{normal} (which is the default), +the following expansion mechanism is used. + +The backslash character @samp{\}, followed by any character, always expands to +the second character. + +Sequences 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 +filtergraph description, and possibly also for the shell, that makes up to four +levels of escaping; using a text file with the @option{textfile} option avoids +these problems. + +The following functions are available: + +@table @command +@item n, frame_num +return the frame number + +@item pts +Return the timestamp of the current frame. + +It can take up to two arguments. + +The first argument is the format of the timestamp; it defaults to @code{flt} for +seconds as a decimal number with microsecond accuracy; @code{hms} stands for a +formatted @var{[-]HH:MM:SS.mmm} timestamp with millisecond accuracy. +@code{gmtime} stands for the timestamp of the frame formatted as UTC time; +@code{localtime} stands for the timestamp of the frame formatted as local time +zone time. If the format is set to @code{hms24hh}, the time is formatted in 24h +format (00-23). + +The second argument is an offset added to the timestamp. + +If the format is set to @code{localtime} or @code{gmtime}, a third argument may +be supplied: a @code{strftime} C function format string. By default, +@var{YYYY-MM-DD HH:MM:SS} format will be used. + +@item expr, e +Evaluate the expression's value and output as a double. + +It must take one argument specifying the expression to be evaluated, which +accepts the following constants: + +@table @option +@item n +the frame number + +@item q +the QR code width + +@item t +the time of the frame + +@item w +return the rendered QR code width + +@item W +return the rendered padded QR code width +@end table + +@item expr_formatted, ef +Evaluate the expression's value and output as a formatted string. + +The first argument is the expression to be evaluated, just as for the @var{expr} function. +The second argument specifies the output format. Allowed values are @samp{x}, +@samp{X}, @samp{d} and @samp{u}. They are treated exactly as in the +@code{printf} function. +The third parameter is optional and sets the number of positions taken by the output. +It can be used to add padding with zeros from the left. + +@item gmtime +The time at which the filter is running, expressed in UTC. +It can accept an argument: a @code{strftime} C function format string. +The format string is extended to support the variable @var{%[1-6]N} +which prints fractions of the second with optionally specified number of digits. + +@item localtime +The time at which the filter is running, expressed in the local time zone. +It can accept an argument: a @code{strftime} C function format string. +The format string is extended to support the variable @var{%[1-6]N} +which prints fractions of the second with optionally specified number of digits. + +@item rand(min, max) +return a random number included between @var{min} and @var{max} +@end table + +@subsection Examples + +@itemize +@item +Generate a QR code encoding the specified text with the default size: +@example +qrencodesrc=text=www.ffmpeg.org +@end example + +@item +Same as below, but select blue on pink colors: +@example +qrencodesrc=text=www.ffmpeg.org:bc=pink:fc=blue +@end example + +@item +Generate a QR code with width of 200 pixels and padding, making the padded width +4/3 of the QR code width: +@example +qrencodesrc=text=www.ffmpeg.org:w=200:W=4/3*w +@end example + +@item +Generate a QR code with padded width of 200 pixels and padding, making the QR +code width 3/4 of the padded width: +@example +qrencodesrc=text=www.ffmpeg.org:W=200:w=3/4*W +@end example + +@item +Generate a QR code encoding the frame number: +@example +qrencodesrc=text=%@{n@} +@end example + +@item +Generate a QR code encoding the GMT timestamp: +@example +qrencodesrc=text=%@{gmtime@} +@end example + +@item +Generate a QR code encoding the timestamp expressed as a float: +@example +qrencodesrc=text=%@{pts@} +@end example + +@end itemize + @anchor{allrgb} @anchor{allyuv} @anchor{color} diff --git a/libavfilter/Makefile b/libavfilter/Makefile index 1f9bbcc1af..b30e436fb3 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -598,6 +598,7 @@ OBJS-$(CONFIG_NULLSRC_FILTER) += vsrc_testsrc.o OBJS-$(CONFIG_OPENCLSRC_FILTER) += vf_program_opencl.o opencl.o OBJS-$(CONFIG_PAL75BARS_FILTER) += vsrc_testsrc.o OBJS-$(CONFIG_PAL100BARS_FILTER) += vsrc_testsrc.o +OBJS-$(CONFIG_QRENCODESRC_FILTER) += vsrc_qrencode.o textutils.o OBJS-$(CONFIG_RGBTESTSRC_FILTER) += vsrc_testsrc.o OBJS-$(CONFIG_SIERPINSKI_FILTER) += vsrc_sierpinski.o OBJS-$(CONFIG_SMPTEBARS_FILTER) += vsrc_testsrc.o diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c index ed7c32be94..c5bf55ed53 100644 --- a/libavfilter/allfilters.c +++ b/libavfilter/allfilters.c @@ -560,6 +560,7 @@ extern const AVFilter ff_vsrc_mandelbrot; extern const AVFilter ff_vsrc_mptestsrc; extern const AVFilter ff_vsrc_nullsrc; extern const AVFilter ff_vsrc_openclsrc; +extern const AVFilter ff_vsrc_qrencodesrc; extern const AVFilter ff_vsrc_pal75bars; extern const AVFilter ff_vsrc_pal100bars; extern const AVFilter ff_vsrc_rgbtestsrc; diff --git a/libavfilter/vsrc_qrencode.c b/libavfilter/vsrc_qrencode.c new file mode 100644 index 0000000000..71e282eaa5 --- /dev/null +++ b/libavfilter/vsrc_qrencode.c @@ -0,0 +1,547 @@ +/* + * 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 QR encoder source. + * + * A QR code (quick-response code) is a type of two-dimensional matrix + * barcode, invented in 1994, by Japanese company Denso Wave for + * labelling automobile parts. + * + * This source uses the libqrencode library to generate QR code: + * https://fukuchi.org/works/qrencode/ + */ + +//#define DEBUG + +#include "libavutil/internal.h" +#include "libavutil/imgutils.h" +#include "libavutil/opt.h" +#include "libavutil/lfg.h" +#include "libavutil/random_seed.h" + +#include "avfilter.h" +#include "drawutils.h" +#include "internal.h" +#include "formats.h" +#include "textutils.h" +#include "video.h" +#include "libswscale/swscale.h" + +#include <qrencode.h> + +enum var_name { + VAR_n, + VAR_q, + VAR_t, + VAR_w, + VAR_W, + VAR_VARS_NB +}; + +static const char *const var_names[] = { + "n", ///< number of frame + "q", ///< width of the QR code + "t", ///< timestamp expressed in seconds + "w", ///< rendered width of the QR code + "W", ///< rendered width of the padded QR code + NULL +}; + +enum Expansion { + EXPANSION_NONE, + EXPANSION_NORMAL +}; + +typedef struct QREncodeContext { + const AVClass *class; + + char *rendered_qrcode_width_expr; + char *rendered_padded_qrcode_width_expr; + + int rendered_qrcode_width; + int rendered_padded_qrcode_width; + + unsigned char *text; + char *textfile; + uint64_t pts; + + int level; + char case_sensitive; + + uint8_t foreground_color[4]; + uint8_t background_color[4]; + + FFDrawContext draw; + FFDrawColor draw_foreground_color; ///< foreground color + FFDrawColor draw_background_color; ///< background color + + /* these are only used when nothing must be encoded */ + FFDrawContext draw0; + FFDrawColor draw0_background_color; ///< background color + + uint8_t *qrcode_data[4]; + int qrcode_linesize[4]; + uint8_t *qrcode_mask_data[4]; + int qrcode_mask_linesize[4]; + int qrcode_width; + int padded_qrcode_width; + + AVRational frame_rate; + + int expansion; ///< expansion mode to use for the text + FFExpandTextContext expand_text; ///< expand text in case expansion is enabled + AVBPrint expanded_text; ///< used to contain the expanded text + + double var_values[VAR_VARS_NB]; + AVLFG lfg; ///< random generator +} QREncodeContext; + +#define OFFSET(x) offsetof(QREncodeContext, x) +#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM + +static const AVOption qrencodesrc_options[] = { + { "rate", "set video rate", OFFSET(frame_rate), AV_OPT_TYPE_VIDEO_RATE, {.str = "25"}, 0, INT_MAX, FLAGS }, + { "r", "set video rate", OFFSET(frame_rate), AV_OPT_TYPE_VIDEO_RATE, {.str = "25"}, 0, INT_MAX, FLAGS }, + { "qrcode_width", "set rendered QR code width expression", OFFSET(rendered_qrcode_width_expr), AV_OPT_TYPE_STRING, {.str = "64"}, 0, INT_MAX, FLAGS }, + { "w", "set rendered QR code width expression", OFFSET(rendered_qrcode_width_expr), AV_OPT_TYPE_STRING, {.str = "64"}, 0, INT_MAX, FLAGS }, + { "padded_qrcode_width", "set rendered padded QR code width expression", OFFSET(rendered_padded_qrcode_width_expr), AV_OPT_TYPE_STRING, {.str = "w"}, 0, INT_MAX, FLAGS }, + { "W", "set rendered padded QR code width expression", OFFSET(rendered_padded_qrcode_width_expr), AV_OPT_TYPE_STRING, {.str = "w"}, 0, INT_MAX, FLAGS }, + { "case_sensitive", "generate code which is case sensitive", OFFSET(case_sensitive), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, FLAGS }, + { "cs", "generate code which is case sensitive", OFFSET(case_sensitive), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, FLAGS }, + + { "level", "error correction level, lowest is L", OFFSET(level), AV_OPT_TYPE_INT, { .i64 = AVCOL_SPC_UNSPECIFIED }, 0, QR_ECLEVEL_H, .flags = FLAGS, "level"}, + { "l", "error correction level, lowest is L", OFFSET(level), AV_OPT_TYPE_INT, { .i64 = AVCOL_SPC_UNSPECIFIED }, 0, QR_ECLEVEL_H, .flags = FLAGS, "level"}, + { "L", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = QR_ECLEVEL_L }, 0, 0, FLAGS, "level" }, + { "M", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = QR_ECLEVEL_M }, 0, 0, FLAGS, "level" }, + { "Q", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = QR_ECLEVEL_Q }, 0, 0, FLAGS, "level" }, + { "H", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = QR_ECLEVEL_H }, 0, 0, FLAGS, "level" }, + + {"expansion", "set the expansion mode", OFFSET(expansion), AV_OPT_TYPE_INT, {.i64=EXPANSION_NORMAL}, 0, 2, FLAGS, "expansion"}, + {"none", "set no expansion", OFFSET(expansion), AV_OPT_TYPE_CONST, {.i64 = EXPANSION_NONE}, 0, 0, FLAGS, "expansion"}, + {"normal", "set normal expansion", OFFSET(expansion), AV_OPT_TYPE_CONST, {.i64 = EXPANSION_NORMAL}, 0, 0, FLAGS, "expansion"}, + + { "foreground_color", "set QR foreground color", OFFSET(foreground_color), AV_OPT_TYPE_COLOR, {.str = "black"}, 0, 0, FLAGS }, + { "fc", "set QR foreground color", OFFSET(foreground_color), AV_OPT_TYPE_COLOR, {.str = "black"}, 0, 0, FLAGS }, + { "background_color", "set QR background color", OFFSET(background_color), AV_OPT_TYPE_COLOR, {.str = "white"}, 0, 0, FLAGS }, + { "bc", "set QR background color", OFFSET(background_color), AV_OPT_TYPE_COLOR, {.str = "white"}, 0, 0, FLAGS }, + + {"text", "set text to encode", OFFSET(text), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, FLAGS}, + {"textfile", "set text file to encode", OFFSET(textfile), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, FLAGS}, + { NULL } +}; + +AVFILTER_DEFINE_CLASS(qrencodesrc); + +static const char *const fun2_names[] = { + "rand" +}; + +static double drand(void *opaque, double min, double max) +{ + return min + (max-min) / UINT_MAX * av_lfg_get(opaque); +} + +static const ff_eval_func2 fun2[] = { + drand, + NULL +}; + +static int func_pts(void *ctx, AVBPrint *bp, const char *function_name, + unsigned argc, char **argv) +{ + QREncodeContext *qr = ((AVFilterContext *)ctx)->priv; + const char *fmt; + const char *strftime_fmt = NULL; + const char *delta = NULL; + double t = qr->var_values[VAR_t]; + + // argv: pts, FMT, [DELTA, strftime_fmt] + + fmt = argc >= 1 ? argv[0] : "flt"; + if (argc >= 2) { + delta = argv[1]; + } + if (argc >= 3) { + strftime_fmt = argv[2]; + } + + return ff_print_pts(ctx, bp, t, delta, fmt, strftime_fmt); +} + +static int func_frame_num(void *ctx, AVBPrint *bp, const char *function_name, + unsigned argc, char **argv) +{ + QREncodeContext *qr = ((AVFilterContext *)ctx)->priv; + + av_bprintf(bp, "%d", (int)qr->var_values[VAR_n]); + 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) +{ + QREncodeContext *qr = ((AVFilterContext *)ctx)->priv; + + return ff_print_eval_expr(ctx, bp, argv[0], + fun2_names, fun2, + var_names, qr->var_values, &qr->lfg); +} + +static int func_eval_expr_formatted(void *ctx, AVBPrint *bp, const char *function_name, + unsigned argc, char **argv) +{ + QREncodeContext *qr = ((AVFilterContext *)ctx)->priv; + int ret; + int positions = -1; + + /* + * argv[0] expression to be converted to `int` + * argv[1] format: 'x', 'X', 'd' or 'u' + * argv[2] positions printed (optional) + */ + + if (argc == 3) { + ret = sscanf(argv[2], "%u", &positions); + if (ret != 1) { + av_log(ctx, AV_LOG_ERROR, "expr_int_format(): Invalid number of positions" + " to print: '%s'\n", argv[2]); + return AVERROR(EINVAL); + } + } + + return ff_print_formatted_eval_expr(ctx, bp, argv[0], + fun2_names, fun2, + var_names, qr->var_values, + &qr->lfg, + argv[1][0], positions); +} + +static FFExpandTextFunction expand_text_functions[] = { + { "expr", 1, 1, func_eval_expr }, + { "e", 1, 1, func_eval_expr }, + { "expr_formatted", 2, 3, func_eval_expr_formatted }, + { "ef", 2, 3, func_eval_expr_formatted }, + { "frame_num", 0, 0, func_frame_num }, + { "n", 0, 0, func_frame_num }, + { "gmtime", 0, 1, func_strftime }, + { "localtime", 0, 1, func_strftime }, + { "pts", 0, 3, func_pts } +}; + +static av_cold int init(AVFilterContext *ctx) +{ + QREncodeContext *qr = ctx->priv; + int ret; + + ff_draw_init(&qr->draw, AV_PIX_FMT_ARGB, FF_DRAW_PROCESS_ALPHA); + ff_draw_color(&qr->draw, &qr->draw_foreground_color, (const uint8_t *)&qr->foreground_color); + ff_draw_color(&qr->draw, &qr->draw_background_color, (const uint8_t *)&qr->background_color); + + av_lfg_init(&qr->lfg, av_get_random_seed()); + +#define RENDER_EXPR(var_name_, expr_name_) \ + ret = av_expr_parse_and_eval(&qr->var_values[VAR_##var_name_], \ + qr->expr_name_##_expr, \ + var_names, qr->var_values, \ + NULL, NULL, \ + fun2_names, fun2, \ + &qr->lfg, 0, ctx); \ + if (ret < 0) { \ + av_log(ctx, AV_LOG_ERROR, \ + "Could not evaluate expression '%s'\n", \ + qr->expr_name_##_expr); \ + return ret; \ + } + + RENDER_EXPR(w, rendered_qrcode_width); + RENDER_EXPR(W, rendered_padded_qrcode_width); + RENDER_EXPR(w, rendered_qrcode_width); + + qr->rendered_qrcode_width = qr->var_values[VAR_w]; + qr->rendered_padded_qrcode_width = qr->var_values[VAR_W]; + + if (qr->rendered_padded_qrcode_width < qr->rendered_qrcode_width) { + av_log(ctx, AV_LOG_ERROR, + "Resulting padded QR code width (%d) is lesser than the QR code width (%d)\n", + qr->rendered_padded_qrcode_width, qr->rendered_qrcode_width); + return AVERROR(EINVAL); + } + + qr->qrcode_width = -1; + + if (qr->textfile) { + if (qr->text) { + av_log(ctx, AV_LOG_ERROR, + "Both text and text file provided. Please provide only one\n"); + return AVERROR(EINVAL); + } + if ((ret = ff_load_textfile(ctx, (const char *)qr->textfile, &(qr->text), NULL)) < 0) + return ret; + } + + av_log(ctx, AV_LOG_VERBOSE, + "w:%d W:%d case_sensitive:%d level:%d\n", + (int)qr->rendered_qrcode_width, (int)qr->rendered_padded_qrcode_width, + qr->case_sensitive, qr->level); + + qr->expand_text = (FFExpandTextContext) { + .log_ctx = ctx, + .functions = expand_text_functions, + .functions_nb = FF_ARRAY_ELEMS(expand_text_functions) + }; + + av_bprint_init(&qr->expanded_text, 0, AV_BPRINT_SIZE_UNLIMITED); + + return 0; +} + +static av_cold void uninit(AVFilterContext *ctx) +{ + QREncodeContext *qr = ctx->priv; + + av_bprint_finalize(&qr->expanded_text, NULL); + av_freep(&qr->qrcode_data[0]); +} + +static int config_props(AVFilterLink *outlink) +{ + QREncodeContext *qr = outlink->src->priv; + + ff_draw_init(&qr->draw0, outlink->format, FF_DRAW_PROCESS_ALPHA); + ff_draw_color(&qr->draw0, &qr->draw0_background_color, (const uint8_t *)&qr->background_color); + + outlink->w = qr->rendered_padded_qrcode_width; + outlink->h = qr->rendered_padded_qrcode_width; + outlink->time_base = av_inv_q(qr->frame_rate); + outlink->frame_rate = qr->frame_rate; + + return 0; +} + +#ifdef DEBUG +static void show_qrcode(AVFilterContext *ctx, const QRcode *qrcode) +{ + int i, j; + char *line = av_malloc(qrcode->width + 1); + const char *p = qrcode->data; + + if (!line) + return; + for (i = 0; i < qrcode->width; i++) { + for (j = 0; j < qrcode->width; j++) + line[j] = (*p++)&1 ? '@' : ' '; + line[j] = 0; + av_log(ctx, AV_LOG_DEBUG, "%3d: %s\n", i, line); + } + av_free(line); +} +#endif + +static int draw_qrcode(AVFilterContext *ctx, AVFrame *picref) +{ + QREncodeContext *qr = ctx->priv; + struct SwsContext *sws = NULL; + QRcode *qrcode = NULL; + int i, j; + int ret; + int offset; + uint8_t *srcp; + uint8_t *dstp0, *dstp; + + if (!qr->expanded_text.str || qr->expanded_text.str[0] == 0) { + ff_fill_rectangle(&qr->draw0, &qr->draw0_background_color, + picref->data, picref->linesize, + 0, 0, qr->rendered_padded_qrcode_width, qr->rendered_padded_qrcode_width); + return 0; + } + + av_log(ctx, AV_LOG_DEBUG, "Encoding string '%s'\n", qr->expanded_text.str); + qrcode = QRcode_encodeString(qr->expanded_text.str, 1, qr->level, QR_MODE_8, + qr->case_sensitive); + if (!qrcode) { + ret = AVERROR(errno); + av_log(ctx, AV_LOG_ERROR, + "Failed to encode string with error \'%s\'\n", av_err2str(ret)); + goto error; + } + + av_log(ctx, AV_LOG_DEBUG, + "Encoded QR with width:%d version:%d\n", qrcode->width, qrcode->version); +#ifdef DEBUG + show_qrcode(ctx, (const QRcode *)qrcode); +#endif + + qr->var_values[VAR_q] = qrcode->width; + + if (qrcode->width != qr->qrcode_width) { + qr->qrcode_width = qrcode->width; + + // compute virtual non-rendered padded size + // Q/q = W/w + qr->padded_qrcode_width = + ((double)qr->rendered_padded_qrcode_width / qr->rendered_qrcode_width) * qrcode->width; + + ret = av_image_alloc(qr->qrcode_data, qr->qrcode_linesize, + qr->padded_qrcode_width, qr->padded_qrcode_width, + AV_PIX_FMT_ARGB, 16); + if (ret < 0) { + av_log(ctx, AV_LOG_ERROR, + "Failed to allocate image for QR code with width %d\n", qr->padded_qrcode_width); + goto error; + } + + ret = av_image_alloc(qr->qrcode_mask_data, qr->qrcode_mask_linesize, + qrcode->width, qrcode->width, + AV_PIX_FMT_GRAY8, 16); + if (ret < 0) { + av_log(ctx, AV_LOG_ERROR, "Failed to allocate image for QR code with width %d\n", qrcode->width); + goto error; + } + + } + + ff_fill_rectangle(&qr->draw, &qr->draw_background_color, + qr->qrcode_data, qr->qrcode_linesize, + 0, 0, qr->padded_qrcode_width, qr->padded_qrcode_width); + + /* fill mask */ + dstp0 = qr->qrcode_mask_data[0]; + srcp = qrcode->data; + + for (i = 0; i < qrcode->width; i++) { + dstp = dstp0; + for (j = 0; j < qrcode->width; j++) + *dstp++ = (*srcp++ & 1) ? 255 : 0; + dstp0 += qr->qrcode_mask_linesize[0]; + } + + offset = (qr->padded_qrcode_width - qr->qrcode_width) / 2; + ff_blend_mask(&qr->draw, &qr->draw_foreground_color, + qr->qrcode_data, qr->qrcode_linesize, + qr->padded_qrcode_width, qr->padded_qrcode_width, + qr->qrcode_mask_data[0], qr->qrcode_mask_linesize[0], qrcode->width, qrcode->width, + 3, 0, offset, offset); + + sws = sws_alloc_context(); + if (!sws) { + ret = AVERROR(ENOMEM); + goto error; + } + + av_opt_set_int(sws, "srcw", qr->padded_qrcode_width, 0); + av_opt_set_int(sws, "srch", qr->padded_qrcode_width, 0); + av_opt_set_int(sws, "src_format", AV_PIX_FMT_ARGB, 0); + av_opt_set_int(sws, "dstw", qr->rendered_padded_qrcode_width, 0); + av_opt_set_int(sws, "dsth", qr->rendered_padded_qrcode_width, 0); + av_opt_set_int(sws, "dst_format", picref->format, 0); + av_opt_set_int(sws, "sws_flags", SWS_POINT, 0); + + if ((ret = sws_init_context(sws, NULL, NULL)) < 0) + goto error; + + sws_scale(sws, + (const uint8_t *const *)&qr->qrcode_data, qr->qrcode_linesize, + 0, qr->padded_qrcode_width, + picref->data, picref->linesize); + +error: + sws_freeContext(sws); + QRcode_free(qrcode); + + return ret; +} + +static int request_frame(AVFilterLink *outlink) +{ + AVFilterContext *ctx = (AVFilterContext *)outlink->src; + QREncodeContext *qr = ctx->priv; + AVFrame *picref = + ff_get_video_buffer(outlink, qr->rendered_padded_qrcode_width, qr->rendered_padded_qrcode_width); + int ret; + + if (!picref) + return AVERROR(ENOMEM); + picref->sample_aspect_ratio = (AVRational) {1, 1}; + qr->var_values[VAR_n] = picref->pts = qr->pts++; + qr->var_values[VAR_t] = qr->pts * av_q2d(outlink->time_base); + + av_bprint_clear(&qr->expanded_text); + + switch (qr->expansion) { + case EXPANSION_NONE: + av_bprintf(&qr->expanded_text, "%s", qr->text); + break; + case EXPANSION_NORMAL: + if ((ret = ff_expand_text(&qr->expand_text, qr->text, &qr->expanded_text)) < 0) + return ret; + break; + } + + if ((ret = draw_qrcode(ctx, picref)) < 0) + return ret; + + return ff_filter_frame(outlink, picref); +} + +static int query_formats(AVFilterContext *ctx) +{ + enum AVPixelFormat pix_fmt; + FFDrawContext draw; + AVFilterFormats *fmts = NULL; + int ret; + + // this is needed to support both the no-draw and draw cases + // for the no-draw case we use FFDrawContext to write on the input picture ref + for (pix_fmt = 0; av_pix_fmt_desc_get(pix_fmt); pix_fmt++) + if (ff_draw_init(&draw, pix_fmt, 0) >= 0 && + sws_isSupportedOutput(pix_fmt) && + (ret = ff_add_format(&fmts, pix_fmt)) < 0) + return ret; + + return ff_set_common_formats(ctx, fmts); +} + +static const AVFilterPad qrencode_outputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_VIDEO, + .request_frame = request_frame, + .config_props = config_props, + } +}; + +const AVFilter ff_vsrc_qrencodesrc = { + .name = "qrencodesrc", + .description = NULL_IF_CONFIG_SMALL("Generate a QR code."), + .priv_size = sizeof(QREncodeContext), + .priv_class = &qrencodesrc_class, + .init = init, + .uninit = uninit, + .inputs = NULL, + FILTER_OUTPUTS(qrencode_outputs), + FILTER_QUERY_FUNC(query_formats), +}; -- 2.34.1 [-- Attachment #3: Type: text/plain, Size: 251 bytes --] _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [PATCH 2/2] lavfi: add qrencodesrc source 2023-12-09 18:13 ` Stefano Sabatini @ 2023-12-11 20:59 ` Kyle Swanson 2023-12-11 23:34 ` Stefano Sabatini 2023-12-17 18:00 ` Stefano Sabatini 1 sibling, 1 reply; 22+ messages in thread From: Kyle Swanson @ 2023-12-11 20:59 UTC (permalink / raw) To: FFmpeg development discussions and patches Hi, On Sat, Dec 9, 2023 at 10:14 AM Stefano Sabatini <stefasab@gmail.com> wrote: > > On date Thursday 2023-11-30 01:49:14 +0100, Stefano Sabatini wrote: > > --- > > configure | 4 + > > libavfilter/Makefile | 1 + > > libavfilter/allfilters.c | 1 + > > libavfilter/vsrc_qrencode.c | 435 ++++++++++++++++++++++++++++++++++++ > > 4 files changed, 441 insertions(+) > > create mode 100644 libavfilter/vsrc_qrencode.c > > Rev2 with padding and doc. libavfilter/vsrc_qrencode.c:42:10: fatal error: 'textutils.h' file not found #include "textutils.h" ^~~~~~~~~~~~~ 1 error generated. Thanks, Kyle _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [PATCH 2/2] lavfi: add qrencodesrc source 2023-12-11 20:59 ` Kyle Swanson @ 2023-12-11 23:34 ` Stefano Sabatini 0 siblings, 0 replies; 22+ messages in thread From: Stefano Sabatini @ 2023-12-11 23:34 UTC (permalink / raw) To: FFmpeg development discussions and patches On date Monday 2023-12-11 12:59:39 -0800, Kyle Swanson wrote: > Hi, > > On Sat, Dec 9, 2023 at 10:14 AM Stefano Sabatini <stefasab@gmail.com> wrote: > > > > On date Thursday 2023-11-30 01:49:14 +0100, Stefano Sabatini wrote: > > > --- > > > configure | 4 + > > > libavfilter/Makefile | 1 + > > > libavfilter/allfilters.c | 1 + > > > libavfilter/vsrc_qrencode.c | 435 ++++++++++++++++++++++++++++++++++++ > > > 4 files changed, 441 insertions(+) > > > create mode 100644 libavfilter/vsrc_qrencode.c > > > > Rev2 with padding and doc. > > libavfilter/vsrc_qrencode.c:42:10: fatal error: 'textutils.h' file not found > #include "textutils.h" > ^~~~~~~~~~~~~ > 1 error generated. This depends on: [PATCH 1/2] lavfi: introduce textutils _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [PATCH 2/2] lavfi: add qrencodesrc source 2023-12-09 18:13 ` Stefano Sabatini 2023-12-11 20:59 ` Kyle Swanson @ 2023-12-17 18:00 ` Stefano Sabatini 2023-12-24 17:43 ` Stefano Sabatini 1 sibling, 1 reply; 22+ messages in thread From: Stefano Sabatini @ 2023-12-17 18:00 UTC (permalink / raw) To: FFmpeg development discussions and patches [-- Attachment #1: Type: text/plain, Size: 517 bytes --] On date Saturday 2023-12-09 19:13:46 +0100, Stefano Sabatini wrote: > On date Thursday 2023-11-30 01:49:14 +0100, Stefano Sabatini wrote: > > --- > > configure | 4 + > > libavfilter/Makefile | 1 + > > libavfilter/allfilters.c | 1 + > > libavfilter/vsrc_qrencode.c | 435 ++++++++++++++++++++++++++++++++++++ > > 4 files changed, 441 insertions(+) > > create mode 100644 libavfilter/vsrc_qrencode.c > > Rev2 with padding and doc. Rev3 including a filter and fancy expressions. [-- Attachment #2: 0001-lavfi-add-qrencode-source-and-filter.patch --] [-- Type: text/x-diff, Size: 48329 bytes --] From 2e0e177f4e6e7a14d69f1e4d927b115b92b334d2 Mon Sep 17 00:00:00 2001 From: Stefano Sabatini <stefasab@gmail.com> Date: Tue, 28 Nov 2023 23:58:15 +0100 Subject: [PATCH] lavfi: add qrencode source and filter --- Changelog | 1 + configure | 7 + doc/filters.texi | 410 ++++++++++++++++++++ libavfilter/Makefile | 2 + libavfilter/allfilters.c | 2 + libavfilter/qrencode.c | 803 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 1225 insertions(+) create mode 100644 libavfilter/qrencode.c diff --git a/Changelog b/Changelog index 67ef92eb02..eaaabf633b 100644 --- a/Changelog +++ b/Changelog @@ -9,6 +9,7 @@ version <next>: - aap filter - demuxing, decoding, filtering, encoding, and muxing in the ffmpeg CLI now all run in parallel +- qrencode filter and qrencodesrc source version 6.1: - libaribcaption decoder diff --git a/configure b/configure index 7d2ee66000..e546308e5d 100755 --- a/configure +++ b/configure @@ -256,6 +256,7 @@ External library support: --enable-libopus enable Opus de/encoding via libopus [no] --enable-libplacebo enable libplacebo library [no] --enable-libpulse enable Pulseaudio input via libpulse [no] + --enable-libqrencode enable QR encode generation via libqrencode [no] --enable-librabbitmq enable RabbitMQ library [no] --enable-librav1e enable AV1 encoding via rav1e [no] --enable-librist enable RIST via librist [no] @@ -1879,6 +1880,7 @@ EXTERNAL_LIBRARY_LIST=" libopus libplacebo libpulse + libqrencode librabbitmq librav1e librist @@ -3771,6 +3773,8 @@ nnedi_filter_deps="gpl" ocr_filter_deps="libtesseract" ocv_filter_deps="libopencv" openclsrc_filter_deps="opencl" +qrencode_filter_deps="libqrencode" +qrencodesrc_filter_deps="libqrencode" overlay_opencl_filter_deps="opencl" overlay_qsv_filter_deps="libmfx" overlay_qsv_filter_select="qsvvpp" @@ -6816,6 +6820,7 @@ enabled libopus && { } enabled libplacebo && require_pkg_config libplacebo "libplacebo >= 4.192.0" libplacebo/vulkan.h pl_vulkan_create enabled libpulse && require_pkg_config libpulse libpulse pulse/pulseaudio.h pa_context_new +enabled libqrencode && require_pkg_config libqrencode libqrencode qrencode.h QRcode_encodeString enabled librabbitmq && require_pkg_config librabbitmq "librabbitmq >= 0.7.1" amqp.h amqp_new_connection enabled librav1e && require_pkg_config librav1e "rav1e >= 0.5.0" rav1e.h rav1e_context_new enabled librist && require_pkg_config librist "librist >= 0.2.7" librist/librist.h rist_receiver_create @@ -7651,6 +7656,8 @@ enabled mcdeint_filter && prepend avfilter_deps "avcodec" enabled movie_filter && prepend avfilter_deps "avformat avcodec" enabled pan_filter && prepend avfilter_deps "swresample" enabled pp_filter && prepend avfilter_deps "postproc" +enabled qrencode_filter && prepend_avfilter_deps "swscale" +enabled qrencodesrc_filter && prepend_avfilter_deps "swscale" enabled removelogo_filter && prepend avfilter_deps "avformat avcodec swscale" enabled sab_filter && prepend avfilter_deps "swscale" enabled scale_filter && prepend avfilter_deps "swscale" diff --git a/doc/filters.texi b/doc/filters.texi index 6d00ba2c3f..7cbf936ba1 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -20088,6 +20088,286 @@ qp=2+2*sin(PI*qp) @end example @end itemize +@section qrencode +Generate a QR code using the libqrencode library (see +@url{https://fukuchi.org/works/qrencode/}), and overlay it on top of the current +frame. + +To enable the compilation of this filter, you need to configure FFmpeg with with +@code{--enable-libqrencode}. + +The QR code is generated from the provided text or text pattern. The +corresponding QR code is scaled and overlayed into the video output according to +the specified options. + +In case no text is specified, no QR code is overlaied. + +This filter accepts the following options: + +@table @option + +@item qrcode_width, q +@item padded_qrcode_width, Q +Specify an expression for the width of the rendered QR code, with and without +padding. The @var{qrcode_width} expression can reference the value set by the +@var{padded_qrcode_width} expression, and vice versa. +By default @var{padded_qrcode_width} is set to @var{qrcode_width}, meaning that +there is no padding. + +These expressions are evaluated for each new frame. + +See the @ref{qrencode_expressions,,qrencode Expressions} section for details. + +@item x +@item y +Specify an expression for positioning the padded QR code top-left corner. The +@var{x} expression can reference the value set by the @var{y} expression, and +vice. + +By default @var{x} and @var{y} are set set to @var{0}, meaning that the QR code +is placed in the top left corner of the input. + +These expressions are evaluated for each new frame. + +See the @ref{qrencode_expressions,,qrencode Expressions} section for details. + +@item case_sensitive, cs +Instruct libqrencode to use case sensitive encoding. This is enabled by +default. This can be disabled to reduce the QR encoding size. + +@item level, l +Specify the QR encoding error correction level. With an higher correction level, +the encoding size will increase but the code will be more robust to corruption. +Lower level is @var{L}. + +It accepts the following values: +@table @samp +@item L +@item M +@item Q +@item H +@end table + +@item expansion +Select how the input text is expanded. Can be either @code{none}, or +@code{normal} (default). See the @ref{qrencode_text_expansion,,qrencode Text expansion} +section below for details. + +@item text +@item textfile +Define the text to be rendered. In case neither is specified, no QR is encoded +(just an empty colored frame). + +In case expansion is enabled, the text is treated as a text template, using the +drawtext expansion mechanism. See section below. + +@item background_color, bc +@item foreground_color, fc +Set the QR code and background color. The default value of +@var{foreground_color} is "black", the default value of @var{background_color} +is "white". + +For the syntax of the color options, check the @ref{color syntax,,"Color" +section in the ffmpeg-utils manual,ffmpeg-utils}. +@end table + +@anchor{qrencode_expressions} +@subsection qrencode Expressions + +The expressions set by the options contain the following constants and functions. + +@table @option +@item dar +input display aspect ratio, it is the same as (@var{w} / @var{h}) * @var{sar} + +@item duration +the current frame's duration, in seconds + +@item hsub +@item vsub +horizontal and vertical chroma subsample values. For example for the +pixel format "yuv422p" @var{hsub} is 2 and @var{vsub} is 1. + +@item main_h, H +the input height + +@item main_w, W +the input width + +@item n +the number of input frame, starting from 0 + +@item pict_type +a number representing the picture type + +@item qr_w, w +the width of the encoded QR code + +@item rendered_qr_w, q +@item rendered_padded_qr_w, Q +the width of the rendered QR code, without and without padding. + +These parameters allow the @var{q} and @var{Q} expressions to refer to each +other, so you can for example specify @code{q=3/4*Q}. + +@item rand(min, max) +return a random number included between @var{min} and @var{max} + +@item sar +the input sample aspect ratio + +@item t +timestamp expressed in seconds, NAN if the input timestamp is unknown + +@item x +@item y +the x and y offset coordinates where the text is drawn. + +These parameters allow the @var{x} and @var{y} expressions to refer to each +other, so you can for example specify @code{y=x/dar}. +@end table + +@anchor{qrencode_text_expansion} +@subsection qrencode Text expansion + +If @option{expansion} is set to @code{none}, the text is printed verbatim. + +If @option{expansion} is set to @code{normal} (which is the default), +the following expansion mechanism is used. + +The backslash character @samp{\}, followed by any character, always expands to +the second character. + +Sequences 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 +filtergraph description, and possibly also for the shell, that makes up to four +levels of escaping; using a text file with the @option{textfile} option avoids +these problems. + +The following functions are available: + +@table @command +@item n, frame_num +return the frame number + +@item pts +Return the presentation timestamp of the current frame. + +It can take up to two arguments. + +The first argument is the format of the timestamp; it defaults to @code{flt} for +seconds as a decimal number with microsecond accuracy; @code{hms} stands for a +formatted @var{[-]HH:MM:SS.mmm} timestamp with millisecond accuracy. +@code{gmtime} stands for the timestamp of the frame formatted as UTC time; +@code{localtime} stands for the timestamp of the frame formatted as local time +zone time. If the format is set to @code{hms24hh}, the time is formatted in 24h +format (00-23). + +The second argument is an offset added to the timestamp. + +If the format is set to @code{localtime} or @code{gmtime}, a third argument may +be supplied: a @code{strftime} C function format string. By default, +@var{YYYY-MM-DD HH:MM:SS} format will be used. + +@item expr, e +Evaluate the expression's value and output as a double. + +It must take one argument specifying the expression to be evaluated, accepting +the constants and functions defined in @ref{qrencode_expressions}. + +@item expr_formatted, ef +Evaluate the expression's value and output as a formatted string. + +The first argument is the expression to be evaluated, just as for the @var{expr} function. +The second argument specifies the output format. Allowed values are @samp{x}, +@samp{X}, @samp{d} and @samp{u}. They are treated exactly as in the +@code{printf} function. +The third parameter is optional and sets the number of positions taken by the output. +It can be used to add padding with zeros from the left. + +@item gmtime +The time at which the filter is running, expressed in UTC. +It can accept an argument: a @code{strftime} C function format string. +The format string is extended to support the variable @var{%[1-6]N} +which prints fractions of the second with optionally specified number of digits. + +@item localtime +The time at which the filter is running, expressed in the local time zone. +It can accept an argument: a @code{strftime} C function format string. +The format string is extended to support the variable @var{%[1-6]N} +which prints fractions of the second with optionally specified number of digits. + +@item rand(min, max) +return a random number included between @var{min} and @var{max} +@end table + +@subsection Examples + +@itemize +@item +Generate a QR code encoding the specified text with the default size, overalaid +in the top left corner of the input video, with the default size: +@example +qrencode=text=www.ffmpeg.org +@end example + +@item +Same as below, but select blue on pink colors: +@example +qrencode=text=www.ffmpeg.org:bc=pink@@0.5:fc=blue +@end example + +@item +Place the QR code in the bottom right corner of the input video: +@example +qrencode=text=www.ffmpeg.org:x=W-Q:y=H-Q +@end example + +@item +Generate a QR code with width of 200 pixels and padding, making the padded width +4/3 of the QR code width: +@example +qrencode=text=www.ffmpeg.org:q=200:Q=4/3*q +@end example + +@item +Generate a QR code with padded width of 200 pixels and padding, making the QR +code width 3/4 of the padded width: +@example +qrencode=text=www.ffmpeg.org:Q=200:q=3/4*Q +@end example + +@item +Make the QR code a fraction of the input video width: +@example +qrencode=text=www.ffmpeg.org:q=W/5 +@end example + +@item +Generate a QR code encoding the frame number: +@example +qrencode=text=%@{n@} +@end example + +@item +Generate a QR code encoding the GMT timestamp: +@example +qrencode=text=%@{gmtime@} +@end example + +@item +Generate a QR code encoding the timestamp expressed as a float: +@example +qrencode=text=%@{pts@} +@end example + +@end itemize + @section random Flush video frames from internal cache of frames into a random order. @@ -28664,6 +28944,136 @@ ffplay -f lavfi life=s=300x200:mold=10:r=60:ratio=0.1:death_color=#C83232:life_c @end example @end itemize +@section qrencodesrc + +Generate a QR code using the libqrencode library (see +@url{https://fukuchi.org/works/qrencode/}). + +To enable the compilation of this source, you need to configure FFmpeg with with +@code{--enable-libqrencode}. + +The QR code is generated from the provided text or text pattern. The +corresponding QR code is scaled and put in the video output according to the +specified output size options. + +In case no text is specified, the QR code is not generated, but an empty colored +output is returned instead. + +This source accepts the following options: + +@table @option + +@item qrcode_width, q +@item padded_qrcode_width, Q +Specify an expression for the width of the rendered QR code, with and without +padding. The @var{qrcode_width} expression can reference the value set by the +@var{padded_qrcode_width} expression, and vice versa. +By default @var{padded_qrcode_width} is set to @var{qrcode_width}, meaning that +there is no padding. + +These expressions are evaluated only once, when initializing the source. +See the @ref{qrencode_expressions,,qrencode Expressions} section for details. + +Note that some of the constants are missing for the source (for example the +@var{x} or @var{t} or ¸@var{n}), since they only makes sense when evaluating the +expression for each frame rather than at initialization time. + +@item rate, r +Specify the frame rate of the sourced video, as the number of frames +generated per second. It has to be a string in the format +@var{frame_rate_num}/@var{frame_rate_den}, an integer number, a floating point +number or a valid video frame rate abbreviation. The default value is +"25". + +@item case_sensitive, cs +Instruct libqrencode to use case sensitive encoding. This is enabled by +default. This can be disabled to reduce the QR encoding size. + +@item level, l +Specify the QR encoding error correction level. With an higher correction level, +the encoding size will increase but the code will be more robust to corruption. +Lower level is @var{L}. + +It accepts the following values: +@table @samp +@item L +@item M +@item Q +@item H +@end table + +@item expansion +Select how the input text is expanded. Can be either @code{none}, or +@code{normal} (default). See the @ref{qrencode_text_expansion,,qrencode Text expansion} +section for details. + +@item text +@item textfile +Define the text to be rendered. In case neither is specified, no QR is encoded +(just an empty colored frame). + +In case expansion is enabled, the text is treated as a text template, using the +drawtext expansion mechanism. See section below. + +@item background_color, bc +@item foreground_color, fc +Set the QR code and background color. The default value of +@var{foreground_color} is "black", the default value of @var{background_color} +is "white". + +For the syntax of the color options, check the @ref{color syntax,,"Color" +section in the ffmpeg-utils manual,ffmpeg-utils}. +@end table + +@subsection Examples + +@itemize +@item +Generate a QR code encoding the specified text with the default size: +@example +qrencodesrc=text=www.ffmpeg.org +@end example + +@item +Same as below, but select blue on pink colors: +@example +qrencodesrc=text=www.ffmpeg.org:bc=pink:fc=blue +@end example + +@item +Generate a QR code with width of 200 pixels and padding, making the padded width +4/3 of the QR code width: +@example +qrencodesrc=text=www.ffmpeg.org:q=200:Q=4/3*q +@end example + +@item +Generate a QR code with padded width of 200 pixels and padding, making the QR +code width 3/4 of the padded width: +@example +qrencodesrc=text=www.ffmpeg.org:Q=200:q=3/4*Q +@end example + +@item +Generate a QR code encoding the frame number: +@example +qrencodesrc=text=%@{n@} +@end example + +@item +Generate a QR code encoding the GMT timestamp: +@example +qrencodesrc=text=%@{gmtime@} +@end example + +@item +Generate a QR code encoding the timestamp expressed as a float: +@example +qrencodesrc=text=%@{pts@} +@end example + +@end itemize + @anchor{allrgb} @anchor{allyuv} @anchor{color} diff --git a/libavfilter/Makefile b/libavfilter/Makefile index 1f9bbcc1af..ec0dd848d8 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -598,6 +598,8 @@ OBJS-$(CONFIG_NULLSRC_FILTER) += vsrc_testsrc.o OBJS-$(CONFIG_OPENCLSRC_FILTER) += vf_program_opencl.o opencl.o OBJS-$(CONFIG_PAL75BARS_FILTER) += vsrc_testsrc.o OBJS-$(CONFIG_PAL100BARS_FILTER) += vsrc_testsrc.o +OBJS-$(CONFIG_QRENCODE_FILTER) += qrencode.o textutils.o +OBJS-$(CONFIG_QRENCODESRC_FILTER) += qrencode.o textutils.o OBJS-$(CONFIG_RGBTESTSRC_FILTER) += vsrc_testsrc.o OBJS-$(CONFIG_SIERPINSKI_FILTER) += vsrc_sierpinski.o OBJS-$(CONFIG_SMPTEBARS_FILTER) += vsrc_testsrc.o diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c index ed7c32be94..a339c0518f 100644 --- a/libavfilter/allfilters.c +++ b/libavfilter/allfilters.c @@ -411,6 +411,7 @@ extern const AVFilter ff_vf_pseudocolor; extern const AVFilter ff_vf_psnr; extern const AVFilter ff_vf_pullup; extern const AVFilter ff_vf_qp; +extern const AVFilter ff_vf_qrencode; extern const AVFilter ff_vf_random; extern const AVFilter ff_vf_readeia608; extern const AVFilter ff_vf_readvitc; @@ -560,6 +561,7 @@ extern const AVFilter ff_vsrc_mandelbrot; extern const AVFilter ff_vsrc_mptestsrc; extern const AVFilter ff_vsrc_nullsrc; extern const AVFilter ff_vsrc_openclsrc; +extern const AVFilter ff_vsrc_qrencodesrc; extern const AVFilter ff_vsrc_pal75bars; extern const AVFilter ff_vsrc_pal100bars; extern const AVFilter ff_vsrc_rgbtestsrc; diff --git a/libavfilter/qrencode.c b/libavfilter/qrencode.c new file mode 100644 index 0000000000..13954808db --- /dev/null +++ b/libavfilter/qrencode.c @@ -0,0 +1,803 @@ +/* + * 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 QR encoder source and filter. + * + * A QR code (quick-response code) is a type of two-dimensional matrix + * barcode, invented in 1994, by Japanese company Denso Wave for + * labelling automobile parts. + * + * This source uses the libqrencode library to generate QR code: + * https://fukuchi.org/works/qrencode/ + */ + +//#define DEBUG + +#include "config_components.h" + +#include "libavutil/internal.h" +#include "libavutil/imgutils.h" +#include "libavutil/opt.h" +#include "libavutil/lfg.h" +#include "libavutil/random_seed.h" + +#include "avfilter.h" +#include "drawutils.h" +#include "internal.h" +#include "formats.h" +#include "textutils.h" +#include "video.h" +#include "libswscale/swscale.h" + +#include <qrencode.h> + +enum var_name { + VAR_dar, + VAR_duration, + VAR_hsub, VAR_vsub, + VAR_main_h, VAR_H, + VAR_main_w, VAR_W, + VAR_n, + VAR_pict_type, + VAR_qr_w, VAR_w, + VAR_rendered_padded_qr_w, VAR_Q, + VAR_rendered_qr_w, VAR_q, + VAR_sar, + VAR_t, + VAR_x, + VAR_y, + VAR_VARS_NB +}; + +static const char *const var_names[] = { + "dar", + "duration", + "hsub", "vsub", + "main_h", "H", ///< height of the input video + "main_w", "W", ///< width of the input video + "n", ///< number of frame + "pict_type", + "qr_w", "w", ///< width of the QR code + "rendered_padded_qr_w", "Q", ///< width of the rendered QR code + "rendered_qr_w", "q", ///< width of the rendered QR code + "sar", + "t", ///< timestamp expressed in seconds + "x", + "y", + NULL +}; + +#define V(name_) qr->var_values[VAR_##name_] + +enum Expansion { + EXPANSION_NONE, + EXPANSION_NORMAL +}; + +typedef struct QREncodeContext { + const AVClass *class; + + char is_source; + char *x_expr; + char *y_expr; + AVExpr *x_pexpr, *y_pexpr; + + char *rendered_qrcode_width_expr; + char *rendered_padded_qrcode_width_expr; + AVExpr *rendered_qrcode_width_pexpr, *rendered_padded_qrcode_width_pexpr; + + int rendered_qrcode_width; + int rendered_padded_qrcode_width; + + unsigned char *text; + char *textfile; + uint64_t pts; + + int level; + char case_sensitive; + + uint8_t foreground_color[4]; + uint8_t background_color[4]; + + FFDrawContext draw; + FFDrawColor draw_foreground_color; ///< foreground color + FFDrawColor draw_background_color; ///< background color + + /* these are only used when nothing must be encoded */ + FFDrawContext draw0; + FFDrawColor draw0_background_color; ///< background color + + uint8_t *qrcode_data[4]; + int qrcode_linesize[4]; + uint8_t *qrcode_mask_data[4]; + int qrcode_mask_linesize[4]; + + /* only used for filter to contain scaled image to blend on top of input */ + uint8_t *rendered_qrcode_data[4]; + int rendered_qrcode_linesize[4]; + + int qrcode_width; + int padded_qrcode_width; + + AVRational frame_rate; + + int expansion; ///< expansion mode to use for the text + FFExpandTextContext expand_text; ///< expand text in case expansion is enabled + AVBPrint expanded_text; ///< used to contain the expanded text + + double var_values[VAR_VARS_NB]; + AVLFG lfg; ///< random generator + AVDictionary *metadata; +} QREncodeContext; + +#define OFFSET(x) offsetof(QREncodeContext, x) +#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM +#define TFLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM|AV_OPT_FLAG_RUNTIME_PARAM + +#define COMMON_OPTIONS \ + { "qrcode_width", "set rendered QR code width expression", OFFSET(rendered_qrcode_width_expr), AV_OPT_TYPE_STRING, {.str = "64"}, 0, INT_MAX, FLAGS }, \ + { "q", "set rendered QR code width expression", OFFSET(rendered_qrcode_width_expr), AV_OPT_TYPE_STRING, {.str = "64"}, 0, INT_MAX, FLAGS }, \ + { "padded_qrcode_width", "set rendered padded QR code width expression", OFFSET(rendered_padded_qrcode_width_expr), AV_OPT_TYPE_STRING, {.str = "q"}, 0, INT_MAX, FLAGS }, \ + { "Q", "set rendered padded QR code width expression", OFFSET(rendered_padded_qrcode_width_expr), AV_OPT_TYPE_STRING, {.str = "q"}, 0, INT_MAX, FLAGS }, \ + { "case_sensitive", "generate code which is case sensitive", OFFSET(case_sensitive), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, FLAGS }, \ + { "cs", "generate code which is case sensitive", OFFSET(case_sensitive), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, FLAGS }, \ + \ + { "level", "error correction level, lowest is L", OFFSET(level), AV_OPT_TYPE_INT, { .i64 = AVCOL_SPC_UNSPECIFIED }, 0, QR_ECLEVEL_H, .flags = FLAGS, "level"}, \ + { "l", "error correction level, lowest is L", OFFSET(level), AV_OPT_TYPE_INT, { .i64 = AVCOL_SPC_UNSPECIFIED }, 0, QR_ECLEVEL_H, .flags = FLAGS, "level"}, \ + { "L", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = QR_ECLEVEL_L }, 0, 0, FLAGS, "level" }, \ + { "M", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = QR_ECLEVEL_M }, 0, 0, FLAGS, "level" }, \ + { "Q", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = QR_ECLEVEL_Q }, 0, 0, FLAGS, "level" }, \ + { "H", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = QR_ECLEVEL_H }, 0, 0, FLAGS, "level" }, \ + \ + {"expansion", "set the expansion mode", OFFSET(expansion), AV_OPT_TYPE_INT, {.i64=EXPANSION_NORMAL}, 0, 2, FLAGS, "expansion"}, \ + {"none", "set no expansion", OFFSET(expansion), AV_OPT_TYPE_CONST, {.i64 = EXPANSION_NONE}, 0, 0, FLAGS, "expansion"}, \ + {"normal", "set normal expansion", OFFSET(expansion), AV_OPT_TYPE_CONST, {.i64 = EXPANSION_NORMAL}, 0, 0, FLAGS, "expansion"}, \ + \ + { "foreground_color", "set QR foreground color", OFFSET(foreground_color), AV_OPT_TYPE_COLOR, {.str = "black"}, 0, 0, FLAGS }, \ + { "fc", "set QR foreground color", OFFSET(foreground_color), AV_OPT_TYPE_COLOR, {.str = "black"}, 0, 0, FLAGS }, \ + { "background_color", "set QR background color", OFFSET(background_color), AV_OPT_TYPE_COLOR, {.str = "white"}, 0, 0, FLAGS }, \ + { "bc", "set QR background color", OFFSET(background_color), AV_OPT_TYPE_COLOR, {.str = "white"}, 0, 0, FLAGS }, \ + \ + {"text", "set text to encode", OFFSET(text), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, FLAGS}, \ + {"textfile", "set text file to encode", OFFSET(textfile), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, FLAGS}, \ + +static const char *const fun2_names[] = { + "rand" +}; + +static double drand(void *opaque, double min, double max) +{ + return min + (max-min) / UINT_MAX * av_lfg_get(opaque); +} + +static const ff_eval_func2 fun2[] = { + drand, + NULL +}; + +static int func_pts(void *ctx, AVBPrint *bp, const char *function_name, + unsigned argc, char **argv) +{ + QREncodeContext *qr = ((AVFilterContext *)ctx)->priv; + const char *fmt; + const char *strftime_fmt = NULL; + const char *delta = NULL; + double t = qr->var_values[VAR_t]; + + // argv: pts, FMT, [DELTA, strftime_fmt] + + fmt = argc >= 1 ? argv[0] : "flt"; + if (argc >= 2) { + delta = argv[1]; + } + if (argc >= 3) { + strftime_fmt = argv[2]; + } + + return ff_print_pts(ctx, bp, t, delta, fmt, strftime_fmt); +} + +static int func_frame_num(void *ctx, AVBPrint *bp, const char *function_name, + unsigned argc, char **argv) +{ + QREncodeContext *qr = ((AVFilterContext *)ctx)->priv; + + av_bprintf(bp, "%d", (int)V(n)); + 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) +{ + QREncodeContext *qr = ((AVFilterContext *)ctx)->priv; + + return ff_print_eval_expr(ctx, bp, argv[0], + fun2_names, fun2, + var_names, qr->var_values, &qr->lfg); +} + +static int func_eval_expr_formatted(void *ctx, AVBPrint *bp, const char *function_name, + unsigned argc, char **argv) +{ + QREncodeContext *qr = ((AVFilterContext *)ctx)->priv; + int ret; + int positions = -1; + + /* + * argv[0] expression to be converted to `int` + * argv[1] format: 'x', 'X', 'd' or 'u' + * argv[2] positions printed (optional) + */ + + if (argc == 3) { + ret = sscanf(argv[2], "%u", &positions); + if (ret != 1) { + av_log(ctx, AV_LOG_ERROR, "expr_int_format(): Invalid number of positions" + " to print: '%s'\n", argv[2]); + return AVERROR(EINVAL); + } + } + + return ff_print_formatted_eval_expr(ctx, bp, argv[0], + fun2_names, fun2, + var_names, qr->var_values, + &qr->lfg, + argv[1][0], positions); +} + +static FFExpandTextFunction expand_text_functions[] = { + { "expr", 1, 1, func_eval_expr }, + { "e", 1, 1, func_eval_expr }, + { "expr_formatted", 2, 3, func_eval_expr_formatted }, + { "ef", 2, 3, func_eval_expr_formatted }, + { "frame_num", 0, 0, func_frame_num }, + { "n", 0, 0, func_frame_num }, + { "gmtime", 0, 1, func_strftime }, + { "localtime", 0, 1, func_strftime }, + { "pts", 0, 3, func_pts } +}; + +static av_cold int init(AVFilterContext *ctx) +{ + QREncodeContext *qr = ctx->priv; + int ret; + + av_lfg_init(&qr->lfg, av_get_random_seed()); + + qr->qrcode_width = -1; + qr->rendered_padded_qrcode_width = -1; + + if (qr->textfile) { + if (qr->text) { + av_log(ctx, AV_LOG_ERROR, + "Both text and text file provided. Please provide only one\n"); + return AVERROR(EINVAL); + } + if ((ret = ff_load_textfile(ctx, (const char *)qr->textfile, &(qr->text), NULL)) < 0) + return ret; + } + + qr->expand_text = (FFExpandTextContext) { + .log_ctx = ctx, + .functions = expand_text_functions, + .functions_nb = FF_ARRAY_ELEMS(expand_text_functions) + }; + + av_bprint_init(&qr->expanded_text, 0, AV_BPRINT_SIZE_UNLIMITED); + + return 0; +} + +static av_cold void uninit(AVFilterContext *ctx) +{ + QREncodeContext *qr = ctx->priv; + + av_expr_free(qr->x_pexpr); + av_expr_free(qr->y_pexpr); + + av_bprint_finalize(&qr->expanded_text, NULL); + + av_freep(&qr->qrcode_data[0]); + av_freep(&qr->rendered_qrcode_data[0]); + av_freep(&qr->qrcode_mask_data[0]); +} + +#ifdef DEBUG +static void show_qrcode(AVFilterContext *ctx, const QRcode *qrcode) +{ + int i, j; + char *line = av_malloc(qrcode->width + 1); + const char *p = qrcode->data; + + if (!line) + return; + for (i = 0; i < qrcode->width; i++) { + for (j = 0; j < qrcode->width; j++) + line[j] = (*p++)&1 ? '@' : ' '; + line[j] = 0; + av_log(ctx, AV_LOG_DEBUG, "%3d: %s\n", i, line); + } + av_free(line); +} +#endif + +static int draw_qrcode(AVFilterContext *ctx, AVFrame *frame) +{ + QREncodeContext *qr = ctx->priv; + struct SwsContext *sws = NULL; + QRcode *qrcode = NULL; + int i, j; + char qrcode_width_changed; + int ret; + int offset; + uint8_t *srcp; + uint8_t *dstp0, *dstp; + + av_bprint_clear(&qr->expanded_text); + + switch (qr->expansion) { + case EXPANSION_NONE: + av_bprintf(&qr->expanded_text, "%s", qr->text); + break; + case EXPANSION_NORMAL: + if ((ret = ff_expand_text(&qr->expand_text, qr->text, &qr->expanded_text)) < 0) + return ret; + break; + } + + if (!qr->expanded_text.str || qr->expanded_text.str[0] == 0) { + if (qr->is_source) { + ff_fill_rectangle(&qr->draw0, &qr->draw0_background_color, + frame->data, frame->linesize, + 0, 0, qr->rendered_padded_qrcode_width, qr->rendered_padded_qrcode_width); + } + + return 0; + } + + av_log(ctx, AV_LOG_DEBUG, "Encoding string '%s'\n", qr->expanded_text.str); + qrcode = QRcode_encodeString(qr->expanded_text.str, 1, qr->level, QR_MODE_8, + qr->case_sensitive); + if (!qrcode) { + ret = AVERROR(errno); + av_log(ctx, AV_LOG_ERROR, + "Failed to encode string with error \'%s\'\n", av_err2str(ret)); + goto end; + } + + av_log(ctx, AV_LOG_DEBUG, + "Encoded QR with width:%d version:%d\n", qrcode->width, qrcode->version); +#ifdef DEBUG + show_qrcode(ctx, (const QRcode *)qrcode); +#endif + + qrcode_width_changed = qr->qrcode_width != qrcode->width; + qr->qrcode_width = qrcode->width; + + // realloc mask if needed + if (qrcode_width_changed) { + av_freep(&qr->qrcode_mask_data[0]); + ret = av_image_alloc(qr->qrcode_mask_data, qr->qrcode_mask_linesize, + qrcode->width, qrcode->width, + AV_PIX_FMT_GRAY8, 16); + if (ret < 0) { + av_log(ctx, AV_LOG_ERROR, + "Failed to allocate image for QR code with width %d\n", qrcode->width); + goto end; + } + } + + /* fill mask */ + dstp0 = qr->qrcode_mask_data[0]; + srcp = qrcode->data; + + for (i = 0; i < qrcode->width; i++) { + dstp = dstp0; + for (j = 0; j < qrcode->width; j++) + *dstp++ = (*srcp++ & 1) ? 255 : 0; + dstp0 += qr->qrcode_mask_linesize[0]; + } + + if (qr->is_source) { + if (qrcode_width_changed) { + /* realloc padded image */ + + // compute virtual non-rendered padded size + // Q/q = W/w + qr->padded_qrcode_width = + ((double)qr->rendered_padded_qrcode_width / qr->rendered_qrcode_width) * qrcode->width; + + av_freep(&qr->qrcode_data[0]); + ret = av_image_alloc(qr->qrcode_data, qr->qrcode_linesize, + qr->padded_qrcode_width, qr->padded_qrcode_width, + AV_PIX_FMT_ARGB, 16); + if (ret < 0) { + av_log(ctx, AV_LOG_ERROR, + "Failed to allocate image for QR code with width %d\n", + qr->padded_qrcode_width); + goto end; + } + } + + /* fill padding */ + ff_fill_rectangle(&qr->draw, &qr->draw_background_color, + qr->qrcode_data, qr->qrcode_linesize, + 0, 0, qr->padded_qrcode_width, qr->padded_qrcode_width); + + /* blend mask */ + offset = (qr->padded_qrcode_width - qr->qrcode_width) / 2; + ff_blend_mask(&qr->draw, &qr->draw_foreground_color, + qr->qrcode_data, qr->qrcode_linesize, + qr->padded_qrcode_width, qr->padded_qrcode_width, + qr->qrcode_mask_data[0], qr->qrcode_mask_linesize[0], qrcode->width, qrcode->width, + 3, 0, offset, offset); + + /* scale padded QR over the frame */ + sws = sws_alloc_context(); + if (!sws) { + ret = AVERROR(ENOMEM); + goto end; + } + + av_opt_set_int(sws, "srcw", qr->padded_qrcode_width, 0); + av_opt_set_int(sws, "srch", qr->padded_qrcode_width, 0); + av_opt_set_int(sws, "src_format", AV_PIX_FMT_ARGB, 0); + av_opt_set_int(sws, "dstw", qr->rendered_padded_qrcode_width, 0); + av_opt_set_int(sws, "dsth", qr->rendered_padded_qrcode_width, 0); + av_opt_set_int(sws, "dst_format", frame->format, 0); + av_opt_set_int(sws, "sws_flags", SWS_POINT, 0); + + if ((ret = sws_init_context(sws, NULL, NULL)) < 0) + goto end; + + sws_scale(sws, + (const uint8_t *const *)&qr->qrcode_data, qr->qrcode_linesize, + 0, qr->padded_qrcode_width, + frame->data, frame->linesize); + } else { +#define EVAL_EXPR(name_) \ + av_expr_eval(qr->name_##_pexpr, qr->var_values, &qr->lfg); + + V(qr_w) = V(w) = qrcode->width; + + V(rendered_qr_w) = V(q) = EVAL_EXPR(rendered_qrcode_width); + V(rendered_padded_qr_w) = V(Q) = EVAL_EXPR(rendered_padded_qrcode_width); + /* It is necessary if q is expressed from Q */ + V(rendered_qr_w) = V(q) = EVAL_EXPR(rendered_qrcode_width); + + V(x) = EVAL_EXPR(x); + V(y) = EVAL_EXPR(y); + /* It is necessary if x is expressed from y */ + V(x) = EVAL_EXPR(x); + + av_log(ctx, AV_LOG_DEBUG, + "Rendering QR code with values n:%d w:%d q:%d Q:%d x:%d y:%d t:%f\n", + (int)V(n), (int)V(w), (int)V(q), (int)V(Q), (int)V(x), (int)V(y), V(t)); + + /* blend rectangle over the target */ + ff_blend_rectangle(&qr->draw, &qr->draw_background_color, + frame->data, frame->linesize, frame->width, frame->height, + V(x), V(y), V(Q), V(Q)); + + if (V(q) != qr->rendered_qrcode_width) { + av_freep(&qr->rendered_qrcode_data[0]); + qr->rendered_qrcode_width = V(q); + + ret = av_image_alloc(qr->rendered_qrcode_data, qr->rendered_qrcode_linesize, + qr->rendered_qrcode_width, qr->rendered_qrcode_width, + AV_PIX_FMT_GRAY8, 16); + if (ret < 0) { + av_log(ctx, AV_LOG_ERROR, + "Failed to allocate image for rendered QR code with width %d\n", + qr->rendered_qrcode_width); + goto end; + } + } + + /* scale mask */ + sws = sws_alloc_context(); + if (!sws) { + ret = AVERROR(ENOMEM); + goto end; + } + + av_opt_set_int(sws, "srcw", qr->qrcode_width, 0); + av_opt_set_int(sws, "srch", qr->qrcode_width, 0); + av_opt_set_int(sws, "src_format", AV_PIX_FMT_GRAY8, 0); + av_opt_set_int(sws, "dstw", qr->rendered_qrcode_width, 0); + av_opt_set_int(sws, "dsth", qr->rendered_qrcode_width, 0); + av_opt_set_int(sws, "dst_format", AV_PIX_FMT_GRAY8, 0); + av_opt_set_int(sws, "sws_flags", SWS_POINT, 0); + + if ((ret = sws_init_context(sws, NULL, NULL)) < 0) + goto end; + + sws_scale(sws, + (const uint8_t *const *)&qr->qrcode_mask_data, qr->qrcode_mask_linesize, + 0, qr->qrcode_width, + qr->rendered_qrcode_data, qr->rendered_qrcode_linesize); + + /* blend mask over the input frame */ + offset = (V(Q) - V(q)) / 2; + ff_blend_mask(&qr->draw, &qr->draw_foreground_color, + frame->data, frame->linesize, frame->width, frame->height, + qr->rendered_qrcode_data[0], qr->rendered_qrcode_linesize[0], + qr->rendered_qrcode_width, qr->rendered_qrcode_width, + 3, 0, V(x) + offset, V(y) + offset); + } + +end: + sws_freeContext(sws); + QRcode_free(qrcode); + + return ret; +} + +#if CONFIG_QRENCODESRC_FILTER + +static const AVOption qrencodesrc_options[] = { + COMMON_OPTIONS + { "rate", "set video rate", OFFSET(frame_rate), AV_OPT_TYPE_VIDEO_RATE, {.str = "25"}, 0, INT_MAX, FLAGS }, + { "r", "set video rate", OFFSET(frame_rate), AV_OPT_TYPE_VIDEO_RATE, {.str = "25"}, 0, INT_MAX, FLAGS }, + { NULL } +}; + +AVFILTER_DEFINE_CLASS(qrencodesrc); + +static int qrencodesrc_config_props(AVFilterLink *outlink) +{ + AVFilterContext *ctx = outlink->src; + QREncodeContext *qr = ctx->priv; + int ret; + + qr->is_source = 1; + V(x) = V(y) = 0; + +#define PARSE_AND_EVAL_EXPR(var_name_, expr_name_) \ + ret = av_expr_parse_and_eval(&qr->var_values[VAR_##var_name_], \ + qr->expr_name_##_expr, \ + var_names, qr->var_values, \ + NULL, NULL, \ + fun2_names, fun2, \ + &qr->lfg, 0, ctx); \ + if (ret < 0) { \ + av_log(ctx, AV_LOG_ERROR, \ + "Could not evaluate expression '%s'\n", \ + qr->expr_name_##_expr); \ + return ret; \ + } + + /* undefined for the source */ + V(main_w) = V(W) = NAN; + V(main_h) = V(H) = NAN; + V(x) = V(y) = V(t) = V(n) = NAN; + V(dar) = V(sar) = 1.0; + + PARSE_AND_EVAL_EXPR(rendered_qr_w, rendered_qrcode_width); + V(q) = V(rendered_qr_w); + PARSE_AND_EVAL_EXPR(rendered_padded_qr_w, rendered_padded_qrcode_width); + V(Q) = V(rendered_padded_qr_w); + PARSE_AND_EVAL_EXPR(rendered_qr_w, rendered_qrcode_width); + V(q) = V(rendered_qr_w); + + qr->rendered_qrcode_width = V(rendered_qr_w); + qr->rendered_padded_qrcode_width = V(rendered_padded_qr_w); + + av_log(ctx, AV_LOG_VERBOSE, + "q:%d Q:%d case_sensitive:%d level:%d\n", + (int)qr->rendered_qrcode_width, (int)qr->rendered_padded_qrcode_width, + qr->case_sensitive, qr->level); + + if (qr->rendered_padded_qrcode_width < qr->rendered_qrcode_width) { + av_log(ctx, AV_LOG_ERROR, + "Resulting padded QR code width (%d) is lesser than the QR code width (%d)\n", + qr->rendered_padded_qrcode_width, qr->rendered_qrcode_width); + return AVERROR(EINVAL); + } + + ff_draw_init(&qr->draw, AV_PIX_FMT_ARGB, FF_DRAW_PROCESS_ALPHA); + ff_draw_color(&qr->draw, &qr->draw_foreground_color, (const uint8_t *)&qr->foreground_color); + ff_draw_color(&qr->draw, &qr->draw_background_color, (const uint8_t *)&qr->background_color); + + ff_draw_init(&qr->draw0, outlink->format, FF_DRAW_PROCESS_ALPHA); + ff_draw_color(&qr->draw0, &qr->draw0_background_color, (const uint8_t *)&qr->background_color); + + outlink->w = qr->rendered_padded_qrcode_width; + outlink->h = qr->rendered_padded_qrcode_width; + outlink->time_base = av_inv_q(qr->frame_rate); + outlink->frame_rate = qr->frame_rate; + + return 0; +} + +static int request_frame(AVFilterLink *outlink) +{ + AVFilterContext *ctx = (AVFilterContext *)outlink->src; + QREncodeContext *qr = ctx->priv; + AVFrame *frame = + ff_get_video_buffer(outlink, qr->rendered_padded_qrcode_width, qr->rendered_padded_qrcode_width); + int ret; + + if (!frame) + return AVERROR(ENOMEM); + frame->sample_aspect_ratio = (AVRational) {1, 1}; + V(n) = frame->pts = qr->pts++; + V(t) = qr->pts * av_q2d(outlink->time_base); + + if ((ret = draw_qrcode(ctx, frame)) < 0) + return ret; + + return ff_filter_frame(outlink, frame); +} + +static int qrencodesrc_query_formats(AVFilterContext *ctx) +{ + enum AVPixelFormat pix_fmt; + FFDrawContext draw; + AVFilterFormats *fmts = NULL; + int ret; + + // this is needed to support both the no-draw and draw cases + // for the no-draw case we use FFDrawContext to write on the input picture ref + for (pix_fmt = 0; av_pix_fmt_desc_get(pix_fmt); pix_fmt++) + if (ff_draw_init(&draw, pix_fmt, 0) >= 0 && + sws_isSupportedOutput(pix_fmt) && + (ret = ff_add_format(&fmts, pix_fmt)) < 0) + return ret; + + return ff_set_common_formats(ctx, fmts); +} + +static const AVFilterPad qrencodesrc_outputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_VIDEO, + .request_frame = request_frame, + .config_props = qrencodesrc_config_props, + } +}; + +const AVFilter ff_vsrc_qrencodesrc = { + .name = "qrencodesrc", + .description = NULL_IF_CONFIG_SMALL("Generate a QR code."), + .priv_size = sizeof(QREncodeContext), + .priv_class = &qrencodesrc_class, + .init = init, + .uninit = uninit, + .inputs = NULL, + FILTER_OUTPUTS(qrencodesrc_outputs), + FILTER_QUERY_FUNC(qrencodesrc_query_formats), +}; + +#endif // CONFIG_QRENCODESRC_FILTER + +#if CONFIG_QRENCODE_FILTER + +static const AVOption qrencode_options[] = { + COMMON_OPTIONS + {"x", "set x expression", OFFSET(x_expr), AV_OPT_TYPE_STRING, {.str="0"}, 0, 0, TFLAGS}, + {"y", "set y expression", OFFSET(y_expr), AV_OPT_TYPE_STRING, {.str="0"}, 0, 0, TFLAGS}, + { NULL } +}; + +AVFILTER_DEFINE_CLASS(qrencode); + +static int qrencode_config_input(AVFilterLink *inlink) +{ + AVFilterContext *ctx = inlink->dst; + QREncodeContext *qr = ctx->priv; + char *expr; + int ret; + + qr->is_source = 0; + + ff_draw_init(&qr->draw, inlink->format, FF_DRAW_PROCESS_ALPHA); + + V(W) = V(main_w) = inlink->w; + V(H) = V(main_h) = inlink->h; + V(sar) = inlink->sample_aspect_ratio.num ? av_q2d(inlink->sample_aspect_ratio) : 1; + V(dar) = (double)inlink->w / inlink->h * V(sar); + V(hsub) = 1 << qr->draw.hsub_max; + V(vsub) = 1 << qr->draw.vsub_max; + V(t) = NAN; + V(x) = V(y) = NAN; + + qr->x_pexpr = qr->y_pexpr = NULL; + qr->x_pexpr = qr->y_pexpr = NULL; + +#define PARSE_EXPR(name_) \ + ret = av_expr_parse(&qr->name_##_pexpr, expr = qr->name_##_expr, var_names, \ + NULL, NULL, fun2_names, fun2, 0, ctx); \ + if (ret < 0) { \ + av_log(ctx, AV_LOG_ERROR, \ + "Could not to parse expression '%s' for '%s'\n", \ + expr, #name_); \ + return AVERROR(EINVAL); \ + } + + PARSE_EXPR(x); + PARSE_EXPR(y); + PARSE_EXPR(rendered_qrcode_width); + PARSE_EXPR(rendered_padded_qrcode_width); + + ff_draw_init(&qr->draw, inlink->format, FF_DRAW_PROCESS_ALPHA); + ff_draw_color(&qr->draw, &qr->draw_foreground_color, (const uint8_t *)&qr->foreground_color); + ff_draw_color(&qr->draw, &qr->draw_background_color, (const uint8_t *)&qr->background_color); + + qr->rendered_qrcode_width = -1; + + return 0; +} + +static int qrencode_query_formats(AVFilterContext *ctx) +{ + return ff_set_common_formats(ctx, ff_draw_supported_pixel_formats(0)); +} + +static int filter_frame(AVFilterLink *inlink, AVFrame *frame) +{ + AVFilterContext *ctx = inlink->dst; + AVFilterLink *outlink = ctx->outputs[0]; + QREncodeContext *qr = ctx->priv; + int ret; + + V(n) = inlink->frame_count_out; + V(t) = frame->pts == AV_NOPTS_VALUE ? + NAN : frame->pts * av_q2d(inlink->time_base); + V(pict_type) = frame->pict_type; + V(duration) = frame->duration * av_q2d(inlink->time_base); + + if ((ret = draw_qrcode(ctx, frame)) < 0) + return ret; + + return ff_filter_frame(outlink, frame); +} + +static const AVFilterPad avfilter_vf_qrencode_inputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_VIDEO, + .flags = AVFILTERPAD_FLAG_NEEDS_WRITABLE, + .filter_frame = filter_frame, + .config_props = qrencode_config_input, + }, +}; + +const AVFilter ff_vf_qrencode = { + .name = "qrencode", + .description = NULL_IF_CONFIG_SMALL("Draw a QR code on top of video frames."), + .priv_size = sizeof(QREncodeContext), + .priv_class = &qrencode_class, + .init = init, + .uninit = uninit, + FILTER_INPUTS(avfilter_vf_qrencode_inputs), + FILTER_OUTPUTS(ff_video_default_filterpad), + FILTER_QUERY_FUNC(qrencode_query_formats), + .flags = AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC, +}; + +#endif // CONFIG_QRENCODE_FILTER -- 2.34.1 [-- Attachment #3: Type: text/plain, Size: 251 bytes --] _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [PATCH 2/2] lavfi: add qrencodesrc source 2023-12-17 18:00 ` Stefano Sabatini @ 2023-12-24 17:43 ` Stefano Sabatini 2024-01-02 21:09 ` Stefano Sabatini 0 siblings, 1 reply; 22+ messages in thread From: Stefano Sabatini @ 2023-12-24 17:43 UTC (permalink / raw) To: FFmpeg development discussions and patches On date Sunday 2023-12-17 19:00:01 +0100, Stefano Sabatini wrote: > On date Saturday 2023-12-09 19:13:46 +0100, Stefano Sabatini wrote: > > On date Thursday 2023-11-30 01:49:14 +0100, Stefano Sabatini wrote: > > > --- > > > configure | 4 + > > > libavfilter/Makefile | 1 + > > > libavfilter/allfilters.c | 1 + > > > libavfilter/vsrc_qrencode.c | 435 ++++++++++++++++++++++++++++++++++++ > > > 4 files changed, 441 insertions(+) > > > create mode 100644 libavfilter/vsrc_qrencode.c > > > > Rev2 with padding and doc. > > Rev3 including a filter and fancy expressions. > From 2e0e177f4e6e7a14d69f1e4d927b115b92b334d2 Mon Sep 17 00:00:00 2001 > From: Stefano Sabatini <stefasab@gmail.com> > Date: Tue, 28 Nov 2023 23:58:15 +0100 > Subject: [PATCH] lavfi: add qrencode source and filter > > --- > Changelog | 1 + > configure | 7 + > doc/filters.texi | 410 ++++++++++++++++++++ > libavfilter/Makefile | 2 + > libavfilter/allfilters.c | 2 + > libavfilter/qrencode.c | 803 +++++++++++++++++++++++++++++++++++++++ > 6 files changed, 1225 insertions(+) > create mode 100644 libavfilter/qrencode.c Will apply in a week or so if I see no comments. _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [PATCH 2/2] lavfi: add qrencodesrc source 2023-12-24 17:43 ` Stefano Sabatini @ 2024-01-02 21:09 ` Stefano Sabatini 0 siblings, 0 replies; 22+ messages in thread From: Stefano Sabatini @ 2024-01-02 21:09 UTC (permalink / raw) To: FFmpeg development discussions and patches On date Sunday 2023-12-24 18:43:04 +0100, Stefano Sabatini wrote: > On date Sunday 2023-12-17 19:00:01 +0100, Stefano Sabatini wrote: > > On date Saturday 2023-12-09 19:13:46 +0100, Stefano Sabatini wrote: > > > On date Thursday 2023-11-30 01:49:14 +0100, Stefano Sabatini wrote: > > > > --- > > > > configure | 4 + > > > > libavfilter/Makefile | 1 + > > > > libavfilter/allfilters.c | 1 + > > > > libavfilter/vsrc_qrencode.c | 435 ++++++++++++++++++++++++++++++++++++ > > > > 4 files changed, 441 insertions(+) > > > > create mode 100644 libavfilter/vsrc_qrencode.c > > > > > > Rev2 with padding and doc. > > > > Rev3 including a filter and fancy expressions. > > > From 2e0e177f4e6e7a14d69f1e4d927b115b92b334d2 Mon Sep 17 00:00:00 2001 > > From: Stefano Sabatini <stefasab@gmail.com> > > Date: Tue, 28 Nov 2023 23:58:15 +0100 > > Subject: [PATCH] lavfi: add qrencode source and filter > > > > --- > > Changelog | 1 + > > configure | 7 + > > doc/filters.texi | 410 ++++++++++++++++++++ > > libavfilter/Makefile | 2 + > > libavfilter/allfilters.c | 2 + > > libavfilter/qrencode.c | 803 +++++++++++++++++++++++++++++++++++++++ > > 6 files changed, 1225 insertions(+) > > create mode 100644 libavfilter/qrencode.c > > Will apply in a week or so if I see no comments. Applied. _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [POC][PATCHSET] Add qrencodesrc source 2023-11-30 0:49 [FFmpeg-devel] [POC][PATCHSET] Add qrencodesrc source Stefano Sabatini 2023-11-30 0:49 ` [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils Stefano Sabatini 2023-11-30 0:49 ` [FFmpeg-devel] [PATCH 2/2] lavfi: add qrencodesrc source Stefano Sabatini @ 2023-11-30 11:07 ` Tomas Härdin 2023-11-30 11:21 ` Paul B Mahol ` (2 more replies) 2 siblings, 3 replies; 22+ messages in thread From: Tomas Härdin @ 2023-11-30 11:07 UTC (permalink / raw) To: FFmpeg development discussions and patches tor 2023-11-30 klockan 01:49 +0100 skrev Stefano Sabatini: > This is meant to introduce functionality to handle QR codes. Why? /Tomas _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [POC][PATCHSET] Add qrencodesrc source 2023-11-30 11:07 ` [FFmpeg-devel] [POC][PATCHSET] Add " Tomas Härdin @ 2023-11-30 11:21 ` Paul B Mahol 2023-12-02 19:19 ` Stefano Sabatini 2023-11-30 11:54 ` Nicolas George [not found] ` <D4F91B2F-6380-48AA-8883-315FA76CA828@cosmin.at> 2 siblings, 1 reply; 22+ messages in thread From: Paul B Mahol @ 2023-11-30 11:21 UTC (permalink / raw) To: FFmpeg development discussions and patches On Thu, Nov 30, 2023 at 12:07 PM Tomas Härdin <git@haerdin.se> wrote: > tor 2023-11-30 klockan 01:49 +0100 skrev Stefano Sabatini: > > This is meant to introduce functionality to handle QR codes. > > Why? > For such trivial functionality using external library is unacceptable. > > /Tomas > _______________________________________________ > ffmpeg-devel mailing list > ffmpeg-devel@ffmpeg.org > https://ffmpeg.org/mailman/listinfo/ffmpeg-devel > > To unsubscribe, visit link above, or email > ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". > _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [POC][PATCHSET] Add qrencodesrc source 2023-11-30 11:21 ` Paul B Mahol @ 2023-12-02 19:19 ` Stefano Sabatini 0 siblings, 0 replies; 22+ messages in thread From: Stefano Sabatini @ 2023-12-02 19:19 UTC (permalink / raw) To: FFmpeg development discussions and patches On date Thursday 2023-11-30 12:21:16 +0100, Paul B Mahol wrote: > On Thu, Nov 30, 2023 at 12:07 PM Tomas Härdin <git@haerdin.se> wrote: > > > tor 2023-11-30 klockan 01:49 +0100 skrev Stefano Sabatini: > > > This is meant to introduce functionality to handle QR codes. > > > > Why? > > > > For such trivial functionality using external library is unacceptable. I don't know, we might implement it (do we have Reed-Solomon encoding utilities BTW?) but it would sound like duplication of effort assuming that the external library is maintained. Also it has potentially more features that we want to implement (e.g. structured and concatenated data, which it might be worth to implement at some point). _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [POC][PATCHSET] Add qrencodesrc source 2023-11-30 11:07 ` [FFmpeg-devel] [POC][PATCHSET] Add " Tomas Härdin 2023-11-30 11:21 ` Paul B Mahol @ 2023-11-30 11:54 ` Nicolas George [not found] ` <D4F91B2F-6380-48AA-8883-315FA76CA828@cosmin.at> 2 siblings, 0 replies; 22+ messages in thread From: Nicolas George @ 2023-11-30 11:54 UTC (permalink / raw) To: FFmpeg development discussions and patches Tomas Härdin (12023-11-30): > Why? Why not? -- Nicolas George _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
[parent not found: <D4F91B2F-6380-48AA-8883-315FA76CA828@cosmin.at>]
* Re: [FFmpeg-devel] [POC][PATCHSET] Add qrencodesrc source [not found] ` <D4F91B2F-6380-48AA-8883-315FA76CA828@cosmin.at> @ 2023-11-30 15:39 ` Cosmin Stejerean via ffmpeg-devel 2023-12-01 15:08 ` Tomas Härdin 0 siblings, 1 reply; 22+ messages in thread From: Cosmin Stejerean via ffmpeg-devel @ 2023-11-30 15:39 UTC (permalink / raw) To: FFmpeg development discussions and patches; +Cc: Cosmin Stejerean > On Nov 30, 2023, at 03:07, Tomas Härdin <git@haerdin.se> wrote: > > tor 2023-11-30 klockan 01:49 +0100 skrev Stefano Sabatini: >> This is meant to introduce functionality to handle QR codes. > > Why? > The why seems to be answered below the section you quoted in the original email > QR codes are robust to lossy coding, therefore it should be possible to use them to compare a generated video with a reference one (in case they cannot be compared frame-by-frame). - Cosmin _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [POC][PATCHSET] Add qrencodesrc source 2023-11-30 15:39 ` Cosmin Stejerean via ffmpeg-devel @ 2023-12-01 15:08 ` Tomas Härdin 2023-12-02 19:27 ` Stefano Sabatini 0 siblings, 1 reply; 22+ messages in thread From: Tomas Härdin @ 2023-12-01 15:08 UTC (permalink / raw) To: FFmpeg development discussions and patches tor 2023-11-30 klockan 15:39 +0000 skrev Cosmin Stejerean via ffmpeg- devel: > > > On Nov 30, 2023, at 03:07, Tomas Härdin <git@haerdin.se> wrote: > > > > tor 2023-11-30 klockan 01:49 +0100 skrev Stefano Sabatini: > > > This is meant to introduce functionality to handle QR codes. > > > > Why? > > > > The why seems to be answered below the section you quoted in the > original email > > > QR codes are robust to lossy coding, therefore it should be > > possible to use them to compare a generated video with a reference > > one (in case > they cannot be compared frame-by-frame). Is the intent to generate per-frame QR codes? I guess that has some merit. Otherwise it seems pointless if it's just for generating a single QR code which could just as well be done with an external program and then overlaying the resulting image. /Tomas _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [FFmpeg-devel] [POC][PATCHSET] Add qrencodesrc source 2023-12-01 15:08 ` Tomas Härdin @ 2023-12-02 19:27 ` Stefano Sabatini 0 siblings, 0 replies; 22+ messages in thread From: Stefano Sabatini @ 2023-12-02 19:27 UTC (permalink / raw) To: FFmpeg development discussions and patches On date Friday 2023-12-01 16:08:23 +0100, Tomas Härdin wrote: > tor 2023-11-30 klockan 15:39 +0000 skrev Cosmin Stejerean via ffmpeg- > devel: > > > > > On Nov 30, 2023, at 03:07, Tomas Härdin <git@haerdin.se> wrote: > > > > > > tor 2023-11-30 klockan 01:49 +0100 skrev Stefano Sabatini: > > > > This is meant to introduce functionality to handle QR codes. > > > > > > Why? > > > > > > > The why seems to be answered below the section you quoted in the > > original email > > > > > QR codes are robust to lossy coding, therefore it should be > > > possible to use them to compare a generated video with a reference > > > one (in case > > they cannot be compared frame-by-frame). > > Is the intent to generate per-frame QR codes? I guess that has some > merit. [...] Yes, this was the main use case (if you see the patches I'm reusing the same expansion mechanism we have in drawtext, for example we can specify "%{n}" to encode the picture number). Then I plan to extend it with a filter overlaying on the input frame, and with a decoder (thinking to use again an external library - quirc - assuming there are no objections in that direction). > Otherwise it seems pointless if it's just for generating a > single QR code which could just as well be done with an external > program and then overlaying the resulting image. But in general, having integrated QR encoding it's more than overlaying a single static QR code, you might want to encode some specific frame metadata or change/disable the QR to encode through a filter command, so it would be IMO good to have it for many different use cases. _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe". ^ permalink raw reply [flat|nested] 22+ messages in thread
end of thread, other threads:[~2024-01-03 15:50 UTC | newest] Thread overview: 22+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2023-11-30 0:49 [FFmpeg-devel] [POC][PATCHSET] Add qrencodesrc source Stefano Sabatini 2023-11-30 0:49 ` [FFmpeg-devel] [PATCH 1/2] lavfi: introduce textutils Stefano Sabatini 2023-12-03 15:05 ` Stefano Sabatini 2023-12-09 18:12 ` Stefano Sabatini 2023-12-16 15:35 ` Stefano Sabatini 2024-01-03 12:58 ` Timo Rothenpieler 2024-01-03 15:50 ` Stefano Sabatini 2023-12-06 20:20 ` Nicolas George 2023-11-30 0:49 ` [FFmpeg-devel] [PATCH 2/2] lavfi: add qrencodesrc source Stefano Sabatini 2023-12-09 18:13 ` Stefano Sabatini 2023-12-11 20:59 ` Kyle Swanson 2023-12-11 23:34 ` Stefano Sabatini 2023-12-17 18:00 ` Stefano Sabatini 2023-12-24 17:43 ` Stefano Sabatini 2024-01-02 21:09 ` Stefano Sabatini 2023-11-30 11:07 ` [FFmpeg-devel] [POC][PATCHSET] Add " Tomas Härdin 2023-11-30 11:21 ` Paul B Mahol 2023-12-02 19:19 ` Stefano Sabatini 2023-11-30 11:54 ` Nicolas George [not found] ` <D4F91B2F-6380-48AA-8883-315FA76CA828@cosmin.at> 2023-11-30 15:39 ` Cosmin Stejerean via ffmpeg-devel 2023-12-01 15:08 ` Tomas Härdin 2023-12-02 19:27 ` Stefano Sabatini
Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel This inbox may be cloned and mirrored by anyone: git clone --mirror https://master.gitmailbox.com/ffmpegdev/0 ffmpegdev/git/0.git # If you have public-inbox 1.1+ installed, you may # initialize and index your mirror using the following commands: public-inbox-init -V2 ffmpegdev ffmpegdev/ https://master.gitmailbox.com/ffmpegdev \ ffmpegdev@gitmailbox.com public-inbox-index ffmpegdev Example config snippet for mirrors. AGPL code for this site: git clone https://public-inbox.org/public-inbox.git