diff mbox series

[FFmpeg-devel,v8,09/13] avfilter/textmod: Add textmod, censor and show_speaker filters

Message ID MN2PR04MB598129269D4677DCCC40F588BAA19@MN2PR04MB5981.namprd04.prod.outlook.com
State Superseded, archived
Headers show
Series [FFmpeg-devel,v8,01/13] global: Prepare AVFrame for subtitle handling
Related show

Checks

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

Commit Message

Soft Works Sept. 21, 2021, 11:54 p.m. UTC
- textmod {S -> S)
  Modify subtitle text in a number of ways

- censor {S -> S)
  Censor subtitles using a word list

- show_speaker {S -> S)
  Prepend speaker names from ASS subtitles to the visible text lines

Signed-off-by: softworkz <softworkz@hotmail.com>
---
 doc/filters.texi         | 229 ++++++++++++
 libavfilter/Makefile     |   5 +
 libavfilter/allfilters.c |   3 +
 libavfilter/sf_textmod.c | 727 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 964 insertions(+)
 create mode 100644 libavfilter/sf_textmod.c

Comments

Andreas Rheinhardt Sept. 22, 2021, 3:08 a.m. UTC | #1
Soft Works:
> - textmod {S -> S)
>   Modify subtitle text in a number of ways
> 
> - censor {S -> S)
>   Censor subtitles using a word list
> 
> - show_speaker {S -> S)
>   Prepend speaker names from ASS subtitles to the visible text lines
> 
> Signed-off-by: softworkz <softworkz@hotmail.com>
> ---
>  doc/filters.texi         | 229 ++++++++++++
>  libavfilter/Makefile     |   5 +
>  libavfilter/allfilters.c |   3 +
>  libavfilter/sf_textmod.c | 727 +++++++++++++++++++++++++++++++++++++++
>  4 files changed, 964 insertions(+)
>  create mode 100644 libavfilter/sf_textmod.c
> 
> diff --git a/doc/filters.texi b/doc/filters.texi
> index 416bda5efb..2d3dcdd7e6 100644
> --- a/doc/filters.texi
> +++ b/doc/filters.texi
> @@ -25079,6 +25079,7 @@ tools.
>  
>  @c man end VIDEO SINKS
>  
> +
>  @chapter Subtitle Filters
>  @c man begin SUBTITLE FILTERS
>  
> @@ -25087,6 +25088,166 @@ existing filters using @code{--disable-filters}.
>  
>  Below is a description of the currently available subtitle filters.
>  
> +
> +@section censor
> +
> +Censor selected words in text subtitles.
> +
> +Inputs:
> +- 0: Subtitles [text]
> +
> +Outputs:
> +- 0: Subtitles [text]
> +
> +It accepts the following parameters:
> +
> +@table @option
> +@item mode
> +The censoring mode to apply.
> +
> +Supported censoring modes are:
> +
> +@table @var
> +@item 0, keep_first_last
> +Replace all characters with the 'censor_char' except the first and the last character of a word.
> +For words with less than 4 characters, the last character will be replaced as well.
> +For words with less than 3 characters, the first character will be replaced as well.
> +@item 1, keep_first
> +Replace all characters with the 'censor_char' except the first character of a word.
> +For words with less than 3 characters, the first character will be replaced as well.
> +@item 2, all
> +Replace all characters with the 'censor_char'.
> +@end table
> +
> +@item words
> +A list of words to censor, separated by 'separator'.
> +
> +@item words_file
> +Specify a file from which to load the contents for the 'words' parameter.
> +
> +@item censor_char
> +Single character used as replacement for censoring.
> +
> +@item separator
> +Delimiter character for words. Used with replace_words and remove_words- Must be a single character.
> +The default is '.'.
> +
> +@end table
> +
> +@subsection Examples
> +
> +@itemize
> +@item
> +Change all characters to upper case while keeping all styles and animations:
> +@example
> +ffmpeg -i "https://streams.videolan.org/ffmpeg/mkv_subtitles.mkv" -filter_complex "[0:s]textmod=mode=to_upper" -map 0 -y out.mkv
> +@end example
> +@item
> +Remove a set of symbol characters for am improved and smoother visual apperance:
> +@example
> +ffmpeg -i "https://streams.videolan.org/ffmpeg/mkv_subtitles.mkv" -filter_complex "[0:s]textmod=mode=remove_chars:find='$&#@*§'" -map 0 -y out.mkv
> +@end example
> +@end itemize
> +
> +
> +@section stripstyles
> +
> +Remove all inline styles from subtitle events.
> +
> +It accepts the following parameters:
> +
> +@table @option
> +@item remove_animated
> +Also remove text which is subject to animation (default: true)
> +Usually, animated text elements are used used in addition to static subtitle lines for creating effects, so in most cases it is safe to remove the animation content.
> +If subtitle text is missing, try setting this to false.
> +
> +@end table
> +
> +@subsection Examples
> +
> +@itemize
> +@item
> +Remove all styles and animations from subtitles:
> +@example
> +ffmpeg -i "https://streams.videolan.org/samples/sub/SSA/subtitle_testing_complex.mkv" -filter_complex "[0:1]stripstyles" -map 0 output.mkv
> +@end example
> +@end itemize
> +
> +@section textmod
> +
> +Modify subtitle text in a number of ways.
> +
> +Inputs:
> +- 0: Subtitles [text]
> +
> +Outputs:
> +- 0: Subtitles [text]
> +
> +It accepts the following parameters:
> +
> +@table @option
> +@item mode
> +The kind of text modification to apply
> +
> +Supported operation modes are:
> +
> +@table @var
> +@item 0, leet
> +Convert subtitle text to 'leet speak'. It's primarily useful for testing as the modification will be visible with almost all text lines.
> +@item 1, to_upper
> +Change all text to upper case. Might improve readability.
> +@item 2, to_lower
> +Change all text to lower case.
> +@item 3, replace_chars
> +Replace one or more characters. Requires the find and replace parameters to be specified. 
> +Both need to be equal in length.
> +The first char in find is replaced by the first char in replace, same for all subsequent chars.
> +@item 4, remove_chars
> +Remove certain characters. Requires the find parameter to be specified. 
> +All chars in the find parameter string will be removed from all subtitle text.
> +@item 5, replace_words
> +Replace one or more words. Requires the find and replace parameters to be specified. Multiple words must be separated by the delimiter char specified vie the separator parameter (default: ','). 
> +The number of words in the find and replace parameters needs to be equal.
> +The first word in find is replaced by the first word in replace, same for all subsequent words
> +@item 6, remove_words
> +Remove certain words. Requires the find parameter to be specified. Multiple words must be separated by the delimiter char specified vie the separator parameter (default: ','). 
> +All words in the find parameter string will be removed from all subtitle text.
> +@end table
> +
> +@item find
> +Required for replace_chars, remove_chars, replace_words and remove_words.
> +
> +@item find_file
> +Specify a file from which to load the contents for the 'find' parameter.
> +
> +@item replace
> +Required for replace_chars and replace_words.
> +
> +@item replace_file
> +Specify a file from which to load the contents for the 'replace' parameter.
> +
> +@item separator
> +Delimiter character for words. Used with replace_words and remove_words- Must be a single character.
> +The default is '.'.
> +
> +@end table
> +
> +@subsection Examples
> +
> +@itemize
> +@item
> +Change all characters to upper case while keeping all styles and animations:
> +@example
> +ffmpeg -i "https://streams.videolan.org/ffmpeg/mkv_subtitles.mkv" -filter_complex "[0:s]textmod=mode=to_upper" -map 0 -y out.mkv
> +@end example
> +@item
> +Remove a set of symbol characters for am improved and smoother visual apperance:
> +@example
> +ffmpeg -i "https://streams.videolan.org/ffmpeg/mkv_subtitles.mkv" -filter_complex "[0:s]textmod=mode=remove_chars:find='$&#@*§'" -map 0 -y out.mkv
> +@end example
> +@end itemize
> +
>  @section graphicsub2video
>  
>  Renders graphic subtitles as video frames. 
> @@ -25131,6 +25292,11 @@ Overlay graphic subtitles onto a video stream.
>  This filter can blend graphical subtitles on a video stream directly, i.e. without creating full-size alpha images first.
>  The blending operation is limited to the area of the subtitle rectangles, which also means that no processing is done at times where no subtitles are to be displayed.
>  
> +- 0: Video [YUV420P, YUV422P, YUV444P, ARGB, RGBA, ABGR, BGRA, RGB24, BGR24]
> +- 1: Subtitles [bitmap]
> +
> +Outputs:
> +- 0: Video (same as input)
>  
>  It accepts the following parameters:
>  
> @@ -25212,6 +25378,69 @@ string containing ASS style format @code{KEY=VALUE} couples separated by ",".
>  
>  @end table
>  
> +@section show_speaker
> +
> +Prepend speaker names to subtitle lines (when available).
> +
> +Subtitles in ASS/SSA format are often including the names of the persons
> +or character for each subtitle line. The show_speaker filter adds those names 
> +to the actual subtitle text to make it visible on playback.
> +
> +Inputs:
> +- 0: Subtitles [text]
> +
> +Outputs:
> +- 0: Subtitles [text]
> +
> +It accepts the following parameters:
> +
> +@table @option
> +@item format
> +The format for prepending speaker names. Default is 'square_brackets'.
> +
> +Supported operation modes are:
> +
> +@table @var
> +@item 0, square_brackets
> +Enclose the speaker name in square brackets, followed by space ('[speaker] text').
> +@item 1, round_brackets
> +Enclose the speaker name in round brackets, followed by space ('(speaker) text').
> +@item 2, colon
> +Separate the speaker name with a colon and space ('speaker: text').
> +@item 3, plain
> +Separate the speaker name with a space only ('speaker text').
> +@end table
> +
> +@item line_break
> +Set thís parameter to insert a line break between speaker name and text instead of the space character.
> +
> +@item style
> +Allows to set a specific style for the speaker name text. 
> +
> +This can be either a named style that exists in the ass subtitle header script (e.g. 'Default') or an ass style override code.
> +Example:  @{\\c&HDD0000&\\be1\\i1\\bord10@}
> +This sets the color to blue, enables edge blurring, italic font and a border of size 10.
> +
> +The behavior is as follows:
> +
> +- When the style parameter is not provided, the filter will find the first position in the event string that is actual text.
> +  The speaker name will be inserted at this position. This allows to have the speaker name shown in the same style like the 
> +  regular text, in case the string would start with a sequence of style codes.
> +- When the style parameter is provided, everything will be prepended to the original text: 
> +  Style Code or Style name >> Speaker Name >> Style Reset Code >> Original Text
> +
> +@end table
> +
> +@subsection Examples
> +
> +@itemize
> +@item
> +Prepend speaker names with blue text, smooth edges and blend/overlay that onto the video.
> +@example
> +ffmpeg -i INPUT -filter_complex "show_speaker=format=colon:style='@{\\c&HDD0000&\\be1@}',[0:v]overlay_textsubs"
> +@end example
> +@end itemize
> +
>  @section textsub2video
>  
>  Converts text subtitles to video frames.
> diff --git a/libavfilter/Makefile b/libavfilter/Makefile
> index c0b1cc7001..e6fef97c08 100644
> --- a/libavfilter/Makefile
> +++ b/libavfilter/Makefile
> @@ -536,6 +536,11 @@ OBJS-$(CONFIG_YUVTESTSRC_FILTER)             += vsrc_testsrc.o
>  
>  OBJS-$(CONFIG_NULLSINK_FILTER)               += vsink_nullsink.o
>  
> +# subtitle filters
> +OBJS-$(CONFIG_CENSOR_FILTER)                 += sf_textmod.o
> +OBJS-$(CONFIG_SHOW_SPEAKER_FILTER)           += sf_textmod.o
> +OBJS-$(CONFIG_TEXTMOD_FILTER)                += sf_textmod.o
> +
>  # multimedia filters
>  OBJS-$(CONFIG_ABITSCOPE_FILTER)              += avf_abitscope.o
>  OBJS-$(CONFIG_ADRAWGRAPH_FILTER)             += f_drawgraph.o
> diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
> index 8543fa22e9..fc72858d18 100644
> --- a/libavfilter/allfilters.c
> +++ b/libavfilter/allfilters.c
> @@ -526,6 +526,9 @@ extern const AVFilter ff_avf_showvolume;
>  extern const AVFilter ff_avf_showwaves;
>  extern const AVFilter ff_avf_showwavespic;
>  extern const AVFilter ff_vaf_spectrumsynth;
> +extern const AVFilter ff_sf_censor;
> +extern const AVFilter ff_sf_show_speaker;
> +extern const AVFilter ff_sf_textmod;
>  extern const AVFilter ff_svf_graphicsub2video;
>  extern const AVFilter ff_svf_textsub2video;
>  
> diff --git a/libavfilter/sf_textmod.c b/libavfilter/sf_textmod.c
> new file mode 100644
> index 0000000000..14049202aa
> --- /dev/null
> +++ b/libavfilter/sf_textmod.c
> @@ -0,0 +1,727 @@
> +/*
> + * Copyright (c) 2021 softworkz
> + *
> + * 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 subtitle filter which allows to modify subtitle text in several ways
> + */
> +
> +#include <libavcodec/ass.h>
> +
> +#include "libavutil/avassert.h"
> +#include "libavutil/avstring.h"
> +#include "libavutil/opt.h"
> +#include "avfilter.h"
> +#include "internal.h"
> +#include "libavcodec/avcodec.h"
> +#include "libavcodec/ass_split.h"
> +#include "libavutil/file.h"
> +
> +static const char* leet_src = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
> +static const char* leet_dst = "abcd3f6#1jklmn0pq257uvwxyzAB(D3F6#1JKLMN0PQ257UVWXYZ";
> +
> +enum TextModFilterType {
> +    TM_TEXTMOD,
> +    TM_CENSOR,
> +    TM_SHOW_SPEAKER,
> +};
> +
> +enum TextModOperation {
> +    OP_LEET,
> +    OP_TO_UPPER,
> +    OP_TO_LOWER,
> +    OP_REPLACE_CHARS,
> +    OP_REMOVE_CHARS,
> +    OP_REPLACE_WORDS,
> +    OP_REMOVE_WORDS,
> +    NB_OPS,
> +};
> +
> +enum CensorMode {
> +    CM_KEEP_FIRST_LAST,
> +    CM_KEEP_FIRST,
> +    CM_ALL,
> +};
> +
> +enum ShowSpeakerMode {
> +    SM_SQUARE_BRACKETS,
> +    SM_ROUND_BRACKETS,
> +    SM_COLON,
> +    SM_PLAIN,
> +};
> +
> +typedef struct TextModContext {
> +    const AVClass *class;
> +    enum AVSubtitleType format;
> +    enum TextModFilterType filter_type;
> +    enum TextModOperation operation;
> +    enum CensorMode censor_mode;
> +    enum ShowSpeakerMode speaker_mode;
> +    char *find;
> +    char *find_file;
> +    char *style;
> +    char *replace;
> +    char *replace_file;
> +    char *separator;
> +    char *censor_char;
> +    char **find_list;
> +    int  line_break;
> +    int  nb_find_list;
> +    char **replace_list;
> +    int  nb_replace_list;
> +} TextModContext;
> +
> +static char **split_string(char *source, int *nb_elems, char delim)
> +{
> +    char **list = NULL;
> +    char *temp = NULL;
> +    char *ptr = av_strtok(source, &delim, &temp);
> +
> +    while (ptr) {
> +        if (strlen(ptr)) {
> +            char *item = av_strdup(ptr);
> +            if (!item)
> +                return NULL;
> +            av_dynarray_add(&list, nb_elems, item);

This function is horrible (and I already wanted to deprecate it, but for
this I would need to fix all current users of it): It automatically
frees the array on error and it returns void, thereby encouraging a
programming style in which errors are not checked. (You are checking
them.) The automatic free on error means that there are basically only
two usecases for it (unless one just ignores any errors): To store
pointers to objects that don't need to be freed at all (like static
objects) or to store non-ownership pointers, like pointers to substrings
of a string. Now that you started strduping the strings you are no
longer of this type. In other words, there will be leaks on error.

Anyway, I don't see why you went the route of using individually
allocated strings.

> +            if (!list)
> +                return NULL;
> +        }
> +
> +        ptr = av_strtok(NULL, &delim, &temp);
> +    }
> +
> +    return list;
> +}
> +
> +static int load_text_from_file(AVFilterContext *ctx, const char *file_name, char **text, char separator)
> +{
> +    int err;
> +    uint8_t *textbuf;
> +    char *tmp;
> +    size_t textbuf_size;
> +    int offset = 0;
> +
> +    if ((err = av_file_map(file_name, &textbuf, &textbuf_size, 0, ctx)) < 0) {
> +        av_log(ctx, AV_LOG_ERROR, "The text file '%s' could not be read or is empty\n", file_name);
> +        return err;
> +    }
> +
> +    if (textbuf_size > 1 && 
> +        (textbuf[0] == 0xFF && textbuf[1] == 0xFE
> +        || textbuf[0] == 0xFE && textbuf[1] == 0xFF)) {
> +        av_log(ctx, AV_LOG_ERROR, "UTF text files are not supported. File: %s\n", file_name);
> +        return AVERROR(EINVAL);
> +    }
> +
> +    if (textbuf_size > 2 && textbuf[0] == 0xEF && textbuf[1] == 0xBB && textbuf[2] == 0xBF)
> +        offset = 3; // UTF-8
> +
> +    if (textbuf_size > SIZE_MAX - 1 || !((tmp = av_strndup((char *)textbuf + offset, textbuf_size - offset)))) {
> +        av_file_unmap(textbuf, textbuf_size);
> +        return AVERROR(ENOMEM);
> +    }
> +
> +    av_file_unmap(textbuf, textbuf_size);
> +
> +    for (size_t i = 0; i < strlen(tmp); i++) {
> +        switch (tmp[i]) {
> +        case '\n':
> +        case '\r':
> +        case '\f':
> +        case '\v':
> +            tmp[i] = separator;
> +        }
> +    }
> +
> +    *text = tmp;
> +
> +    return 0;
> +}
> +
> +static int load_files(AVFilterContext *ctx)
> +{
> +    TextModContext *s = ctx->priv;
> +    int ret;
> +
> +    if (!s->separator || strlen(s->separator) != 1) {
> +        av_log(ctx, AV_LOG_ERROR, "A single character needs to be specified for the separator parameter.\n");
> +        return AVERROR(EINVAL);
> +    }
> +
> +    if (s->find_file && strlen(s->find_file)) {
> +        ret = load_text_from_file(ctx, s->find_file, &s->find, s->separator[0]);
> +        if (ret < 0 )
> +            return ret;
> +    }
> +
> +    if (s->replace_file && strlen(s->replace_file)) {
> +        ret = load_text_from_file(ctx, s->replace_file, &s->replace, s->separator[0]);
> +        if (ret < 0 )
> +            return ret;
> +    }
> +
> +    return 0;
> +}
> +
> +static int init_censor(AVFilterContext *ctx)
> +{
> +    TextModContext *s = ctx->priv;
> +    char **list = NULL;
> +    int ret;
> +
> +    s->filter_type = TM_CENSOR;
> +    s->operation = OP_REPLACE_WORDS;
> +
> +    ret = load_files(ctx);
> +    if (ret < 0 )
> +        return ret;
> +
> +    if (!s->find || !strlen(s->find)) {
> +        av_log(ctx, AV_LOG_ERROR, "Either the 'words' or the 'words_file' parameter needs to be specified\n");
> +        return AVERROR(EINVAL);
> +    }
> +
> +    if (!s->censor_char || strlen(s->censor_char) != 1) {
> +        av_log(ctx, AV_LOG_ERROR, "A single character needs to be specified for the censor_char parameter\n");
> +        return AVERROR(EINVAL);
> +    }
> +
> +    s->find_list = split_string(s->find, &s->nb_find_list, *s->separator);
> +    if (!s->find_list)
> +        return AVERROR(ENOMEM);
> +
> +    for (unsigned i = 0; i < s->nb_find_list; i++) {
> +        char *item = av_strdup(s->find_list[i]);
> +        size_t len, start = 0, end;
> +        if (!item)
> +            return AVERROR(ENOMEM);
> +
> +        len = end = strlen(item);
> +
> +        switch (s->censor_mode) {
> +        case CM_KEEP_FIRST_LAST:
> +
> +            if (len > 2)
> +                start = 1;
> +            if (len > 3)
> +                end--;
> +
> +            break;
> +        case CM_KEEP_FIRST:
> +
> +            if (len > 2)
> +                start = 1;
> +
> +            break;
> +        }
> +
> +        for (size_t n = start; n < end; n++) {
> +            item[n] = s->censor_char[0];
> +        }
> +
> +        av_dynarray_add(&list, &s->nb_replace_list, item);

Leak on error, as above.
(Notice that you do not need to allocate the replace_list entries
individually; all you need to do is strdup s->find before it is split
into little pieces. Then you can get the offset of the nth replace
string in the duplicate of s->find by s->find_list[n] - s->find.)
Finally, when dealing with the replace list you already know in advance
how many entries there will be, so you don't have to reallocate.

> +    }
> +
> +    s->replace_list = list;
> +    if (!s->replace_list)
> +        return AVERROR(ENOMEM);
> +
> +    return 0;
> +}
> +
> +static int init_show_speaker(AVFilterContext *ctx)
> +{
> +    TextModContext *s = ctx->priv;
> +    s->filter_type = TM_SHOW_SPEAKER;
> +
> +    return 0;
> +}
> +
> +static int init(AVFilterContext *ctx)
> +{
> +    TextModContext *s = ctx->priv;
> +    int ret;
> +
> +    ret = load_files(ctx);
> +    if (ret < 0 )
> +        return ret;
> +
> +    switch (s->operation) {
> +    case OP_REPLACE_CHARS:
> +    case OP_REMOVE_CHARS:
> +    case OP_REPLACE_WORDS:
> +    case OP_REMOVE_WORDS:
> +        if (!s->find || !strlen(s->find)) {
> +            av_log(ctx, AV_LOG_ERROR, "Selected mode requires the 'find' parameter to be specified\n");
> +            return AVERROR(EINVAL);
> +        }
> +        break;
> +    }
> +
> +    switch (s->operation) {
> +    case OP_REPLACE_CHARS:
> +    case OP_REPLACE_WORDS:
> +        if (!s->replace || !strlen(s->replace)) {
> +            av_log(ctx, AV_LOG_ERROR, "Selected mode requires the 'replace' parameter to be specified\n");
> +            return AVERROR(EINVAL);
> +        }
> +        break;
> +    }
> +
> +    if (s->operation == OP_REPLACE_CHARS && strlen(s->find) != strlen(s->replace)) {
> +        av_log(ctx, AV_LOG_ERROR, "Selected mode requires the 'find' and 'replace' parameters to have the same length\n");
> +        return AVERROR(EINVAL);
> +    }
> +
> +    if (s->operation == OP_REPLACE_WORDS || s->operation == OP_REMOVE_WORDS) {
> +        if (!s->separator || strlen(s->separator) != 1) {
> +            av_log(ctx, AV_LOG_ERROR, "Selected mode requires a single separator char to be specified\n");
> +            return AVERROR(EINVAL);
> +        }
> +
> +        s->find_list = split_string(s->find, &s->nb_find_list, *s->separator);
> +        if (!s->find_list)
> +            return AVERROR(ENOMEM);
> +
> +        if (s->operation == OP_REPLACE_WORDS) {
> +
> +            s->replace_list = split_string(s->replace, &s->nb_replace_list, *s->separator);
> +            if (!s->replace_list)
> +                return AVERROR(ENOMEM);
> +
> +            if (s->nb_find_list != s->nb_replace_list) {
> +                av_log(ctx, AV_LOG_ERROR, "The number of words in 'find' and 'replace' needs to be equal\n");
> +                return AVERROR(EINVAL);
> +            }
> +        }
> +    }
> +
> +    return 0;
> +}
> +
> +static void uninit(AVFilterContext *ctx)
> +{
> +    TextModContext *s = ctx->priv;
> +
> +    for (int i = 0; i < s->nb_find_list; i++)
> +        av_freep(&s->find_list[i]);
> +
> +    s->nb_find_list = 0;
> +    av_freep(&s->find_list);
> +
> +    for (int i = 0; i < s->nb_replace_list; i++)
> +        av_freep(&s->replace_list[i]);
> +
> +    s->nb_replace_list = 0;
> +    av_freep(&s->replace_list);
> +}
> +
> +static int query_formats(AVFilterContext *ctx)
> +{
> +    AVFilterFormats *formats;
> +    AVFilterLink *inlink = ctx->inputs[0];
> +    AVFilterLink *outlink = ctx->outputs[0];
> +    static const enum AVSubtitleType subtitle_fmts[] = { AV_SUBTITLE_FMT_ASS, AV_SUBTITLE_FMT_NONE };
> +    int ret;
> +
> +    /* set input formats */
> +    formats = ff_make_format_list(subtitle_fmts);
> +    if ((ret = ff_formats_ref(formats, &inlink->outcfg.formats)) < 0)
> +        return ret;
> +
> +    /* set output formats */
> +    if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0)
> +        return ret;
> +
> +    return 0;
> +}
> +
> +
> +
> +static char *process_text(TextModContext *s, char *text)
> +{
> +    const char *char_src = s->find;
> +    const char *char_dst = s->replace;
> +    char *result         = NULL;
> +    int escape_level     = 0, k = 0;
> +
> +    switch (s->operation) {
> +    case OP_LEET:
> +    case OP_REPLACE_CHARS:
> +
> +        if (s->operation == OP_LEET) {
> +            char_src = leet_src;
> +            char_dst = leet_dst;
> +        }
> +
> +        result = av_strdup(text);
> +        if (!result)
> +            return NULL;
> +
> +        for (size_t n = 0; n < strlen(result); n++) {
> +            if (result[n] == '{')
> +                escape_level++;
> +
> +            if (!escape_level) {
> +                size_t len = strlen(char_src);
> +                for (size_t t = 0; t < len; t++) {
> +                    if (result[n] == char_src[t]) {
> +                        result[n] = char_dst[t];
> +                        break;
> +                    }
> +                }
> +            }
> +
> +            if (result[n] == '}')
> +                escape_level--;
> +        }
> +
> +        break;
> +    case OP_TO_UPPER:
> +    case OP_TO_LOWER:
> +
> +        result = av_strdup(text);
> +        if (!result)
> +            return NULL;
> +
> +        for (size_t n = 0; n < strlen(result); n++) {
> +            if (result[n] == '{')
> +                escape_level++;
> +            if (!escape_level)
> +                result[n] = s->operation == OP_TO_LOWER ? av_tolower(result[n]) : av_toupper(result[n]);
> +            if (result[n] == '}')
> +                escape_level--;
> +        }
> +
> +        break;
> +    case OP_REMOVE_CHARS:
> +
> +        result = av_strdup(text);
> +        if (!result)
> +            return NULL;
> +
> +        for (size_t n = 0; n < strlen(result); n++) {
> +            int skip_char = 0;
> +
> +            if (result[n] == '{')
> +                escape_level++;
> +
> +            if (!escape_level) {
> +                size_t len = strlen(char_src);
> +                for (size_t t = 0; t < len; t++) {
> +                    if (result[n] == char_src[t]) {
> +                        skip_char = 1;
> +                        break;
> +                    }
> +                }
> +            }
> +
> +            if (!skip_char)
> +                result[k++] = result[n];
> +
> +            if (result[n] == '}')
> +                escape_level--;
> +        }
> +
> +        result[k] = 0;
> +
> +        break;
> +    case OP_REPLACE_WORDS:
> +    case OP_REMOVE_WORDS:
> +
> +        result = av_strdup(text);
> +        if (!result)
> +            return NULL;
> +
> +        for (int n = 0; n < s->nb_find_list; n++) {
> +            char *tmp           = result;
> +            const char *replace = (s->operation == OP_REPLACE_WORDS) ? s->replace_list[n] : "";
> +
> +            result = av_strireplace(result, s->find_list[n], replace);
> +            if (!result)
> +                return NULL;
> +
> +            av_free(tmp);
> +        }
> +
> +        break;
> +    }
> +
> +    return result;
> +}
> +
> +static char *process_dialog_show_speaker(TextModContext *s, char *ass_line)
> +{
> +    ASSDialog *dialog = ff_ass_split_dialog(NULL, ass_line);
> +    int escape_level = 0;
> +    unsigned pos = 0, len;
> +    char *result, *text;
> +    AVBPrint pbuf;
> +
> +    if (!dialog)
> +        return NULL;
> +
> +    text = process_text(s, dialog->text);
> +    if (!text)
> +        return NULL;
> +
> +    if (!dialog->name || !strlen(dialog->name) || !dialog->text || !strlen(dialog->text))
> +        return av_strdup(ass_line);
> +
> +    // Find insertion point in case the line starts with style codes
> +    len = (unsigned)strlen(dialog->text);
> +    for (unsigned i = 0; i < len; i++) {
> +
> +        if (dialog->text[i] == '{')
> +            escape_level++;
> +
> +        if (dialog->text[i] == '}')
> +            escape_level--;
> +
> +        if (escape_level == 0) {
> +            pos = i;
> +            break;
> +        }
> +    }
> +
> +    if (s->style && strlen(s->style))
> +        // When a style is specified reset the insertion point 
> +        // (always add speaker plus style at the start in that case)
> +        pos = 0;
> +
> +    if (pos >= len - 1)
> +        return av_strdup(ass_line);
> +
> +    av_bprint_init(&pbuf, 1, AV_BPRINT_SIZE_UNLIMITED);
> +
> +    if (pos > 0) {
> +        av_bprint_append_data(&pbuf, dialog->text, pos);
> +    }
> +
> +    if (s->style && strlen(s->style)) {
> +        if (s->style[0] == '{')
> +            // Assume complete and valid style code, e.g. {\c&HFF0000&}
> +            av_bprintf(&pbuf, "%s", s->style);
> +        else
> +            // Otherwise it must be a style name
> +            av_bprintf(&pbuf, "{\\r%s}", s->style);
> +    }
> +
> +    switch (s->speaker_mode) {
> +    case SM_SQUARE_BRACKETS:
> +        av_bprintf(&pbuf, "[%s]", dialog->name);
> +        break;
> +    case SM_ROUND_BRACKETS:
> +        av_bprintf(&pbuf, "(%s)", dialog->name);
> +        break;
> +    case SM_COLON:
> +        av_bprintf(&pbuf, "%s:", dialog->name);
> +        break;
> +    case SM_PLAIN:
> +        av_bprintf(&pbuf, "%s", dialog->name);
> +        break;
> +    }
> +
> +    if (s->style && strlen(s->style)) {
> +        // Reset line style
> +        if (dialog->style && strlen(dialog->style) && !av_strcasecmp(dialog->style, "default"))
> +            av_bprintf(&pbuf, "{\\r%s}", dialog->style);
> +        else
> +            av_bprintf(&pbuf, "{\\r}");
> +    }
> +
> +    if (s->line_break)
> +        av_bprintf(&pbuf, "\\N");
> +    else
> +        av_bprintf(&pbuf, " ");
> +
> +    av_bprint_append_data(&pbuf, dialog->text + pos, len - pos);
> +
> +    av_bprint_finalize(&pbuf, &text);
> +
> +    result = ff_ass_get_dialog(dialog->readorder, dialog->layer, dialog->style, dialog->name, text);
> +
> +    av_free(text);
> +    ff_ass_free_dialog(&dialog);
> +    return result;
> +}
> +
> +static char *process_dialog(TextModContext *s, char *ass_line)
> +{
> +    ASSDialog *dialog;
> +    char *result, *text;
> +
> +    if (s->filter_type == TM_SHOW_SPEAKER)
> +        return process_dialog_show_speaker(s, ass_line);
> +
> +    dialog = ff_ass_split_dialog(NULL, ass_line);
> +    if (!dialog)
> +        return NULL;
> +
> +    text = process_text(s, dialog->text);
> +    if (!text)
> +        return NULL;
> +
> +    result = ff_ass_get_dialog(dialog->readorder, dialog->layer, dialog->style, dialog->name, text);
> +
> +    av_free(text);
> +    ff_ass_free_dialog(&dialog);
> +    return result;
> +}
> +
> +static int filter_frame(AVFilterLink *inlink, AVFrame *frame)
> +{
> +    TextModContext *s = inlink->dst->priv;
> +    AVFilterLink *outlink = inlink->dst->outputs[0];
> +
> +    outlink->format = inlink->format;
> +
> +    av_frame_make_writable(frame);
> +
> +    if (!frame)
> +        return AVERROR(ENOMEM);
> +
> +    for (unsigned i = 0; i < frame->num_subtitle_areas; i++) {
> +
> +        AVSubtitleArea *area = frame->subtitle_areas[i];
> +
> +        if (area->ass) {
> +            char *tmp = area->ass;
> +            area->ass = process_dialog(s, area->ass);
> +            av_free(tmp);
> +            if (!area->ass)
> +                return AVERROR(ENOMEM);
> +        }
> +    }
> +
> +    return ff_filter_frame(outlink, frame);
> +}
> +
> +#define OFFSET(x) offsetof(TextModContext, x)
> +#define FLAGS (AV_OPT_FLAG_SUBTITLE_PARAM | AV_OPT_FLAG_FILTERING_PARAM)
> +
> +static const AVOption textmod_options[] = {
> +    { "mode",             "set operation mode",              OFFSET(operation),    AV_OPT_TYPE_INT,    {.i64=OP_LEET},          OP_LEET, NB_OPS-1, FLAGS, "mode" },
> +    {   "leet",           "convert text to 'leet speak'",    0,                    AV_OPT_TYPE_CONST,  {.i64=OP_LEET},          0,       0,        FLAGS, "mode" },
> +    {   "to_upper",       "change to upper case",            0,                    AV_OPT_TYPE_CONST,  {.i64=OP_TO_UPPER},      0,       0,        FLAGS, "mode" },
> +    {   "to_lower",       "change to lower case",            0,                    AV_OPT_TYPE_CONST,  {.i64=OP_TO_LOWER},      0,       0,        FLAGS, "mode" },
> +    {   "replace_chars",  "replace characters",              0,                    AV_OPT_TYPE_CONST,  {.i64=OP_REPLACE_CHARS}, 0,       0,        FLAGS, "mode" },
> +    {   "remove_chars",   "remove characters",               0,                    AV_OPT_TYPE_CONST,  {.i64=OP_REMOVE_CHARS},  0,       0,        FLAGS, "mode" },
> +    {   "replace_words",  "replace words",                   0,                    AV_OPT_TYPE_CONST,  {.i64=OP_REPLACE_WORDS}, 0,       0,        FLAGS, "mode" },
> +    {   "remove_words",   "remove words",                    0,                    AV_OPT_TYPE_CONST,  {.i64=OP_REMOVE_WORDS},  0,       0,        FLAGS, "mode" },
> +    { "find",             "chars/words to find or remove",   OFFSET(find),         AV_OPT_TYPE_STRING, {.str = NULL},           0,       0,        FLAGS, NULL   },
> +    { "find_file",        "load find param from file",       OFFSET(find_file),    AV_OPT_TYPE_STRING, {.str = NULL},           0,       0,        FLAGS, NULL   },
> +    { "replace",          "chars/words to replace",          OFFSET(replace),      AV_OPT_TYPE_STRING, {.str = NULL},           0,       0,        FLAGS, NULL   },
> +    { "replace_file",     "load replace param from file",    OFFSET(replace_file), AV_OPT_TYPE_STRING, {.str = NULL},           0,       0,        FLAGS, NULL   },
> +    { "separator",        "word separator",                  OFFSET(separator),    AV_OPT_TYPE_STRING, {.str = ","},            0,       0,        FLAGS, NULL   },
> +    { NULL },
> +};
> +
> +
> +static const AVOption censor_options[] = {
> +    { "mode",               "set censoring mode",        OFFSET(censor_mode), AV_OPT_TYPE_INT,    {.i64=CM_KEEP_FIRST_LAST}, 0, 2, FLAGS, "mode" },
> +    {   "keep_first_last",  "censor inner chars",        0,                   AV_OPT_TYPE_CONST,  {.i64=CM_KEEP_FIRST_LAST}, 0, 0, FLAGS, "mode" },
> +    {   "keep_first",       "censor all but first char", 0,                   AV_OPT_TYPE_CONST,  {.i64=CM_KEEP_FIRST},      0, 0, FLAGS, "mode" },
> +    {   "all",              "censor all chars",          0,                   AV_OPT_TYPE_CONST,  {.i64=CM_ALL},             0, 0, FLAGS, "mode" },
> +    { "words",              "list of words to censor",   OFFSET(find),        AV_OPT_TYPE_STRING, {.str = NULL},             0, 0, FLAGS, NULL   },
> +    { "words_file",         "path to word list file",    OFFSET(find_file),   AV_OPT_TYPE_STRING, {.str = NULL},             0, 0, FLAGS, NULL   },
> +    { "separator",          "word separator",            OFFSET(separator),   AV_OPT_TYPE_STRING, {.str = ","},              0, 0, FLAGS, NULL   },
> +    { "censor_char",        "replacement character",     OFFSET(censor_char), AV_OPT_TYPE_STRING, {.str = "*"},              0, 0, FLAGS, NULL   },
> +    { NULL },
> +};
> +
> +static const AVOption show_speaker_options[] = {
> +    { "format",             "speaker name formatting",        OFFSET(speaker_mode), AV_OPT_TYPE_INT,    {.i64=SM_SQUARE_BRACKETS}, 0, 2, FLAGS, "format" },
> +    {   "square_brackets",  "[speaker] text",                 0,                    AV_OPT_TYPE_CONST,  {.i64=SM_SQUARE_BRACKETS}, 0, 0, FLAGS, "format" },
> +    {   "round_brackets",   "(speaker) text",                 0,                    AV_OPT_TYPE_CONST,  {.i64=SM_ROUND_BRACKETS},  0, 0, FLAGS, "format" },
> +    {   "colon",            "speaker: text",                  0,                    AV_OPT_TYPE_CONST,  {.i64=SM_COLON},           0, 0, FLAGS, "format" },
> +    {   "plain",            "speaker text",                   0,                    AV_OPT_TYPE_CONST,  {.i64=SM_PLAIN},           0, 0, FLAGS, "format" },
> +    { "line_break",         "insert line break",              OFFSET(line_break),   AV_OPT_TYPE_BOOL,   {.i64=0},                  0, 1, FLAGS, NULL     },
> +    { "style",              "ass type name or style code",    OFFSET(style),        AV_OPT_TYPE_STRING, {.str = NULL},             0, 0, FLAGS, NULL     },
> +    { NULL },
> +};
> +
> +AVFILTER_DEFINE_CLASS(textmod);
> +AVFILTER_DEFINE_CLASS(censor);
> +AVFILTER_DEFINE_CLASS(show_speaker);
> +
> +static const AVFilterPad inputs[] = {
> +    {
> +        .name         = "default",
> +        .type         = AVMEDIA_TYPE_SUBTITLE,
> +        .filter_frame = filter_frame,
> +    },
> +};
> +
> +static const AVFilterPad outputs[] = {
> +    {
> +        .name          = "default",
> +        .type          = AVMEDIA_TYPE_SUBTITLE,
> +    },
> +};
> +
> +/*
> + * Example:
> + * ffmpeg -i "http://streams.videolan.org/samples/sub/SSA/subtitle_testing_complex.mkv" -filter_complex "[0:1]textmod=mode=to_lower" -map 0 -y output.mkv 
> + */
> +const AVFilter ff_sf_textmod = {
> +    .name          = "textmod",
> +    .description   = NULL_IF_CONFIG_SMALL("Modify subtitle text in several ways"),
> +    .init          = init,
> +    .uninit        = uninit,
> +    .query_formats = query_formats,
> +    .priv_size     = sizeof(TextModContext),
> +    .priv_class    = &textmod_class,
> +    FILTER_INPUTS(inputs),
> +    FILTER_OUTPUTS(outputs),
> +};
> +
> +/*
> + * Example:
> + * ffmpeg -i "http://streams.videolan.org/samples/sub/SSA/subtitle_testing_complex.mkv" -filter_complex "[0:1]censor=words='diss,louder,hope,beam,word':censor_char='#'" -map 0 -y output.mkv
> + */
> +const AVFilter ff_sf_censor = {
> +    .name          = "censor",
> +    .description   = NULL_IF_CONFIG_SMALL("Censor words in subtitle text"),
> +    .init          = init_censor,
> +    .uninit        = uninit,
> +    .query_formats = query_formats,
> +    .priv_size     = sizeof(TextModContext),
> +    .priv_class    = &censor_class,
> +    FILTER_INPUTS(inputs),
> +    FILTER_OUTPUTS(outputs),
> +};
> +
> +/*
> + *  Example:
> + *
> + *  ffmpeg -ss 00:04:30 -i "U:\TestMedia\subtitle_burnin\subtitlevideo.mkv" -filter_complex "show_speaker=style='{\\c&HDD0000&\\be1\\\i1\\bord10}:line_break=1',[0:v]overlay_textsubs" -map 0 -c:v libx265 output.mkv
> + */

These examples belong into filters.texi.

> +const AVFilter ff_sf_show_speaker = {
> +    .name          = "show_speaker",
> +    .description   = NULL_IF_CONFIG_SMALL("Prepend speaker names to text subtitles (when available)"),
> +    .init          = init_show_speaker,
> +    .uninit        = uninit,
> +    .query_formats = query_formats,
> +    .priv_size     = sizeof(TextModContext),
> +    .priv_class    = &show_speaker_class,
> +    FILTER_INPUTS(inputs),
> +    FILTER_OUTPUTS(outputs),
> +};
>
Soft Works Sept. 22, 2021, 3:36 a.m. UTC | #2
> -----Original Message-----
> From: ffmpeg-devel <ffmpeg-devel-bounces@ffmpeg.org> On Behalf Of Andreas
> Rheinhardt
> Sent: Wednesday, 22 September 2021 05:08
> To: ffmpeg-devel@ffmpeg.org
> Subject: Re: [FFmpeg-devel] [PATCH v8 09/13] avfilter/textmod: Add textmod,
> censor and show_speaker filters
> 

[..]
 
> This function is horrible (and I already wanted to deprecate it, but for
> this I would need to fix all current users of it): It automatically
> frees the array on error and it returns void, thereby encouraging a
> programming style in which errors are not checked. (You are checking
> them.) The automatic free on error means that there are basically only
> two usecases for it (unless one just ignores any errors): To store
> pointers to objects that don't need to be freed at all (like static
> objects) or to store non-ownership pointers, like pointers to substrings
> of a string. Now that you started strduping the strings you are no
> longer of this type. In other words, there will be leaks on error.
> 
> Anyway, I don't see why you went the route of using individually
> allocated strings.

[..]

> Leak on error, as above.
> (Notice that you do not need to allocate the replace_list entries
> individually; all you need to do is strdup s->find before it is split
> into little pieces. Then you can get the offset of the nth replace
> string in the duplicate of s->find by s->find_list[n] - s->find.)
> Finally, when dealing with the replace list you already know in advance
> how many entries there will be, so you don't have to reallocate.

The reason why I changed to individual allocation is that eventually 
I want to be able to:

- Split by multiple separators, e.g. '\r', '\n' in addition to the 
  chosen separator
- Remove empty entries (when two separators follow after each other)
- Trim leading and trailing whitespace from each entry

Doing all these things with pointers to the full string seems to be 
too much of a hazzle and error prone.

> > + *
> > + *  ffmpeg -ss 00:04:30 -i
> "U:\TestMedia\subtitle_burnin\subtitlevideo.mkv" -filter_complex
> "show_speaker=style='{\\c&HDD0000&\\be1\\\i1\\bord10}:line_break=1',[0:v]over
> lay_textsubs" -map 0 -c:v libx265 output.mkv
> > + */
> 
> These examples belong into filters.texi.

They are in filters.texi.
I left them here as well for easier reference, but I can remove these.

Thanks again,
softworkz
Andreas Rheinhardt Sept. 22, 2021, 3:49 a.m. UTC | #3
Soft Works:
> 
> 
>> -----Original Message-----
>> From: ffmpeg-devel <ffmpeg-devel-bounces@ffmpeg.org> On Behalf Of Andreas
>> Rheinhardt
>> Sent: Wednesday, 22 September 2021 05:08
>> To: ffmpeg-devel@ffmpeg.org
>> Subject: Re: [FFmpeg-devel] [PATCH v8 09/13] avfilter/textmod: Add textmod,
>> censor and show_speaker filters
>>
> 
> [..]
>  
>> This function is horrible (and I already wanted to deprecate it, but for
>> this I would need to fix all current users of it): It automatically
>> frees the array on error and it returns void, thereby encouraging a
>> programming style in which errors are not checked. (You are checking
>> them.) The automatic free on error means that there are basically only
>> two usecases for it (unless one just ignores any errors): To store
>> pointers to objects that don't need to be freed at all (like static
>> objects) or to store non-ownership pointers, like pointers to substrings
>> of a string. Now that you started strduping the strings you are no
>> longer of this type. In other words, there will be leaks on error.
>>
>> Anyway, I don't see why you went the route of using individually
>> allocated strings.
> 
> [..]
> 
>> Leak on error, as above.
>> (Notice that you do not need to allocate the replace_list entries
>> individually; all you need to do is strdup s->find before it is split
>> into little pieces. Then you can get the offset of the nth replace
>> string in the duplicate of s->find by s->find_list[n] - s->find.)
>> Finally, when dealing with the replace list you already know in advance
>> how many entries there will be, so you don't have to reallocate.
> 
> The reason why I changed to individual allocation is that eventually 
> I want to be able to:
> 
> - Split by multiple separators, e.g. '\r', '\n' in addition to the 
>   chosen separator
> - Remove empty entries (when two separators follow after each other)
> - Trim leading and trailing whitespace from each entry
> 
> Doing all these things with pointers to the full string seems to be 
> too much of a hazzle and error prone.
> 

1. av_strtok() can separate by multiple separators: The deliminator is
actually supposed to be a NUL-terminated string. In other words: Using a
pointer to a char is wrong and may crash.
2. av_strtok() already trims empty entries. (And if it did not, all you
needed to do were to check for emptiness before adding the pointer to
the list, like you already do.)
3. The last thing is also easily achievable with non-allocated pointers.

As long as you don't want to enlarge a substring, there is no need for
allocations.

- Andreas
diff mbox series

Patch

diff --git a/doc/filters.texi b/doc/filters.texi
index 416bda5efb..2d3dcdd7e6 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -25079,6 +25079,7 @@  tools.
 
 @c man end VIDEO SINKS
 
+
 @chapter Subtitle Filters
 @c man begin SUBTITLE FILTERS
 
@@ -25087,6 +25088,166 @@  existing filters using @code{--disable-filters}.
 
 Below is a description of the currently available subtitle filters.
 
+
+@section censor
+
+Censor selected words in text subtitles.
+
+Inputs:
+- 0: Subtitles [text]
+
+Outputs:
+- 0: Subtitles [text]
+
+It accepts the following parameters:
+
+@table @option
+@item mode
+The censoring mode to apply.
+
+Supported censoring modes are:
+
+@table @var
+@item 0, keep_first_last
+Replace all characters with the 'censor_char' except the first and the last character of a word.
+For words with less than 4 characters, the last character will be replaced as well.
+For words with less than 3 characters, the first character will be replaced as well.
+@item 1, keep_first
+Replace all characters with the 'censor_char' except the first character of a word.
+For words with less than 3 characters, the first character will be replaced as well.
+@item 2, all
+Replace all characters with the 'censor_char'.
+@end table
+
+@item words
+A list of words to censor, separated by 'separator'.
+
+@item words_file
+Specify a file from which to load the contents for the 'words' parameter.
+
+@item censor_char
+Single character used as replacement for censoring.
+
+@item separator
+Delimiter character for words. Used with replace_words and remove_words- Must be a single character.
+The default is '.'.
+
+@end table
+
+@subsection Examples
+
+@itemize
+@item
+Change all characters to upper case while keeping all styles and animations:
+@example
+ffmpeg -i "https://streams.videolan.org/ffmpeg/mkv_subtitles.mkv" -filter_complex "[0:s]textmod=mode=to_upper" -map 0 -y out.mkv
+@end example
+@item
+Remove a set of symbol characters for am improved and smoother visual apperance:
+@example
+ffmpeg -i "https://streams.videolan.org/ffmpeg/mkv_subtitles.mkv" -filter_complex "[0:s]textmod=mode=remove_chars:find='$&#@*§'" -map 0 -y out.mkv
+@end example
+@end itemize
+
+
+@section stripstyles
+
+Remove all inline styles from subtitle events.
+
+It accepts the following parameters:
+
+@table @option
+@item remove_animated
+Also remove text which is subject to animation (default: true)
+Usually, animated text elements are used used in addition to static subtitle lines for creating effects, so in most cases it is safe to remove the animation content.
+If subtitle text is missing, try setting this to false.
+
+@end table
+
+@subsection Examples
+
+@itemize
+@item
+Remove all styles and animations from subtitles:
+@example
+ffmpeg -i "https://streams.videolan.org/samples/sub/SSA/subtitle_testing_complex.mkv" -filter_complex "[0:1]stripstyles" -map 0 output.mkv
+@end example
+@end itemize
+
+@section textmod
+
+Modify subtitle text in a number of ways.
+
+Inputs:
+- 0: Subtitles [text]
+
+Outputs:
+- 0: Subtitles [text]
+
+It accepts the following parameters:
+
+@table @option
+@item mode
+The kind of text modification to apply
+
+Supported operation modes are:
+
+@table @var
+@item 0, leet
+Convert subtitle text to 'leet speak'. It's primarily useful for testing as the modification will be visible with almost all text lines.
+@item 1, to_upper
+Change all text to upper case. Might improve readability.
+@item 2, to_lower
+Change all text to lower case.
+@item 3, replace_chars
+Replace one or more characters. Requires the find and replace parameters to be specified. 
+Both need to be equal in length.
+The first char in find is replaced by the first char in replace, same for all subsequent chars.
+@item 4, remove_chars
+Remove certain characters. Requires the find parameter to be specified. 
+All chars in the find parameter string will be removed from all subtitle text.
+@item 5, replace_words
+Replace one or more words. Requires the find and replace parameters to be specified. Multiple words must be separated by the delimiter char specified vie the separator parameter (default: ','). 
+The number of words in the find and replace parameters needs to be equal.
+The first word in find is replaced by the first word in replace, same for all subsequent words
+@item 6, remove_words
+Remove certain words. Requires the find parameter to be specified. Multiple words must be separated by the delimiter char specified vie the separator parameter (default: ','). 
+All words in the find parameter string will be removed from all subtitle text.
+@end table
+
+@item find
+Required for replace_chars, remove_chars, replace_words and remove_words.
+
+@item find_file
+Specify a file from which to load the contents for the 'find' parameter.
+
+@item replace
+Required for replace_chars and replace_words.
+
+@item replace_file
+Specify a file from which to load the contents for the 'replace' parameter.
+
+@item separator
+Delimiter character for words. Used with replace_words and remove_words- Must be a single character.
+The default is '.'.
+
+@end table
+
+@subsection Examples
+
+@itemize
+@item
+Change all characters to upper case while keeping all styles and animations:
+@example
+ffmpeg -i "https://streams.videolan.org/ffmpeg/mkv_subtitles.mkv" -filter_complex "[0:s]textmod=mode=to_upper" -map 0 -y out.mkv
+@end example
+@item
+Remove a set of symbol characters for am improved and smoother visual apperance:
+@example
+ffmpeg -i "https://streams.videolan.org/ffmpeg/mkv_subtitles.mkv" -filter_complex "[0:s]textmod=mode=remove_chars:find='$&#@*§'" -map 0 -y out.mkv
+@end example
+@end itemize
+
 @section graphicsub2video
 
 Renders graphic subtitles as video frames. 
@@ -25131,6 +25292,11 @@  Overlay graphic subtitles onto a video stream.
 This filter can blend graphical subtitles on a video stream directly, i.e. without creating full-size alpha images first.
 The blending operation is limited to the area of the subtitle rectangles, which also means that no processing is done at times where no subtitles are to be displayed.
 
+- 0: Video [YUV420P, YUV422P, YUV444P, ARGB, RGBA, ABGR, BGRA, RGB24, BGR24]
+- 1: Subtitles [bitmap]
+
+Outputs:
+- 0: Video (same as input)
 
 It accepts the following parameters:
 
@@ -25212,6 +25378,69 @@  string containing ASS style format @code{KEY=VALUE} couples separated by ",".
 
 @end table
 
+@section show_speaker
+
+Prepend speaker names to subtitle lines (when available).
+
+Subtitles in ASS/SSA format are often including the names of the persons
+or character for each subtitle line. The show_speaker filter adds those names 
+to the actual subtitle text to make it visible on playback.
+
+Inputs:
+- 0: Subtitles [text]
+
+Outputs:
+- 0: Subtitles [text]
+
+It accepts the following parameters:
+
+@table @option
+@item format
+The format for prepending speaker names. Default is 'square_brackets'.
+
+Supported operation modes are:
+
+@table @var
+@item 0, square_brackets
+Enclose the speaker name in square brackets, followed by space ('[speaker] text').
+@item 1, round_brackets
+Enclose the speaker name in round brackets, followed by space ('(speaker) text').
+@item 2, colon
+Separate the speaker name with a colon and space ('speaker: text').
+@item 3, plain
+Separate the speaker name with a space only ('speaker text').
+@end table
+
+@item line_break
+Set thís parameter to insert a line break between speaker name and text instead of the space character.
+
+@item style
+Allows to set a specific style for the speaker name text. 
+
+This can be either a named style that exists in the ass subtitle header script (e.g. 'Default') or an ass style override code.
+Example:  @{\\c&HDD0000&\\be1\\i1\\bord10@}
+This sets the color to blue, enables edge blurring, italic font and a border of size 10.
+
+The behavior is as follows:
+
+- When the style parameter is not provided, the filter will find the first position in the event string that is actual text.
+  The speaker name will be inserted at this position. This allows to have the speaker name shown in the same style like the 
+  regular text, in case the string would start with a sequence of style codes.
+- When the style parameter is provided, everything will be prepended to the original text: 
+  Style Code or Style name >> Speaker Name >> Style Reset Code >> Original Text
+
+@end table
+
+@subsection Examples
+
+@itemize
+@item
+Prepend speaker names with blue text, smooth edges and blend/overlay that onto the video.
+@example
+ffmpeg -i INPUT -filter_complex "show_speaker=format=colon:style='@{\\c&HDD0000&\\be1@}',[0:v]overlay_textsubs"
+@end example
+@end itemize
+
 @section textsub2video
 
 Converts text subtitles to video frames.
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index c0b1cc7001..e6fef97c08 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -536,6 +536,11 @@  OBJS-$(CONFIG_YUVTESTSRC_FILTER)             += vsrc_testsrc.o
 
 OBJS-$(CONFIG_NULLSINK_FILTER)               += vsink_nullsink.o
 
+# subtitle filters
+OBJS-$(CONFIG_CENSOR_FILTER)                 += sf_textmod.o
+OBJS-$(CONFIG_SHOW_SPEAKER_FILTER)           += sf_textmod.o
+OBJS-$(CONFIG_TEXTMOD_FILTER)                += sf_textmod.o
+
 # multimedia filters
 OBJS-$(CONFIG_ABITSCOPE_FILTER)              += avf_abitscope.o
 OBJS-$(CONFIG_ADRAWGRAPH_FILTER)             += f_drawgraph.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index 8543fa22e9..fc72858d18 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -526,6 +526,9 @@  extern const AVFilter ff_avf_showvolume;
 extern const AVFilter ff_avf_showwaves;
 extern const AVFilter ff_avf_showwavespic;
 extern const AVFilter ff_vaf_spectrumsynth;
+extern const AVFilter ff_sf_censor;
+extern const AVFilter ff_sf_show_speaker;
+extern const AVFilter ff_sf_textmod;
 extern const AVFilter ff_svf_graphicsub2video;
 extern const AVFilter ff_svf_textsub2video;
 
diff --git a/libavfilter/sf_textmod.c b/libavfilter/sf_textmod.c
new file mode 100644
index 0000000000..14049202aa
--- /dev/null
+++ b/libavfilter/sf_textmod.c
@@ -0,0 +1,727 @@ 
+/*
+ * Copyright (c) 2021 softworkz
+ *
+ * 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 subtitle filter which allows to modify subtitle text in several ways
+ */
+
+#include <libavcodec/ass.h>
+
+#include "libavutil/avassert.h"
+#include "libavutil/avstring.h"
+#include "libavutil/opt.h"
+#include "avfilter.h"
+#include "internal.h"
+#include "libavcodec/avcodec.h"
+#include "libavcodec/ass_split.h"
+#include "libavutil/file.h"
+
+static const char* leet_src = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+static const char* leet_dst = "abcd3f6#1jklmn0pq257uvwxyzAB(D3F6#1JKLMN0PQ257UVWXYZ";
+
+enum TextModFilterType {
+    TM_TEXTMOD,
+    TM_CENSOR,
+    TM_SHOW_SPEAKER,
+};
+
+enum TextModOperation {
+    OP_LEET,
+    OP_TO_UPPER,
+    OP_TO_LOWER,
+    OP_REPLACE_CHARS,
+    OP_REMOVE_CHARS,
+    OP_REPLACE_WORDS,
+    OP_REMOVE_WORDS,
+    NB_OPS,
+};
+
+enum CensorMode {
+    CM_KEEP_FIRST_LAST,
+    CM_KEEP_FIRST,
+    CM_ALL,
+};
+
+enum ShowSpeakerMode {
+    SM_SQUARE_BRACKETS,
+    SM_ROUND_BRACKETS,
+    SM_COLON,
+    SM_PLAIN,
+};
+
+typedef struct TextModContext {
+    const AVClass *class;
+    enum AVSubtitleType format;
+    enum TextModFilterType filter_type;
+    enum TextModOperation operation;
+    enum CensorMode censor_mode;
+    enum ShowSpeakerMode speaker_mode;
+    char *find;
+    char *find_file;
+    char *style;
+    char *replace;
+    char *replace_file;
+    char *separator;
+    char *censor_char;
+    char **find_list;
+    int  line_break;
+    int  nb_find_list;
+    char **replace_list;
+    int  nb_replace_list;
+} TextModContext;
+
+static char **split_string(char *source, int *nb_elems, char delim)
+{
+    char **list = NULL;
+    char *temp = NULL;
+    char *ptr = av_strtok(source, &delim, &temp);
+
+    while (ptr) {
+        if (strlen(ptr)) {
+            char *item = av_strdup(ptr);
+            if (!item)
+                return NULL;
+            av_dynarray_add(&list, nb_elems, item);
+            if (!list)
+                return NULL;
+        }
+
+        ptr = av_strtok(NULL, &delim, &temp);
+    }
+
+    return list;
+}
+
+static int load_text_from_file(AVFilterContext *ctx, const char *file_name, char **text, char separator)
+{
+    int err;
+    uint8_t *textbuf;
+    char *tmp;
+    size_t textbuf_size;
+    int offset = 0;
+
+    if ((err = av_file_map(file_name, &textbuf, &textbuf_size, 0, ctx)) < 0) {
+        av_log(ctx, AV_LOG_ERROR, "The text file '%s' could not be read or is empty\n", file_name);
+        return err;
+    }
+
+    if (textbuf_size > 1 && 
+        (textbuf[0] == 0xFF && textbuf[1] == 0xFE
+        || textbuf[0] == 0xFE && textbuf[1] == 0xFF)) {
+        av_log(ctx, AV_LOG_ERROR, "UTF text files are not supported. File: %s\n", file_name);
+        return AVERROR(EINVAL);
+    }
+
+    if (textbuf_size > 2 && textbuf[0] == 0xEF && textbuf[1] == 0xBB && textbuf[2] == 0xBF)
+        offset = 3; // UTF-8
+
+    if (textbuf_size > SIZE_MAX - 1 || !((tmp = av_strndup((char *)textbuf + offset, textbuf_size - offset)))) {
+        av_file_unmap(textbuf, textbuf_size);
+        return AVERROR(ENOMEM);
+    }
+
+    av_file_unmap(textbuf, textbuf_size);
+
+    for (size_t i = 0; i < strlen(tmp); i++) {
+        switch (tmp[i]) {
+        case '\n':
+        case '\r':
+        case '\f':
+        case '\v':
+            tmp[i] = separator;
+        }
+    }
+
+    *text = tmp;
+
+    return 0;
+}
+
+static int load_files(AVFilterContext *ctx)
+{
+    TextModContext *s = ctx->priv;
+    int ret;
+
+    if (!s->separator || strlen(s->separator) != 1) {
+        av_log(ctx, AV_LOG_ERROR, "A single character needs to be specified for the separator parameter.\n");
+        return AVERROR(EINVAL);
+    }
+
+    if (s->find_file && strlen(s->find_file)) {
+        ret = load_text_from_file(ctx, s->find_file, &s->find, s->separator[0]);
+        if (ret < 0 )
+            return ret;
+    }
+
+    if (s->replace_file && strlen(s->replace_file)) {
+        ret = load_text_from_file(ctx, s->replace_file, &s->replace, s->separator[0]);
+        if (ret < 0 )
+            return ret;
+    }
+
+    return 0;
+}
+
+static int init_censor(AVFilterContext *ctx)
+{
+    TextModContext *s = ctx->priv;
+    char **list = NULL;
+    int ret;
+
+    s->filter_type = TM_CENSOR;
+    s->operation = OP_REPLACE_WORDS;
+
+    ret = load_files(ctx);
+    if (ret < 0 )
+        return ret;
+
+    if (!s->find || !strlen(s->find)) {
+        av_log(ctx, AV_LOG_ERROR, "Either the 'words' or the 'words_file' parameter needs to be specified\n");
+        return AVERROR(EINVAL);
+    }
+
+    if (!s->censor_char || strlen(s->censor_char) != 1) {
+        av_log(ctx, AV_LOG_ERROR, "A single character needs to be specified for the censor_char parameter\n");
+        return AVERROR(EINVAL);
+    }
+
+    s->find_list = split_string(s->find, &s->nb_find_list, *s->separator);
+    if (!s->find_list)
+        return AVERROR(ENOMEM);
+
+    for (unsigned i = 0; i < s->nb_find_list; i++) {
+        char *item = av_strdup(s->find_list[i]);
+        size_t len, start = 0, end;
+        if (!item)
+            return AVERROR(ENOMEM);
+
+        len = end = strlen(item);
+
+        switch (s->censor_mode) {
+        case CM_KEEP_FIRST_LAST:
+
+            if (len > 2)
+                start = 1;
+            if (len > 3)
+                end--;
+
+            break;
+        case CM_KEEP_FIRST:
+
+            if (len > 2)
+                start = 1;
+
+            break;
+        }
+
+        for (size_t n = start; n < end; n++) {
+            item[n] = s->censor_char[0];
+        }
+
+        av_dynarray_add(&list, &s->nb_replace_list, item);
+    }
+
+    s->replace_list = list;
+    if (!s->replace_list)
+        return AVERROR(ENOMEM);
+
+    return 0;
+}
+
+static int init_show_speaker(AVFilterContext *ctx)
+{
+    TextModContext *s = ctx->priv;
+    s->filter_type = TM_SHOW_SPEAKER;
+
+    return 0;
+}
+
+static int init(AVFilterContext *ctx)
+{
+    TextModContext *s = ctx->priv;
+    int ret;
+
+    ret = load_files(ctx);
+    if (ret < 0 )
+        return ret;
+
+    switch (s->operation) {
+    case OP_REPLACE_CHARS:
+    case OP_REMOVE_CHARS:
+    case OP_REPLACE_WORDS:
+    case OP_REMOVE_WORDS:
+        if (!s->find || !strlen(s->find)) {
+            av_log(ctx, AV_LOG_ERROR, "Selected mode requires the 'find' parameter to be specified\n");
+            return AVERROR(EINVAL);
+        }
+        break;
+    }
+
+    switch (s->operation) {
+    case OP_REPLACE_CHARS:
+    case OP_REPLACE_WORDS:
+        if (!s->replace || !strlen(s->replace)) {
+            av_log(ctx, AV_LOG_ERROR, "Selected mode requires the 'replace' parameter to be specified\n");
+            return AVERROR(EINVAL);
+        }
+        break;
+    }
+
+    if (s->operation == OP_REPLACE_CHARS && strlen(s->find) != strlen(s->replace)) {
+        av_log(ctx, AV_LOG_ERROR, "Selected mode requires the 'find' and 'replace' parameters to have the same length\n");
+        return AVERROR(EINVAL);
+    }
+
+    if (s->operation == OP_REPLACE_WORDS || s->operation == OP_REMOVE_WORDS) {
+        if (!s->separator || strlen(s->separator) != 1) {
+            av_log(ctx, AV_LOG_ERROR, "Selected mode requires a single separator char to be specified\n");
+            return AVERROR(EINVAL);
+        }
+
+        s->find_list = split_string(s->find, &s->nb_find_list, *s->separator);
+        if (!s->find_list)
+            return AVERROR(ENOMEM);
+
+        if (s->operation == OP_REPLACE_WORDS) {
+
+            s->replace_list = split_string(s->replace, &s->nb_replace_list, *s->separator);
+            if (!s->replace_list)
+                return AVERROR(ENOMEM);
+
+            if (s->nb_find_list != s->nb_replace_list) {
+                av_log(ctx, AV_LOG_ERROR, "The number of words in 'find' and 'replace' needs to be equal\n");
+                return AVERROR(EINVAL);
+            }
+        }
+    }
+
+    return 0;
+}
+
+static void uninit(AVFilterContext *ctx)
+{
+    TextModContext *s = ctx->priv;
+
+    for (int i = 0; i < s->nb_find_list; i++)
+        av_freep(&s->find_list[i]);
+
+    s->nb_find_list = 0;
+    av_freep(&s->find_list);
+
+    for (int i = 0; i < s->nb_replace_list; i++)
+        av_freep(&s->replace_list[i]);
+
+    s->nb_replace_list = 0;
+    av_freep(&s->replace_list);
+}
+
+static int query_formats(AVFilterContext *ctx)
+{
+    AVFilterFormats *formats;
+    AVFilterLink *inlink = ctx->inputs[0];
+    AVFilterLink *outlink = ctx->outputs[0];
+    static const enum AVSubtitleType subtitle_fmts[] = { AV_SUBTITLE_FMT_ASS, AV_SUBTITLE_FMT_NONE };
+    int ret;
+
+    /* set input formats */
+    formats = ff_make_format_list(subtitle_fmts);
+    if ((ret = ff_formats_ref(formats, &inlink->outcfg.formats)) < 0)
+        return ret;
+
+    /* set output formats */
+    if ((ret = ff_formats_ref(formats, &outlink->incfg.formats)) < 0)
+        return ret;
+
+    return 0;
+}
+
+
+
+static char *process_text(TextModContext *s, char *text)
+{
+    const char *char_src = s->find;
+    const char *char_dst = s->replace;
+    char *result         = NULL;
+    int escape_level     = 0, k = 0;
+
+    switch (s->operation) {
+    case OP_LEET:
+    case OP_REPLACE_CHARS:
+
+        if (s->operation == OP_LEET) {
+            char_src = leet_src;
+            char_dst = leet_dst;
+        }
+
+        result = av_strdup(text);
+        if (!result)
+            return NULL;
+
+        for (size_t n = 0; n < strlen(result); n++) {
+            if (result[n] == '{')
+                escape_level++;
+
+            if (!escape_level) {
+                size_t len = strlen(char_src);
+                for (size_t t = 0; t < len; t++) {
+                    if (result[n] == char_src[t]) {
+                        result[n] = char_dst[t];
+                        break;
+                    }
+                }
+            }
+
+            if (result[n] == '}')
+                escape_level--;
+        }
+
+        break;
+    case OP_TO_UPPER:
+    case OP_TO_LOWER:
+
+        result = av_strdup(text);
+        if (!result)
+            return NULL;
+
+        for (size_t n = 0; n < strlen(result); n++) {
+            if (result[n] == '{')
+                escape_level++;
+            if (!escape_level)
+                result[n] = s->operation == OP_TO_LOWER ? av_tolower(result[n]) : av_toupper(result[n]);
+            if (result[n] == '}')
+                escape_level--;
+        }
+
+        break;
+    case OP_REMOVE_CHARS:
+
+        result = av_strdup(text);
+        if (!result)
+            return NULL;
+
+        for (size_t n = 0; n < strlen(result); n++) {
+            int skip_char = 0;
+
+            if (result[n] == '{')
+                escape_level++;
+
+            if (!escape_level) {
+                size_t len = strlen(char_src);
+                for (size_t t = 0; t < len; t++) {
+                    if (result[n] == char_src[t]) {
+                        skip_char = 1;
+                        break;
+                    }
+                }
+            }
+
+            if (!skip_char)
+                result[k++] = result[n];
+
+            if (result[n] == '}')
+                escape_level--;
+        }
+
+        result[k] = 0;
+
+        break;
+    case OP_REPLACE_WORDS:
+    case OP_REMOVE_WORDS:
+
+        result = av_strdup(text);
+        if (!result)
+            return NULL;
+
+        for (int n = 0; n < s->nb_find_list; n++) {
+            char *tmp           = result;
+            const char *replace = (s->operation == OP_REPLACE_WORDS) ? s->replace_list[n] : "";
+
+            result = av_strireplace(result, s->find_list[n], replace);
+            if (!result)
+                return NULL;
+
+            av_free(tmp);
+        }
+
+        break;
+    }
+
+    return result;
+}
+
+static char *process_dialog_show_speaker(TextModContext *s, char *ass_line)
+{
+    ASSDialog *dialog = ff_ass_split_dialog(NULL, ass_line);
+    int escape_level = 0;
+    unsigned pos = 0, len;
+    char *result, *text;
+    AVBPrint pbuf;
+
+    if (!dialog)
+        return NULL;
+
+    text = process_text(s, dialog->text);
+    if (!text)
+        return NULL;
+
+    if (!dialog->name || !strlen(dialog->name) || !dialog->text || !strlen(dialog->text))
+        return av_strdup(ass_line);
+
+    // Find insertion point in case the line starts with style codes
+    len = (unsigned)strlen(dialog->text);
+    for (unsigned i = 0; i < len; i++) {
+
+        if (dialog->text[i] == '{')
+            escape_level++;
+
+        if (dialog->text[i] == '}')
+            escape_level--;
+
+        if (escape_level == 0) {
+            pos = i;
+            break;
+        }
+    }
+
+    if (s->style && strlen(s->style))
+        // When a style is specified reset the insertion point 
+        // (always add speaker plus style at the start in that case)
+        pos = 0;
+
+    if (pos >= len - 1)
+        return av_strdup(ass_line);
+
+    av_bprint_init(&pbuf, 1, AV_BPRINT_SIZE_UNLIMITED);
+
+    if (pos > 0) {
+        av_bprint_append_data(&pbuf, dialog->text, pos);
+    }
+
+    if (s->style && strlen(s->style)) {
+        if (s->style[0] == '{')
+            // Assume complete and valid style code, e.g. {\c&HFF0000&}
+            av_bprintf(&pbuf, "%s", s->style);
+        else
+            // Otherwise it must be a style name
+            av_bprintf(&pbuf, "{\\r%s}", s->style);
+    }
+
+    switch (s->speaker_mode) {
+    case SM_SQUARE_BRACKETS:
+        av_bprintf(&pbuf, "[%s]", dialog->name);
+        break;
+    case SM_ROUND_BRACKETS:
+        av_bprintf(&pbuf, "(%s)", dialog->name);
+        break;
+    case SM_COLON:
+        av_bprintf(&pbuf, "%s:", dialog->name);
+        break;
+    case SM_PLAIN:
+        av_bprintf(&pbuf, "%s", dialog->name);
+        break;
+    }
+
+    if (s->style && strlen(s->style)) {
+        // Reset line style
+        if (dialog->style && strlen(dialog->style) && !av_strcasecmp(dialog->style, "default"))
+            av_bprintf(&pbuf, "{\\r%s}", dialog->style);
+        else
+            av_bprintf(&pbuf, "{\\r}");
+    }
+
+    if (s->line_break)
+        av_bprintf(&pbuf, "\\N");
+    else
+        av_bprintf(&pbuf, " ");
+
+    av_bprint_append_data(&pbuf, dialog->text + pos, len - pos);
+
+    av_bprint_finalize(&pbuf, &text);
+
+    result = ff_ass_get_dialog(dialog->readorder, dialog->layer, dialog->style, dialog->name, text);
+
+    av_free(text);
+    ff_ass_free_dialog(&dialog);
+    return result;
+}
+
+static char *process_dialog(TextModContext *s, char *ass_line)
+{
+    ASSDialog *dialog;
+    char *result, *text;
+
+    if (s->filter_type == TM_SHOW_SPEAKER)
+        return process_dialog_show_speaker(s, ass_line);
+
+    dialog = ff_ass_split_dialog(NULL, ass_line);
+    if (!dialog)
+        return NULL;
+
+    text = process_text(s, dialog->text);
+    if (!text)
+        return NULL;
+
+    result = ff_ass_get_dialog(dialog->readorder, dialog->layer, dialog->style, dialog->name, text);
+
+    av_free(text);
+    ff_ass_free_dialog(&dialog);
+    return result;
+}
+
+static int filter_frame(AVFilterLink *inlink, AVFrame *frame)
+{
+    TextModContext *s = inlink->dst->priv;
+    AVFilterLink *outlink = inlink->dst->outputs[0];
+
+    outlink->format = inlink->format;
+
+    av_frame_make_writable(frame);
+
+    if (!frame)
+        return AVERROR(ENOMEM);
+
+    for (unsigned i = 0; i < frame->num_subtitle_areas; i++) {
+
+        AVSubtitleArea *area = frame->subtitle_areas[i];
+
+        if (area->ass) {
+            char *tmp = area->ass;
+            area->ass = process_dialog(s, area->ass);
+            av_free(tmp);
+            if (!area->ass)
+                return AVERROR(ENOMEM);
+        }
+    }
+
+    return ff_filter_frame(outlink, frame);
+}
+
+#define OFFSET(x) offsetof(TextModContext, x)
+#define FLAGS (AV_OPT_FLAG_SUBTITLE_PARAM | AV_OPT_FLAG_FILTERING_PARAM)
+
+static const AVOption textmod_options[] = {
+    { "mode",             "set operation mode",              OFFSET(operation),    AV_OPT_TYPE_INT,    {.i64=OP_LEET},          OP_LEET, NB_OPS-1, FLAGS, "mode" },
+    {   "leet",           "convert text to 'leet speak'",    0,                    AV_OPT_TYPE_CONST,  {.i64=OP_LEET},          0,       0,        FLAGS, "mode" },
+    {   "to_upper",       "change to upper case",            0,                    AV_OPT_TYPE_CONST,  {.i64=OP_TO_UPPER},      0,       0,        FLAGS, "mode" },
+    {   "to_lower",       "change to lower case",            0,                    AV_OPT_TYPE_CONST,  {.i64=OP_TO_LOWER},      0,       0,        FLAGS, "mode" },
+    {   "replace_chars",  "replace characters",              0,                    AV_OPT_TYPE_CONST,  {.i64=OP_REPLACE_CHARS}, 0,       0,        FLAGS, "mode" },
+    {   "remove_chars",   "remove characters",               0,                    AV_OPT_TYPE_CONST,  {.i64=OP_REMOVE_CHARS},  0,       0,        FLAGS, "mode" },
+    {   "replace_words",  "replace words",                   0,                    AV_OPT_TYPE_CONST,  {.i64=OP_REPLACE_WORDS}, 0,       0,        FLAGS, "mode" },
+    {   "remove_words",   "remove words",                    0,                    AV_OPT_TYPE_CONST,  {.i64=OP_REMOVE_WORDS},  0,       0,        FLAGS, "mode" },
+    { "find",             "chars/words to find or remove",   OFFSET(find),         AV_OPT_TYPE_STRING, {.str = NULL},           0,       0,        FLAGS, NULL   },
+    { "find_file",        "load find param from file",       OFFSET(find_file),    AV_OPT_TYPE_STRING, {.str = NULL},           0,       0,        FLAGS, NULL   },
+    { "replace",          "chars/words to replace",          OFFSET(replace),      AV_OPT_TYPE_STRING, {.str = NULL},           0,       0,        FLAGS, NULL   },
+    { "replace_file",     "load replace param from file",    OFFSET(replace_file), AV_OPT_TYPE_STRING, {.str = NULL},           0,       0,        FLAGS, NULL   },
+    { "separator",        "word separator",                  OFFSET(separator),    AV_OPT_TYPE_STRING, {.str = ","},            0,       0,        FLAGS, NULL   },
+    { NULL },
+};
+
+
+static const AVOption censor_options[] = {
+    { "mode",               "set censoring mode",        OFFSET(censor_mode), AV_OPT_TYPE_INT,    {.i64=CM_KEEP_FIRST_LAST}, 0, 2, FLAGS, "mode" },
+    {   "keep_first_last",  "censor inner chars",        0,                   AV_OPT_TYPE_CONST,  {.i64=CM_KEEP_FIRST_LAST}, 0, 0, FLAGS, "mode" },
+    {   "keep_first",       "censor all but first char", 0,                   AV_OPT_TYPE_CONST,  {.i64=CM_KEEP_FIRST},      0, 0, FLAGS, "mode" },
+    {   "all",              "censor all chars",          0,                   AV_OPT_TYPE_CONST,  {.i64=CM_ALL},             0, 0, FLAGS, "mode" },
+    { "words",              "list of words to censor",   OFFSET(find),        AV_OPT_TYPE_STRING, {.str = NULL},             0, 0, FLAGS, NULL   },
+    { "words_file",         "path to word list file",    OFFSET(find_file),   AV_OPT_TYPE_STRING, {.str = NULL},             0, 0, FLAGS, NULL   },
+    { "separator",          "word separator",            OFFSET(separator),   AV_OPT_TYPE_STRING, {.str = ","},              0, 0, FLAGS, NULL   },
+    { "censor_char",        "replacement character",     OFFSET(censor_char), AV_OPT_TYPE_STRING, {.str = "*"},              0, 0, FLAGS, NULL   },
+    { NULL },
+};
+
+static const AVOption show_speaker_options[] = {
+    { "format",             "speaker name formatting",        OFFSET(speaker_mode), AV_OPT_TYPE_INT,    {.i64=SM_SQUARE_BRACKETS}, 0, 2, FLAGS, "format" },
+    {   "square_brackets",  "[speaker] text",                 0,                    AV_OPT_TYPE_CONST,  {.i64=SM_SQUARE_BRACKETS}, 0, 0, FLAGS, "format" },
+    {   "round_brackets",   "(speaker) text",                 0,                    AV_OPT_TYPE_CONST,  {.i64=SM_ROUND_BRACKETS},  0, 0, FLAGS, "format" },
+    {   "colon",            "speaker: text",                  0,                    AV_OPT_TYPE_CONST,  {.i64=SM_COLON},           0, 0, FLAGS, "format" },
+    {   "plain",            "speaker text",                   0,                    AV_OPT_TYPE_CONST,  {.i64=SM_PLAIN},           0, 0, FLAGS, "format" },
+    { "line_break",         "insert line break",              OFFSET(line_break),   AV_OPT_TYPE_BOOL,   {.i64=0},                  0, 1, FLAGS, NULL     },
+    { "style",              "ass type name or style code",    OFFSET(style),        AV_OPT_TYPE_STRING, {.str = NULL},             0, 0, FLAGS, NULL     },
+    { NULL },
+};
+
+AVFILTER_DEFINE_CLASS(textmod);
+AVFILTER_DEFINE_CLASS(censor);
+AVFILTER_DEFINE_CLASS(show_speaker);
+
+static const AVFilterPad inputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_SUBTITLE,
+        .filter_frame = filter_frame,
+    },
+};
+
+static const AVFilterPad outputs[] = {
+    {
+        .name          = "default",
+        .type          = AVMEDIA_TYPE_SUBTITLE,
+    },
+};
+
+/*
+ * Example:
+ * ffmpeg -i "http://streams.videolan.org/samples/sub/SSA/subtitle_testing_complex.mkv" -filter_complex "[0:1]textmod=mode=to_lower" -map 0 -y output.mkv 
+ */
+const AVFilter ff_sf_textmod = {
+    .name          = "textmod",
+    .description   = NULL_IF_CONFIG_SMALL("Modify subtitle text in several ways"),
+    .init          = init,
+    .uninit        = uninit,
+    .query_formats = query_formats,
+    .priv_size     = sizeof(TextModContext),
+    .priv_class    = &textmod_class,
+    FILTER_INPUTS(inputs),
+    FILTER_OUTPUTS(outputs),
+};
+
+/*
+ * Example:
+ * ffmpeg -i "http://streams.videolan.org/samples/sub/SSA/subtitle_testing_complex.mkv" -filter_complex "[0:1]censor=words='diss,louder,hope,beam,word':censor_char='#'" -map 0 -y output.mkv
+ */
+const AVFilter ff_sf_censor = {
+    .name          = "censor",
+    .description   = NULL_IF_CONFIG_SMALL("Censor words in subtitle text"),
+    .init          = init_censor,
+    .uninit        = uninit,
+    .query_formats = query_formats,
+    .priv_size     = sizeof(TextModContext),
+    .priv_class    = &censor_class,
+    FILTER_INPUTS(inputs),
+    FILTER_OUTPUTS(outputs),
+};
+
+/*
+ *  Example:
+ *
+ *  ffmpeg -ss 00:04:30 -i "U:\TestMedia\subtitle_burnin\subtitlevideo.mkv" -filter_complex "show_speaker=style='{\\c&HDD0000&\\be1\\\i1\\bord10}:line_break=1',[0:v]overlay_textsubs" -map 0 -c:v libx265 output.mkv
+ */
+const AVFilter ff_sf_show_speaker = {
+    .name          = "show_speaker",
+    .description   = NULL_IF_CONFIG_SMALL("Prepend speaker names to text subtitles (when available)"),
+    .init          = init_show_speaker,
+    .uninit        = uninit,
+    .query_formats = query_formats,
+    .priv_size     = sizeof(TextModContext),
+    .priv_class    = &show_speaker_class,
+    FILTER_INPUTS(inputs),
+    FILTER_OUTPUTS(outputs),
+};