diff mbox series

[FFmpeg-devel] drawtext filter

Message ID 8d713a21-f2f0-5c8d-fa93-0278965509e5@tiscali.it
State New
Headers show
Series [FFmpeg-devel] drawtext filter | expand

Checks

Context Check Description
andriy/commit_msg_x86 warning The first line of the commit message must start with a context terminated by a colon and a space, for example "lavu/opt: " or "doc: ".
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Francesco Carusi Jan. 26, 2023, 10:40 a.m. UTC
Hi, I'm new to contributing to ffmpeg!

I modified the drawtext filter to improve text rendering and add some 
features. You can find a high level description of the changes at this link:

https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md

I'm also attaching the patch file.
I looked for the filter maintainer to discuss about the changes I made 
but it looks like there isn't any, am I correct?

Please let me know if this is the right way to submit my contribution.
Thanks!
From e08148915ab207ed473665dfef0fa207d3b1a3a0 Mon Sep 17 00:00:00 2001
From: yethie <klimklim@tiscali.it>
Date: Wed, 25 Jan 2023 23:40:37 +0100
Subject: [PATCH 1/1] enhanced drawtext filter

---
 configure                 |    5 +-
 doc/filters.texi          |   87 ++-
 libavfilter/vf_drawtext.c | 1238 +++++++++++++++++++++++++++----------
 3 files changed, 996 insertions(+), 334 deletions(-)

Comments

Paul B Mahol Jan. 26, 2023, 10:50 a.m. UTC | #1
On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
> Hi, I'm new to contributing to ffmpeg!
>
> I modified the drawtext filter to improve text rendering and add some
> features. You can find a high level description of the changes at this
> link:
>
> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>
> I'm also attaching the patch file.
> I looked for the filter maintainer to discuss about the changes I made
> but it looks like there isn't any, am I correct?
>
> Please let me know if this is the right way to submit my contribution.

Why filter can not support normal commands for options? Like most/all
other filters that have support for changing options values at
runtime.

The reinit and yours added change option(s) are very
strange/inconvenient things to do.
Francesco Carusi Jan. 26, 2023, 1:17 p.m. UTC | #2
The drawtext reinit command is also used in the docs as an example for 
the sendcmd filter, so I thought it was fine to use commands in that 
way. In my opinion it is also a convenient way to modify multiple 
options at the same time.
Should the command match the name of a filter option instead?


On 26/01/2023 11:50, Paul B Mahol wrote:
> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>> Hi, I'm new to contributing to ffmpeg!
>>
>> I modified the drawtext filter to improve text rendering and add some
>> features. You can find a high level description of the changes at this
>> link:
>>
>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>
>> I'm also attaching the patch file.
>> I looked for the filter maintainer to discuss about the changes I made
>> but it looks like there isn't any, am I correct?
>>
>> Please let me know if this is the right way to submit my contribution.
> Why filter can not support normal commands for options? Like most/all
> other filters that have support for changing options values at
> runtime.
>
> The reinit and yours added change option(s) are very
> strange/inconvenient things to do.
> _______________________________________________
> 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".
Paul B Mahol Jan. 26, 2023, 1:21 p.m. UTC | #3
On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
> The drawtext reinit command is also used in the docs as an example for
> the sendcmd filter, so I thought it was fine to use commands in that
> way. In my opinion it is also a convenient way to modify multiple
> options at the same time.
> Should the command match the name of a filter option instead?
>

Please do not top post.

It is much better to use already existing options for commands that is
more intuitive to users. Also multiple options can be set at runtime,
there is no such limitation.

>
> On 26/01/2023 11:50, Paul B Mahol wrote:
>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>> Hi, I'm new to contributing to ffmpeg!
>>>
>>> I modified the drawtext filter to improve text rendering and add some
>>> features. You can find a high level description of the changes at this
>>> link:
>>>
>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>
>>> I'm also attaching the patch file.
>>> I looked for the filter maintainer to discuss about the changes I made
>>> but it looks like there isn't any, am I correct?
>>>
>>> Please let me know if this is the right way to submit my contribution.
>> Why filter can not support normal commands for options? Like most/all
>> other filters that have support for changing options values at
>> runtime.
>>
>> The reinit and yours added change option(s) are very
>> strange/inconvenient things to do.
>> _______________________________________________
>> 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".
>
Francesco Carusi Jan. 26, 2023, 4:25 p.m. UTC | #4
On 26/01/2023 14:21, Paul B Mahol wrote:
> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>> The drawtext reinit command is also used in the docs as an example for
>> the sendcmd filter, so I thought it was fine to use commands in that
>> way. In my opinion it is also a convenient way to modify multiple
>> options at the same time.
>> Should the command match the name of a filter option instead?
>>
> Please do not top post.
>
> It is much better to use already existing options for commands that is
> more intuitive to users. Also multiple options can be set at runtime,
> there is no such limitation.
ok, I'm going to remove the "change" command and add commands that match 
the options that it included.

>> On 26/01/2023 11:50, Paul B Mahol wrote:
>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>> Hi, I'm new to contributing to ffmpeg!
>>>>
>>>> I modified the drawtext filter to improve text rendering and add some
>>>> features. You can find a high level description of the changes at this
>>>> link:
>>>>
>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>>
>>>> I'm also attaching the patch file.
>>>> I looked for the filter maintainer to discuss about the changes I made
>>>> but it looks like there isn't any, am I correct?
>>>>
>>>> Please let me know if this is the right way to submit my contribution.
>>> Why filter can not support normal commands for options? Like most/all
>>> other filters that have support for changing options values at
>>> runtime.
>>>
>>> The reinit and yours added change option(s) are very
>>> strange/inconvenient things to do.
>>> _______________________________________________
>>> 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".
>>
> _______________________________________________
> 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".
Paul B Mahol Jan. 26, 2023, 4:37 p.m. UTC | #5
On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
> On 26/01/2023 14:21, Paul B Mahol wrote:
>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>> The drawtext reinit command is also used in the docs as an example for
>>> the sendcmd filter, so I thought it was fine to use commands in that
>>> way. In my opinion it is also a convenient way to modify multiple
>>> options at the same time.
>>> Should the command match the name of a filter option instead?
>>>
>> Please do not top post.
>>
>> It is much better to use already existing options for commands that is
>> more intuitive to users. Also multiple options can be set at runtime,
>> there is no such limitation.
> ok, I'm going to remove the "change" command and add commands that match
> the options that it included.

Thanks, feel free to ask questions on #ffmpeg-devel irc channel.

>
>>> On 26/01/2023 11:50, Paul B Mahol wrote:
>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>> Hi, I'm new to contributing to ffmpeg!
>>>>>
>>>>> I modified the drawtext filter to improve text rendering and add some
>>>>> features. You can find a high level description of the changes at this
>>>>> link:
>>>>>
>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>>>
>>>>> I'm also attaching the patch file.
>>>>> I looked for the filter maintainer to discuss about the changes I made
>>>>> but it looks like there isn't any, am I correct?
>>>>>
>>>>> Please let me know if this is the right way to submit my contribution.
>>>> Why filter can not support normal commands for options? Like most/all
>>>> other filters that have support for changing options values at
>>>> runtime.
>>>>
>>>> The reinit and yours added change option(s) are very
>>>> strange/inconvenient things to do.
>>>> _______________________________________________
>>>> 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".
>>>
>> _______________________________________________
>> 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".
>
Francesco Carusi Jan. 27, 2023, 5:18 p.m. UTC | #6
On 26/01/2023 17:37, Paul B Mahol wrote:
> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>> On 26/01/2023 14:21, Paul B Mahol wrote:
>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>> The drawtext reinit command is also used in the docs as an example for
>>>> the sendcmd filter, so I thought it was fine to use commands in that
>>>> way. In my opinion it is also a convenient way to modify multiple
>>>> options at the same time.
>>>> Should the command match the name of a filter option instead?
>>>>
>>> Please do not top post.
>>>
>>> It is much better to use already existing options for commands that is
>>> more intuitive to users. Also multiple options can be set at runtime,
>>> there is no such limitation.
>> ok, I'm going to remove the "change" command and add commands that match
>> the options that it included.
> Thanks, feel free to ask questions on #ffmpeg-devel irc channel.
I'm attaching the updated patch, I also updated the document at
https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
Thanks
>
>>>> On 26/01/2023 11:50, Paul B Mahol wrote:
>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>> Hi, I'm new to contributing to ffmpeg!
>>>>>>
>>>>>> I modified the drawtext filter to improve text rendering and add some
>>>>>> features. You can find a high level description of the changes at this
>>>>>> link:
>>>>>>
>>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>>>>
>>>>>> I'm also attaching the patch file.
>>>>>> I looked for the filter maintainer to discuss about the changes I made
>>>>>> but it looks like there isn't any, am I correct?
>>>>>>
>>>>>> Please let me know if this is the right way to submit my contribution.
>>>>> Why filter can not support normal commands for options? Like most/all
>>>>> other filters that have support for changing options values at
>>>>> runtime.
>>>>>
>>>>> The reinit and yours added change option(s) are very
>>>>> strange/inconvenient things to do.
>>>>> _______________________________________________
>>>>> 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".
>>>>
>>> _______________________________________________
>>> 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".
>>
> _______________________________________________
> 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".
From f753a9379b5f19dde0464880bd17b5b00440505e Mon Sep 17 00:00:00 2001
From: yethie <klimklim@tiscali.it>
Date: Fri, 27 Jan 2023 17:24:46 +0100
Subject: [PATCH 1/1] enhanced drawtext filter

---
 configure                 |    5 +-
 doc/filters.texi          |   85 ++-
 libavfilter/vf_drawtext.c | 1181 +++++++++++++++++++++++++++----------
 3 files changed, 935 insertions(+), 336 deletions(-)

diff --git a/configure b/configure
index 6e88c32223..8c2c434c66 100755
--- a/configure
+++ b/configure
@@ -235,6 +235,7 @@ External library support:
   --enable-libfontconfig   enable libfontconfig, useful for drawtext filter [no]
   --enable-libfreetype     enable libfreetype, needed for drawtext filter [no]
   --enable-libfribidi      enable libfribidi, improves drawtext filter [no]
+  --enable-libharfbuzz     enable libharfbuzz, needed for drawtext filter [no]
   --enable-libglslang      enable GLSL->SPIRV compilation via libglslang [no]
   --enable-libgme          enable Game Music Emu via libgme [no]
   --enable-libgsm          enable GSM de/encoding via libgsm [no]
@@ -1818,6 +1819,7 @@ EXTERNAL_LIBRARY_LIST="
     libfontconfig
     libfreetype
     libfribidi
+    libharfbuzz
     libglslang
     libgme
     libgsm
@@ -3657,7 +3659,7 @@ dilation_opencl_filter_deps="opencl"
 dnn_classify_filter_select="dnn"
 dnn_detect_filter_select="dnn"
 dnn_processing_filter_select="dnn"
-drawtext_filter_deps="libfreetype"
+drawtext_filter_deps="libfreetype libharfbuzz"
 drawtext_filter_suggest="libfontconfig libfribidi"
 elbg_filter_deps="avcodec"
 eq_filter_deps="gpl"
@@ -6577,6 +6579,7 @@ enabled fontconfig        && enable libfontconfig
 enabled libfontconfig     && require_pkg_config libfontconfig fontconfig "fontconfig/fontconfig.h" FcInit
 enabled libfreetype       && require_pkg_config libfreetype freetype2 "ft2build.h FT_FREETYPE_H" FT_Init_FreeType
 enabled libfribidi        && require_pkg_config libfribidi fribidi fribidi.h fribidi_version_info
+enabled libharfbuzz       && require_pkg_config libharfbuzz harfbuzz hb.h hb_buffer_create
 enabled libglslang && { check_lib spirv_compiler glslang/Include/glslang_c_interface.h glslang_initialize_process \
                             -lglslang -lMachineIndependent -lOSDependent -lHLSL -lOGLCompiler -lGenericCodeGen \
                             -lSPVRemapper -lSPIRV -lSPIRV-Tools-opt -lSPIRV-Tools -lpthread -lstdc++ -lm ||
diff --git a/doc/filters.texi b/doc/filters.texi
index 4f4d2ea807..e71e6ab8fc 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -12032,7 +12032,7 @@ Draw a text string or text from a specified file on top of a video, using the
 libfreetype library.
 
 To enable compilation of this filter, you need to configure FFmpeg with
-@code{--enable-libfreetype}.
+@code{--enable-libfreetype} and @code{--enable-libharfbuzz}.
 To enable default font fallback and the @var{font} option you need to
 configure FFmpeg with @code{--enable-libfontconfig}.
 To enable the @var{text_shaping} option, you need to configure FFmpeg with
@@ -12049,9 +12049,27 @@ Used to draw a box around text using the background color.
 The value must be either 1 (enable) or 0 (disable).
 The default value of @var{box} is 0.
 
+@item boxw
+Set the width of the box to be drawn around text.
+The default value of @var{boxw} is computed automatically to match the text width
+
+@item boxh
+Set the height of the box to be drawn around text.
+The default value of @var{boxh} is computed automatically to match the text height
+
 @item boxborderw
 Set the width of the border to be drawn around the box using @var{boxcolor}.
-The default value of @var{boxborderw} is 0.
+The value must be specified using one of the following formats:
+@itemize @bullet
+@item @code{boxborderw=10} set the width of all the borders to 10
+@item @code{boxborderw=10|20} set the width of the top and bottom borders to 10
+    and the width of the left and right borders to 20
+@item @code{boxborderw=10|20|30} set the width of the top border to 10, the width
+    of the bottom border to 30 and the width of the left and right borders to 20
+@item @code{boxborderw=10|20|30|40} set the borders width to 10 (top), 20 (right),
+    30 (bottom), 40 (left)
+@end itemize
+The default value of @var{boxborderw} is "0".
 
 @item boxcolor
 The color to be used for drawing box around text. For the syntax of this
@@ -12060,8 +12078,22 @@ option, check the @ref{color syntax,,"Color" section in the ffmpeg-utils manual,
 The default value of @var{boxcolor} is "white".
 
 @item line_spacing
-Set the line spacing in pixels of the border to be drawn around the box using @var{box}.
-The default value of @var{line_spacing} is 0.
+Set the line spacing in pixels. The default value of @var{line_spacing} is 0.
+
+@item text_align
+Set the vertical and horizontal alignment of the text with respect to the box boundaries.
+The value must contain exactly two letters, one for the vertical alignment (T=top,
+M=middle, B=bottom) and one for the horizontal alignment (L=left, C=center, R=right).
+
+@item y_align
+Specify what the @var{y} value is referred to. Possible values are:
+@itemize @bullet
+@item @code{text} the top of the highest glyph of the first text line is placed at @var{y}
+@item @code{baseline} the baseline of the first text line is placed at @var{y}
+@item @code{font} the baseline of the first text line is placed at @var{y} plus the
+    ascent (in pixels) defined in the font metrics
+@end itemize
+The default value of @var{y_align} is "text" for backward compatibility.
 
 @item borderw
 Set the width of the border to be drawn around the text using @var{bordercolor}.
@@ -12070,7 +12102,6 @@ The default value of @var{borderw} is 0.
 @item bordercolor
 Set the color to be used for drawing border around text. For the syntax of this
 option, check the @ref{color syntax,,"Color" section in the ffmpeg-utils manual,ffmpeg-utils}.
-
 The default value of @var{bordercolor} is "black".
 
 @item expansion
@@ -12168,10 +12199,6 @@ values. The default value for both is "0".
 The starting frame number for the n/frame_num variable. The default value
 is "0".
 
-@item tabsize
-The size in number of spaces to use for rendering the tab.
-Default value is 4.
-
 @item timecode
 Set the initial timecode representation in "hh:mm:ss[:;.]ff"
 format. It can be used with or without text parameter. @var{timecode_rate}
@@ -12269,6 +12296,18 @@ contained in the rendered text, it is equivalent to @var{ascent} -
 maximum glyph width, that is the maximum width for all the glyphs
 contained in the rendered text
 
+@item font_a
+the ascent size defined in the font metrics
+
+@item font_d
+the descent size defined in the font metrics
+
+@item top_a
+the maximum ascender of the glyphs of the first text line
+
+@item bottom_d
+the maximum descender of the glyphs of the last text line
+
 @item n
 the number of input frame, starting from 0
 
@@ -12433,11 +12472,34 @@ Full filter invocation with sendcmd would look like this:
 @example
 sendcmd=c='56.0 drawtext reinit fontsize=56\:fontcolor=green\:text=Hello\\ World'
 @end example
-@end table
 
 If the entire argument can't be parsed or applied as valid values then the filter will
 continue with its existing parameters.
 
+@end table
+
+The following options are also supported as @ref{commands}:
+
+@itemize @bullet
+@item x
+@item y
+@item alpha
+@item fontsize
+@item fontcolor
+@item boxcolor
+@item bordercolor
+@item shadowcolor
+@item box
+@item boxw
+@item boxh
+@item boxborderw
+@item line_spacing
+@item text_align
+@item shadowx
+@item shadowy
+@item borderw
+@end itemize
+
 @subsection Examples
 
 @itemize
@@ -12559,6 +12621,9 @@ For more information about fontconfig, check:
 For more information about libfribidi, check:
 @url{http://fribidi.org/}.
 
+For more information about libharfbuzz, check:
+@url{https://github.com/harfbuzz/harfbuzz}.
+
 @section edgedetect
 
 Detect and draw edges. The filter uses the Canny Edge Detection algorithm.
diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c
index 50012bb258..005070b825 100644
--- a/libavfilter/vf_drawtext.c
+++ b/libavfilter/vf_drawtext.c
@@ -1,4 +1,5 @@
 /*
+ * Copyright (c) 2023 Francesco Carusi
  * Copyright (c) 2011 Stefano Sabatini
  * Copyright (c) 2010 S.N. Hemanth Meenakshisundaram
  * Copyright (c) 2003 Gustavo Sverzut Barbieri <gsbarbieri@yahoo.com.br>
@@ -20,6 +21,26 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  */
 
+/*
+ * Changelog - 2023
+ *
+ * - This filter now depends on libharfbuzz for text shaping.
+ * - Glyphs position is now accurate to 1/4 pixel in both directions
+ * - The size of the background box can now be forced with the boxw
+ *   and boxh parameters
+ * - Text can be aligned horizontally (top, middle, bottom) and vertically
+ *   (left, center, right) relative to the background box
+ * - The default line height is now the one defined in the font
+ * - The new y_align parameter specifies if the user provided y value is
+ *   referred to the top of the text, to the font baseline or to the
+ *   top of the font.
+ * - The boxborderw parameter can now contain a different value for each border
+ *   (e.g. boxborderw=top|right|bottom|left)
+ * - Many filter parameters are now supported as commands.
+ * - The following new variables can be used in the x and y expressions:
+ *   font_a, font_d, top_a, bottom_d
+ */
+
 /**
  * @file
  * drawtext filter, based on the original vhook/drawtext.c
@@ -72,16 +93,26 @@
 #include FT_GLYPH_H
 #include FT_STROKER_H
 
+#include <hb.h>
+#include <hb-ft.h>
+
+// Ceiling operation for positive integers division
+#define POS_CEIL(x, y) ((x)/(y) + ((x)%(y) != 0))
+
 static const char *const var_names[] = {
     "dar",
     "hsub", "vsub",
-    "line_h", "lh",           ///< line height, same as max_glyph_h
+    "line_h", "lh",           ///< line height
     "main_h", "h", "H",       ///< height of the input video
     "main_w", "w", "W",       ///< width  of the input video
-    "max_glyph_a", "ascent",  ///< max glyph ascent
-    "max_glyph_d", "descent", ///< min glyph descent
+    "max_glyph_a", "ascent",  ///< max glyph ascender
+    "max_glyph_d", "descent", ///< min glyph descender
     "max_glyph_h",            ///< max glyph height
     "max_glyph_w",            ///< max glyph width
+    "font_a",                 ///< font-defined ascent
+    "font_d",                 ///< font-defined descent
+    "top_a",                  ///< max glyph ascender of the top line
+    "bottom_d",               ///< max glyph descender of the bottom line
     "n",                      ///< number of frame
     "sar",
     "t",                      ///< timestamp expressed in seconds
@@ -125,6 +156,10 @@ enum var_name {
     VAR_MAX_GLYPH_D, VAR_DESCENT,
     VAR_MAX_GLYPH_H,
     VAR_MAX_GLYPH_W,
+    VAR_FONT_A,
+    VAR_FONT_D,
+    VAR_TOP_A,
+    VAR_BOTTOM_D,
     VAR_N,
     VAR_SAR,
     VAR_T,
@@ -148,12 +183,84 @@ enum expansion_mode {
     EXP_STRFTIME,
 };
 
+enum y_alignment {
+    YA_TEXT,
+    YA_BASELINE,
+    YA_FONT,
+};
+
+typedef struct HarfbuzzData {
+    hb_buffer_t* buf;
+    hb_font_t* font;
+    unsigned int glyph_count;
+    hb_glyph_info_t* glyph_info;
+    hb_glyph_position_t* glyph_pos;
+} HarfbuzzData;
+
+/** Information about a single glyph in a text line */
+typedef struct GlyphInfo {
+    uint32_t code;                  ///< the glyph code point
+    int x;                          ///< the x position of the glyph
+    int y;                          ///< the y position of the glyph
+    int shift_x64;                  ///< the horizontal shift of the glyph in 26.6 units
+    int shift_y64;                  ///< the vertical shift of the glyph in 26.6 units
+} GlyphInfo;
+
+/** Information about a single line of text */
+typedef struct TextLine {
+    int offset_left64;              ///< offset between the origin and
+                                    ///  the leftmost pixel of the first glyph
+    int offset_right64;             ///< maximum offset between the origin and
+                                    ///  the rightmost pixel of the last glyph
+    int width64;                    ///< width of the line
+    HarfbuzzData hb_data;           ///< libharfbuzz data of this text line
+    GlyphInfo* glyphs;              ///< array of glyphs in this text line
+    int cluster_offset;             ///< the offset at which this line begins
+} TextLine;
+
+/** A glyph as loaded and rendered using libfreetype */
+typedef struct Glyph {
+    FT_Glyph glyph;
+    FT_Glyph border_glyph;
+    uint32_t code;
+    unsigned int fontsize;
+    /** Glyph bitmaps with 1/4 pixel precision in both directions */
+    FT_BitmapGlyph bglyph[16];
+    /** Outlined glyph bitmaps with 1/4 pixel precision in both directions */
+    FT_BitmapGlyph border_bglyph[16];
+    FT_BBox bbox;
+} Glyph;
+
+/** Global text metrics */
+typedef struct TextMetrics {
+    int offset_top64;               ///< ascender amount of the first line (in 26.6 units)
+    int offset_bottom64;            ///< descender amount of the last line (in 26.6 units)
+    int offset_left64;              ///< maximum offset between the origin and
+                                    ///  the leftmost pixel of the first glyph
+                                    ///  of each line (in 26.6 units)
+    int offset_right64;             ///< maximum offset between the origin and
+                                    ///  the rightmost pixel of the last glyph
+                                    ///  of each line (in 26.6 units)
+    int line_height64;              ///< the font-defined line height
+    int width;                      ///< width of the longest line - ceil(width64/64)
+    int height;                     ///< total height of the text - ceil(height64/64)
+
+    int min_y64;                    ///< minimum value of bbox.yMin among glyphs (in 26.6 units)
+    int max_y64;                    ///< maximum value of bbox.yMax among glyphs (in 26.6 units)
+    int min_x64;                    ///< minimum value of bbox.xMin among glyphs (in 26.6 units)
+    int max_x64;                    ///< maximum value of bbox.xMax among glyphs (in 26.6 units)
+
+    // Position of the background box (without borders)
+    int rect_x;                     ///< x position of the box
+    int rect_y;                     ///< y position of the box
+} TextMetrics;
+
 typedef struct DrawTextContext {
     const AVClass *class;
     int exp_mode;                   ///< expansion mode to use for the text
     int reinit;                     ///< tells if the filter is being reinited
 #if CONFIG_LIBFONTCONFIG
-    uint8_t *font;              ///< font to be used
+    uint8_t *font;                  ///< font to be used
 #endif
     uint8_t *fontfile;              ///< font to be used
     uint8_t *text;                  ///< text to be drawn
@@ -161,11 +268,9 @@ typedef struct DrawTextContext {
     uint8_t *fontcolor_expr;        ///< fontcolor expression to evaluate
     AVBPrint expanded_fontcolor;    ///< used to contain the expanded fontcolor spec
     int ft_load_flags;              ///< flags used for loading fonts, see FT_LOAD_*
-    FT_Vector *positions;           ///< positions for each element in the text
-    size_t nb_positions;            ///< number of elements of positions array
     char *textfile;                 ///< file with text to be drawn
-    int x;                          ///< x position to start drawing text
-    int y;                          ///< y position to start drawing text
+    double x;                       ///< x position to start drawing text
+    double y;                       ///< y position to start drawing text
     int max_glyph_w;                ///< max glyph width
     int max_glyph_h;                ///< max glyph height
     int shadowx, shadowy;
@@ -177,8 +282,14 @@ typedef struct DrawTextContext {
 
     int line_spacing;               ///< lines spacing in pixels
     short int draw_box;             ///< draw box around text - true or false
-    int boxborderw;                 ///< box border width
-    int use_kerning;                ///< font kerning is used - true/false
+    char* boxborderw;               ///< box border width (padding)
+                                    ///  allowed formats: "all", "vert|oriz", "top|right|bottom|left"
+    int bb_top;                     ///< the size of the top box border
+    int bb_right;                   ///< the size of the right box border
+    int bb_bottom;                  ///< the size of the bottom box border
+    int bb_left;                    ///< the size of the left box border
+    int box_width;                  ///< the width of box
+    int box_height;                 ///< the height of box
     int tabsize;                    ///< tab size
     int fix_bounds;                 ///< do we let it go out of frame bounds - t/f
 
@@ -213,31 +324,46 @@ typedef struct DrawTextContext {
     int text_shaping;               ///< 1 to shape the text before drawing it
 #endif
     AVDictionary *metadata;
+
+    int boxw;                       ///< the value of the boxw parameter
+    int boxh;                       ///< the value of the boxh parameter
+    uint8_t *text_align;            ///< the horizontal and vertical text alignment
+    int y_align;                    ///< the value of the y_align parameter
+
+    TextLine *lines;                ///< computed information about text lines
+    int line_count;                 ///< the number of text lines
+    uint32_t *tab_clusters;         ///< the position of tab characters in the text
+    int tab_count;                  ///< the number of tab characters
+    int blank_advance64;            ///< the size of the space character
 } DrawTextContext;
 
 #define OFFSET(x) offsetof(DrawTextContext, 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
 
 static const AVOption drawtext_options[]= {
-    {"fontfile",    "set font file",        OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"text",        "set text",             OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"textfile",    "set text file",        OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"fontcolor",   "set foreground color", OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
+    {"fontfile",       "set font file",         OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"text",           "set text",              OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, TFLAGS},
+    {"textfile",       "set text file",         OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"fontcolor",      "set foreground color",  OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, TFLAGS},
     {"fontcolor_expr", "set foreground color expression", OFFSET(fontcolor_expr), AV_OPT_TYPE_STRING, {.str=""}, 0, 0, FLAGS},
-    {"boxcolor",    "set box color",        OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
-    {"bordercolor", "set border color",     OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
-    {"shadowcolor", "set shadow color",     OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
-    {"box",         "set box",              OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0,        1       , FLAGS},
-    {"boxborderw",  "set box border width", OFFSET(boxborderw),         AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"line_spacing",  "set line spacing in pixels", OFFSET(line_spacing),   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX,FLAGS},
-    {"fontsize",    "set font size",        OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0 , FLAGS},
-    {"x",           "set x expression",     OFFSET(x_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
-    {"y",           "set y expression",     OFFSET(y_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
-    {"shadowx",     "set shadow x offset",  OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"shadowy",     "set shadow y offset",  OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"borderw",     "set border width",     OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"tabsize",     "set tab size",         OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0,        INT_MAX , FLAGS},
-    {"basetime",    "set base time",        OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX , FLAGS},
+    {"boxcolor",       "set box color",         OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, TFLAGS},
+    {"bordercolor",    "set border color",      OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, TFLAGS},
+    {"shadowcolor",    "set shadow color",      OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, TFLAGS},
+    {"box",            "set box",               OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0, 1, TFLAGS},
+    {"boxborderw",     "set box borders width", OFFSET(boxborderw),         AV_OPT_TYPE_STRING, {.str="0"},   0, 0, TFLAGS},
+    {"line_spacing",   "set line spacing in pixels", OFFSET(line_spacing), AV_OPT_TYPE_INT,    {.i64=-1},    INT_MIN, INT_MAX, TFLAGS},
+    {"fontsize",       "set font size",         OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, TFLAGS},
+    {"text_align",     "set text alignment",    OFFSET(text_align),         AV_OPT_TYPE_STRING, {.str="TL"},  0, 0, TFLAGS},
+    {"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},
+    {"boxw",           "set box width",         OFFSET(boxw),               AV_OPT_TYPE_INT,    {.i64=0},     0, INT_MAX, TFLAGS},
+    {"boxh",           "set box height",        OFFSET(boxh),               AV_OPT_TYPE_INT,    {.i64=0},     0, INT_MAX, TFLAGS},
+    {"shadowx",        "set shadow x offset",   OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, TFLAGS},
+    {"shadowy",        "set shadow y offset",   OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, TFLAGS},
+    {"borderw",        "set border width",      OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, TFLAGS},
+    {"tabsize",        "set tab size",          OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0, INT_MAX, TFLAGS},
+    {"basetime",       "set base time",         OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX, FLAGS},
 #if CONFIG_LIBFONTCONFIG
     { "font",        "Font name",            OFFSET(font),               AV_OPT_TYPE_STRING, { .str = "Sans" },           .flags = FLAGS },
 #endif
@@ -246,17 +372,21 @@ static const AVOption drawtext_options[]= {
         {"none",     "set no expansion",                    OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_NONE},     0, 0, FLAGS, "expansion"},
         {"normal",   "set normal expansion",                OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_NORMAL},   0, 0, FLAGS, "expansion"},
         {"strftime", "set strftime expansion (deprecated)", OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_STRFTIME}, 0, 0, FLAGS, "expansion"},
+    {"y_align",   "set the y alignment",    OFFSET(y_align), AV_OPT_TYPE_INT,  {.i64=YA_TEXT}, 0, 2, TFLAGS, "y_align"},
+        {"text",     "y is referred to the top of the first text line", OFFSET(y_align), AV_OPT_TYPE_CONST, {.i64=YA_TEXT},     0, 0, FLAGS, "y_align"},
+        {"baseline", "y is referred to the baseline of the first line", OFFSET(y_align), AV_OPT_TYPE_CONST, {.i64=YA_BASELINE}, 0, 0, FLAGS, "y_align"},
+        {"font",     "y is referred to the font defined line metrics",  OFFSET(y_align), AV_OPT_TYPE_CONST, {.i64=YA_FONT},     0, 0, FLAGS, "y_align"},
 
     {"timecode",        "set initial timecode",             OFFSET(tc_opt_string), AV_OPT_TYPE_STRING,   {.str=NULL}, 0, 0, FLAGS},
-    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},           0,        1, FLAGS},
-    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"reload",     "reload text file at specified frame interval", OFFSET(reload),     AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
-    { "alpha",       "apply alpha while rendering", OFFSET(a_expr),      AV_OPT_TYPE_STRING, { .str = "1"     },          .flags = FLAGS },
-    {"fix_bounds", "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
-    {"start_number", "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
-    {"text_source", "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
+    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},    0, 1, FLAGS},
+    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"reload",          "reload text file at specified frame interval", OFFSET(reload), AV_OPT_TYPE_INT, {.i64=0},    0, INT_MAX, FLAGS},
+    {"alpha",           "apply alpha while rendering",      OFFSET(a_expr),        AV_OPT_TYPE_STRING,   {.str = "1"}, .flags = TFLAGS},
+    {"fix_bounds",      "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
+    {"start_number",    "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
+    {"text_source",     "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
 
 #if CONFIG_LIBFRIBIDI
     {"text_shaping", "attempt to shape text before drawing", OFFSET(text_shaping), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS},
@@ -297,18 +427,24 @@ static const struct ft_error {
 
 #define FT_ERRMSG(e) ft_errors[e].err_msg
 
-typedef struct Glyph {
-    FT_Glyph glyph;
-    FT_Glyph border_glyph;
-    uint32_t code;
-    unsigned int fontsize;
-    FT_Bitmap bitmap; ///< array holding bitmaps of font
-    FT_Bitmap border_bitmap; ///< array holding bitmaps of font border
-    FT_BBox bbox;
-    int advance;
-    int bitmap_left;
-    int bitmap_top;
-} Glyph;
+
+// Loads and (optionally) renders a glyph
+static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code,
+     int8_t shift_x64, int8_t shift_y64);
+
+// Shapes a line of text using libharfbuzz
+static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const char* text, int textLen);
+
+// Performs text measurements
+static int measure_text(AVFilterContext *ctx, TextMetrics *metrics);
+
+// Draws glyphs on the frame
+static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
+                       FFDrawColor *color, TextMetrics *metrics,
+                       int x, int y, int borderw);
+
+// Draws text on the frame
+static int draw_text(AVFilterContext *ctx, AVFrame *frame);
 
 static int glyph_cmp(const void *key, const void *b)
 {
@@ -316,80 +452,9 @@ static int glyph_cmp(const void *key, const void *b)
     int64_t diff = (int64_t)a->code - (int64_t)bb->code;
 
     if (diff != 0)
-         return diff > 0 ? 1 : -1;
+        return diff > 0 ? 1 : -1;
     else
-         return FFDIFFSIGN((int64_t)a->fontsize, (int64_t)bb->fontsize);
-}
-
-/**
- * Load glyphs corresponding to the UTF-32 codepoint code.
- */
-static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code)
-{
-    DrawTextContext *s = ctx->priv;
-    FT_BitmapGlyph bitmapglyph;
-    Glyph *glyph;
-    struct AVTreeNode *node = NULL;
-    int ret;
-
-    /* load glyph into s->face->glyph */
-    if (FT_Load_Char(s->face, code, s->ft_load_flags))
-        return AVERROR(EINVAL);
-
-    glyph = av_mallocz(sizeof(*glyph));
-    if (!glyph) {
-        ret = AVERROR(ENOMEM);
-        goto error;
-    }
-    glyph->code  = code;
-    glyph->fontsize = s->fontsize;
-
-    if (FT_Get_Glyph(s->face->glyph, &glyph->glyph)) {
-        ret = AVERROR(EINVAL);
-        goto error;
-    }
-    if (s->borderw) {
-        glyph->border_glyph = glyph->glyph;
-        if (FT_Glyph_StrokeBorder(&glyph->border_glyph, s->stroker, 0, 0) ||
-            FT_Glyph_To_Bitmap(&glyph->border_glyph, FT_RENDER_MODE_NORMAL, 0, 1)) {
-            ret = AVERROR_EXTERNAL;
-            goto error;
-        }
-        bitmapglyph = (FT_BitmapGlyph) glyph->border_glyph;
-        glyph->border_bitmap = bitmapglyph->bitmap;
-    }
-    if (FT_Glyph_To_Bitmap(&glyph->glyph, FT_RENDER_MODE_NORMAL, 0, 1)) {
-        ret = AVERROR_EXTERNAL;
-        goto error;
-    }
-    bitmapglyph = (FT_BitmapGlyph) glyph->glyph;
-
-    glyph->bitmap      = bitmapglyph->bitmap;
-    glyph->bitmap_left = bitmapglyph->left;
-    glyph->bitmap_top  = bitmapglyph->top;
-    glyph->advance     = s->face->glyph->advance.x >> 6;
-
-    /* measure text height to calculate text_height (or the maximum text height) */
-    FT_Glyph_Get_CBox(glyph->glyph, ft_glyph_bbox_pixels, &glyph->bbox);
-
-    /* cache the newly created glyph */
-    if (!(node = av_tree_node_alloc())) {
-        ret = AVERROR(ENOMEM);
-        goto error;
-    }
-    av_tree_insert(&s->glyphs, glyph, glyph_cmp, &node);
-
-    if (glyph_ptr)
-        *glyph_ptr = glyph;
-    return 0;
-
-error:
-    if (glyph)
-        av_freep(&glyph->glyph);
-
-    av_freep(&glyph);
-    av_freep(&node);
-    return ret;
+        return FFDIFFSIGN((int64_t)a->fontsize, (int64_t)bb->fontsize);
 }
 
 static av_cold int set_fontsize(AVFilterContext *ctx, unsigned int fontsize)
@@ -439,7 +504,6 @@ static av_cold int update_fontsize(AVFilterContext *ctx)
            return err;
 
         size = av_expr_eval(s->fontsize_pexpr, s->var_values, &s->prng);
-
         if (!isnan(size)) {
             roundedsize = round(size);
             // test for overflow before cast
@@ -447,7 +511,6 @@ static av_cold int update_fontsize(AVFilterContext *ctx)
                 av_log(ctx, AV_LOG_ERROR, "fontsize overflow\n");
                 return AVERROR(EINVAL);
             }
-
             fontsize = roundedsize;
         }
     }
@@ -548,7 +611,7 @@ static int load_font_fontconfig(AVFilterContext *ctx)
         goto fail;
     }
 
-    av_log(ctx, AV_LOG_INFO, "Using \"%s\"\n", filename);
+    av_log(ctx, AV_LOG_VERBOSE, "Using \"%s\"\n", filename);
     if (parse_err)
         s->default_fontsize = size + 0.5;
 
@@ -690,6 +753,7 @@ static int shape_text(AVFilterContext *ctx)
     s->text = tmp;
     len = fribidi_unicode_to_charset(FRIBIDI_CHAR_SET_UTF8,
                                      unicodestr, len, s->text);
+
     ret = 0;
 
 out:
@@ -711,11 +775,36 @@ static enum AVFrameSideDataType text_source_string_parse(const char *text_source
     }
 }
 
+// Convert a string formatted as "n1|n2|...|nN" into an integer array
+static int string_to_array(const char* source, int* result, int result_size) {
+    int counter = 0, size = strlen(source) + 1;
+    char *saveptr, *curval, *dup = av_malloc(size);
+    av_strlcpy(dup, source, size);
+    if(result_size > 0 && (curval = av_strtok(dup, "|", &saveptr))) {
+        do {
+            result[counter++] = atoi(curval);
+        } while((curval = av_strtok(NULL, "|", &saveptr)) && counter < result_size);
+    }
+    av_free(dup);
+    return counter;
+}
+
+static int validate_text_align(char* text_align) {
+    int err = 0;
+    if(strlen(text_align) != 2
+        || strchr("LCRTMB", text_align[0]) == NULL || strchr("LCRTMB", text_align[1]) == NULL
+        || (strchr("TMB", text_align[0]) != NULL && strchr("LCR", text_align[1]) == NULL)
+        || (strchr("LCR", text_align[0]) != NULL && strchr("TMB", text_align[1]) == NULL)) {
+        err = AVERROR(EINVAL);
+    }
+
+    return err;
+}
+
 static av_cold int init(AVFilterContext *ctx)
 {
     int err;
     DrawTextContext *s = ctx->priv;
-    Glyph *glyph;
 
     av_expr_free(s->fontsize_pexpr);
     s->fontsize_pexpr = NULL;
@@ -777,6 +866,14 @@ static av_cold int init(AVFilterContext *ctx)
         return AVERROR(EINVAL);
     }
 
+    if((err = validate_text_align(s->text_align))) {
+        av_log(ctx, AV_LOG_ERROR,
+               "The value provided for parameter 'text_align' is not valid,\n");
+        av_log(ctx, AV_LOG_ERROR,
+               "please specify a two characters string containing only one letter for horizontal alignment ('LCR') and one for vertical alignment ('TMB')\n");
+        return err;
+    }
+
 #if CONFIG_LIBFRIBIDI
     if (s->text_shaping)
         if ((err = shape_text(ctx)) < 0)
@@ -795,26 +892,19 @@ static av_cold int init(AVFilterContext *ctx)
     if ((err = update_fontsize(ctx)) < 0)
         return err;
 
+    // Always init the stroker, may be needed if borderw is set via the "change" command
+    if (FT_Stroker_New(s->library, &s->stroker)) {
+        av_log(ctx, AV_LOG_ERROR, "Coult not init FT stroker\n");
+        return AVERROR_EXTERNAL;
+    }
+
     if (s->borderw) {
-        if (FT_Stroker_New(s->library, &s->stroker)) {
-            av_log(ctx, AV_LOG_ERROR, "Coult not init FT stroker\n");
-            return AVERROR_EXTERNAL;
-        }
         FT_Stroker_Set(s->stroker, s->borderw << 6, FT_STROKER_LINECAP_ROUND,
                        FT_STROKER_LINEJOIN_ROUND, 0);
     }
 
-    s->use_kerning = FT_HAS_KERNING(s->face);
-
     /* load the fallback glyph with code 0 */
-    load_glyph(ctx, NULL, 0);
-
-    /* set the tabsize in pixels */
-    if ((err = load_glyph(ctx, &glyph, ' ')) < 0) {
-        av_log(ctx, AV_LOG_ERROR, "Could not set tabsize.\n");
-        return err;
-    }
-    s->tabsize *= glyph->advance;
+    load_glyph(ctx, NULL, 0, 0, 0);
 
     if (s->exp_mode == EXP_STRFTIME &&
         (strchr(s->text, '%') || strchr(s->text, '\\')))
@@ -831,12 +921,36 @@ static int query_formats(AVFilterContext *ctx)
     return ff_set_common_formats(ctx, ff_draw_supported_pixel_formats(0));
 }
 
+static int glyph_enu_border_free(void *opaque, void *elem) {
+    Glyph *glyph = elem;
+
+    if(glyph->border_glyph != NULL) {
+        for(int t = 0; t < 16; ++t) {
+            if(glyph->border_bglyph[t] != NULL) {
+                FT_Done_Glyph((FT_Glyph)glyph->border_bglyph[t]);
+                glyph->border_bglyph[t] = NULL;
+            }
+        }
+        FT_Done_Glyph(glyph->border_glyph);
+        glyph->border_glyph = NULL;
+    }
+    return 0;
+}
+
 static int glyph_enu_free(void *opaque, void *elem)
 {
     Glyph *glyph = elem;
 
     FT_Done_Glyph(glyph->glyph);
     FT_Done_Glyph(glyph->border_glyph);
+    for(int t = 0; t < 16; ++t) {
+        if(glyph->bglyph[t] != NULL) {
+            FT_Done_Glyph((FT_Glyph)glyph->bglyph[t]);
+        }
+        if(glyph->border_bglyph[t] != NULL) {
+            FT_Done_Glyph((FT_Glyph)glyph->border_bglyph[t]);
+        }
+    }
     av_free(elem);
     return 0;
 }
@@ -852,9 +966,6 @@ static av_cold void uninit(AVFilterContext *ctx)
 
     s->x_pexpr = s->y_pexpr = s->a_pexpr = s->fontsize_pexpr = NULL;
 
-    av_freep(&s->positions);
-    s->nb_positions = 0;
-
     av_tree_enumerate(s->glyphs, NULL, NULL, glyph_enu_free);
     av_tree_destroy(s->glyphs);
     s->glyphs = NULL;
@@ -880,15 +991,15 @@ static int config_input(AVFilterLink *inlink)
     ff_draw_color(&s->dc, &s->bordercolor, s->bordercolor.rgba);
     ff_draw_color(&s->dc, &s->boxcolor,    s->boxcolor.rgba);
 
-    s->var_values[VAR_w]     = s->var_values[VAR_W]     = s->var_values[VAR_MAIN_W] = inlink->w;
-    s->var_values[VAR_h]     = s->var_values[VAR_H]     = s->var_values[VAR_MAIN_H] = inlink->h;
-    s->var_values[VAR_SAR]   = inlink->sample_aspect_ratio.num ? av_q2d(inlink->sample_aspect_ratio) : 1;
-    s->var_values[VAR_DAR]   = (double)inlink->w / inlink->h * s->var_values[VAR_SAR];
-    s->var_values[VAR_HSUB]  = 1 << s->dc.hsub_max;
-    s->var_values[VAR_VSUB]  = 1 << s->dc.vsub_max;
-    s->var_values[VAR_X]     = NAN;
-    s->var_values[VAR_Y]     = NAN;
-    s->var_values[VAR_T]     = NAN;
+    s->var_values[VAR_w]    = s->var_values[VAR_W] = s->var_values[VAR_MAIN_W] = inlink->w;
+    s->var_values[VAR_h]    = s->var_values[VAR_H] = s->var_values[VAR_MAIN_H] = inlink->h;
+    s->var_values[VAR_SAR]  = inlink->sample_aspect_ratio.num ? av_q2d(inlink->sample_aspect_ratio) : 1;
+    s->var_values[VAR_DAR]  = (double)inlink->w / inlink->h * s->var_values[VAR_SAR];
+    s->var_values[VAR_HSUB] = 1 << s->dc.hsub_max;
+    s->var_values[VAR_VSUB] = 1 << s->dc.vsub_max;
+    s->var_values[VAR_X]    = NAN;
+    s->var_values[VAR_Y]    = NAN;
+    s->var_values[VAR_T]    = NAN;
 
     av_lfg_init(&s->prng, av_get_random_seed());
 
@@ -948,8 +1059,30 @@ static int command(AVFilterContext *ctx, const char *cmd, const char *arg, char
 
         ctx->priv = new;
         return config_input(ctx->inputs[0]);
-    } else
-        return AVERROR(ENOSYS);
+    } else {
+//        av_log(ctx, AV_LOG_DEBUG, "Command '%s' '%s'\n", cmd, arg);
+        int old_borderw = old->borderw;
+        if ((ret = ff_filter_process_command(ctx, cmd, arg, res, res_len, flags)) < 0) {
+            return ret;
+        }
+        if(strcmp(cmd, "borderw") == 0) {
+            if(old->borderw != old_borderw) {
+                FT_Stroker_Set(old->stroker, old->borderw << 6, FT_STROKER_LINECAP_ROUND,
+                            FT_STROKER_LINEJOIN_ROUND, 0);
+                av_tree_enumerate(old->glyphs, NULL, NULL, glyph_enu_border_free);                            
+            }
+        } else if(strcmp(cmd, "text_align") == 0) {
+            // Cleanup border glyphs
+            if(validate_text_align(old->text_align) != 0) {
+                av_log(ctx, AV_LOG_ERROR,
+                    "Invalid command value '%s' for 'text_align'\n", old->text_align);
+            }
+        } else if(strcmp(cmd, "fontsize") == 0) {
+            av_expr_free(old->fontsize_pexpr);
+            old->fontsize_pexpr = NULL;
+        }
+        return config_input(ctx->inputs[0]);
+    }
 
 fail:
     av_log(ctx, AV_LOG_ERROR, "Failed to process command. Continuing with existing parameters.\n");
@@ -1318,91 +1451,422 @@ static int expand_text(AVFilterContext *ctx, char *text, AVBPrint *bp)
     return 0;
 }
 
+static void update_color_with_alpha(DrawTextContext *s, FFDrawColor *color, const FFDrawColor incolor)
+{
+    *color = incolor;
+    color->rgba[3] = (color->rgba[3] * s->alpha) / 255;
+    ff_draw_color(&s->dc, color, color->rgba);
+}
+
+static void update_alpha(DrawTextContext *s)
+{
+    double alpha = av_expr_eval(s->a_pexpr, s->var_values, &s->prng);
+
+    if (isnan(alpha))
+        return;
+
+    if (alpha >= 1.0)
+        s->alpha = 255;
+    else if (alpha <= 0)
+        s->alpha = 0;
+    else
+        s->alpha = 256 * alpha;
+}
+
+static inline int get_subpixel_idx(int shift_x64, int shift_y64) {
+    int idx = (shift_x64 >> 2) + (shift_y64 >> 4);
+    return idx;
+}
+
+static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code, int8_t shift_x64, int8_t shift_y64)
+{
+    DrawTextContext *s = ctx->priv;
+    Glyph dummy = { 0 };
+    Glyph *glyph;
+    FT_Vector shift;
+    struct AVTreeNode *node = NULL;
+    int ret = 0;
+
+    /* get glyph */
+    dummy.code = code;
+    dummy.fontsize = s->fontsize;
+    glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
+    if(!glyph) {
+//        av_log(ctx, AV_LOG_DEBUG, "Glyph: %d not cached -> loading...\n", code);
+        if (FT_Load_Glyph(s->face, code, s->ft_load_flags)) {
+            return AVERROR(EINVAL);
+        }
+        glyph = av_mallocz(sizeof(*glyph));
+        if (!glyph) {
+            ret = AVERROR(ENOMEM);
+            goto error;
+        }
+        glyph->code  = code;
+        glyph->fontsize = s->fontsize;
+        if (FT_Get_Glyph(s->face->glyph, &glyph->glyph)) {
+            ret = AVERROR(EINVAL);
+            goto error;
+        }
+        if (s->borderw) {
+            glyph->border_glyph = glyph->glyph;
+            if (FT_Glyph_StrokeBorder(&glyph->border_glyph, s->stroker, 0, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+        }
+        /* measure text height to calculate text_height (or the maximum text height) */
+        FT_Glyph_Get_CBox(glyph->glyph, FT_GLYPH_BBOX_SUBPIXELS, &glyph->bbox);
+
+        /* cache the newly created glyph */
+        if (!(node = av_tree_node_alloc())) {
+            ret = AVERROR(ENOMEM);
+            goto error;
+        }
+        av_tree_insert(&s->glyphs, glyph, glyph_cmp, &node);
+    } else {
+        if(s->borderw && !glyph->border_glyph) {
+            glyph->border_glyph = glyph->glyph;
+            if (FT_Glyph_StrokeBorder(&glyph->border_glyph, s->stroker, 0, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+        }
+    }
+
+    // Check if a bitmap is needed
+    if(shift_x64 >= 0 && shift_y64 >= 0) {
+        // Get the bitmap subpixel index (0 -> 15)
+        int idx = get_subpixel_idx(shift_x64, shift_y64);
+        shift.x = shift_x64;
+        shift.y = shift_y64;
+
+        if(!glyph->bglyph[idx]) {
+            FT_Glyph tmp_glyph = glyph->glyph;
+            // av_log(ctx, AV_LOG_DEBUG, "Rendering bitmap [%d] for glyph: %d\n", idx, code);
+            if(FT_Glyph_To_Bitmap(&tmp_glyph, FT_RENDER_MODE_NORMAL, &shift, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+            glyph->bglyph[idx] = (FT_BitmapGlyph)tmp_glyph;
+            if(glyph->bglyph[idx]->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) {
+                av_log(ctx, AV_LOG_ERROR, "Monocromatic (1bpp) fonts are not supported.\n");
+                ret = AVERROR(EINVAL);
+                goto error;
+            }
+        }
+        if (s->borderw && !glyph->border_bglyph[idx]) {
+            FT_Glyph tmp_glyph = glyph->border_glyph;
+            // av_log(ctx, AV_LOG_DEBUG, "Rendering border bitmap [%d] for glyph: %d\n", idx, code);
+            if(FT_Glyph_To_Bitmap(&tmp_glyph, FT_RENDER_MODE_NORMAL, &shift, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+            glyph->border_bglyph[idx] = (FT_BitmapGlyph)tmp_glyph;
+        }
+    }
+    if(glyph_ptr) {
+        *glyph_ptr = glyph;
+    }
+    return 0;
+
+error:
+    if (glyph && glyph->glyph)
+        FT_Done_Glyph(glyph->glyph);
+
+    av_freep(&glyph);
+    av_freep(&node);
+    return ret;
+}
+
 static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
-                       int width, int height,
                        FFDrawColor *color,
+                       TextMetrics *metrics,
                        int x, int y, int borderw)
 {
-    char *text = s->expanded_text.str;
-    uint32_t code = 0;
-    int i, x1, y1;
-    uint8_t *p;
-    Glyph *glyph = NULL;
+    int g, l, x1, y1, w1, h1, idx;
+    int dx = 0, dy = 0, pdx = 0;
+    GlyphInfo *info;
+    Glyph dummy = { 0 }, *glyph;
+    FT_Bitmap bitmap;
+    FT_BitmapGlyph b_glyph;
+    uint8_t j_center = 0, j_right = 0, j_middle = 0, j_bottom = 0;
+    int line_w, offset_y = 0;
+    int clip_x = 0, clip_y = 0;
+
+    j_center = strstr(s->text_align, "C") > 0;
+    j_right = strstr(s->text_align, "R") > 0;
+    j_middle = strstr(s->text_align, "M") > 0;
+    j_bottom = strstr(s->text_align, "B") > 0;
+    // av_log(s, AV_LOG_DEBUG, "Outer rectangle - w: %d, h: %d\n", s->box_width, s->box_height);
+    // av_log(s, AV_LOG_DEBUG, "Text position: %s\n", s->text_align);
+
+    if(j_middle) {
+        offset_y = (s->box_height - metrics->height) / 2;
+    } else if(j_bottom) {
+        offset_y = s->box_height - metrics->height;
+    }
 
-    for (i = 0, p = text; *p; i++) {
-        FT_Bitmap bitmap;
-        Glyph dummy = { 0 };
-        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_invalid;);
-continue_on_invalid:
+    clip_x = FFMIN(metrics->rect_x + s->box_width + s->bb_right, frame->width);
+    clip_y = FFMIN(metrics->rect_y + s->box_height + s->bb_bottom, frame->height);
+
+    // av_log(s, AV_LOG_DEBUG, "Drawing text at (%d, %d) (clip_x: %d, clip_y: %d)\n",
+    //     x, y, clip_x, clip_y);
+
+    for(l = 0; l < s->line_count; ++l) {
+        TextLine *line = &s->lines[l];
+        line_w = POS_CEIL(line->width64, 64);
+        for(g = 0; g < line->hb_data.glyph_count; ++g) {
+            info = &line->glyphs[g];
+            dummy.fontsize = s->fontsize;
+            dummy.code = info->code;
+            glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
+            if(!glyph) {
+                return AVERROR(EINVAL);
+            }
 
-        /* skip new line chars, just go to new line */
-        if (code == '\n' || code == '\r' || code == '\t')
-            continue;
+            idx = get_subpixel_idx(info->shift_x64, info->shift_y64);
+            b_glyph = borderw ? glyph->border_bglyph[idx] : glyph->bglyph[idx];
+            bitmap = b_glyph->bitmap;
+            x1 = x + info->x + b_glyph->left;
+            y1 = y + info->y - b_glyph->top + offset_y;
+            w1 = bitmap.width;
+            h1 = bitmap.rows;
+
+            if(j_center) {
+                x1 += (s->box_width - line_w) / 2;
+            } else if(j_right) {
+                x1 += s->box_width - line_w;
+            }
 
-        dummy.code = code;
-        dummy.fontsize = s->fontsize;
-        glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
+            // Offset of the glyph's bitmap in the visible region
+            dx = dy = 0;
+            if(x1 < metrics->rect_x - s->bb_left) {
+                dx = metrics->rect_x - s->bb_left - x1;
+                x1 = metrics->rect_x - s->bb_left;
+            }
+            if(y1 < metrics->rect_y - s->bb_top) {
+                dy = metrics->rect_y - s->bb_top - y1;
+                y1 = metrics->rect_y - s->bb_top;
+            }
 
-        bitmap = borderw ? glyph->border_bitmap : glyph->bitmap;
+            // check if the glyph is empty or out of the clipping region
+            if(dx >= w1 || dy >= h1 || x1 >= clip_x || y1 >= clip_y) {
+                // av_log(s, AV_LOG_DEBUG, "Glyph (code: %d - line: %d - glyph: %d - dx: %d - wx: %d) is empty or out of the clipping region\n",
+                //     info->code, l, g, dx, w1);
+                // av_log(s, AV_LOG_DEBUG, "Glyph %d -- dx: %d, dy: %d, x1: %d, y1: %d, w1: %d, h1: %d\n",
+                //     info->code, dx, dy, x1, y1, w1, h1);
+                continue;
+            }
 
-        if (glyph->bitmap.pixel_mode != FT_PIXEL_MODE_MONO &&
-            glyph->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY)
-            return AVERROR(EINVAL);
+            pdx = dx + dy * bitmap.pitch;
+            w1 = FFMIN(clip_x - x1, w1 - dx);
+            h1 = FFMIN(clip_y - y1, h1 - dy);
 
-        x1 = s->positions[i].x+s->x+x - borderw;
-        y1 = s->positions[i].y+s->y+y - borderw;
+            // av_log(s, AV_LOG_DEBUG, "Drawing glyph %d[%d] (line: %d num: %d) at (%d, %d) (info.x: %d, info.y: %d)\n",
+            //     info->code, idx, l, g, x1, y1, info->x, info->y);
 
-        ff_blend_mask(&s->dc, color,
-                      frame->data, frame->linesize, width, height,
-                      bitmap.buffer, bitmap.pitch,
-                      bitmap.width, bitmap.rows,
-                      bitmap.pixel_mode == FT_PIXEL_MODE_MONO ? 0 : 3,
-                      0, x1, y1);
+            ff_blend_mask(&s->dc, color, frame->data, frame->linesize, clip_x, clip_y,
+                bitmap.buffer + pdx, bitmap.pitch, w1, h1, 3, 0, x1, y1);
+        }
     }
 
     return 0;
 }
 
+static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const char* text, int textLen) {
+    hb->buf = hb_buffer_create();
+    hb_buffer_set_direction(hb->buf, HB_DIRECTION_LTR);
+    hb_buffer_set_script(hb->buf, HB_SCRIPT_LATIN);
+    hb_buffer_set_language(hb->buf, hb_language_from_string("en", -1));
+    hb_buffer_guess_segment_properties(hb->buf);
+    hb->font = hb_ft_font_create(s->face, NULL);
+    hb_ft_font_set_funcs(hb->font);
+    hb_buffer_add_utf8(hb->buf, text, textLen, 0, -1);
+    hb_shape(hb->font, hb->buf, NULL, 0);
+    hb->glyph_info = hb_buffer_get_glyph_infos(hb->buf, &hb->glyph_count);
+    hb->glyph_pos = hb_buffer_get_glyph_positions(hb->buf, &hb->glyph_count);
+}
 
-static void update_color_with_alpha(DrawTextContext *s, FFDrawColor *color, const FFDrawColor incolor)
-{
-    *color = incolor;
-    color->rgba[3] = (color->rgba[3] * s->alpha) / 255;
-    ff_draw_color(&s->dc, color, color->rgba);
+static void hb_destroy(HarfbuzzData *hb) {
+    hb_buffer_destroy(hb->buf);
+    hb_font_destroy(hb->font);
+    hb->buf = NULL;
+    hb->font = NULL;
+    hb->glyph_info = NULL;
+    hb->glyph_pos = NULL;
 }
 
-static void update_alpha(DrawTextContext *s)
-{
-    double alpha = av_expr_eval(s->a_pexpr, s->var_values, &s->prng);
+static int measure_text(AVFilterContext *ctx, TextMetrics *metrics) {
+    DrawTextContext *s = ctx->priv;
+    char* text = s->expanded_text.str;
+    char *textdup = av_strdup(text), *start = textdup;
+    int num_chars = 0;
+    int width64 = 0, w64 = 0;
+    int cur_min_y64 = 0, first_max_y64 = -32000;
+    int first_min_x64 = 32000, last_max_x64 = -32000;
+    int min_y64 = 32000, max_y64 = -32000, min_x64 = 32000, max_x64 = -32000;
+    int line_count = 0;
+    uint32_t code = 0;
+    Glyph *glyph = NULL;
 
-    if (isnan(alpha))
-        return;
+    int i, tab_idx = 0, last_tab_idx = 0, line_offset = 0;
+    char* p;
+    int ret = 0;
+
+    // Count the lines and the tab characters
+    s->tab_count = 0;
+    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) {
+            ++line_count;
+            if(code == 0) {
+                break;
+            }
+        } else if(code == '\t') {
+            ++s->tab_count;
+        }
+    }
 
-    if (alpha >= 1.0)
-        s->alpha = 255;
-    else if (alpha <= 0)
-        s->alpha = 0;
-    else
-        s->alpha = 256 * alpha;
+    // Evaluate the width of the space character if needed to replace tabs
+    if(s->tab_count > 0 && !s->blank_advance64) {
+        HarfbuzzData hb_data;
+        shape_text_hb(s, &hb_data, " ", 1);
+        s->blank_advance64 = hb_data.glyph_pos[0].x_advance;
+        hb_destroy(&hb_data);
+    }
+
+    s->line_count = line_count;
+    s->lines = av_mallocz(line_count * sizeof(TextLine));
+    s->tab_clusters = av_mallocz(s->tab_count * sizeof(uint32_t));
+    for(i = 0; i < s->tab_count; ++i) {
+        s->tab_clusters[i] = -1;
+    }
+
+    // av_log(s, AV_LOG_DEBUG, "Starting text measurement...\n");
+    line_count = 0;
+    for (i = 0, p = textdup; 1; i++) {
+        if(*p == '\t') {
+            s->tab_clusters[tab_idx++] = i;
+            *p = ' ';
+        }
+        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_failed2;);
+continue_on_failed2:
+        if(is_newline(code) || code == 0) {
+            TextLine *cur_line = &s->lines[line_count];
+            HarfbuzzData *hb = &cur_line->hb_data;
+            cur_line->cluster_offset = line_offset;
+            shape_text_hb(s, hb, start, num_chars);
+            w64 = 0;
+            cur_min_y64 = 32000;
+            for(int t = 0; t < hb->glyph_count; ++t) {
+                uint8_t is_tab = last_tab_idx < s->tab_count &&
+                    hb->glyph_info[t].cluster == s->tab_clusters[last_tab_idx] - line_offset;
+                if(is_tab) {
+                    ++last_tab_idx;
+                }
+                ret = load_glyph(ctx, &glyph, hb->glyph_info[t].codepoint, -1, -1);
+                if(ret != 0) {
+                    break;
+                }
+                if(line_count == 0) {
+                    first_max_y64 = FFMAX(glyph->bbox.yMax, first_max_y64);
+                }
+                if(t == 0) {
+                    // TODO (OFFSET LEFT)
+                    // w64 += glyph->bbox.xMin;
+                    cur_line->offset_left64 = glyph->bbox.xMin;
+                    first_min_x64 = FFMIN(glyph->bbox.xMin, first_min_x64);
+                }
+                if(t == hb->glyph_count - 1) {
+                    w64 += glyph->bbox.xMax;
+                    last_max_x64 = FFMAX(glyph->bbox.xMax, last_max_x64);
+                    cur_line->offset_right64 = glyph->bbox.xMax;
+                } else {
+                    if(is_tab) {
+                        int size = s->blank_advance64 * s->tabsize;
+                        w64 = (w64 / size + 1) * size;
+                    } else {
+                        w64 += hb->glyph_pos[t].x_advance;
+                    }
+                }
+                cur_min_y64 = FFMIN(glyph->bbox.yMin, cur_min_y64);
+                min_y64 = FFMIN(glyph->bbox.yMin, min_y64);
+                max_y64 = FFMAX(glyph->bbox.yMax, max_y64);
+                min_x64 = FFMIN(glyph->bbox.xMin, min_x64);
+                max_x64 = FFMAX(glyph->bbox.xMax, max_x64);
+
+                // av_log(s, AV_LOG_DEBUG, "    Glyph: %d -- yMin: %ld -- yMax: %ld -- xMin: %ld -- xMax: %ld\n",
+                //     hb->glyph_info[t].codepoint, glyph->bbox.yMin, glyph->bbox.yMax, glyph->bbox.xMin, glyph->bbox.xMax);
+                // av_log(s, AV_LOG_DEBUG, "      min_y64: %d -- max_y64: %d -- min_x64: %d -- max_x64: %d\n",
+                //     min_y64, max_y64, min_x64, max_x64);
+            }
+
+            if(ret == 0) {
+                // TODO (OFFSET LEFT)
+                // cur_line->width64 = w64 - cur_line->offset_left64;
+                cur_line->width64 = w64;
+
+                av_log(s, AV_LOG_DEBUG, "  Line: %d -- glyphs count: %d - width64: %d - offset_left64: %d - offset_right64: %d)\n",
+                    line_count, hb->glyph_count, cur_line->width64, cur_line->offset_left64, cur_line->offset_right64);
+
+                if(w64 > width64) {
+                    width64 = w64;
+                }
+                num_chars = -1;
+                start = p;
+                ++line_count;
+                line_offset = i + 1;
+            }
+        }
+
+        if(code == 0 || ret != 0) break;
+        ++num_chars;
+    }
+
+    if(ret == 0) {
+        metrics->line_height64 = s->face->size->metrics.height;
+        
+        // TODO (LEFT OFFSET) 
+        // metrics->width = POS_CEIL(width64 - first_min_x64, 64);
+        metrics->width = POS_CEIL(width64, 64);
+        if(s->y_align == YA_FONT) {
+            metrics->height = POS_CEIL(metrics->line_height64 * line_count, 64);
+        } else {
+            int height64 = (metrics->line_height64 + s->line_spacing * 64) *
+                (FFMAX(0, line_count - 1)) + first_max_y64 - cur_min_y64;
+            metrics->height = POS_CEIL(height64, 64);
+        }
+        metrics->offset_top64 = first_max_y64;
+        metrics->offset_right64 = last_max_x64;
+        metrics->offset_bottom64 = cur_min_y64;
+        metrics->offset_left64 = first_min_x64;
+        metrics->min_x64 = min_x64;
+        metrics->min_y64 = min_y64;
+        metrics->max_x64 = max_x64;
+        metrics->max_y64 = max_y64;
+
+        // av_log(s, AV_LOG_DEBUG, "  Text: width: %d | height: %d\n", metrics->width, metrics->height);
+        // av_log(s, AV_LOG_DEBUG, "      off_t64: %d | off_r64: %d | off_b64: %d | off_l64: %d\n",
+        //     metrics->offset_top64, metrics->offset_right64, metrics->offset_bottom64, metrics->offset_left64);
+        // av_log(s, AV_LOG_DEBUG, "      min_x64: %d | min_y64: %d | max_x64: %d | max_y64: %d\n",
+        //     metrics->min_x64, metrics->min_y64, metrics->max_x64, metrics->max_y64);
+        // av_log(s, AV_LOG_DEBUG, "Text measurement completed\n");
+    }
+
+    av_free(textdup);
+    return ret;
 }
 
-static int draw_text(AVFilterContext *ctx, AVFrame *frame,
-                     int width, int height)
+static int draw_text(AVFilterContext *ctx, AVFrame *frame)
 {
     DrawTextContext *s = ctx->priv;
     AVFilterLink *inlink = ctx->inputs[0];
-
-    uint32_t code = 0, prev_code = 0;
-    int x = 0, y = 0, i = 0, ret;
-    int max_text_line_w = 0, len;
-    int box_w, box_h;
-    char *text;
-    uint8_t *p;
-    int y_min = 32000, y_max = -32000;
-    int x_min = 32000, x_max = -32000;
-    FT_Vector delta;
-    Glyph *glyph = NULL, *prev_glyph = NULL;
-    Glyph dummy = { 0 };
+    int x = 0, y = 0, ret;
+    int shift_x64, shift_y64;
+    int x64, y64;
+    int offset_left = 0;
+    Glyph *glyph = NULL;
 
     time_t now = time(0);
     struct tm ltime;
@@ -1413,6 +1877,17 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame,
     FFDrawColor bordercolor;
     FFDrawColor boxcolor;
 
+    int width = frame->width;
+    int height = frame->height;
+    int rec_x = 0, rec_y = 0, rec_width = 0, rec_height = 0;
+    int is_outside = 0;
+    int last_tab_idx = 0;
+
+    TextMetrics metrics;
+
+    // av_log(s, AV_LOG_DEBUG, "ascend: %ld descent: %ld height: %ld\n",
+    //     s->face->size->metrics.ascender, s->face->size->metrics.descender, s->face->size->metrics.height);
+
     av_bprint_clear(bp);
 
     if(s->basetime != AV_NOPTS_VALUE)
@@ -1441,13 +1916,6 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame,
 
     if (!av_bprint_is_complete(bp))
         return AVERROR(ENOMEM);
-    text = s->expanded_text.str;
-    if ((len = s->expanded_text.len) > s->nb_positions) {
-        if (!(s->positions =
-              av_realloc(s->positions, len*sizeof(*s->positions))))
-            return AVERROR(ENOMEM);
-        s->nb_positions = len;
-    }
 
     if (s->fontcolor_expr[0]) {
         /* If expression is set, evaluate and replace the static value */
@@ -1463,85 +1931,28 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame,
         ff_draw_color(&s->dc, &s->fontcolor, s->fontcolor.rgba);
     }
 
-    x = 0;
-    y = 0;
-
-    if ((ret = update_fontsize(ctx)) < 0)
+    if ((ret = update_fontsize(ctx)) < 0) {
         return ret;
-
-    /* load and cache glyphs */
-    for (i = 0, p = text; *p; i++) {
-        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_invalid;);
-continue_on_invalid:
-
-        /* get glyph */
-        dummy.code = code;
-        dummy.fontsize = s->fontsize;
-        glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
-        if (!glyph) {
-            ret = load_glyph(ctx, &glyph, code);
-            if (ret < 0)
-                return ret;
-        }
-
-        y_min = FFMIN(glyph->bbox.yMin, y_min);
-        y_max = FFMAX(glyph->bbox.yMax, y_max);
-        x_min = FFMIN(glyph->bbox.xMin, x_min);
-        x_max = FFMAX(glyph->bbox.xMax, x_max);
     }
-    s->max_glyph_h = y_max - y_min;
-    s->max_glyph_w = x_max - x_min;
-
-    /* compute and save position for each glyph */
-    glyph = NULL;
-    for (i = 0, p = text; *p; i++) {
-        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_invalid2;);
-continue_on_invalid2:
 
-        /* skip the \n in the sequence \r\n */
-        if (prev_code == '\r' && code == '\n')
-            continue;
-
-        prev_code = code;
-        if (is_newline(code)) {
-
-            max_text_line_w = FFMAX(max_text_line_w, x);
-            y += s->max_glyph_h + s->line_spacing;
-            x = 0;
-            continue;
-        }
-
-        /* get glyph */
-        prev_glyph = glyph;
-        dummy.code = code;
-        dummy.fontsize = s->fontsize;
-        glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
-
-        /* kerning */
-        if (s->use_kerning && prev_glyph && glyph->code) {
-            FT_Get_Kerning(s->face, prev_glyph->code, glyph->code,
-                           ft_kerning_default, &delta);
-            x += delta.x >> 6;
-        }
-
-        /* save position */
-        s->positions[i].x = x + glyph->bitmap_left;
-        s->positions[i].y = y - glyph->bitmap_top + y_max;
-        if (code == '\t') x  = (x / s->tabsize + 1)*s->tabsize;
-        else              x += glyph->advance;
-    }
+    measure_text(ctx, &metrics);
 
-    max_text_line_w = FFMAX(x, max_text_line_w);
+    s->max_glyph_h = POS_CEIL(metrics.max_y64 - metrics.min_y64, 64);
+    s->max_glyph_w = POS_CEIL(metrics.max_x64 - metrics.min_x64, 64);
 
-    s->var_values[VAR_TW] = s->var_values[VAR_TEXT_W] = max_text_line_w;
-    s->var_values[VAR_TH] = s->var_values[VAR_TEXT_H] = y + s->max_glyph_h;
+    s->var_values[VAR_TW] = s->var_values[VAR_TEXT_W] = metrics.width;
+    s->var_values[VAR_TH] = s->var_values[VAR_TEXT_H] = metrics.height;
 
     s->var_values[VAR_MAX_GLYPH_W] = s->max_glyph_w;
     s->var_values[VAR_MAX_GLYPH_H] = s->max_glyph_h;
-    s->var_values[VAR_MAX_GLYPH_A] = s->var_values[VAR_ASCENT ] = y_max;
-    s->var_values[VAR_MAX_GLYPH_D] = s->var_values[VAR_DESCENT] = y_min;
+    s->var_values[VAR_MAX_GLYPH_A] = s->var_values[VAR_ASCENT] = POS_CEIL(metrics.max_y64, 64);
+    s->var_values[VAR_FONT_A] = s->face->size->metrics.ascender / 64;
+    s->var_values[VAR_MAX_GLYPH_D] = s->var_values[VAR_DESCENT] = POS_CEIL(metrics.min_y64, 64);
+    s->var_values[VAR_FONT_D] = -s->face->size->metrics.descender / 64;
 
-    s->var_values[VAR_LINE_H] = s->var_values[VAR_LH] = s->max_glyph_h;
+    s->var_values[VAR_TOP_A] = POS_CEIL(metrics.offset_top64, 64);
+    s->var_values[VAR_BOTTOM_D] = POS_CEIL(metrics.offset_bottom64, 64);
+    s->var_values[VAR_LINE_H] = s->var_values[VAR_LH] = metrics.line_height64 / 64.;
 
     if (s->text_source == AV_FRAME_DATA_DETECTION_BBOXES) {
         s->var_values[VAR_X] = s->x;
@@ -1559,56 +1970,176 @@ continue_on_invalid2:
     update_color_with_alpha(s, &bordercolor, s->bordercolor);
     update_color_with_alpha(s, &boxcolor   , s->boxcolor   );
 
-    box_w = max_text_line_w;
-    box_h = y + s->max_glyph_h;
+    if (s->draw_box && s->boxborderw) {
+        int bbsize[4];
+        int count;
+        count = string_to_array(s->boxborderw, bbsize, 4);
+        if(count == 1) {
+            s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = bbsize[0];
+        } else if(count == 2) {
+            s->bb_top = s->bb_bottom = bbsize[0];
+            s->bb_right = s->bb_left = bbsize[1];
+        } else if(count == 3) {
+            s->bb_top = bbsize[0];
+            s->bb_right = s->bb_left = bbsize[1];
+            s->bb_bottom = bbsize[2];
+        } else if(count == 4) {
+            s->bb_top = bbsize[0];
+            s->bb_right = bbsize[1];
+            s->bb_bottom = bbsize[2];
+            s->bb_left = bbsize[3];
+        }
+    } else {
+        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = 0;
+    }
 
     if (s->fix_bounds) {
-
         /* calculate footprint of text effects */
-        int boxoffset     = s->draw_box ? FFMAX(s->boxborderw, 0) : 0;
         int borderoffset  = s->borderw  ? FFMAX(s->borderw, 0) : 0;
 
-        int offsetleft = FFMAX3(boxoffset, borderoffset,
+        int offsetleft = FFMAX3(FFMAX(s->bb_left, 0), borderoffset,
                                 (s->shadowx < 0 ? FFABS(s->shadowx) : 0));
-        int offsettop = FFMAX3(boxoffset, borderoffset,
+        int offsettop = FFMAX3(FFMAX(s->bb_top, 0), borderoffset,
                                 (s->shadowy < 0 ? FFABS(s->shadowy) : 0));
-
-        int offsetright = FFMAX3(boxoffset, borderoffset,
+        int offsetright = FFMAX3(FFMAX(s->bb_right, 0), borderoffset,
                                  (s->shadowx > 0 ? s->shadowx : 0));
-        int offsetbottom = FFMAX3(boxoffset, borderoffset,
+        int offsetbottom = FFMAX3(FFMAX(s->bb_bottom, 0), borderoffset,
                                   (s->shadowy > 0 ? s->shadowy : 0));
 
-
         if (s->x - offsetleft < 0) s->x = offsetleft;
         if (s->y - offsettop < 0)  s->y = offsettop;
 
-        if (s->x + box_w + offsetright > width)
-            s->x = FFMAX(width - box_w - offsetright, 0);
-        if (s->y + box_h + offsetbottom > height)
-            s->y = FFMAX(height - box_h - offsetbottom, 0);
+        if (s->x + metrics.width + offsetright > width)
+            s->x = FFMAX(width - metrics.width - offsetright, 0);
+        if (s->y + metrics.height + offsetbottom > height)
+            s->y = FFMAX(height - metrics.height - offsetbottom, 0);
     }
 
-    /* draw box */
-    if (s->draw_box)
-        ff_blend_rectangle(&s->dc, &boxcolor,
-                           frame->data, frame->linesize, width, height,
-                           s->x - s->boxborderw, s->y - s->boxborderw,
-                           box_w + s->boxborderw * 2, box_h + s->boxborderw * 2);
+    x = 0;
+    y = 0;
+    x64 = (int)(s->x * 64.);
+    if(s->y_align == YA_FONT) {
+        y64 = (int)(s->y * 64. + s->face->size->metrics.ascender);
+    } else if(s->y_align == YA_BASELINE) {
+        y64 = (int)(s->y * 64.);
+    } else {
+        y64 = (int)(s->y * 64. + metrics.offset_top64);
+    }
 
-    if (s->shadowx || s->shadowy) {
-        if ((ret = draw_glyphs(s, frame, width, height,
-                               &shadowcolor, s->shadowx, s->shadowy, 0)) < 0)
-            return ret;
+    for(int l = 0; l < s->line_count; ++l) {
+        TextLine *line = &s->lines[l];
+        HarfbuzzData *hb = &line->hb_data;
+        line->glyphs = av_mallocz(hb->glyph_count * sizeof(GlyphInfo));
+
+        for(int t = 0; t < hb->glyph_count; ++t) {
+            GlyphInfo *g_info = &line->glyphs[t];
+            uint8_t is_tab = last_tab_idx < s->tab_count &&
+                hb->glyph_info[t].cluster == s->tab_clusters[last_tab_idx] - line->cluster_offset;
+            int true_x, true_y;
+            if(is_tab) {
+                ++last_tab_idx;
+            }
+            true_x = x + hb->glyph_pos[t].x_offset;
+            true_y = y + hb->glyph_pos[t].y_offset;
+            shift_x64 = (((x64 + true_x) >> 4) & 0b0011) << 4;
+            shift_y64 = ((4 - (((y64 + true_y) >> 4) & 0b0011)) & 0b0011) << 4;
+
+            ret = load_glyph(ctx, &glyph, hb->glyph_info[t].codepoint, shift_x64, shift_y64);
+            if (ret != 0) {
+                return ret;
+            }
+            g_info->code = hb->glyph_info[t].codepoint;
+            g_info->x = (x64 + true_x) >> 6;
+            g_info->y = ((y64 + true_y) >> 6) + (shift_y64 > 0 ? 1 : 0);
+            g_info->shift_x64 = shift_x64;
+            g_info->shift_y64 = shift_y64;
+
+            if(!is_tab) {
+                x += hb->glyph_pos[t].x_advance;
+            } else {
+                int size = s->blank_advance64 * s->tabsize;
+                x = (x / size + 1) * size;
+            }
+            y += hb->glyph_pos[t].y_advance;
+        }
+
+        y += metrics.line_height64 + s->line_spacing * 64;
+        x = 0;
     }
 
-    if (s->borderw) {
-        if ((ret = draw_glyphs(s, frame, width, height,
-                               &bordercolor, 0, 0, s->borderw)) < 0)
+// TODO (LEFT OFFSET)
+//    offset_left = metrics.offset_left64 / 64;
+    offset_left = 0;
+    metrics.rect_x = s->x;
+    if(s->y_align == YA_BASELINE) {
+        metrics.rect_y = s->y - metrics.offset_top64 / 64;
+    } else {
+        metrics.rect_y = s->y;
+    }
+    
+    s->box_width = s->boxw == 0 ? metrics.width : s->boxw;
+    s->box_height = s->boxh == 0 ? metrics.height : s->boxh;
+
+    if(!s->draw_box) {
+        // Create a border for the clipping region to take into account subpixel
+        // errors in text measurement and effects.
+        int borderoffset = s->borderw ? FFMAX(s->borderw, 0) : 0;
+        s->bb_left = borderoffset + (s->shadowx < 0 ? FFABS(s->shadowx) : 0) + 1;
+        s->bb_top = borderoffset + (s->shadowy < 0 ? FFABS(s->shadowy) : 0) + 1;
+        s->bb_right = borderoffset + (s->shadowx > 0 ? s->shadowx : 0) + 1;
+        s->bb_bottom = borderoffset + (s->shadowy > 0 ? s->shadowy : 0) + 1;
+    }
+
+    /* Check if the whole box is out of the frame */        
+    is_outside = metrics.rect_x - s->bb_left >= width ||
+                    metrics.rect_y - s->bb_top >= height ||
+                    metrics.rect_x + s->box_width + s->bb_right <= 0 ||
+                    metrics.rect_y + s->box_height + s->bb_bottom <= 0;
+
+    if(!is_outside) {
+        /* draw box */
+        if (s->draw_box) {
+            rec_x = metrics.rect_x - s->bb_left;
+            rec_y = metrics.rect_y - s->bb_top;
+            rec_width = s->box_width + s->bb_right + s->bb_left;
+            rec_height = s->box_height + s->bb_bottom + s->bb_top;
+            // av_log(s, AV_LOG_DEBUG, "rect_x: %d -> bb_left: %d\n",
+            //     metrics.rect_x, s->bb_left);
+            // av_log(s, AV_LOG_DEBUG, "Rect -> (x: %d - y: %d - dx: %d - dy: %d)\n",
+            //     rec_x, rec_y, rec_width, rec_height);
+            ff_blend_rectangle(&s->dc, &boxcolor,
+                frame->data, frame->linesize, width, height,
+                rec_x, rec_y, rec_width, rec_height);
+        }
+
+        if (s->shadowx || s->shadowy) {
+            if ((ret = draw_glyphs(s, frame, &shadowcolor, &metrics,
+                    s->shadowx - offset_left, s->shadowy, s->borderw)) < 0) {
+                return ret;
+            }
+        }
+
+        if (s->borderw) {
+            if ((ret = draw_glyphs(s, frame, &bordercolor, &metrics,
+                    -offset_left, 0, s->borderw)) < 0) {
+                return ret;
+            }
+        }
+
+        if ((ret = draw_glyphs(s, frame, &fontcolor, &metrics, -offset_left,
+                0, 0)) < 0) {
             return ret;
+        }
     }
-    if ((ret = draw_glyphs(s, frame, width, height,
-                           &fontcolor, 0, 0, 0)) < 0)
-        return ret;
+
+    // FREE data structures
+    for(int l = 0; l < s->line_count; ++l) {
+        TextLine *line = &s->lines[l];
+        av_freep(&line->glyphs);
+        hb_destroy(&line->hb_data);
+    }
+    av_freep(&s->lines);
+    av_freep(&s->tab_clusters);
 
     return 0;
 }
@@ -1680,13 +2211,13 @@ FF_ENABLE_DEPRECATION_WARNINGS
             s->x = bbox->x;
             s->y = bbox->y - s->fontsize;
         }
-        draw_text(ctx, frame, frame->width, frame->height);
+        draw_text(ctx, frame);
     }
 
-    av_log(ctx, AV_LOG_DEBUG, "n:%d t:%f text_w:%d text_h:%d x:%d y:%d\n",
-           (int)s->var_values[VAR_N], s->var_values[VAR_T],
-           (int)s->var_values[VAR_TEXT_W], (int)s->var_values[VAR_TEXT_H],
-           s->x, s->y);
+    // av_log(ctx, AV_LOG_DEBUG, "n:%d t:%f text_w:%d text_h:%d x:%.2f y:%.2f\n",
+    //        (int)s->var_values[VAR_N], s->var_values[VAR_T],
+    //        (int)s->var_values[VAR_TEXT_W], (int)s->var_values[VAR_TEXT_H],
+    //        s->x, s->y);
 
     return ff_filter_frame(outlink, frame);
 }
Paul B Mahol Jan. 27, 2023, 5:31 p.m. UTC | #7
On 1/27/23, Francesco Carusi <klimklim@tiscali.it> wrote:
> On 26/01/2023 17:37, Paul B Mahol wrote:
>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>> On 26/01/2023 14:21, Paul B Mahol wrote:
>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>> The drawtext reinit command is also used in the docs as an example for
>>>>> the sendcmd filter, so I thought it was fine to use commands in that
>>>>> way. In my opinion it is also a convenient way to modify multiple
>>>>> options at the same time.
>>>>> Should the command match the name of a filter option instead?
>>>>>
>>>> Please do not top post.
>>>>
>>>> It is much better to use already existing options for commands that is
>>>> more intuitive to users. Also multiple options can be set at runtime,
>>>> there is no such limitation.
>>> ok, I'm going to remove the "change" command and add commands that match
>>> the options that it included.
>> Thanks, feel free to ask questions on #ffmpeg-devel irc channel.
> I'm attaching the updated patch, I also updated the document at
> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
> Thanks

Amazing, I like demos!

Could improve code style of newly added/changed lines?
For example opening { put on separate line. So code style is in sync
with rest of codebase.

The commands stuff does not need to use strcmp to detect if option
value have been changed, you could avoid strcmp by just caching old
value prior to calling function the picks new values, and after that
just compare old with new and then if it differs call needed code.

>>
>>>>> On 26/01/2023 11:50, Paul B Mahol wrote:
>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>> Hi, I'm new to contributing to ffmpeg!
>>>>>>>
>>>>>>> I modified the drawtext filter to improve text rendering and add
>>>>>>> some
>>>>>>> features. You can find a high level description of the changes at
>>>>>>> this
>>>>>>> link:
>>>>>>>
>>>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>>>>>
>>>>>>> I'm also attaching the patch file.
>>>>>>> I looked for the filter maintainer to discuss about the changes I
>>>>>>> made
>>>>>>> but it looks like there isn't any, am I correct?
>>>>>>>
>>>>>>> Please let me know if this is the right way to submit my
>>>>>>> contribution.
>>>>>> Why filter can not support normal commands for options? Like most/all
>>>>>> other filters that have support for changing options values at
>>>>>> runtime.
>>>>>>
>>>>>> The reinit and yours added change option(s) are very
>>>>>> strange/inconvenient things to do.
>>>>>> _______________________________________________
>>>>>> 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".
>>>>>
>>>> _______________________________________________
>>>> 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".
>>>
>> _______________________________________________
>> 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".
>
Francesco Carusi Jan. 28, 2023, 2:45 p.m. UTC | #8
On 27/01/2023 18:31, Paul B Mahol wrote:
> On 1/27/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>> On 26/01/2023 17:37, Paul B Mahol wrote:
>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>> On 26/01/2023 14:21, Paul B Mahol wrote:
>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>> The drawtext reinit command is also used in the docs as an example for
>>>>>> the sendcmd filter, so I thought it was fine to use commands in that
>>>>>> way. In my opinion it is also a convenient way to modify multiple
>>>>>> options at the same time.
>>>>>> Should the command match the name of a filter option instead?
>>>>>>
>>>>> Please do not top post.
>>>>>
>>>>> It is much better to use already existing options for commands that is
>>>>> more intuitive to users. Also multiple options can be set at runtime,
>>>>> there is no such limitation.
>>>> ok, I'm going to remove the "change" command and add commands that match
>>>> the options that it included.
>>> Thanks, feel free to ask questions on #ffmpeg-devel irc channel.
>> I'm attaching the updated patch, I also updated the document at
>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>> Thanks
> Amazing, I like demos!
>
> Could improve code style of newly added/changed lines?
> For example opening { put on separate line. So code style is in sync
> with rest of codebase.
Sure, I'll put opening { on a new line for functions, not for control 
statements, like in the rest of the code. Is it fine?
> The commands stuff does not need to use strcmp to detect if option
> value have been changed, you could avoid strcmp by just caching old
> value prior to calling function the picks new values, and after that
> just compare old with new and then if it differs call needed code.
I'll cache the numeric values. I think that caching string values is not 
the preferred solution because in addition to the strcmp needed to check 
the value, it would also need a strdup to cache the previous value, even 
when the command does not involve those options. Does it sound reasonable?
>>>>>> On 26/01/2023 11:50, Paul B Mahol wrote:
>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>>> Hi, I'm new to contributing to ffmpeg!
>>>>>>>>
>>>>>>>> I modified the drawtext filter to improve text rendering and add
>>>>>>>> some
>>>>>>>> features. You can find a high level description of the changes at
>>>>>>>> this
>>>>>>>> link:
>>>>>>>>
>>>>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>>>>>>
>>>>>>>> I'm also attaching the patch file.
>>>>>>>> I looked for the filter maintainer to discuss about the changes I
>>>>>>>> made
>>>>>>>> but it looks like there isn't any, am I correct?
>>>>>>>>
>>>>>>>> Please let me know if this is the right way to submit my
>>>>>>>> contribution.
>>>>>>> Why filter can not support normal commands for options? Like most/all
>>>>>>> other filters that have support for changing options values at
>>>>>>> runtime.
>>>>>>>
>>>>>>> The reinit and yours added change option(s) are very
>>>>>>> strange/inconvenient things to do.
>>>>>>> _______________________________________________
>>>>>>> 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".
>>>>>>
>>>>> _______________________________________________
>>>>> 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".
>>>>
>>> _______________________________________________
>>> 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".
Paul B Mahol Jan. 28, 2023, 3:32 p.m. UTC | #9
On 1/28/23, Francesco Carusi <klimklim@tiscali.it> wrote:
> On 27/01/2023 18:31, Paul B Mahol wrote:
>> On 1/27/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>> On 26/01/2023 17:37, Paul B Mahol wrote:
>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>> On 26/01/2023 14:21, Paul B Mahol wrote:
>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>> The drawtext reinit command is also used in the docs as an example
>>>>>>> for
>>>>>>> the sendcmd filter, so I thought it was fine to use commands in that
>>>>>>> way. In my opinion it is also a convenient way to modify multiple
>>>>>>> options at the same time.
>>>>>>> Should the command match the name of a filter option instead?
>>>>>>>
>>>>>> Please do not top post.
>>>>>>
>>>>>> It is much better to use already existing options for commands that
>>>>>> is
>>>>>> more intuitive to users. Also multiple options can be set at runtime,
>>>>>> there is no such limitation.
>>>>> ok, I'm going to remove the "change" command and add commands that
>>>>> match
>>>>> the options that it included.
>>>> Thanks, feel free to ask questions on #ffmpeg-devel irc channel.
>>> I'm attaching the updated patch, I also updated the document at
>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>> Thanks
>> Amazing, I like demos!
>>
>> Could improve code style of newly added/changed lines?
>> For example opening { put on separate line. So code style is in sync
>> with rest of codebase.
> Sure, I'll put opening { on a new line for functions, not for control
> statements, like in the rest of the code. Is it fine?

Yes. Thanks.

>> The commands stuff does not need to use strcmp to detect if option
>> value have been changed, you could avoid strcmp by just caching old
>> value prior to calling function the picks new values, and after that
>> just compare old with new and then if it differs call needed code.
> I'll cache the numeric values. I think that caching string values is not
> the preferred solution because in addition to the strcmp needed to check
> the value, it would also need a strdup to cache the previous value, even
> when the command does not involve those options. Does it sound reasonable?

Yes.

>>>>>>> On 26/01/2023 11:50, Paul B Mahol wrote:
>>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>>>> Hi, I'm new to contributing to ffmpeg!
>>>>>>>>>
>>>>>>>>> I modified the drawtext filter to improve text rendering and add
>>>>>>>>> some
>>>>>>>>> features. You can find a high level description of the changes at
>>>>>>>>> this
>>>>>>>>> link:
>>>>>>>>>
>>>>>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>>>>>>>
>>>>>>>>> I'm also attaching the patch file.
>>>>>>>>> I looked for the filter maintainer to discuss about the changes I
>>>>>>>>> made
>>>>>>>>> but it looks like there isn't any, am I correct?
>>>>>>>>>
>>>>>>>>> Please let me know if this is the right way to submit my
>>>>>>>>> contribution.
>>>>>>>> Why filter can not support normal commands for options? Like
>>>>>>>> most/all
>>>>>>>> other filters that have support for changing options values at
>>>>>>>> runtime.
>>>>>>>>
>>>>>>>> The reinit and yours added change option(s) are very
>>>>>>>> strange/inconvenient things to do.
>>>>>>>> _______________________________________________
>>>>>>>> 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".
>>>>>>>
>>>>>> _______________________________________________
>>>>>> 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".
>>>>>
>>>> _______________________________________________
>>>> 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".
>
> _______________________________________________
> 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".
>
Francesco Carusi Jan. 30, 2023, 10:48 a.m. UTC | #10
On 28/01/2023 16:32, Paul B Mahol wrote:
> On 1/28/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>> On 27/01/2023 18:31, Paul B Mahol wrote:
>>> On 1/27/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>> On 26/01/2023 17:37, Paul B Mahol wrote:
>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>> On 26/01/2023 14:21, Paul B Mahol wrote:
>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>>> The drawtext reinit command is also used in the docs as an example
>>>>>>>> for
>>>>>>>> the sendcmd filter, so I thought it was fine to use commands in that
>>>>>>>> way. In my opinion it is also a convenient way to modify multiple
>>>>>>>> options at the same time.
>>>>>>>> Should the command match the name of a filter option instead?
>>>>>>>>
>>>>>>> Please do not top post.
>>>>>>>
>>>>>>> It is much better to use already existing options for commands that
>>>>>>> is
>>>>>>> more intuitive to users. Also multiple options can be set at runtime,
>>>>>>> there is no such limitation.
>>>>>> ok, I'm going to remove the "change" command and add commands that
>>>>>> match
>>>>>> the options that it included.
>>>>> Thanks, feel free to ask questions on #ffmpeg-devel irc channel.
>>>> I'm attaching the updated patch, I also updated the document at
>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>> Thanks
>>> Amazing, I like demos!
>>>
>>> Could improve code style of newly added/changed lines?
>>> For example opening { put on separate line. So code style is in sync
>>> with rest of codebase.
>> Sure, I'll put opening { on a new line for functions, not for control
>> statements, like in the rest of the code. Is it fine?
> Yes. Thanks.
>
>>> The commands stuff does not need to use strcmp to detect if option
>>> value have been changed, you could avoid strcmp by just caching old
>>> value prior to calling function the picks new values, and after that
>>> just compare old with new and then if it differs call needed code.
>> I'll cache the numeric values. I think that caching string values is not
>> the preferred solution because in addition to the strcmp needed to check
>> the value, it would also need a strdup to cache the previous value, even
>> when the command does not involve those options. Does it sound reasonable?
> Yes.
I'm attaching the patch that includes the changes we discussed.

>
>>>>>>>> On 26/01/2023 11:50, Paul B Mahol wrote:
>>>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>>>>> Hi, I'm new to contributing to ffmpeg!
>>>>>>>>>>
>>>>>>>>>> I modified the drawtext filter to improve text rendering and add
>>>>>>>>>> some
>>>>>>>>>> features. You can find a high level description of the changes at
>>>>>>>>>> this
>>>>>>>>>> link:
>>>>>>>>>>
>>>>>>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>>>>>>>>
>>>>>>>>>> I'm also attaching the patch file.
>>>>>>>>>> I looked for the filter maintainer to discuss about the changes I
>>>>>>>>>> made
>>>>>>>>>> but it looks like there isn't any, am I correct?
>>>>>>>>>>
>>>>>>>>>> Please let me know if this is the right way to submit my
>>>>>>>>>> contribution.
>>>>>>>>> Why filter can not support normal commands for options? Like
>>>>>>>>> most/all
>>>>>>>>> other filters that have support for changing options values at
>>>>>>>>> runtime.
>>>>>>>>>
>>>>>>>>> The reinit and yours added change option(s) are very
>>>>>>>>> strange/inconvenient things to do.
>>>>>>>>> _______________________________________________
>>>>>>>>> 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".
>>>>>>>>
>>>>>>> _______________________________________________
>>>>>>> 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".
>>>>>>
>>>>> _______________________________________________
>>>>> 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".
>> _______________________________________________
>> 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".
From 7b006d958b0d226269362b9a811bd210ae41adcd Mon Sep 17 00:00:00 2001
From: yethie <klimklim@tiscali.it>
Date: Mon, 30 Jan 2023 11:41:53 +0100
Subject: [PATCH 1/1] enhanced drawtext filter

---
 configure                 |    5 +-
 doc/filters.texi          |   85 ++-
 libavfilter/vf_drawtext.c | 1182 +++++++++++++++++++++++++++----------
 3 files changed, 938 insertions(+), 334 deletions(-)

diff --git a/configure b/configure
index 47790d10f5..bd214076e0 100755
--- a/configure
+++ b/configure
@@ -235,6 +235,7 @@ External library support:
   --enable-libfontconfig   enable libfontconfig, useful for drawtext filter [no]
   --enable-libfreetype     enable libfreetype, needed for drawtext filter [no]
   --enable-libfribidi      enable libfribidi, improves drawtext filter [no]
+  --enable-libharfbuzz     enable libharfbuzz, needed for drawtext filter [no]
   --enable-libglslang      enable GLSL->SPIRV compilation via libglslang [no]
   --enable-libgme          enable Game Music Emu via libgme [no]
   --enable-libgsm          enable GSM de/encoding via libgsm [no]
@@ -1818,6 +1819,7 @@ EXTERNAL_LIBRARY_LIST="
     libfontconfig
     libfreetype
     libfribidi
+    libharfbuzz
     libglslang
     libgme
     libgsm
@@ -3658,7 +3660,7 @@ dilation_opencl_filter_deps="opencl"
 dnn_classify_filter_select="dnn"
 dnn_detect_filter_select="dnn"
 dnn_processing_filter_select="dnn"
-drawtext_filter_deps="libfreetype"
+drawtext_filter_deps="libfreetype libharfbuzz"
 drawtext_filter_suggest="libfontconfig libfribidi"
 elbg_filter_deps="avcodec"
 eq_filter_deps="gpl"
@@ -6582,6 +6584,7 @@ enabled fontconfig        && enable libfontconfig
 enabled libfontconfig     && require_pkg_config libfontconfig fontconfig "fontconfig/fontconfig.h" FcInit
 enabled libfreetype       && require_pkg_config libfreetype freetype2 "ft2build.h FT_FREETYPE_H" FT_Init_FreeType
 enabled libfribidi        && require_pkg_config libfribidi fribidi fribidi.h fribidi_version_info
+enabled libharfbuzz       && require_pkg_config libharfbuzz harfbuzz hb.h hb_buffer_create
 enabled libglslang && { check_lib spirv_compiler glslang/Include/glslang_c_interface.h glslang_initialize_process \
                             -lglslang -lMachineIndependent -lOSDependent -lHLSL -lOGLCompiler -lGenericCodeGen \
                             -lSPVRemapper -lSPIRV -lSPIRV-Tools-opt -lSPIRV-Tools -lpthread -lstdc++ -lm ||
diff --git a/doc/filters.texi b/doc/filters.texi
index 3a54c68f3e..6ca1ad9b29 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -12032,7 +12032,7 @@ Draw a text string or text from a specified file on top of a video, using the
 libfreetype library.
 
 To enable compilation of this filter, you need to configure FFmpeg with
-@code{--enable-libfreetype}.
+@code{--enable-libfreetype} and @code{--enable-libharfbuzz}.
 To enable default font fallback and the @var{font} option you need to
 configure FFmpeg with @code{--enable-libfontconfig}.
 To enable the @var{text_shaping} option, you need to configure FFmpeg with
@@ -12049,9 +12049,27 @@ Used to draw a box around text using the background color.
 The value must be either 1 (enable) or 0 (disable).
 The default value of @var{box} is 0.
 
+@item boxw
+Set the width of the box to be drawn around text.
+The default value of @var{boxw} is computed automatically to match the text width
+
+@item boxh
+Set the height of the box to be drawn around text.
+The default value of @var{boxh} is computed automatically to match the text height
+
 @item boxborderw
 Set the width of the border to be drawn around the box using @var{boxcolor}.
-The default value of @var{boxborderw} is 0.
+The value must be specified using one of the following formats:
+@itemize @bullet
+@item @code{boxborderw=10} set the width of all the borders to 10
+@item @code{boxborderw=10|20} set the width of the top and bottom borders to 10
+    and the width of the left and right borders to 20
+@item @code{boxborderw=10|20|30} set the width of the top border to 10, the width
+    of the bottom border to 30 and the width of the left and right borders to 20
+@item @code{boxborderw=10|20|30|40} set the borders width to 10 (top), 20 (right),
+    30 (bottom), 40 (left)
+@end itemize
+The default value of @var{boxborderw} is "0".
 
 @item boxcolor
 The color to be used for drawing box around text. For the syntax of this
@@ -12060,8 +12078,22 @@ option, check the @ref{color syntax,,"Color" section in the ffmpeg-utils manual,
 The default value of @var{boxcolor} is "white".
 
 @item line_spacing
-Set the line spacing in pixels of the border to be drawn around the box using @var{box}.
-The default value of @var{line_spacing} is 0.
+Set the line spacing in pixels. The default value of @var{line_spacing} is 0.
+
+@item text_align
+Set the vertical and horizontal alignment of the text with respect to the box boundaries.
+The value must contain exactly two letters, one for the vertical alignment (T=top,
+M=middle, B=bottom) and one for the horizontal alignment (L=left, C=center, R=right).
+
+@item y_align
+Specify what the @var{y} value is referred to. Possible values are:
+@itemize @bullet
+@item @code{text} the top of the highest glyph of the first text line is placed at @var{y}
+@item @code{baseline} the baseline of the first text line is placed at @var{y}
+@item @code{font} the baseline of the first text line is placed at @var{y} plus the
+    ascent (in pixels) defined in the font metrics
+@end itemize
+The default value of @var{y_align} is "text" for backward compatibility.
 
 @item borderw
 Set the width of the border to be drawn around the text using @var{bordercolor}.
@@ -12070,7 +12102,6 @@ The default value of @var{borderw} is 0.
 @item bordercolor
 Set the color to be used for drawing border around text. For the syntax of this
 option, check the @ref{color syntax,,"Color" section in the ffmpeg-utils manual,ffmpeg-utils}.
-
 The default value of @var{bordercolor} is "black".
 
 @item expansion
@@ -12168,10 +12199,6 @@ values. The default value for both is "0".
 The starting frame number for the n/frame_num variable. The default value
 is "0".
 
-@item tabsize
-The size in number of spaces to use for rendering the tab.
-Default value is 4.
-
 @item timecode
 Set the initial timecode representation in "hh:mm:ss[:;.]ff"
 format. It can be used with or without text parameter. @var{timecode_rate}
@@ -12269,6 +12296,18 @@ contained in the rendered text, it is equivalent to @var{ascent} -
 maximum glyph width, that is the maximum width for all the glyphs
 contained in the rendered text
 
+@item font_a
+the ascent size defined in the font metrics
+
+@item font_d
+the descent size defined in the font metrics
+
+@item top_a
+the maximum ascender of the glyphs of the first text line
+
+@item bottom_d
+the maximum descender of the glyphs of the last text line
+
 @item n
 the number of input frame, starting from 0
 
@@ -12433,11 +12472,34 @@ Full filter invocation with sendcmd would look like this:
 @example
 sendcmd=c='56.0 drawtext reinit fontsize=56\:fontcolor=green\:text=Hello\\ World'
 @end example
-@end table
 
 If the entire argument can't be parsed or applied as valid values then the filter will
 continue with its existing parameters.
 
+@end table
+
+The following options are also supported as @ref{commands}:
+
+@itemize @bullet
+@item x
+@item y
+@item alpha
+@item fontsize
+@item fontcolor
+@item boxcolor
+@item bordercolor
+@item shadowcolor
+@item box
+@item boxw
+@item boxh
+@item boxborderw
+@item line_spacing
+@item text_align
+@item shadowx
+@item shadowy
+@item borderw
+@end itemize
+
 @subsection Examples
 
 @itemize
@@ -12559,6 +12621,9 @@ For more information about fontconfig, check:
 For more information about libfribidi, check:
 @url{http://fribidi.org/}.
 
+For more information about libharfbuzz, check:
+@url{https://github.com/harfbuzz/harfbuzz}.
+
 @section edgedetect
 
 Detect and draw edges. The filter uses the Canny Edge Detection algorithm.
diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c
index 50012bb258..43dd38ae8d 100644
--- a/libavfilter/vf_drawtext.c
+++ b/libavfilter/vf_drawtext.c
@@ -1,4 +1,5 @@
 /*
+ * Copyright (c) 2023 Francesco Carusi
  * Copyright (c) 2011 Stefano Sabatini
  * Copyright (c) 2010 S.N. Hemanth Meenakshisundaram
  * Copyright (c) 2003 Gustavo Sverzut Barbieri <gsbarbieri@yahoo.com.br>
@@ -20,6 +21,26 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  */
 
+/*
+ * Changelog - 2023
+ *
+ * - This filter now depends on libharfbuzz for text shaping.
+ * - Glyphs position is now accurate to 1/4 pixel in both directions
+ * - The size of the background box can now be forced with the boxw
+ *   and boxh parameters
+ * - Text can be aligned horizontally (top, middle, bottom) and vertically
+ *   (left, center, right) relative to the background box
+ * - The default line height is now the one defined in the font
+ * - The new y_align parameter specifies if the user provided y value is
+ *   referred to the top of the text, to the font baseline or to the
+ *   top of the font.
+ * - The boxborderw parameter can now contain a different value for each border
+ *   (e.g. boxborderw=top|right|bottom|left)
+ * - Many filter parameters are now supported as commands.
+ * - The following new variables can be used in the x and y expressions:
+ *   font_a, font_d, top_a, bottom_d
+ */
+
 /**
  * @file
  * drawtext filter, based on the original vhook/drawtext.c
@@ -72,16 +93,26 @@
 #include FT_GLYPH_H
 #include FT_STROKER_H
 
+#include <hb.h>
+#include <hb-ft.h>
+
+// Ceiling operation for positive integers division
+#define POS_CEIL(x, y) ((x)/(y) + ((x)%(y) != 0))
+
 static const char *const var_names[] = {
     "dar",
     "hsub", "vsub",
-    "line_h", "lh",           ///< line height, same as max_glyph_h
+    "line_h", "lh",           ///< line height
     "main_h", "h", "H",       ///< height of the input video
     "main_w", "w", "W",       ///< width  of the input video
-    "max_glyph_a", "ascent",  ///< max glyph ascent
-    "max_glyph_d", "descent", ///< min glyph descent
+    "max_glyph_a", "ascent",  ///< max glyph ascender
+    "max_glyph_d", "descent", ///< min glyph descender
     "max_glyph_h",            ///< max glyph height
     "max_glyph_w",            ///< max glyph width
+    "font_a",                 ///< font-defined ascent
+    "font_d",                 ///< font-defined descent
+    "top_a",                  ///< max glyph ascender of the top line
+    "bottom_d",               ///< max glyph descender of the bottom line
     "n",                      ///< number of frame
     "sar",
     "t",                      ///< timestamp expressed in seconds
@@ -125,6 +156,10 @@ enum var_name {
     VAR_MAX_GLYPH_D, VAR_DESCENT,
     VAR_MAX_GLYPH_H,
     VAR_MAX_GLYPH_W,
+    VAR_FONT_A,
+    VAR_FONT_D,
+    VAR_TOP_A,
+    VAR_BOTTOM_D,
     VAR_N,
     VAR_SAR,
     VAR_T,
@@ -148,12 +183,84 @@ enum expansion_mode {
     EXP_STRFTIME,
 };
 
+enum y_alignment {
+    YA_TEXT,
+    YA_BASELINE,
+    YA_FONT,
+};
+
+typedef struct HarfbuzzData {
+    hb_buffer_t* buf;
+    hb_font_t* font;
+    unsigned int glyph_count;
+    hb_glyph_info_t* glyph_info;
+    hb_glyph_position_t* glyph_pos;
+} HarfbuzzData;
+
+/** Information about a single glyph in a text line */
+typedef struct GlyphInfo {
+    uint32_t code;                  ///< the glyph code point
+    int x;                          ///< the x position of the glyph
+    int y;                          ///< the y position of the glyph
+    int shift_x64;                  ///< the horizontal shift of the glyph in 26.6 units
+    int shift_y64;                  ///< the vertical shift of the glyph in 26.6 units
+} GlyphInfo;
+
+/** Information about a single line of text */
+typedef struct TextLine {
+    int offset_left64;              ///< offset between the origin and
+                                    ///  the leftmost pixel of the first glyph
+    int offset_right64;             ///< maximum offset between the origin and
+                                    ///  the rightmost pixel of the last glyph
+    int width64;                    ///< width of the line
+    HarfbuzzData hb_data;           ///< libharfbuzz data of this text line
+    GlyphInfo* glyphs;              ///< array of glyphs in this text line
+    int cluster_offset;             ///< the offset at which this line begins
+} TextLine;
+
+/** A glyph as loaded and rendered using libfreetype */
+typedef struct Glyph {
+    FT_Glyph glyph;
+    FT_Glyph border_glyph;
+    uint32_t code;
+    unsigned int fontsize;
+    /** Glyph bitmaps with 1/4 pixel precision in both directions */
+    FT_BitmapGlyph bglyph[16];
+    /** Outlined glyph bitmaps with 1/4 pixel precision in both directions */
+    FT_BitmapGlyph border_bglyph[16];
+    FT_BBox bbox;
+} Glyph;
+
+/** Global text metrics */
+typedef struct TextMetrics {
+    int offset_top64;               ///< ascender amount of the first line (in 26.6 units)
+    int offset_bottom64;            ///< descender amount of the last line (in 26.6 units)
+    int offset_left64;              ///< maximum offset between the origin and
+                                    ///  the leftmost pixel of the first glyph
+                                    ///  of each line (in 26.6 units)
+    int offset_right64;             ///< maximum offset between the origin and
+                                    ///  the rightmost pixel of the last glyph
+                                    ///  of each line (in 26.6 units)
+    int line_height64;              ///< the font-defined line height
+    int width;                      ///< width of the longest line - ceil(width64/64)
+    int height;                     ///< total height of the text - ceil(height64/64)
+
+    int min_y64;                    ///< minimum value of bbox.yMin among glyphs (in 26.6 units)
+    int max_y64;                    ///< maximum value of bbox.yMax among glyphs (in 26.6 units)
+    int min_x64;                    ///< minimum value of bbox.xMin among glyphs (in 26.6 units)
+    int max_x64;                    ///< maximum value of bbox.xMax among glyphs (in 26.6 units)
+
+    // Position of the background box (without borders)
+    int rect_x;                     ///< x position of the box
+    int rect_y;                     ///< y position of the box
+} TextMetrics;
+
 typedef struct DrawTextContext {
     const AVClass *class;
     int exp_mode;                   ///< expansion mode to use for the text
     int reinit;                     ///< tells if the filter is being reinited
 #if CONFIG_LIBFONTCONFIG
-    uint8_t *font;              ///< font to be used
+    uint8_t *font;                  ///< font to be used
 #endif
     uint8_t *fontfile;              ///< font to be used
     uint8_t *text;                  ///< text to be drawn
@@ -161,11 +268,9 @@ typedef struct DrawTextContext {
     uint8_t *fontcolor_expr;        ///< fontcolor expression to evaluate
     AVBPrint expanded_fontcolor;    ///< used to contain the expanded fontcolor spec
     int ft_load_flags;              ///< flags used for loading fonts, see FT_LOAD_*
-    FT_Vector *positions;           ///< positions for each element in the text
-    size_t nb_positions;            ///< number of elements of positions array
     char *textfile;                 ///< file with text to be drawn
-    int x;                          ///< x position to start drawing text
-    int y;                          ///< y position to start drawing text
+    double x;                       ///< x position to start drawing text
+    double y;                       ///< y position to start drawing text
     int max_glyph_w;                ///< max glyph width
     int max_glyph_h;                ///< max glyph height
     int shadowx, shadowy;
@@ -177,8 +282,14 @@ typedef struct DrawTextContext {
 
     int line_spacing;               ///< lines spacing in pixels
     short int draw_box;             ///< draw box around text - true or false
-    int boxborderw;                 ///< box border width
-    int use_kerning;                ///< font kerning is used - true/false
+    char* boxborderw;               ///< box border width (padding)
+                                    ///  allowed formats: "all", "vert|oriz", "top|right|bottom|left"
+    int bb_top;                     ///< the size of the top box border
+    int bb_right;                   ///< the size of the right box border
+    int bb_bottom;                  ///< the size of the bottom box border
+    int bb_left;                    ///< the size of the left box border
+    int box_width;                  ///< the width of box
+    int box_height;                 ///< the height of box
     int tabsize;                    ///< tab size
     int fix_bounds;                 ///< do we let it go out of frame bounds - t/f
 
@@ -213,31 +324,46 @@ typedef struct DrawTextContext {
     int text_shaping;               ///< 1 to shape the text before drawing it
 #endif
     AVDictionary *metadata;
+
+    int boxw;                       ///< the value of the boxw parameter
+    int boxh;                       ///< the value of the boxh parameter
+    uint8_t *text_align;            ///< the horizontal and vertical text alignment
+    int y_align;                    ///< the value of the y_align parameter
+
+    TextLine *lines;                ///< computed information about text lines
+    int line_count;                 ///< the number of text lines
+    uint32_t *tab_clusters;         ///< the position of tab characters in the text
+    int tab_count;                  ///< the number of tab characters
+    int blank_advance64;            ///< the size of the space character
 } DrawTextContext;
 
 #define OFFSET(x) offsetof(DrawTextContext, 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
 
 static const AVOption drawtext_options[]= {
-    {"fontfile",    "set font file",        OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"text",        "set text",             OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"textfile",    "set text file",        OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"fontcolor",   "set foreground color", OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
+    {"fontfile",       "set font file",         OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"text",           "set text",              OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, TFLAGS},
+    {"textfile",       "set text file",         OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"fontcolor",      "set foreground color",  OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, TFLAGS},
     {"fontcolor_expr", "set foreground color expression", OFFSET(fontcolor_expr), AV_OPT_TYPE_STRING, {.str=""}, 0, 0, FLAGS},
-    {"boxcolor",    "set box color",        OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
-    {"bordercolor", "set border color",     OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
-    {"shadowcolor", "set shadow color",     OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
-    {"box",         "set box",              OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0,        1       , FLAGS},
-    {"boxborderw",  "set box border width", OFFSET(boxborderw),         AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"line_spacing",  "set line spacing in pixels", OFFSET(line_spacing),   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX,FLAGS},
-    {"fontsize",    "set font size",        OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0 , FLAGS},
-    {"x",           "set x expression",     OFFSET(x_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
-    {"y",           "set y expression",     OFFSET(y_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
-    {"shadowx",     "set shadow x offset",  OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"shadowy",     "set shadow y offset",  OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"borderw",     "set border width",     OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"tabsize",     "set tab size",         OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0,        INT_MAX , FLAGS},
-    {"basetime",    "set base time",        OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX , FLAGS},
+    {"boxcolor",       "set box color",         OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, TFLAGS},
+    {"bordercolor",    "set border color",      OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, TFLAGS},
+    {"shadowcolor",    "set shadow color",      OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, TFLAGS},
+    {"box",            "set box",               OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0, 1, TFLAGS},
+    {"boxborderw",     "set box borders width", OFFSET(boxborderw),         AV_OPT_TYPE_STRING, {.str="0"},   0, 0, TFLAGS},
+    {"line_spacing",   "set line spacing in pixels", OFFSET(line_spacing), AV_OPT_TYPE_INT,    {.i64=-1},    INT_MIN, INT_MAX, TFLAGS},
+    {"fontsize",       "set font size",         OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, TFLAGS},
+    {"text_align",     "set text alignment",    OFFSET(text_align),         AV_OPT_TYPE_STRING, {.str="TL"},  0, 0, TFLAGS},
+    {"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},
+    {"boxw",           "set box width",         OFFSET(boxw),               AV_OPT_TYPE_INT,    {.i64=0},     0, INT_MAX, TFLAGS},
+    {"boxh",           "set box height",        OFFSET(boxh),               AV_OPT_TYPE_INT,    {.i64=0},     0, INT_MAX, TFLAGS},
+    {"shadowx",        "set shadow x offset",   OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, TFLAGS},
+    {"shadowy",        "set shadow y offset",   OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, TFLAGS},
+    {"borderw",        "set border width",      OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, TFLAGS},
+    {"tabsize",        "set tab size",          OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0, INT_MAX, TFLAGS},
+    {"basetime",       "set base time",         OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX, FLAGS},
 #if CONFIG_LIBFONTCONFIG
     { "font",        "Font name",            OFFSET(font),               AV_OPT_TYPE_STRING, { .str = "Sans" },           .flags = FLAGS },
 #endif
@@ -246,17 +372,21 @@ static const AVOption drawtext_options[]= {
         {"none",     "set no expansion",                    OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_NONE},     0, 0, FLAGS, "expansion"},
         {"normal",   "set normal expansion",                OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_NORMAL},   0, 0, FLAGS, "expansion"},
         {"strftime", "set strftime expansion (deprecated)", OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_STRFTIME}, 0, 0, FLAGS, "expansion"},
+    {"y_align",   "set the y alignment",    OFFSET(y_align), AV_OPT_TYPE_INT,  {.i64=YA_TEXT}, 0, 2, TFLAGS, "y_align"},
+        {"text",     "y is referred to the top of the first text line", OFFSET(y_align), AV_OPT_TYPE_CONST, {.i64=YA_TEXT},     0, 0, FLAGS, "y_align"},
+        {"baseline", "y is referred to the baseline of the first line", OFFSET(y_align), AV_OPT_TYPE_CONST, {.i64=YA_BASELINE}, 0, 0, FLAGS, "y_align"},
+        {"font",     "y is referred to the font defined line metrics",  OFFSET(y_align), AV_OPT_TYPE_CONST, {.i64=YA_FONT},     0, 0, FLAGS, "y_align"},
 
     {"timecode",        "set initial timecode",             OFFSET(tc_opt_string), AV_OPT_TYPE_STRING,   {.str=NULL}, 0, 0, FLAGS},
-    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},           0,        1, FLAGS},
-    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"reload",     "reload text file at specified frame interval", OFFSET(reload),     AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
-    { "alpha",       "apply alpha while rendering", OFFSET(a_expr),      AV_OPT_TYPE_STRING, { .str = "1"     },          .flags = FLAGS },
-    {"fix_bounds", "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
-    {"start_number", "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
-    {"text_source", "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
+    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},    0, 1, FLAGS},
+    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"reload",          "reload text file at specified frame interval", OFFSET(reload), AV_OPT_TYPE_INT, {.i64=0},    0, INT_MAX, FLAGS},
+    {"alpha",           "apply alpha while rendering",      OFFSET(a_expr),        AV_OPT_TYPE_STRING,   {.str = "1"}, .flags = TFLAGS},
+    {"fix_bounds",      "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
+    {"start_number",    "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
+    {"text_source",     "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
 
 #if CONFIG_LIBFRIBIDI
     {"text_shaping", "attempt to shape text before drawing", OFFSET(text_shaping), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS},
@@ -297,18 +427,24 @@ static const struct ft_error {
 
 #define FT_ERRMSG(e) ft_errors[e].err_msg
 
-typedef struct Glyph {
-    FT_Glyph glyph;
-    FT_Glyph border_glyph;
-    uint32_t code;
-    unsigned int fontsize;
-    FT_Bitmap bitmap; ///< array holding bitmaps of font
-    FT_Bitmap border_bitmap; ///< array holding bitmaps of font border
-    FT_BBox bbox;
-    int advance;
-    int bitmap_left;
-    int bitmap_top;
-} Glyph;
+
+// Loads and (optionally) renders a glyph
+static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code,
+     int8_t shift_x64, int8_t shift_y64);
+
+// Shapes a line of text using libharfbuzz
+static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const char* text, int textLen);
+
+// Performs text measurements
+static int measure_text(AVFilterContext *ctx, TextMetrics *metrics);
+
+// Draws glyphs on the frame
+static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
+                       FFDrawColor *color, TextMetrics *metrics,
+                       int x, int y, int borderw);
+
+// Draws text on the frame
+static int draw_text(AVFilterContext *ctx, AVFrame *frame);
 
 static int glyph_cmp(const void *key, const void *b)
 {
@@ -316,80 +452,9 @@ static int glyph_cmp(const void *key, const void *b)
     int64_t diff = (int64_t)a->code - (int64_t)bb->code;
 
     if (diff != 0)
-         return diff > 0 ? 1 : -1;
+        return diff > 0 ? 1 : -1;
     else
-         return FFDIFFSIGN((int64_t)a->fontsize, (int64_t)bb->fontsize);
-}
-
-/**
- * Load glyphs corresponding to the UTF-32 codepoint code.
- */
-static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code)
-{
-    DrawTextContext *s = ctx->priv;
-    FT_BitmapGlyph bitmapglyph;
-    Glyph *glyph;
-    struct AVTreeNode *node = NULL;
-    int ret;
-
-    /* load glyph into s->face->glyph */
-    if (FT_Load_Char(s->face, code, s->ft_load_flags))
-        return AVERROR(EINVAL);
-
-    glyph = av_mallocz(sizeof(*glyph));
-    if (!glyph) {
-        ret = AVERROR(ENOMEM);
-        goto error;
-    }
-    glyph->code  = code;
-    glyph->fontsize = s->fontsize;
-
-    if (FT_Get_Glyph(s->face->glyph, &glyph->glyph)) {
-        ret = AVERROR(EINVAL);
-        goto error;
-    }
-    if (s->borderw) {
-        glyph->border_glyph = glyph->glyph;
-        if (FT_Glyph_StrokeBorder(&glyph->border_glyph, s->stroker, 0, 0) ||
-            FT_Glyph_To_Bitmap(&glyph->border_glyph, FT_RENDER_MODE_NORMAL, 0, 1)) {
-            ret = AVERROR_EXTERNAL;
-            goto error;
-        }
-        bitmapglyph = (FT_BitmapGlyph) glyph->border_glyph;
-        glyph->border_bitmap = bitmapglyph->bitmap;
-    }
-    if (FT_Glyph_To_Bitmap(&glyph->glyph, FT_RENDER_MODE_NORMAL, 0, 1)) {
-        ret = AVERROR_EXTERNAL;
-        goto error;
-    }
-    bitmapglyph = (FT_BitmapGlyph) glyph->glyph;
-
-    glyph->bitmap      = bitmapglyph->bitmap;
-    glyph->bitmap_left = bitmapglyph->left;
-    glyph->bitmap_top  = bitmapglyph->top;
-    glyph->advance     = s->face->glyph->advance.x >> 6;
-
-    /* measure text height to calculate text_height (or the maximum text height) */
-    FT_Glyph_Get_CBox(glyph->glyph, ft_glyph_bbox_pixels, &glyph->bbox);
-
-    /* cache the newly created glyph */
-    if (!(node = av_tree_node_alloc())) {
-        ret = AVERROR(ENOMEM);
-        goto error;
-    }
-    av_tree_insert(&s->glyphs, glyph, glyph_cmp, &node);
-
-    if (glyph_ptr)
-        *glyph_ptr = glyph;
-    return 0;
-
-error:
-    if (glyph)
-        av_freep(&glyph->glyph);
-
-    av_freep(&glyph);
-    av_freep(&node);
-    return ret;
+        return FFDIFFSIGN((int64_t)a->fontsize, (int64_t)bb->fontsize);
 }
 
 static av_cold int set_fontsize(AVFilterContext *ctx, unsigned int fontsize)
@@ -439,7 +504,6 @@ static av_cold int update_fontsize(AVFilterContext *ctx)
            return err;
 
         size = av_expr_eval(s->fontsize_pexpr, s->var_values, &s->prng);
-
         if (!isnan(size)) {
             roundedsize = round(size);
             // test for overflow before cast
@@ -447,7 +511,6 @@ static av_cold int update_fontsize(AVFilterContext *ctx)
                 av_log(ctx, AV_LOG_ERROR, "fontsize overflow\n");
                 return AVERROR(EINVAL);
             }
-
             fontsize = roundedsize;
         }
     }
@@ -548,7 +611,7 @@ static int load_font_fontconfig(AVFilterContext *ctx)
         goto fail;
     }
 
-    av_log(ctx, AV_LOG_INFO, "Using \"%s\"\n", filename);
+    av_log(ctx, AV_LOG_VERBOSE, "Using \"%s\"\n", filename);
     if (parse_err)
         s->default_fontsize = size + 0.5;
 
@@ -690,6 +753,7 @@ static int shape_text(AVFilterContext *ctx)
     s->text = tmp;
     len = fribidi_unicode_to_charset(FRIBIDI_CHAR_SET_UTF8,
                                      unicodestr, len, s->text);
+
     ret = 0;
 
 out:
@@ -711,11 +775,38 @@ static enum AVFrameSideDataType text_source_string_parse(const char *text_source
     }
 }
 
+// Convert a string formatted as "n1|n2|...|nN" into an integer array
+static int string_to_array(const char* source, int* result, int result_size)
+{
+    int counter = 0, size = strlen(source) + 1;
+    char *saveptr, *curval, *dup = av_malloc(size);
+    av_strlcpy(dup, source, size);
+    if(result_size > 0 && (curval = av_strtok(dup, "|", &saveptr))) {
+        do {
+            result[counter++] = atoi(curval);
+        } while((curval = av_strtok(NULL, "|", &saveptr)) && counter < result_size);
+    }
+    av_free(dup);
+    return counter;
+}
+
+static int validate_text_align(char* text_align)
+{
+    int err = 0;
+    if(strlen(text_align) != 2
+        || strchr("LCRTMB", text_align[0]) == NULL || strchr("LCRTMB", text_align[1]) == NULL
+        || (strchr("TMB", text_align[0]) != NULL && strchr("LCR", text_align[1]) == NULL)
+        || (strchr("LCR", text_align[0]) != NULL && strchr("TMB", text_align[1]) == NULL)) {
+        err = AVERROR(EINVAL);
+    }
+
+    return err;
+}
+
 static av_cold int init(AVFilterContext *ctx)
 {
     int err;
     DrawTextContext *s = ctx->priv;
-    Glyph *glyph;
 
     av_expr_free(s->fontsize_pexpr);
     s->fontsize_pexpr = NULL;
@@ -777,6 +868,14 @@ static av_cold int init(AVFilterContext *ctx)
         return AVERROR(EINVAL);
     }
 
+    if((err = validate_text_align(s->text_align))) {
+        av_log(ctx, AV_LOG_ERROR,
+               "The value provided for parameter 'text_align' is not valid,\n");
+        av_log(ctx, AV_LOG_ERROR,
+               "please specify a two characters string containing only one letter for horizontal alignment ('LCR') and one for vertical alignment ('TMB')\n");
+        return err;
+    }
+
 #if CONFIG_LIBFRIBIDI
     if (s->text_shaping)
         if ((err = shape_text(ctx)) < 0)
@@ -795,26 +894,19 @@ static av_cold int init(AVFilterContext *ctx)
     if ((err = update_fontsize(ctx)) < 0)
         return err;
 
+    // Always init the stroker, may be needed if borderw is set via command
+    if (FT_Stroker_New(s->library, &s->stroker)) {
+        av_log(ctx, AV_LOG_ERROR, "Coult not init FT stroker\n");
+        return AVERROR_EXTERNAL;
+    }
+
     if (s->borderw) {
-        if (FT_Stroker_New(s->library, &s->stroker)) {
-            av_log(ctx, AV_LOG_ERROR, "Coult not init FT stroker\n");
-            return AVERROR_EXTERNAL;
-        }
         FT_Stroker_Set(s->stroker, s->borderw << 6, FT_STROKER_LINECAP_ROUND,
                        FT_STROKER_LINEJOIN_ROUND, 0);
     }
 
-    s->use_kerning = FT_HAS_KERNING(s->face);
-
     /* load the fallback glyph with code 0 */
-    load_glyph(ctx, NULL, 0);
-
-    /* set the tabsize in pixels */
-    if ((err = load_glyph(ctx, &glyph, ' ')) < 0) {
-        av_log(ctx, AV_LOG_ERROR, "Could not set tabsize.\n");
-        return err;
-    }
-    s->tabsize *= glyph->advance;
+    load_glyph(ctx, NULL, 0, 0, 0);
 
     if (s->exp_mode == EXP_STRFTIME &&
         (strchr(s->text, '%') || strchr(s->text, '\\')))
@@ -831,12 +923,37 @@ static int query_formats(AVFilterContext *ctx)
     return ff_set_common_formats(ctx, ff_draw_supported_pixel_formats(0));
 }
 
+static int glyph_enu_border_free(void *opaque, void *elem)
+{
+    Glyph *glyph = elem;
+
+    if(glyph->border_glyph != NULL) {
+        for(int t = 0; t < 16; ++t) {
+            if(glyph->border_bglyph[t] != NULL) {
+                FT_Done_Glyph((FT_Glyph)glyph->border_bglyph[t]);
+                glyph->border_bglyph[t] = NULL;
+            }
+        }
+        FT_Done_Glyph(glyph->border_glyph);
+        glyph->border_glyph = NULL;
+    }
+    return 0;
+}
+
 static int glyph_enu_free(void *opaque, void *elem)
 {
     Glyph *glyph = elem;
 
     FT_Done_Glyph(glyph->glyph);
     FT_Done_Glyph(glyph->border_glyph);
+    for(int t = 0; t < 16; ++t) {
+        if(glyph->bglyph[t] != NULL) {
+            FT_Done_Glyph((FT_Glyph)glyph->bglyph[t]);
+        }
+        if(glyph->border_bglyph[t] != NULL) {
+            FT_Done_Glyph((FT_Glyph)glyph->border_bglyph[t]);
+        }
+    }
     av_free(elem);
     return 0;
 }
@@ -852,9 +969,6 @@ static av_cold void uninit(AVFilterContext *ctx)
 
     s->x_pexpr = s->y_pexpr = s->a_pexpr = s->fontsize_pexpr = NULL;
 
-    av_freep(&s->positions);
-    s->nb_positions = 0;
-
     av_tree_enumerate(s->glyphs, NULL, NULL, glyph_enu_free);
     av_tree_destroy(s->glyphs);
     s->glyphs = NULL;
@@ -880,15 +994,15 @@ static int config_input(AVFilterLink *inlink)
     ff_draw_color(&s->dc, &s->bordercolor, s->bordercolor.rgba);
     ff_draw_color(&s->dc, &s->boxcolor,    s->boxcolor.rgba);
 
-    s->var_values[VAR_w]     = s->var_values[VAR_W]     = s->var_values[VAR_MAIN_W] = inlink->w;
-    s->var_values[VAR_h]     = s->var_values[VAR_H]     = s->var_values[VAR_MAIN_H] = inlink->h;
-    s->var_values[VAR_SAR]   = inlink->sample_aspect_ratio.num ? av_q2d(inlink->sample_aspect_ratio) : 1;
-    s->var_values[VAR_DAR]   = (double)inlink->w / inlink->h * s->var_values[VAR_SAR];
-    s->var_values[VAR_HSUB]  = 1 << s->dc.hsub_max;
-    s->var_values[VAR_VSUB]  = 1 << s->dc.vsub_max;
-    s->var_values[VAR_X]     = NAN;
-    s->var_values[VAR_Y]     = NAN;
-    s->var_values[VAR_T]     = NAN;
+    s->var_values[VAR_w]    = s->var_values[VAR_W] = s->var_values[VAR_MAIN_W] = inlink->w;
+    s->var_values[VAR_h]    = s->var_values[VAR_H] = s->var_values[VAR_MAIN_H] = inlink->h;
+    s->var_values[VAR_SAR]  = inlink->sample_aspect_ratio.num ? av_q2d(inlink->sample_aspect_ratio) : 1;
+    s->var_values[VAR_DAR]  = (double)inlink->w / inlink->h * s->var_values[VAR_SAR];
+    s->var_values[VAR_HSUB] = 1 << s->dc.hsub_max;
+    s->var_values[VAR_VSUB] = 1 << s->dc.vsub_max;
+    s->var_values[VAR_X]    = NAN;
+    s->var_values[VAR_Y]    = NAN;
+    s->var_values[VAR_T]    = NAN;
 
     av_lfg_init(&s->prng, av_get_random_seed());
 
@@ -948,8 +1062,28 @@ static int command(AVFilterContext *ctx, const char *cmd, const char *arg, char
 
         ctx->priv = new;
         return config_input(ctx->inputs[0]);
-    } else
-        return AVERROR(ENOSYS);
+    } else {
+//        av_log(ctx, AV_LOG_DEBUG, "Command '%s' '%s'\n", cmd, arg);
+        int old_borderw = old->borderw;
+        if ((ret = ff_filter_process_command(ctx, cmd, arg, res, res_len, flags)) < 0) {
+            return ret;
+        }
+        if(old->borderw != old_borderw) {
+            FT_Stroker_Set(old->stroker, old->borderw << 6, FT_STROKER_LINECAP_ROUND,
+                        FT_STROKER_LINEJOIN_ROUND, 0);
+            // Dispose the old border glyphs
+            av_tree_enumerate(old->glyphs, NULL, NULL, glyph_enu_border_free);                            
+        } else if(strcmp(cmd, "text_align") == 0) {
+            if(validate_text_align(old->text_align) != 0) {
+                av_log(ctx, AV_LOG_ERROR,
+                    "Invalid command value '%s' for 'text_align'\n", old->text_align);
+            }
+        } else if(strcmp(cmd, "fontsize") == 0) {
+            av_expr_free(old->fontsize_pexpr);
+            old->fontsize_pexpr = NULL;
+        }
+        return config_input(ctx->inputs[0]);
+    }
 
 fail:
     av_log(ctx, AV_LOG_ERROR, "Failed to process command. Continuing with existing parameters.\n");
@@ -1318,91 +1452,426 @@ static int expand_text(AVFilterContext *ctx, char *text, AVBPrint *bp)
     return 0;
 }
 
+static void update_color_with_alpha(DrawTextContext *s, FFDrawColor *color, const FFDrawColor incolor)
+{
+    *color = incolor;
+    color->rgba[3] = (color->rgba[3] * s->alpha) / 255;
+    ff_draw_color(&s->dc, color, color->rgba);
+}
+
+static void update_alpha(DrawTextContext *s)
+{
+    double alpha = av_expr_eval(s->a_pexpr, s->var_values, &s->prng);
+
+    if (isnan(alpha))
+        return;
+
+    if (alpha >= 1.0)
+        s->alpha = 255;
+    else if (alpha <= 0)
+        s->alpha = 0;
+    else
+        s->alpha = 256 * alpha;
+}
+
+static inline int get_subpixel_idx(int shift_x64, int shift_y64)
+{
+    int idx = (shift_x64 >> 2) + (shift_y64 >> 4);
+    return idx;
+}
+
+static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code, int8_t shift_x64, int8_t shift_y64)
+{
+    DrawTextContext *s = ctx->priv;
+    Glyph dummy = { 0 };
+    Glyph *glyph;
+    FT_Vector shift;
+    struct AVTreeNode *node = NULL;
+    int ret = 0;
+
+    /* get glyph */
+    dummy.code = code;
+    dummy.fontsize = s->fontsize;
+    glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
+    if(!glyph) {
+//        av_log(ctx, AV_LOG_DEBUG, "Glyph: %d not cached -> loading...\n", code);
+        if (FT_Load_Glyph(s->face, code, s->ft_load_flags)) {
+            return AVERROR(EINVAL);
+        }
+        glyph = av_mallocz(sizeof(*glyph));
+        if (!glyph) {
+            ret = AVERROR(ENOMEM);
+            goto error;
+        }
+        glyph->code  = code;
+        glyph->fontsize = s->fontsize;
+        if (FT_Get_Glyph(s->face->glyph, &glyph->glyph)) {
+            ret = AVERROR(EINVAL);
+            goto error;
+        }
+        if (s->borderw) {
+            glyph->border_glyph = glyph->glyph;
+            if (FT_Glyph_StrokeBorder(&glyph->border_glyph, s->stroker, 0, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+        }
+        /* measure text height to calculate text_height (or the maximum text height) */
+        FT_Glyph_Get_CBox(glyph->glyph, FT_GLYPH_BBOX_SUBPIXELS, &glyph->bbox);
+
+        /* cache the newly created glyph */
+        if (!(node = av_tree_node_alloc())) {
+            ret = AVERROR(ENOMEM);
+            goto error;
+        }
+        av_tree_insert(&s->glyphs, glyph, glyph_cmp, &node);
+    } else {
+        if(s->borderw && !glyph->border_glyph) {
+            glyph->border_glyph = glyph->glyph;
+            if (FT_Glyph_StrokeBorder(&glyph->border_glyph, s->stroker, 0, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+        }
+    }
+
+    // Check if a bitmap is needed
+    if(shift_x64 >= 0 && shift_y64 >= 0) {
+        // Get the bitmap subpixel index (0 -> 15)
+        int idx = get_subpixel_idx(shift_x64, shift_y64);
+        shift.x = shift_x64;
+        shift.y = shift_y64;
+
+        if(!glyph->bglyph[idx]) {
+            FT_Glyph tmp_glyph = glyph->glyph;
+            // av_log(ctx, AV_LOG_DEBUG, "Rendering bitmap [%d] for glyph: %d\n", idx, code);
+            if(FT_Glyph_To_Bitmap(&tmp_glyph, FT_RENDER_MODE_NORMAL, &shift, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+            glyph->bglyph[idx] = (FT_BitmapGlyph)tmp_glyph;
+            if(glyph->bglyph[idx]->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) {
+                av_log(ctx, AV_LOG_ERROR, "Monocromatic (1bpp) fonts are not supported.\n");
+                ret = AVERROR(EINVAL);
+                goto error;
+            }
+        }
+        if (s->borderw && !glyph->border_bglyph[idx]) {
+            FT_Glyph tmp_glyph = glyph->border_glyph;
+            // av_log(ctx, AV_LOG_DEBUG, "Rendering border bitmap [%d] for glyph: %d\n", idx, code);
+            if(FT_Glyph_To_Bitmap(&tmp_glyph, FT_RENDER_MODE_NORMAL, &shift, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+            glyph->border_bglyph[idx] = (FT_BitmapGlyph)tmp_glyph;
+        }
+    }
+    if(glyph_ptr) {
+        *glyph_ptr = glyph;
+    }
+    return 0;
+
+error:
+    if (glyph && glyph->glyph)
+        FT_Done_Glyph(glyph->glyph);
+
+    av_freep(&glyph);
+    av_freep(&node);
+    return ret;
+}
+
 static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
-                       int width, int height,
                        FFDrawColor *color,
+                       TextMetrics *metrics,
                        int x, int y, int borderw)
 {
-    char *text = s->expanded_text.str;
-    uint32_t code = 0;
-    int i, x1, y1;
-    uint8_t *p;
-    Glyph *glyph = NULL;
+    int g, l, x1, y1, w1, h1, idx;
+    int dx = 0, dy = 0, pdx = 0;
+    GlyphInfo *info;
+    Glyph dummy = { 0 }, *glyph;
+    FT_Bitmap bitmap;
+    FT_BitmapGlyph b_glyph;
+    uint8_t j_center = 0, j_right = 0, j_middle = 0, j_bottom = 0;
+    int line_w, offset_y = 0;
+    int clip_x = 0, clip_y = 0;
+
+    j_center = strstr(s->text_align, "C") > 0;
+    j_right = strstr(s->text_align, "R") > 0;
+    j_middle = strstr(s->text_align, "M") > 0;
+    j_bottom = strstr(s->text_align, "B") > 0;
+    // av_log(s, AV_LOG_DEBUG, "Outer rectangle - w: %d, h: %d\n", s->box_width, s->box_height);
+    // av_log(s, AV_LOG_DEBUG, "Text position: %s\n", s->text_align);
+
+    if(j_middle) {
+        offset_y = (s->box_height - metrics->height) / 2;
+    } else if(j_bottom) {
+        offset_y = s->box_height - metrics->height;
+    }
 
-    for (i = 0, p = text; *p; i++) {
-        FT_Bitmap bitmap;
-        Glyph dummy = { 0 };
-        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_invalid;);
-continue_on_invalid:
+    clip_x = FFMIN(metrics->rect_x + s->box_width + s->bb_right, frame->width);
+    clip_y = FFMIN(metrics->rect_y + s->box_height + s->bb_bottom, frame->height);
+
+    // av_log(s, AV_LOG_DEBUG, "Drawing text at (%d, %d) (clip_x: %d, clip_y: %d)\n",
+    //     x, y, clip_x, clip_y);
+
+    for(l = 0; l < s->line_count; ++l) {
+        TextLine *line = &s->lines[l];
+        line_w = POS_CEIL(line->width64, 64);
+        for(g = 0; g < line->hb_data.glyph_count; ++g) {
+            info = &line->glyphs[g];
+            dummy.fontsize = s->fontsize;
+            dummy.code = info->code;
+            glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
+            if(!glyph) {
+                return AVERROR(EINVAL);
+            }
 
-        /* skip new line chars, just go to new line */
-        if (code == '\n' || code == '\r' || code == '\t')
-            continue;
+            idx = get_subpixel_idx(info->shift_x64, info->shift_y64);
+            b_glyph = borderw ? glyph->border_bglyph[idx] : glyph->bglyph[idx];
+            bitmap = b_glyph->bitmap;
+            x1 = x + info->x + b_glyph->left;
+            y1 = y + info->y - b_glyph->top + offset_y;
+            w1 = bitmap.width;
+            h1 = bitmap.rows;
+
+            if(j_center) {
+                x1 += (s->box_width - line_w) / 2;
+            } else if(j_right) {
+                x1 += s->box_width - line_w;
+            }
 
-        dummy.code = code;
-        dummy.fontsize = s->fontsize;
-        glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
+            // Offset of the glyph's bitmap in the visible region
+            dx = dy = 0;
+            if(x1 < metrics->rect_x - s->bb_left) {
+                dx = metrics->rect_x - s->bb_left - x1;
+                x1 = metrics->rect_x - s->bb_left;
+            }
+            if(y1 < metrics->rect_y - s->bb_top) {
+                dy = metrics->rect_y - s->bb_top - y1;
+                y1 = metrics->rect_y - s->bb_top;
+            }
 
-        bitmap = borderw ? glyph->border_bitmap : glyph->bitmap;
+            // check if the glyph is empty or out of the clipping region
+            if(dx >= w1 || dy >= h1 || x1 >= clip_x || y1 >= clip_y) {
+                // av_log(s, AV_LOG_DEBUG, "Glyph (code: %d - line: %d - glyph: %d - dx: %d - wx: %d) is empty or out of the clipping region\n",
+                //     info->code, l, g, dx, w1);
+                // av_log(s, AV_LOG_DEBUG, "Glyph %d -- dx: %d, dy: %d, x1: %d, y1: %d, w1: %d, h1: %d\n",
+                //     info->code, dx, dy, x1, y1, w1, h1);
+                continue;
+            }
 
-        if (glyph->bitmap.pixel_mode != FT_PIXEL_MODE_MONO &&
-            glyph->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY)
-            return AVERROR(EINVAL);
+            pdx = dx + dy * bitmap.pitch;
+            w1 = FFMIN(clip_x - x1, w1 - dx);
+            h1 = FFMIN(clip_y - y1, h1 - dy);
 
-        x1 = s->positions[i].x+s->x+x - borderw;
-        y1 = s->positions[i].y+s->y+y - borderw;
+            // av_log(s, AV_LOG_DEBUG, "Drawing glyph %d[%d] (line: %d num: %d) at (%d, %d) (info.x: %d, info.y: %d)\n",
+            //     info->code, idx, l, g, x1, y1, info->x, info->y);
 
-        ff_blend_mask(&s->dc, color,
-                      frame->data, frame->linesize, width, height,
-                      bitmap.buffer, bitmap.pitch,
-                      bitmap.width, bitmap.rows,
-                      bitmap.pixel_mode == FT_PIXEL_MODE_MONO ? 0 : 3,
-                      0, x1, y1);
+            ff_blend_mask(&s->dc, color, frame->data, frame->linesize, clip_x, clip_y,
+                bitmap.buffer + pdx, bitmap.pitch, w1, h1, 3, 0, x1, y1);
+        }
     }
 
     return 0;
 }
 
+static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const char* text, int textLen) 
+{
+    hb->buf = hb_buffer_create();
+    hb_buffer_set_direction(hb->buf, HB_DIRECTION_LTR);
+    hb_buffer_set_script(hb->buf, HB_SCRIPT_LATIN);
+    hb_buffer_set_language(hb->buf, hb_language_from_string("en", -1));
+    hb_buffer_guess_segment_properties(hb->buf);
+    hb->font = hb_ft_font_create(s->face, NULL);
+    hb_ft_font_set_funcs(hb->font);
+    hb_buffer_add_utf8(hb->buf, text, textLen, 0, -1);
+    hb_shape(hb->font, hb->buf, NULL, 0);
+    hb->glyph_info = hb_buffer_get_glyph_infos(hb->buf, &hb->glyph_count);
+    hb->glyph_pos = hb_buffer_get_glyph_positions(hb->buf, &hb->glyph_count);
+}
 
-static void update_color_with_alpha(DrawTextContext *s, FFDrawColor *color, const FFDrawColor incolor)
+static void hb_destroy(HarfbuzzData *hb) 
 {
-    *color = incolor;
-    color->rgba[3] = (color->rgba[3] * s->alpha) / 255;
-    ff_draw_color(&s->dc, color, color->rgba);
+    hb_buffer_destroy(hb->buf);
+    hb_font_destroy(hb->font);
+    hb->buf = NULL;
+    hb->font = NULL;
+    hb->glyph_info = NULL;
+    hb->glyph_pos = NULL;
 }
 
-static void update_alpha(DrawTextContext *s)
+static int measure_text(AVFilterContext *ctx, TextMetrics *metrics)
 {
-    double alpha = av_expr_eval(s->a_pexpr, s->var_values, &s->prng);
+    DrawTextContext *s = ctx->priv;
+    char* text = s->expanded_text.str;
+    char *textdup = av_strdup(text), *start = textdup;
+    int num_chars = 0;
+    int width64 = 0, w64 = 0;
+    int cur_min_y64 = 0, first_max_y64 = -32000;
+    int first_min_x64 = 32000, last_max_x64 = -32000;
+    int min_y64 = 32000, max_y64 = -32000, min_x64 = 32000, max_x64 = -32000;
+    int line_count = 0;
+    uint32_t code = 0;
+    Glyph *glyph = NULL;
 
-    if (isnan(alpha))
-        return;
+    int i, tab_idx = 0, last_tab_idx = 0, line_offset = 0;
+    char* p;
+    int ret = 0;
+
+    // Count the lines and the tab characters
+    s->tab_count = 0;
+    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) {
+            ++line_count;
+            if(code == 0) {
+                break;
+            }
+        } else if(code == '\t') {
+            ++s->tab_count;
+        }
+    }
 
-    if (alpha >= 1.0)
-        s->alpha = 255;
-    else if (alpha <= 0)
-        s->alpha = 0;
-    else
-        s->alpha = 256 * alpha;
+    // Evaluate the width of the space character if needed to replace tabs
+    if(s->tab_count > 0 && !s->blank_advance64) {
+        HarfbuzzData hb_data;
+        shape_text_hb(s, &hb_data, " ", 1);
+        s->blank_advance64 = hb_data.glyph_pos[0].x_advance;
+        hb_destroy(&hb_data);
+    }
+
+    s->line_count = line_count;
+    s->lines = av_mallocz(line_count * sizeof(TextLine));
+    s->tab_clusters = av_mallocz(s->tab_count * sizeof(uint32_t));
+    for(i = 0; i < s->tab_count; ++i) {
+        s->tab_clusters[i] = -1;
+    }
+
+    // av_log(s, AV_LOG_DEBUG, "Starting text measurement...\n");
+    line_count = 0;
+    for (i = 0, p = textdup; 1; i++) {
+        if(*p == '\t') {
+            s->tab_clusters[tab_idx++] = i;
+            *p = ' ';
+        }
+        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_failed2;);
+continue_on_failed2:
+        if(is_newline(code) || code == 0) {
+            TextLine *cur_line = &s->lines[line_count];
+            HarfbuzzData *hb = &cur_line->hb_data;
+            cur_line->cluster_offset = line_offset;
+            shape_text_hb(s, hb, start, num_chars);
+            w64 = 0;
+            cur_min_y64 = 32000;
+            for(int t = 0; t < hb->glyph_count; ++t) {
+                uint8_t is_tab = last_tab_idx < s->tab_count &&
+                    hb->glyph_info[t].cluster == s->tab_clusters[last_tab_idx] - line_offset;
+                if(is_tab) {
+                    ++last_tab_idx;
+                }
+                ret = load_glyph(ctx, &glyph, hb->glyph_info[t].codepoint, -1, -1);
+                if(ret != 0) {
+                    break;
+                }
+                if(line_count == 0) {
+                    first_max_y64 = FFMAX(glyph->bbox.yMax, first_max_y64);
+                }
+                if(t == 0) {
+                    // TODO (OFFSET LEFT)
+                    // w64 += glyph->bbox.xMin;
+                    cur_line->offset_left64 = glyph->bbox.xMin;
+                    first_min_x64 = FFMIN(glyph->bbox.xMin, first_min_x64);
+                }
+                if(t == hb->glyph_count - 1) {
+                    w64 += glyph->bbox.xMax;
+                    last_max_x64 = FFMAX(glyph->bbox.xMax, last_max_x64);
+                    cur_line->offset_right64 = glyph->bbox.xMax;
+                } else {
+                    if(is_tab) {
+                        int size = s->blank_advance64 * s->tabsize;
+                        w64 = (w64 / size + 1) * size;
+                    } else {
+                        w64 += hb->glyph_pos[t].x_advance;
+                    }
+                }
+                cur_min_y64 = FFMIN(glyph->bbox.yMin, cur_min_y64);
+                min_y64 = FFMIN(glyph->bbox.yMin, min_y64);
+                max_y64 = FFMAX(glyph->bbox.yMax, max_y64);
+                min_x64 = FFMIN(glyph->bbox.xMin, min_x64);
+                max_x64 = FFMAX(glyph->bbox.xMax, max_x64);
+
+                // av_log(s, AV_LOG_DEBUG, "    Glyph: %d -- yMin: %ld -- yMax: %ld -- xMin: %ld -- xMax: %ld\n",
+                //     hb->glyph_info[t].codepoint, glyph->bbox.yMin, glyph->bbox.yMax, glyph->bbox.xMin, glyph->bbox.xMax);
+                // av_log(s, AV_LOG_DEBUG, "      min_y64: %d -- max_y64: %d -- min_x64: %d -- max_x64: %d\n",
+                //     min_y64, max_y64, min_x64, max_x64);
+            }
+
+            if(ret == 0) {
+                // TODO (OFFSET LEFT)
+                // cur_line->width64 = w64 - cur_line->offset_left64;
+                cur_line->width64 = w64;
+
+                av_log(s, AV_LOG_DEBUG, "  Line: %d -- glyphs count: %d - width64: %d - offset_left64: %d - offset_right64: %d)\n",
+                    line_count, hb->glyph_count, cur_line->width64, cur_line->offset_left64, cur_line->offset_right64);
+
+                if(w64 > width64) {
+                    width64 = w64;
+                }
+                num_chars = -1;
+                start = p;
+                ++line_count;
+                line_offset = i + 1;
+            }
+        }
+
+        if(code == 0 || ret != 0) break;
+        ++num_chars;
+    }
+
+    if(ret == 0) {
+        metrics->line_height64 = s->face->size->metrics.height;
+        
+        // TODO (LEFT OFFSET) 
+        // metrics->width = POS_CEIL(width64 - first_min_x64, 64);
+        metrics->width = POS_CEIL(width64, 64);
+        if(s->y_align == YA_FONT) {
+            metrics->height = POS_CEIL(metrics->line_height64 * line_count, 64);
+        } else {
+            int height64 = (metrics->line_height64 + s->line_spacing * 64) *
+                (FFMAX(0, line_count - 1)) + first_max_y64 - cur_min_y64;
+            metrics->height = POS_CEIL(height64, 64);
+        }
+        metrics->offset_top64 = first_max_y64;
+        metrics->offset_right64 = last_max_x64;
+        metrics->offset_bottom64 = cur_min_y64;
+        metrics->offset_left64 = first_min_x64;
+        metrics->min_x64 = min_x64;
+        metrics->min_y64 = min_y64;
+        metrics->max_x64 = max_x64;
+        metrics->max_y64 = max_y64;
+
+        // av_log(s, AV_LOG_DEBUG, "  Text: width: %d | height: %d\n", metrics->width, metrics->height);
+        // av_log(s, AV_LOG_DEBUG, "      off_t64: %d | off_r64: %d | off_b64: %d | off_l64: %d\n",
+        //     metrics->offset_top64, metrics->offset_right64, metrics->offset_bottom64, metrics->offset_left64);
+        // av_log(s, AV_LOG_DEBUG, "      min_x64: %d | min_y64: %d | max_x64: %d | max_y64: %d\n",
+        //     metrics->min_x64, metrics->min_y64, metrics->max_x64, metrics->max_y64);
+        // av_log(s, AV_LOG_DEBUG, "Text measurement completed\n");
+    }
+
+    av_free(textdup);
+    return ret;
 }
 
-static int draw_text(AVFilterContext *ctx, AVFrame *frame,
-                     int width, int height)
+static int draw_text(AVFilterContext *ctx, AVFrame *frame)
 {
     DrawTextContext *s = ctx->priv;
     AVFilterLink *inlink = ctx->inputs[0];
-
-    uint32_t code = 0, prev_code = 0;
-    int x = 0, y = 0, i = 0, ret;
-    int max_text_line_w = 0, len;
-    int box_w, box_h;
-    char *text;
-    uint8_t *p;
-    int y_min = 32000, y_max = -32000;
-    int x_min = 32000, x_max = -32000;
-    FT_Vector delta;
-    Glyph *glyph = NULL, *prev_glyph = NULL;
-    Glyph dummy = { 0 };
+    int x = 0, y = 0, ret;
+    int shift_x64, shift_y64;
+    int x64, y64;
+    int offset_left = 0;
+    Glyph *glyph = NULL;
 
     time_t now = time(0);
     struct tm ltime;
@@ -1413,6 +1882,17 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame,
     FFDrawColor bordercolor;
     FFDrawColor boxcolor;
 
+    int width = frame->width;
+    int height = frame->height;
+    int rec_x = 0, rec_y = 0, rec_width = 0, rec_height = 0;
+    int is_outside = 0;
+    int last_tab_idx = 0;
+
+    TextMetrics metrics;
+
+    // av_log(s, AV_LOG_DEBUG, "ascend: %ld descent: %ld height: %ld\n",
+    //     s->face->size->metrics.ascender, s->face->size->metrics.descender, s->face->size->metrics.height);
+
     av_bprint_clear(bp);
 
     if(s->basetime != AV_NOPTS_VALUE)
@@ -1441,13 +1921,6 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame,
 
     if (!av_bprint_is_complete(bp))
         return AVERROR(ENOMEM);
-    text = s->expanded_text.str;
-    if ((len = s->expanded_text.len) > s->nb_positions) {
-        if (!(s->positions =
-              av_realloc(s->positions, len*sizeof(*s->positions))))
-            return AVERROR(ENOMEM);
-        s->nb_positions = len;
-    }
 
     if (s->fontcolor_expr[0]) {
         /* If expression is set, evaluate and replace the static value */
@@ -1463,85 +1936,28 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame,
         ff_draw_color(&s->dc, &s->fontcolor, s->fontcolor.rgba);
     }
 
-    x = 0;
-    y = 0;
-
-    if ((ret = update_fontsize(ctx)) < 0)
+    if ((ret = update_fontsize(ctx)) < 0) {
         return ret;
-
-    /* load and cache glyphs */
-    for (i = 0, p = text; *p; i++) {
-        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_invalid;);
-continue_on_invalid:
-
-        /* get glyph */
-        dummy.code = code;
-        dummy.fontsize = s->fontsize;
-        glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
-        if (!glyph) {
-            ret = load_glyph(ctx, &glyph, code);
-            if (ret < 0)
-                return ret;
-        }
-
-        y_min = FFMIN(glyph->bbox.yMin, y_min);
-        y_max = FFMAX(glyph->bbox.yMax, y_max);
-        x_min = FFMIN(glyph->bbox.xMin, x_min);
-        x_max = FFMAX(glyph->bbox.xMax, x_max);
     }
-    s->max_glyph_h = y_max - y_min;
-    s->max_glyph_w = x_max - x_min;
-
-    /* compute and save position for each glyph */
-    glyph = NULL;
-    for (i = 0, p = text; *p; i++) {
-        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_invalid2;);
-continue_on_invalid2:
 
-        /* skip the \n in the sequence \r\n */
-        if (prev_code == '\r' && code == '\n')
-            continue;
-
-        prev_code = code;
-        if (is_newline(code)) {
-
-            max_text_line_w = FFMAX(max_text_line_w, x);
-            y += s->max_glyph_h + s->line_spacing;
-            x = 0;
-            continue;
-        }
-
-        /* get glyph */
-        prev_glyph = glyph;
-        dummy.code = code;
-        dummy.fontsize = s->fontsize;
-        glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
-
-        /* kerning */
-        if (s->use_kerning && prev_glyph && glyph->code) {
-            FT_Get_Kerning(s->face, prev_glyph->code, glyph->code,
-                           ft_kerning_default, &delta);
-            x += delta.x >> 6;
-        }
-
-        /* save position */
-        s->positions[i].x = x + glyph->bitmap_left;
-        s->positions[i].y = y - glyph->bitmap_top + y_max;
-        if (code == '\t') x  = (x / s->tabsize + 1)*s->tabsize;
-        else              x += glyph->advance;
-    }
+    measure_text(ctx, &metrics);
 
-    max_text_line_w = FFMAX(x, max_text_line_w);
+    s->max_glyph_h = POS_CEIL(metrics.max_y64 - metrics.min_y64, 64);
+    s->max_glyph_w = POS_CEIL(metrics.max_x64 - metrics.min_x64, 64);
 
-    s->var_values[VAR_TW] = s->var_values[VAR_TEXT_W] = max_text_line_w;
-    s->var_values[VAR_TH] = s->var_values[VAR_TEXT_H] = y + s->max_glyph_h;
+    s->var_values[VAR_TW] = s->var_values[VAR_TEXT_W] = metrics.width;
+    s->var_values[VAR_TH] = s->var_values[VAR_TEXT_H] = metrics.height;
 
     s->var_values[VAR_MAX_GLYPH_W] = s->max_glyph_w;
     s->var_values[VAR_MAX_GLYPH_H] = s->max_glyph_h;
-    s->var_values[VAR_MAX_GLYPH_A] = s->var_values[VAR_ASCENT ] = y_max;
-    s->var_values[VAR_MAX_GLYPH_D] = s->var_values[VAR_DESCENT] = y_min;
+    s->var_values[VAR_MAX_GLYPH_A] = s->var_values[VAR_ASCENT] = POS_CEIL(metrics.max_y64, 64);
+    s->var_values[VAR_FONT_A] = s->face->size->metrics.ascender / 64;
+    s->var_values[VAR_MAX_GLYPH_D] = s->var_values[VAR_DESCENT] = POS_CEIL(metrics.min_y64, 64);
+    s->var_values[VAR_FONT_D] = -s->face->size->metrics.descender / 64;
 
-    s->var_values[VAR_LINE_H] = s->var_values[VAR_LH] = s->max_glyph_h;
+    s->var_values[VAR_TOP_A] = POS_CEIL(metrics.offset_top64, 64);
+    s->var_values[VAR_BOTTOM_D] = POS_CEIL(metrics.offset_bottom64, 64);
+    s->var_values[VAR_LINE_H] = s->var_values[VAR_LH] = metrics.line_height64 / 64.;
 
     if (s->text_source == AV_FRAME_DATA_DETECTION_BBOXES) {
         s->var_values[VAR_X] = s->x;
@@ -1559,56 +1975,176 @@ continue_on_invalid2:
     update_color_with_alpha(s, &bordercolor, s->bordercolor);
     update_color_with_alpha(s, &boxcolor   , s->boxcolor   );
 
-    box_w = max_text_line_w;
-    box_h = y + s->max_glyph_h;
+    if (s->draw_box && s->boxborderw) {
+        int bbsize[4];
+        int count;
+        count = string_to_array(s->boxborderw, bbsize, 4);
+        if(count == 1) {
+            s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = bbsize[0];
+        } else if(count == 2) {
+            s->bb_top = s->bb_bottom = bbsize[0];
+            s->bb_right = s->bb_left = bbsize[1];
+        } else if(count == 3) {
+            s->bb_top = bbsize[0];
+            s->bb_right = s->bb_left = bbsize[1];
+            s->bb_bottom = bbsize[2];
+        } else if(count == 4) {
+            s->bb_top = bbsize[0];
+            s->bb_right = bbsize[1];
+            s->bb_bottom = bbsize[2];
+            s->bb_left = bbsize[3];
+        }
+    } else {
+        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = 0;
+    }
 
     if (s->fix_bounds) {
-
         /* calculate footprint of text effects */
-        int boxoffset     = s->draw_box ? FFMAX(s->boxborderw, 0) : 0;
         int borderoffset  = s->borderw  ? FFMAX(s->borderw, 0) : 0;
 
-        int offsetleft = FFMAX3(boxoffset, borderoffset,
+        int offsetleft = FFMAX3(FFMAX(s->bb_left, 0), borderoffset,
                                 (s->shadowx < 0 ? FFABS(s->shadowx) : 0));
-        int offsettop = FFMAX3(boxoffset, borderoffset,
+        int offsettop = FFMAX3(FFMAX(s->bb_top, 0), borderoffset,
                                 (s->shadowy < 0 ? FFABS(s->shadowy) : 0));
-
-        int offsetright = FFMAX3(boxoffset, borderoffset,
+        int offsetright = FFMAX3(FFMAX(s->bb_right, 0), borderoffset,
                                  (s->shadowx > 0 ? s->shadowx : 0));
-        int offsetbottom = FFMAX3(boxoffset, borderoffset,
+        int offsetbottom = FFMAX3(FFMAX(s->bb_bottom, 0), borderoffset,
                                   (s->shadowy > 0 ? s->shadowy : 0));
 
-
         if (s->x - offsetleft < 0) s->x = offsetleft;
         if (s->y - offsettop < 0)  s->y = offsettop;
 
-        if (s->x + box_w + offsetright > width)
-            s->x = FFMAX(width - box_w - offsetright, 0);
-        if (s->y + box_h + offsetbottom > height)
-            s->y = FFMAX(height - box_h - offsetbottom, 0);
+        if (s->x + metrics.width + offsetright > width)
+            s->x = FFMAX(width - metrics.width - offsetright, 0);
+        if (s->y + metrics.height + offsetbottom > height)
+            s->y = FFMAX(height - metrics.height - offsetbottom, 0);
     }
 
-    /* draw box */
-    if (s->draw_box)
-        ff_blend_rectangle(&s->dc, &boxcolor,
-                           frame->data, frame->linesize, width, height,
-                           s->x - s->boxborderw, s->y - s->boxborderw,
-                           box_w + s->boxborderw * 2, box_h + s->boxborderw * 2);
+    x = 0;
+    y = 0;
+    x64 = (int)(s->x * 64.);
+    if(s->y_align == YA_FONT) {
+        y64 = (int)(s->y * 64. + s->face->size->metrics.ascender);
+    } else if(s->y_align == YA_BASELINE) {
+        y64 = (int)(s->y * 64.);
+    } else {
+        y64 = (int)(s->y * 64. + metrics.offset_top64);
+    }
 
-    if (s->shadowx || s->shadowy) {
-        if ((ret = draw_glyphs(s, frame, width, height,
-                               &shadowcolor, s->shadowx, s->shadowy, 0)) < 0)
-            return ret;
+    for(int l = 0; l < s->line_count; ++l) {
+        TextLine *line = &s->lines[l];
+        HarfbuzzData *hb = &line->hb_data;
+        line->glyphs = av_mallocz(hb->glyph_count * sizeof(GlyphInfo));
+
+        for(int t = 0; t < hb->glyph_count; ++t) {
+            GlyphInfo *g_info = &line->glyphs[t];
+            uint8_t is_tab = last_tab_idx < s->tab_count &&
+                hb->glyph_info[t].cluster == s->tab_clusters[last_tab_idx] - line->cluster_offset;
+            int true_x, true_y;
+            if(is_tab) {
+                ++last_tab_idx;
+            }
+            true_x = x + hb->glyph_pos[t].x_offset;
+            true_y = y + hb->glyph_pos[t].y_offset;
+            shift_x64 = (((x64 + true_x) >> 4) & 0b0011) << 4;
+            shift_y64 = ((4 - (((y64 + true_y) >> 4) & 0b0011)) & 0b0011) << 4;
+
+            ret = load_glyph(ctx, &glyph, hb->glyph_info[t].codepoint, shift_x64, shift_y64);
+            if (ret != 0) {
+                return ret;
+            }
+            g_info->code = hb->glyph_info[t].codepoint;
+            g_info->x = (x64 + true_x) >> 6;
+            g_info->y = ((y64 + true_y) >> 6) + (shift_y64 > 0 ? 1 : 0);
+            g_info->shift_x64 = shift_x64;
+            g_info->shift_y64 = shift_y64;
+
+            if(!is_tab) {
+                x += hb->glyph_pos[t].x_advance;
+            } else {
+                int size = s->blank_advance64 * s->tabsize;
+                x = (x / size + 1) * size;
+            }
+            y += hb->glyph_pos[t].y_advance;
+        }
+
+        y += metrics.line_height64 + s->line_spacing * 64;
+        x = 0;
     }
 
-    if (s->borderw) {
-        if ((ret = draw_glyphs(s, frame, width, height,
-                               &bordercolor, 0, 0, s->borderw)) < 0)
+// TODO (LEFT OFFSET)
+//    offset_left = metrics.offset_left64 / 64;
+    offset_left = 0;
+    metrics.rect_x = s->x;
+    if(s->y_align == YA_BASELINE) {
+        metrics.rect_y = s->y - metrics.offset_top64 / 64;
+    } else {
+        metrics.rect_y = s->y;
+    }
+    
+    s->box_width = s->boxw == 0 ? metrics.width : s->boxw;
+    s->box_height = s->boxh == 0 ? metrics.height : s->boxh;
+
+    if(!s->draw_box) {
+        // Create a border for the clipping region to take into account subpixel
+        // errors in text measurement and effects.
+        int borderoffset = s->borderw ? FFMAX(s->borderw, 0) : 0;
+        s->bb_left = borderoffset + (s->shadowx < 0 ? FFABS(s->shadowx) : 0) + 1;
+        s->bb_top = borderoffset + (s->shadowy < 0 ? FFABS(s->shadowy) : 0) + 1;
+        s->bb_right = borderoffset + (s->shadowx > 0 ? s->shadowx : 0) + 1;
+        s->bb_bottom = borderoffset + (s->shadowy > 0 ? s->shadowy : 0) + 1;
+    }
+
+    /* Check if the whole box is out of the frame */        
+    is_outside = metrics.rect_x - s->bb_left >= width ||
+                    metrics.rect_y - s->bb_top >= height ||
+                    metrics.rect_x + s->box_width + s->bb_right <= 0 ||
+                    metrics.rect_y + s->box_height + s->bb_bottom <= 0;
+
+    if(!is_outside) {
+        /* draw box */
+        if (s->draw_box) {
+            rec_x = metrics.rect_x - s->bb_left;
+            rec_y = metrics.rect_y - s->bb_top;
+            rec_width = s->box_width + s->bb_right + s->bb_left;
+            rec_height = s->box_height + s->bb_bottom + s->bb_top;
+            // av_log(s, AV_LOG_DEBUG, "rect_x: %d -> bb_left: %d\n",
+            //     metrics.rect_x, s->bb_left);
+            // av_log(s, AV_LOG_DEBUG, "Rect -> (x: %d - y: %d - dx: %d - dy: %d)\n",
+            //     rec_x, rec_y, rec_width, rec_height);
+            ff_blend_rectangle(&s->dc, &boxcolor,
+                frame->data, frame->linesize, width, height,
+                rec_x, rec_y, rec_width, rec_height);
+        }
+
+        if (s->shadowx || s->shadowy) {
+            if ((ret = draw_glyphs(s, frame, &shadowcolor, &metrics,
+                    s->shadowx - offset_left, s->shadowy, s->borderw)) < 0) {
+                return ret;
+            }
+        }
+
+        if (s->borderw) {
+            if ((ret = draw_glyphs(s, frame, &bordercolor, &metrics,
+                    -offset_left, 0, s->borderw)) < 0) {
+                return ret;
+            }
+        }
+
+        if ((ret = draw_glyphs(s, frame, &fontcolor, &metrics, -offset_left,
+                0, 0)) < 0) {
             return ret;
+        }
     }
-    if ((ret = draw_glyphs(s, frame, width, height,
-                           &fontcolor, 0, 0, 0)) < 0)
-        return ret;
+
+    // FREE data structures
+    for(int l = 0; l < s->line_count; ++l) {
+        TextLine *line = &s->lines[l];
+        av_freep(&line->glyphs);
+        hb_destroy(&line->hb_data);
+    }
+    av_freep(&s->lines);
+    av_freep(&s->tab_clusters);
 
     return 0;
 }
@@ -1680,13 +2216,13 @@ FF_ENABLE_DEPRECATION_WARNINGS
             s->x = bbox->x;
             s->y = bbox->y - s->fontsize;
         }
-        draw_text(ctx, frame, frame->width, frame->height);
+        draw_text(ctx, frame);
     }
 
-    av_log(ctx, AV_LOG_DEBUG, "n:%d t:%f text_w:%d text_h:%d x:%d y:%d\n",
-           (int)s->var_values[VAR_N], s->var_values[VAR_T],
-           (int)s->var_values[VAR_TEXT_W], (int)s->var_values[VAR_TEXT_H],
-           s->x, s->y);
+    // av_log(ctx, AV_LOG_DEBUG, "n:%d t:%f text_w:%d text_h:%d x:%.2f y:%.2f\n",
+    //        (int)s->var_values[VAR_N], s->var_values[VAR_T],
+    //        (int)s->var_values[VAR_TEXT_W], (int)s->var_values[VAR_TEXT_H],
+    //        s->x, s->y);
 
     return ff_filter_frame(outlink, frame);
 }
Anton Khirnov Jan. 30, 2023, 12:08 p.m. UTC | #11
Quoting Francesco Carusi (2023-01-30 11:48:36)
> From 7b006d958b0d226269362b9a811bd210ae41adcd Mon Sep 17 00:00:00 2001
> From: yethie <klimklim@tiscali.it>
> Date: Mon, 30 Jan 2023 11:41:53 +0100
> Subject: [PATCH 1/1] enhanced drawtext filter

This is not the way we do development. Every logical change you're doing
should be in its own patch with its own commit message describing what
it changes and why.
See http://ffmpeg.org/developer.html for more information.
Paul B Mahol Jan. 30, 2023, 12:19 p.m. UTC | #12
On 1/30/23, Francesco Carusi <klimklim@tiscali.it> wrote:
> On 28/01/2023 16:32, Paul B Mahol wrote:
>> On 1/28/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>> On 27/01/2023 18:31, Paul B Mahol wrote:
>>>> On 1/27/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>> On 26/01/2023 17:37, Paul B Mahol wrote:
>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>> On 26/01/2023 14:21, Paul B Mahol wrote:
>>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>>>> The drawtext reinit command is also used in the docs as an example
>>>>>>>>> for
>>>>>>>>> the sendcmd filter, so I thought it was fine to use commands in
>>>>>>>>> that
>>>>>>>>> way. In my opinion it is also a convenient way to modify multiple
>>>>>>>>> options at the same time.
>>>>>>>>> Should the command match the name of a filter option instead?
>>>>>>>>>
>>>>>>>> Please do not top post.
>>>>>>>>
>>>>>>>> It is much better to use already existing options for commands that
>>>>>>>> is
>>>>>>>> more intuitive to users. Also multiple options can be set at
>>>>>>>> runtime,
>>>>>>>> there is no such limitation.
>>>>>>> ok, I'm going to remove the "change" command and add commands that
>>>>>>> match
>>>>>>> the options that it included.
>>>>>> Thanks, feel free to ask questions on #ffmpeg-devel irc channel.
>>>>> I'm attaching the updated patch, I also updated the document at
>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>>> Thanks
>>>> Amazing, I like demos!
>>>>
>>>> Could improve code style of newly added/changed lines?
>>>> For example opening { put on separate line. So code style is in sync
>>>> with rest of codebase.
>>> Sure, I'll put opening { on a new line for functions, not for control
>>> statements, like in the rest of the code. Is it fine?
>> Yes. Thanks.
>>
>>>> The commands stuff does not need to use strcmp to detect if option
>>>> value have been changed, you could avoid strcmp by just caching old
>>>> value prior to calling function the picks new values, and after that
>>>> just compare old with new and then if it differs call needed code.
>>> I'll cache the numeric values. I think that caching string values is not
>>> the preferred solution because in addition to the strcmp needed to check
>>> the value, it would also need a strdup to cache the previous value, even
>>> when the command does not involve those options. Does it sound
>>> reasonable?
>> Yes.
> I'm attaching the patch that includes the changes we discussed.

space between 'for' and '('

Do not keep old code in comments if its no longer relevant or working.



>
>>
>>>>>>>>> On 26/01/2023 11:50, Paul B Mahol wrote:
>>>>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>>>>>> Hi, I'm new to contributing to ffmpeg!
>>>>>>>>>>>
>>>>>>>>>>> I modified the drawtext filter to improve text rendering and add
>>>>>>>>>>> some
>>>>>>>>>>> features. You can find a high level description of the changes
>>>>>>>>>>> at
>>>>>>>>>>> this
>>>>>>>>>>> link:
>>>>>>>>>>>
>>>>>>>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>>>>>>>>>
>>>>>>>>>>> I'm also attaching the patch file.
>>>>>>>>>>> I looked for the filter maintainer to discuss about the changes
>>>>>>>>>>> I
>>>>>>>>>>> made
>>>>>>>>>>> but it looks like there isn't any, am I correct?
>>>>>>>>>>>
>>>>>>>>>>> Please let me know if this is the right way to submit my
>>>>>>>>>>> contribution.
>>>>>>>>>> Why filter can not support normal commands for options? Like
>>>>>>>>>> most/all
>>>>>>>>>> other filters that have support for changing options values at
>>>>>>>>>> runtime.
>>>>>>>>>>
>>>>>>>>>> The reinit and yours added change option(s) are very
>>>>>>>>>> strange/inconvenient things to do.
>>>>>>>>>> _______________________________________________
>>>>>>>>>> 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".
>>>>>>>>>
>>>>>>>> _______________________________________________
>>>>>>>> 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".
>>>>>>>
>>>>>> _______________________________________________
>>>>>> 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".
>>> _______________________________________________
>>> 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".
>
Francesco Carusi Feb. 3, 2023, 2:18 p.m. UTC | #13
On 30/01/2023 13:19, Paul B Mahol wrote:
> On 1/30/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>> On 28/01/2023 16:32, Paul B Mahol wrote:
>>> On 1/28/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>> On 27/01/2023 18:31, Paul B Mahol wrote:
>>>>> On 1/27/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>> On 26/01/2023 17:37, Paul B Mahol wrote:
>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>>> On 26/01/2023 14:21, Paul B Mahol wrote:
>>>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>>>>> The drawtext reinit command is also used in the docs as an example
>>>>>>>>>> for
>>>>>>>>>> the sendcmd filter, so I thought it was fine to use commands in
>>>>>>>>>> that
>>>>>>>>>> way. In my opinion it is also a convenient way to modify multiple
>>>>>>>>>> options at the same time.
>>>>>>>>>> Should the command match the name of a filter option instead?
>>>>>>>>>>
>>>>>>>>> Please do not top post.
>>>>>>>>>
>>>>>>>>> It is much better to use already existing options for commands that
>>>>>>>>> is
>>>>>>>>> more intuitive to users. Also multiple options can be set at
>>>>>>>>> runtime,
>>>>>>>>> there is no such limitation.
>>>>>>>> ok, I'm going to remove the "change" command and add commands that
>>>>>>>> match
>>>>>>>> the options that it included.
>>>>>>> Thanks, feel free to ask questions on #ffmpeg-devel irc channel.
>>>>>> I'm attaching the updated patch, I also updated the document at
>>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>>>> Thanks
>>>>> Amazing, I like demos!
>>>>>
>>>>> Could improve code style of newly added/changed lines?
>>>>> For example opening { put on separate line. So code style is in sync
>>>>> with rest of codebase.
>>>> Sure, I'll put opening { on a new line for functions, not for control
>>>> statements, like in the rest of the code. Is it fine?
>>> Yes. Thanks.
>>>
>>>>> The commands stuff does not need to use strcmp to detect if option
>>>>> value have been changed, you could avoid strcmp by just caching old
>>>>> value prior to calling function the picks new values, and after that
>>>>> just compare old with new and then if it differs call needed code.
>>>> I'll cache the numeric values. I think that caching string values is not
>>>> the preferred solution because in addition to the strcmp needed to check
>>>> the value, it would also need a strdup to cache the previous value, even
>>>> when the command does not involve those options. Does it sound
>>>> reasonable?
>>> Yes.
>> I'm attaching the patch that includes the changes we discussed.
> space between 'for' and '('
>
> Do not keep old code in comments if its no longer relevant or working.

Ok I added spaces between control statements (if, for, while) and '(', 
and also cleaned up comments.
Following Anton Khirnov suggestion I tried to split the changes into 
multiple commits. However the first one is quite bit since it contains a 
major change in how the filter works and cannot be split further.
Patches attached.

>>>>>>>>>> On 26/01/2023 11:50, Paul B Mahol wrote:
>>>>>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>>>>>>> Hi, I'm new to contributing to ffmpeg!
>>>>>>>>>>>>
>>>>>>>>>>>> I modified the drawtext filter to improve text rendering and add
>>>>>>>>>>>> some
>>>>>>>>>>>> features. You can find a high level description of the changes
>>>>>>>>>>>> at
>>>>>>>>>>>> this
>>>>>>>>>>>> link:
>>>>>>>>>>>>
>>>>>>>>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>>>>>>>>>>
>>>>>>>>>>>> I'm also attaching the patch file.
>>>>>>>>>>>> I looked for the filter maintainer to discuss about the changes
>>>>>>>>>>>> I
>>>>>>>>>>>> made
>>>>>>>>>>>> but it looks like there isn't any, am I correct?
>>>>>>>>>>>>
>>>>>>>>>>>> Please let me know if this is the right way to submit my
>>>>>>>>>>>> contribution.
>>>>>>>>>>> Why filter can not support normal commands for options? Like
>>>>>>>>>>> most/all
>>>>>>>>>>> other filters that have support for changing options values at
>>>>>>>>>>> runtime.
>>>>>>>>>>>
>>>>>>>>>>> The reinit and yours added change option(s) are very
>>>>>>>>>>> strange/inconvenient things to do.
>>>>>>>>>>> _______________________________________________
>>>>>>>>>>> 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".
>>>>>>>>>>
>>>>>>>>> _______________________________________________
>>>>>>>>> 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".
>>>>>>>>
>>>>>>> _______________________________________________
>>>>>>> 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".
>>>> _______________________________________________
>>>> 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".
> _______________________________________________
> 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".
From 50a25f1502a215000d0b4bd337204804b59f407d Mon Sep 17 00:00:00 2001
From: yethie <klimklim@tiscali.it>
Date: Fri, 3 Feb 2023 14:13:59 +0100
Subject: [PATCH 1/7] use libharfbuzz to shape text . the filter now depends on
 libharfbuzz . the default line height is set equal to the one defined in the
 font metrics . subpixel precision to 1/4 pixel is now used when rendering
 glyphs

---
 configure                 |   5 +-
 doc/filters.texi          |   8 +-
 libavfilter/vf_drawtext.c | 961 +++++++++++++++++++++++++-------------
 3 files changed, 648 insertions(+), 326 deletions(-)

diff --git a/configure b/configure
index c726076da1..54504c0dbc 100755
--- a/configure
+++ b/configure
@@ -235,6 +235,7 @@ External library support:
   --enable-libfontconfig   enable libfontconfig, useful for drawtext filter [no]
   --enable-libfreetype     enable libfreetype, needed for drawtext filter [no]
   --enable-libfribidi      enable libfribidi, improves drawtext filter [no]
+  --enable-libharfbuzz     enable libharfbuzz, needed for drawtext filter [no]
   --enable-libglslang      enable GLSL->SPIRV compilation via libglslang [no]
   --enable-libgme          enable Game Music Emu via libgme [no]
   --enable-libgsm          enable GSM de/encoding via libgsm [no]
@@ -1818,6 +1819,7 @@ EXTERNAL_LIBRARY_LIST="
     libfontconfig
     libfreetype
     libfribidi
+    libharfbuzz
     libglslang
     libgme
     libgsm
@@ -3660,7 +3662,7 @@ dilation_opencl_filter_deps="opencl"
 dnn_classify_filter_select="dnn"
 dnn_detect_filter_select="dnn"
 dnn_processing_filter_select="dnn"
-drawtext_filter_deps="libfreetype"
+drawtext_filter_deps="libfreetype libharfbuzz"
 drawtext_filter_suggest="libfontconfig libfribidi"
 elbg_filter_deps="avcodec"
 eq_filter_deps="gpl"
@@ -6584,6 +6586,7 @@ enabled fontconfig        && enable libfontconfig
 enabled libfontconfig     && require_pkg_config libfontconfig fontconfig "fontconfig/fontconfig.h" FcInit
 enabled libfreetype       && require_pkg_config libfreetype freetype2 "ft2build.h FT_FREETYPE_H" FT_Init_FreeType
 enabled libfribidi        && require_pkg_config libfribidi fribidi fribidi.h fribidi_version_info
+enabled libharfbuzz       && require_pkg_config libharfbuzz harfbuzz hb.h hb_buffer_create
 enabled libglslang && { check_lib spirv_compiler glslang/Include/glslang_c_interface.h glslang_initialize_process \
                             -lglslang -lMachineIndependent -lOSDependent -lHLSL -lOGLCompiler -lGenericCodeGen \
                             -lSPVRemapper -lSPIRV -lSPIRV-Tools-opt -lSPIRV-Tools -lpthread -lstdc++ -lm ||
diff --git a/doc/filters.texi b/doc/filters.texi
index 3a54c68f3e..49d7218e84 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -12032,7 +12032,7 @@ Draw a text string or text from a specified file on top of a video, using the
 libfreetype library.
 
 To enable compilation of this filter, you need to configure FFmpeg with
-@code{--enable-libfreetype}.
+@code{--enable-libfreetype} and @code{--enable-libharfbuzz}.
 To enable default font fallback and the @var{font} option you need to
 configure FFmpeg with @code{--enable-libfontconfig}.
 To enable the @var{text_shaping} option, you need to configure FFmpeg with
@@ -12060,8 +12060,7 @@ option, check the @ref{color syntax,,"Color" section in the ffmpeg-utils manual,
 The default value of @var{boxcolor} is "white".
 
 @item line_spacing
-Set the line spacing in pixels of the border to be drawn around the box using @var{box}.
-The default value of @var{line_spacing} is 0.
+Set the line spacing in pixels. The default value of @var{line_spacing} is 0.
 
 @item borderw
 Set the width of the border to be drawn around the text using @var{bordercolor}.
@@ -12559,6 +12558,9 @@ For more information about fontconfig, check:
 For more information about libfribidi, check:
 @url{http://fribidi.org/}.
 
+For more information about libharfbuzz, check:
+@url{https://github.com/harfbuzz/harfbuzz}.
+
 @section edgedetect
 
 Detect and draw edges. The filter uses the Canny Edge Detection algorithm.
diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c
index 50012bb258..7a0a255c5e 100644
--- a/libavfilter/vf_drawtext.c
+++ b/libavfilter/vf_drawtext.c
@@ -1,4 +1,5 @@
 /*
+ * Copyright (c) 2023 Francesco Carusi
  * Copyright (c) 2011 Stefano Sabatini
  * Copyright (c) 2010 S.N. Hemanth Meenakshisundaram
  * Copyright (c) 2003 Gustavo Sverzut Barbieri <gsbarbieri@yahoo.com.br>
@@ -20,6 +21,14 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  */
 
+/*
+ * Changelog - 2023
+ *
+ * - This filter now depends on libharfbuzz for text shaping.
+ * - Glyphs position is now accurate to 1/4 pixel in both directions
+ * - The default line height is now the one defined in the font
+ */
+
 /**
  * @file
  * drawtext filter, based on the original vhook/drawtext.c
@@ -72,14 +81,20 @@
 #include FT_GLYPH_H
 #include FT_STROKER_H
 
+#include <hb.h>
+#include <hb-ft.h>
+
+// Ceiling operation for positive integers division
+#define POS_CEIL(x, y) ((x)/(y) + ((x)%(y) != 0))
+
 static const char *const var_names[] = {
     "dar",
     "hsub", "vsub",
-    "line_h", "lh",           ///< line height, same as max_glyph_h
+    "line_h", "lh",           ///< line height
     "main_h", "h", "H",       ///< height of the input video
     "main_w", "w", "W",       ///< width  of the input video
-    "max_glyph_a", "ascent",  ///< max glyph ascent
-    "max_glyph_d", "descent", ///< min glyph descent
+    "max_glyph_a", "ascent",  ///< max glyph ascender
+    "max_glyph_d", "descent", ///< min glyph descender
     "max_glyph_h",            ///< max glyph height
     "max_glyph_w",            ///< max glyph width
     "n",                      ///< number of frame
@@ -148,12 +163,78 @@ enum expansion_mode {
     EXP_STRFTIME,
 };
 
+typedef struct HarfbuzzData {
+    hb_buffer_t* buf;
+    hb_font_t* font;
+    unsigned int glyph_count;
+    hb_glyph_info_t* glyph_info;
+    hb_glyph_position_t* glyph_pos;
+} HarfbuzzData;
+
+/** Information about a single glyph in a text line */
+typedef struct GlyphInfo {
+    uint32_t code;                  ///< the glyph code point
+    int x;                          ///< the x position of the glyph
+    int y;                          ///< the y position of the glyph
+    int shift_x64;                  ///< the horizontal shift of the glyph in 26.6 units
+    int shift_y64;                  ///< the vertical shift of the glyph in 26.6 units
+} GlyphInfo;
+
+/** Information about a single line of text */
+typedef struct TextLine {
+    int offset_left64;              ///< offset between the origin and
+                                    ///  the leftmost pixel of the first glyph
+    int offset_right64;             ///< maximum offset between the origin and
+                                    ///  the rightmost pixel of the last glyph
+    int width64;                    ///< width of the line
+    HarfbuzzData hb_data;           ///< libharfbuzz data of this text line
+    GlyphInfo* glyphs;              ///< array of glyphs in this text line
+    int cluster_offset;             ///< the offset at which this line begins
+} TextLine;
+
+/** A glyph as loaded and rendered using libfreetype */
+typedef struct Glyph {
+    FT_Glyph glyph;
+    FT_Glyph border_glyph;
+    uint32_t code;
+    unsigned int fontsize;
+    /** Glyph bitmaps with 1/4 pixel precision in both directions */
+    FT_BitmapGlyph bglyph[16];
+    /** Outlined glyph bitmaps with 1/4 pixel precision in both directions */
+    FT_BitmapGlyph border_bglyph[16];
+    FT_BBox bbox;
+} Glyph;
+
+/** Global text metrics */
+typedef struct TextMetrics {
+    int offset_top64;               ///< ascender amount of the first line (in 26.6 units)
+    int offset_bottom64;            ///< descender amount of the last line (in 26.6 units)
+    int offset_left64;              ///< maximum offset between the origin and
+                                    ///  the leftmost pixel of the first glyph
+                                    ///  of each line (in 26.6 units)
+    int offset_right64;             ///< maximum offset between the origin and
+                                    ///  the rightmost pixel of the last glyph
+                                    ///  of each line (in 26.6 units)
+    int line_height64;              ///< the font-defined line height
+    int width;                      ///< width of the longest line - ceil(width64/64)
+    int height;                     ///< total height of the text - ceil(height64/64)
+
+    int min_y64;                    ///< minimum value of bbox.yMin among glyphs (in 26.6 units)
+    int max_y64;                    ///< maximum value of bbox.yMax among glyphs (in 26.6 units)
+    int min_x64;                    ///< minimum value of bbox.xMin among glyphs (in 26.6 units)
+    int max_x64;                    ///< maximum value of bbox.xMax among glyphs (in 26.6 units)
+
+    // Position of the background box (without borders)
+    int rect_x;                     ///< x position of the box
+    int rect_y;                     ///< y position of the box
+} TextMetrics;
+
 typedef struct DrawTextContext {
     const AVClass *class;
     int exp_mode;                   ///< expansion mode to use for the text
     int reinit;                     ///< tells if the filter is being reinited
 #if CONFIG_LIBFONTCONFIG
-    uint8_t *font;              ///< font to be used
+    uint8_t *font;                  ///< font to be used
 #endif
     uint8_t *fontfile;              ///< font to be used
     uint8_t *text;                  ///< text to be drawn
@@ -161,11 +242,9 @@ typedef struct DrawTextContext {
     uint8_t *fontcolor_expr;        ///< fontcolor expression to evaluate
     AVBPrint expanded_fontcolor;    ///< used to contain the expanded fontcolor spec
     int ft_load_flags;              ///< flags used for loading fonts, see FT_LOAD_*
-    FT_Vector *positions;           ///< positions for each element in the text
-    size_t nb_positions;            ///< number of elements of positions array
     char *textfile;                 ///< file with text to be drawn
-    int x;                          ///< x position to start drawing text
-    int y;                          ///< y position to start drawing text
+    double x;                       ///< x position to start drawing text
+    double y;                       ///< y position to start drawing text
     int max_glyph_w;                ///< max glyph width
     int max_glyph_h;                ///< max glyph height
     int shadowx, shadowy;
@@ -178,7 +257,12 @@ typedef struct DrawTextContext {
     int line_spacing;               ///< lines spacing in pixels
     short int draw_box;             ///< draw box around text - true or false
     int boxborderw;                 ///< box border width
-    int use_kerning;                ///< font kerning is used - true/false
+    int bb_top;                     ///< the size of the top box border
+    int bb_right;                   ///< the size of the right box border
+    int bb_bottom;                  ///< the size of the bottom box border
+    int bb_left;                    ///< the size of the left box border
+    int box_width;                  ///< the width of box
+    int box_height;                 ///< the height of box
     int tabsize;                    ///< tab size
     int fix_bounds;                 ///< do we let it go out of frame bounds - t/f
 
@@ -213,31 +297,37 @@ typedef struct DrawTextContext {
     int text_shaping;               ///< 1 to shape the text before drawing it
 #endif
     AVDictionary *metadata;
+
+    TextLine *lines;                ///< computed information about text lines
+    int line_count;                 ///< the number of text lines
+    uint32_t *tab_clusters;         ///< the position of tab characters in the text
+    int tab_count;                  ///< the number of tab characters
+    int blank_advance64;            ///< the size of the space character
 } DrawTextContext;
 
 #define OFFSET(x) offsetof(DrawTextContext, x)
 #define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM
 
 static const AVOption drawtext_options[]= {
-    {"fontfile",    "set font file",        OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"text",        "set text",             OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"textfile",    "set text file",        OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"fontcolor",   "set foreground color", OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
+    {"fontfile",       "set font file",         OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"text",           "set text",              OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"textfile",       "set text file",         OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"fontcolor",      "set foreground color",  OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
     {"fontcolor_expr", "set foreground color expression", OFFSET(fontcolor_expr), AV_OPT_TYPE_STRING, {.str=""}, 0, 0, FLAGS},
-    {"boxcolor",    "set box color",        OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
-    {"bordercolor", "set border color",     OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
-    {"shadowcolor", "set shadow color",     OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
-    {"box",         "set box",              OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0,        1       , FLAGS},
-    {"boxborderw",  "set box border width", OFFSET(boxborderw),         AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"line_spacing",  "set line spacing in pixels", OFFSET(line_spacing),   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX,FLAGS},
-    {"fontsize",    "set font size",        OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0 , FLAGS},
-    {"x",           "set x expression",     OFFSET(x_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
-    {"y",           "set y expression",     OFFSET(y_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
-    {"shadowx",     "set shadow x offset",  OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"shadowy",     "set shadow y offset",  OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"borderw",     "set border width",     OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"tabsize",     "set tab size",         OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0,        INT_MAX , FLAGS},
-    {"basetime",    "set base time",        OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX , FLAGS},
+    {"boxcolor",       "set box color",         OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
+    {"bordercolor",    "set border color",      OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
+    {"shadowcolor",    "set shadow color",      OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
+    {"box",            "set box",               OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0, 1, FLAGS},
+    {"boxborderw",     "set box borders width", OFFSET(boxborderw),         AV_OPT_TYPE_INT,    {.i64=0},     0, INT_MAX, FLAGS},
+    {"line_spacing",   "set line spacing in pixels", OFFSET(line_spacing),  AV_OPT_TYPE_INT,    {.i64=-1},    INT_MIN, INT_MAX, FLAGS},
+    {"fontsize",       "set font size",         OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"x",              "set x expression",      OFFSET(x_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
+    {"y",              "set y expression",      OFFSET(y_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
+    {"shadowx",        "set shadow x offset",   OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
+    {"shadowy",        "set shadow y offset",   OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
+    {"borderw",        "set border width",      OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
+    {"tabsize",        "set tab size",          OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0, INT_MAX, FLAGS},
+    {"basetime",       "set base time",         OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX, FLAGS},
 #if CONFIG_LIBFONTCONFIG
     { "font",        "Font name",            OFFSET(font),               AV_OPT_TYPE_STRING, { .str = "Sans" },           .flags = FLAGS },
 #endif
@@ -248,15 +338,15 @@ static const AVOption drawtext_options[]= {
         {"strftime", "set strftime expansion (deprecated)", OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_STRFTIME}, 0, 0, FLAGS, "expansion"},
 
     {"timecode",        "set initial timecode",             OFFSET(tc_opt_string), AV_OPT_TYPE_STRING,   {.str=NULL}, 0, 0, FLAGS},
-    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},           0,        1, FLAGS},
-    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"reload",     "reload text file at specified frame interval", OFFSET(reload),     AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
-    { "alpha",       "apply alpha while rendering", OFFSET(a_expr),      AV_OPT_TYPE_STRING, { .str = "1"     },          .flags = FLAGS },
-    {"fix_bounds", "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
-    {"start_number", "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
-    {"text_source", "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
+    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},    0, 1, FLAGS},
+    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"reload",          "reload text file at specified frame interval", OFFSET(reload), AV_OPT_TYPE_INT, {.i64=0},    0, INT_MAX, FLAGS},
+    {"alpha",           "apply alpha while rendering",      OFFSET(a_expr),        AV_OPT_TYPE_STRING,   {.str = "1"}, .flags = FLAGS},
+    {"fix_bounds",      "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
+    {"start_number",    "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
+    {"text_source",     "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
 
 #if CONFIG_LIBFRIBIDI
     {"text_shaping", "attempt to shape text before drawing", OFFSET(text_shaping), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS},
@@ -297,18 +387,24 @@ static const struct ft_error {
 
 #define FT_ERRMSG(e) ft_errors[e].err_msg
 
-typedef struct Glyph {
-    FT_Glyph glyph;
-    FT_Glyph border_glyph;
-    uint32_t code;
-    unsigned int fontsize;
-    FT_Bitmap bitmap; ///< array holding bitmaps of font
-    FT_Bitmap border_bitmap; ///< array holding bitmaps of font border
-    FT_BBox bbox;
-    int advance;
-    int bitmap_left;
-    int bitmap_top;
-} Glyph;
+
+// Loads and (optionally) renders a glyph
+static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code,
+     int8_t shift_x64, int8_t shift_y64);
+
+// Shapes a line of text using libharfbuzz
+static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const char* text, int textLen);
+
+// Performs text measurements
+static int measure_text(AVFilterContext *ctx, TextMetrics *metrics);
+
+// Draws glyphs on the frame
+static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
+                       FFDrawColor *color, TextMetrics *metrics,
+                       int x, int y, int borderw);
+
+// Draws text on the frame
+static int draw_text(AVFilterContext *ctx, AVFrame *frame);
 
 static int glyph_cmp(const void *key, const void *b)
 {
@@ -316,80 +412,9 @@ static int glyph_cmp(const void *key, const void *b)
     int64_t diff = (int64_t)a->code - (int64_t)bb->code;
 
     if (diff != 0)
-         return diff > 0 ? 1 : -1;
+        return diff > 0 ? 1 : -1;
     else
-         return FFDIFFSIGN((int64_t)a->fontsize, (int64_t)bb->fontsize);
-}
-
-/**
- * Load glyphs corresponding to the UTF-32 codepoint code.
- */
-static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code)
-{
-    DrawTextContext *s = ctx->priv;
-    FT_BitmapGlyph bitmapglyph;
-    Glyph *glyph;
-    struct AVTreeNode *node = NULL;
-    int ret;
-
-    /* load glyph into s->face->glyph */
-    if (FT_Load_Char(s->face, code, s->ft_load_flags))
-        return AVERROR(EINVAL);
-
-    glyph = av_mallocz(sizeof(*glyph));
-    if (!glyph) {
-        ret = AVERROR(ENOMEM);
-        goto error;
-    }
-    glyph->code  = code;
-    glyph->fontsize = s->fontsize;
-
-    if (FT_Get_Glyph(s->face->glyph, &glyph->glyph)) {
-        ret = AVERROR(EINVAL);
-        goto error;
-    }
-    if (s->borderw) {
-        glyph->border_glyph = glyph->glyph;
-        if (FT_Glyph_StrokeBorder(&glyph->border_glyph, s->stroker, 0, 0) ||
-            FT_Glyph_To_Bitmap(&glyph->border_glyph, FT_RENDER_MODE_NORMAL, 0, 1)) {
-            ret = AVERROR_EXTERNAL;
-            goto error;
-        }
-        bitmapglyph = (FT_BitmapGlyph) glyph->border_glyph;
-        glyph->border_bitmap = bitmapglyph->bitmap;
-    }
-    if (FT_Glyph_To_Bitmap(&glyph->glyph, FT_RENDER_MODE_NORMAL, 0, 1)) {
-        ret = AVERROR_EXTERNAL;
-        goto error;
-    }
-    bitmapglyph = (FT_BitmapGlyph) glyph->glyph;
-
-    glyph->bitmap      = bitmapglyph->bitmap;
-    glyph->bitmap_left = bitmapglyph->left;
-    glyph->bitmap_top  = bitmapglyph->top;
-    glyph->advance     = s->face->glyph->advance.x >> 6;
-
-    /* measure text height to calculate text_height (or the maximum text height) */
-    FT_Glyph_Get_CBox(glyph->glyph, ft_glyph_bbox_pixels, &glyph->bbox);
-
-    /* cache the newly created glyph */
-    if (!(node = av_tree_node_alloc())) {
-        ret = AVERROR(ENOMEM);
-        goto error;
-    }
-    av_tree_insert(&s->glyphs, glyph, glyph_cmp, &node);
-
-    if (glyph_ptr)
-        *glyph_ptr = glyph;
-    return 0;
-
-error:
-    if (glyph)
-        av_freep(&glyph->glyph);
-
-    av_freep(&glyph);
-    av_freep(&node);
-    return ret;
+        return FFDIFFSIGN((int64_t)a->fontsize, (int64_t)bb->fontsize);
 }
 
 static av_cold int set_fontsize(AVFilterContext *ctx, unsigned int fontsize)
@@ -439,7 +464,6 @@ static av_cold int update_fontsize(AVFilterContext *ctx)
            return err;
 
         size = av_expr_eval(s->fontsize_pexpr, s->var_values, &s->prng);
-
         if (!isnan(size)) {
             roundedsize = round(size);
             // test for overflow before cast
@@ -447,7 +471,6 @@ static av_cold int update_fontsize(AVFilterContext *ctx)
                 av_log(ctx, AV_LOG_ERROR, "fontsize overflow\n");
                 return AVERROR(EINVAL);
             }
-
             fontsize = roundedsize;
         }
     }
@@ -548,7 +571,7 @@ static int load_font_fontconfig(AVFilterContext *ctx)
         goto fail;
     }
 
-    av_log(ctx, AV_LOG_INFO, "Using \"%s\"\n", filename);
+    av_log(ctx, AV_LOG_VERBOSE, "Using \"%s\"\n", filename);
     if (parse_err)
         s->default_fontsize = size + 0.5;
 
@@ -690,6 +713,7 @@ static int shape_text(AVFilterContext *ctx)
     s->text = tmp;
     len = fribidi_unicode_to_charset(FRIBIDI_CHAR_SET_UTF8,
                                      unicodestr, len, s->text);
+
     ret = 0;
 
 out:
@@ -715,7 +739,6 @@ static av_cold int init(AVFilterContext *ctx)
 {
     int err;
     DrawTextContext *s = ctx->priv;
-    Glyph *glyph;
 
     av_expr_free(s->fontsize_pexpr);
     s->fontsize_pexpr = NULL;
@@ -804,17 +827,8 @@ static av_cold int init(AVFilterContext *ctx)
                        FT_STROKER_LINEJOIN_ROUND, 0);
     }
 
-    s->use_kerning = FT_HAS_KERNING(s->face);
-
     /* load the fallback glyph with code 0 */
-    load_glyph(ctx, NULL, 0);
-
-    /* set the tabsize in pixels */
-    if ((err = load_glyph(ctx, &glyph, ' ')) < 0) {
-        av_log(ctx, AV_LOG_ERROR, "Could not set tabsize.\n");
-        return err;
-    }
-    s->tabsize *= glyph->advance;
+    load_glyph(ctx, NULL, 0, 0, 0);
 
     if (s->exp_mode == EXP_STRFTIME &&
         (strchr(s->text, '%') || strchr(s->text, '\\')))
@@ -837,6 +851,14 @@ static int glyph_enu_free(void *opaque, void *elem)
 
     FT_Done_Glyph(glyph->glyph);
     FT_Done_Glyph(glyph->border_glyph);
+    for (int t = 0; t < 16; ++t) {
+        if (glyph->bglyph[t] != NULL) {
+            FT_Done_Glyph((FT_Glyph)glyph->bglyph[t]);
+        }
+        if (glyph->border_bglyph[t] != NULL) {
+            FT_Done_Glyph((FT_Glyph)glyph->border_bglyph[t]);
+        }
+    }
     av_free(elem);
     return 0;
 }
@@ -852,9 +874,6 @@ static av_cold void uninit(AVFilterContext *ctx)
 
     s->x_pexpr = s->y_pexpr = s->a_pexpr = s->fontsize_pexpr = NULL;
 
-    av_freep(&s->positions);
-    s->nb_positions = 0;
-
     av_tree_enumerate(s->glyphs, NULL, NULL, glyph_enu_free);
     av_tree_destroy(s->glyphs);
     s->glyphs = NULL;
@@ -880,15 +899,15 @@ static int config_input(AVFilterLink *inlink)
     ff_draw_color(&s->dc, &s->bordercolor, s->bordercolor.rgba);
     ff_draw_color(&s->dc, &s->boxcolor,    s->boxcolor.rgba);
 
-    s->var_values[VAR_w]     = s->var_values[VAR_W]     = s->var_values[VAR_MAIN_W] = inlink->w;
-    s->var_values[VAR_h]     = s->var_values[VAR_H]     = s->var_values[VAR_MAIN_H] = inlink->h;
-    s->var_values[VAR_SAR]   = inlink->sample_aspect_ratio.num ? av_q2d(inlink->sample_aspect_ratio) : 1;
-    s->var_values[VAR_DAR]   = (double)inlink->w / inlink->h * s->var_values[VAR_SAR];
-    s->var_values[VAR_HSUB]  = 1 << s->dc.hsub_max;
-    s->var_values[VAR_VSUB]  = 1 << s->dc.vsub_max;
-    s->var_values[VAR_X]     = NAN;
-    s->var_values[VAR_Y]     = NAN;
-    s->var_values[VAR_T]     = NAN;
+    s->var_values[VAR_w]    = s->var_values[VAR_W] = s->var_values[VAR_MAIN_W] = inlink->w;
+    s->var_values[VAR_h]    = s->var_values[VAR_H] = s->var_values[VAR_MAIN_H] = inlink->h;
+    s->var_values[VAR_SAR]  = inlink->sample_aspect_ratio.num ? av_q2d(inlink->sample_aspect_ratio) : 1;
+    s->var_values[VAR_DAR]  = (double)inlink->w / inlink->h * s->var_values[VAR_SAR];
+    s->var_values[VAR_HSUB] = 1 << s->dc.hsub_max;
+    s->var_values[VAR_VSUB] = 1 << s->dc.vsub_max;
+    s->var_values[VAR_X]    = NAN;
+    s->var_values[VAR_Y]    = NAN;
+    s->var_values[VAR_T]    = NAN;
 
     av_lfg_init(&s->prng, av_get_random_seed());
 
@@ -948,8 +967,7 @@ static int command(AVFilterContext *ctx, const char *cmd, const char *arg, char
 
         ctx->priv = new;
         return config_input(ctx->inputs[0]);
-    } else
-        return AVERROR(ENOSYS);
+    }
 
 fail:
     av_log(ctx, AV_LOG_ERROR, "Failed to process command. Continuing with existing parameters.\n");
@@ -1318,91 +1336,369 @@ static int expand_text(AVFilterContext *ctx, char *text, AVBPrint *bp)
     return 0;
 }
 
-static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
-                       int width, int height,
-                       FFDrawColor *color,
-                       int x, int y, int borderw)
+static void update_color_with_alpha(DrawTextContext *s, FFDrawColor *color, const FFDrawColor incolor)
 {
-    char *text = s->expanded_text.str;
-    uint32_t code = 0;
-    int i, x1, y1;
-    uint8_t *p;
-    Glyph *glyph = NULL;
+    *color = incolor;
+    color->rgba[3] = (color->rgba[3] * s->alpha) / 255;
+    ff_draw_color(&s->dc, color, color->rgba);
+}
 
-    for (i = 0, p = text; *p; i++) {
-        FT_Bitmap bitmap;
-        Glyph dummy = { 0 };
-        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_invalid;);
-continue_on_invalid:
+static void update_alpha(DrawTextContext *s)
+{
+    double alpha = av_expr_eval(s->a_pexpr, s->var_values, &s->prng);
 
-        /* skip new line chars, just go to new line */
-        if (code == '\n' || code == '\r' || code == '\t')
-            continue;
+    if (isnan(alpha))
+        return;
 
-        dummy.code = code;
-        dummy.fontsize = s->fontsize;
-        glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
+    if (alpha >= 1.0)
+        s->alpha = 255;
+    else if (alpha <= 0)
+        s->alpha = 0;
+    else
+        s->alpha = 256 * alpha;
+}
+
+static inline int get_subpixel_idx(int shift_x64, int shift_y64)
+{
+    int idx = (shift_x64 >> 2) + (shift_y64 >> 4);
+    return idx;
+}
 
-        bitmap = borderw ? glyph->border_bitmap : glyph->bitmap;
+static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code, int8_t shift_x64, int8_t shift_y64)
+{
+    DrawTextContext *s = ctx->priv;
+    Glyph dummy = { 0 };
+    Glyph *glyph;
+    FT_Vector shift;
+    struct AVTreeNode *node = NULL;
+    int ret = 0;
 
-        if (glyph->bitmap.pixel_mode != FT_PIXEL_MODE_MONO &&
-            glyph->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY)
+    /* get glyph */
+    dummy.code = code;
+    dummy.fontsize = s->fontsize;
+    glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
+    if (!glyph) {
+        if (FT_Load_Glyph(s->face, code, s->ft_load_flags)) {
             return AVERROR(EINVAL);
+        }
+        glyph = av_mallocz(sizeof(*glyph));
+        if (!glyph) {
+            ret = AVERROR(ENOMEM);
+            goto error;
+        }
+        glyph->code  = code;
+        glyph->fontsize = s->fontsize;
+        if (FT_Get_Glyph(s->face->glyph, &glyph->glyph)) {
+            ret = AVERROR(EINVAL);
+            goto error;
+        }
+        if (s->borderw) {
+            glyph->border_glyph = glyph->glyph;
+            if (FT_Glyph_StrokeBorder(&glyph->border_glyph, s->stroker, 0, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+        }
+        /* measure text height to calculate text_height (or the maximum text height) */
+        FT_Glyph_Get_CBox(glyph->glyph, FT_GLYPH_BBOX_SUBPIXELS, &glyph->bbox);
+
+        /* cache the newly created glyph */
+        if (!(node = av_tree_node_alloc())) {
+            ret = AVERROR(ENOMEM);
+            goto error;
+        }
+        av_tree_insert(&s->glyphs, glyph, glyph_cmp, &node);
+    } else {
+        if (s->borderw && !glyph->border_glyph) {
+            glyph->border_glyph = glyph->glyph;
+            if (FT_Glyph_StrokeBorder(&glyph->border_glyph, s->stroker, 0, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+        }
+    }
+
+    // Check if a bitmap is needed
+    if (shift_x64 >= 0 && shift_y64 >= 0) {
+        // Get the bitmap subpixel index (0 -> 15)
+        int idx = get_subpixel_idx(shift_x64, shift_y64);
+        shift.x = shift_x64;
+        shift.y = shift_y64;
+
+        if (!glyph->bglyph[idx]) {
+            FT_Glyph tmp_glyph = glyph->glyph;
+            if (FT_Glyph_To_Bitmap(&tmp_glyph, FT_RENDER_MODE_NORMAL, &shift, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+            glyph->bglyph[idx] = (FT_BitmapGlyph)tmp_glyph;
+            if (glyph->bglyph[idx]->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) {
+                av_log(ctx, AV_LOG_ERROR, "Monocromatic (1bpp) fonts are not supported.\n");
+                ret = AVERROR(EINVAL);
+                goto error;
+            }
+        }
+        if (s->borderw && !glyph->border_bglyph[idx]) {
+            FT_Glyph tmp_glyph = glyph->border_glyph;
+            if (FT_Glyph_To_Bitmap(&tmp_glyph, FT_RENDER_MODE_NORMAL, &shift, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+            glyph->border_bglyph[idx] = (FT_BitmapGlyph)tmp_glyph;
+        }
+    }
+    if (glyph_ptr) {
+        *glyph_ptr = glyph;
+    }
+    return 0;
 
-        x1 = s->positions[i].x+s->x+x - borderw;
-        y1 = s->positions[i].y+s->y+y - borderw;
+error:
+    if (glyph && glyph->glyph)
+        FT_Done_Glyph(glyph->glyph);
 
-        ff_blend_mask(&s->dc, color,
-                      frame->data, frame->linesize, width, height,
-                      bitmap.buffer, bitmap.pitch,
-                      bitmap.width, bitmap.rows,
-                      bitmap.pixel_mode == FT_PIXEL_MODE_MONO ? 0 : 3,
-                      0, x1, y1);
+    av_freep(&glyph);
+    av_freep(&node);
+    return ret;
+}
+
+static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
+                       FFDrawColor *color,
+                       TextMetrics *metrics,
+                       int x, int y, int borderw)
+{
+    int g, l, x1, y1, w1, h1, idx;
+    int dx = 0, dy = 0, pdx = 0;
+    GlyphInfo *info;
+    Glyph dummy = { 0 }, *glyph;
+    FT_Bitmap bitmap;
+    FT_BitmapGlyph b_glyph;
+    int clip_x = 0, clip_y = 0;
+
+    clip_x = FFMIN(metrics->rect_x + s->box_width + s->bb_right, frame->width);
+    clip_y = FFMIN(metrics->rect_y + s->box_height + s->bb_bottom, frame->height);
+
+    for (l = 0; l < s->line_count; ++l) {
+        TextLine *line = &s->lines[l];
+        for (g = 0; g < line->hb_data.glyph_count; ++g) {
+            info = &line->glyphs[g];
+            dummy.fontsize = s->fontsize;
+            dummy.code = info->code;
+            glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
+            if (!glyph) {
+                return AVERROR(EINVAL);
+            }
+
+            idx = get_subpixel_idx(info->shift_x64, info->shift_y64);
+            b_glyph = borderw ? glyph->border_bglyph[idx] : glyph->bglyph[idx];
+            bitmap = b_glyph->bitmap;
+            x1 = x + info->x + b_glyph->left;
+            y1 = y + info->y - b_glyph->top;
+            w1 = bitmap.width;
+            h1 = bitmap.rows;
+
+            // Offset of the glyph's bitmap in the visible region
+            dx = dy = 0;
+            if (x1 < metrics->rect_x - s->bb_left) {
+                dx = metrics->rect_x - s->bb_left - x1;
+                x1 = metrics->rect_x - s->bb_left;
+            }
+            if (y1 < metrics->rect_y - s->bb_top) {
+                dy = metrics->rect_y - s->bb_top - y1;
+                y1 = metrics->rect_y - s->bb_top;
+            }
+
+            // check if the glyph is empty or out of the clipping region
+            if (dx >= w1 || dy >= h1 || x1 >= clip_x || y1 >= clip_y) {
+                continue;
+            }
+
+            pdx = dx + dy * bitmap.pitch;
+            w1 = FFMIN(clip_x - x1, w1 - dx);
+            h1 = FFMIN(clip_y - y1, h1 - dy);
+
+            ff_blend_mask(&s->dc, color, frame->data, frame->linesize, clip_x, clip_y,
+                bitmap.buffer + pdx, bitmap.pitch, w1, h1, 3, 0, x1, y1);
+        }
     }
 
     return 0;
 }
 
+static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const char* text, int textLen) 
+{
+    hb->buf = hb_buffer_create();
+    hb_buffer_set_direction(hb->buf, HB_DIRECTION_LTR);
+    hb_buffer_set_script(hb->buf, HB_SCRIPT_LATIN);
+    hb_buffer_set_language(hb->buf, hb_language_from_string("en", -1));
+    hb_buffer_guess_segment_properties(hb->buf);
+    hb->font = hb_ft_font_create(s->face, NULL);
+    hb_ft_font_set_funcs(hb->font);
+    hb_buffer_add_utf8(hb->buf, text, textLen, 0, -1);
+    hb_shape(hb->font, hb->buf, NULL, 0);
+    hb->glyph_info = hb_buffer_get_glyph_infos(hb->buf, &hb->glyph_count);
+    hb->glyph_pos = hb_buffer_get_glyph_positions(hb->buf, &hb->glyph_count);
+}
 
-static void update_color_with_alpha(DrawTextContext *s, FFDrawColor *color, const FFDrawColor incolor)
+static void hb_destroy(HarfbuzzData *hb) 
 {
-    *color = incolor;
-    color->rgba[3] = (color->rgba[3] * s->alpha) / 255;
-    ff_draw_color(&s->dc, color, color->rgba);
+    hb_buffer_destroy(hb->buf);
+    hb_font_destroy(hb->font);
+    hb->buf = NULL;
+    hb->font = NULL;
+    hb->glyph_info = NULL;
+    hb->glyph_pos = NULL;
 }
 
-static void update_alpha(DrawTextContext *s)
+static int measure_text(AVFilterContext *ctx, TextMetrics *metrics)
 {
-    double alpha = av_expr_eval(s->a_pexpr, s->var_values, &s->prng);
+    DrawTextContext *s = ctx->priv;
+    char* text = s->expanded_text.str;
+    char *textdup = av_strdup(text), *start = textdup;
+    int num_chars = 0;
+    int width64 = 0, w64 = 0;
+    int cur_min_y64 = 0, first_max_y64 = -32000;
+    int first_min_x64 = 32000, last_max_x64 = -32000;
+    int min_y64 = 32000, max_y64 = -32000, min_x64 = 32000, max_x64 = -32000;
+    int line_count = 0;
+    uint32_t code = 0;
+    Glyph *glyph = NULL;
 
-    if (isnan(alpha))
-        return;
+    int i, tab_idx = 0, last_tab_idx = 0, line_offset = 0;
+    char* p;
+    int ret = 0;
+
+    // Count the lines and the tab characters
+    s->tab_count = 0;
+    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) {
+            ++line_count;
+            if (code == 0) {
+                break;
+            }
+        } else if (code == '\t') {
+            ++s->tab_count;
+        }
+    }
 
-    if (alpha >= 1.0)
-        s->alpha = 255;
-    else if (alpha <= 0)
-        s->alpha = 0;
-    else
-        s->alpha = 256 * alpha;
+    // Evaluate the width of the space character if needed to replace tabs
+    if (s->tab_count > 0 && !s->blank_advance64) {
+        HarfbuzzData hb_data;
+        shape_text_hb(s, &hb_data, " ", 1);
+        s->blank_advance64 = hb_data.glyph_pos[0].x_advance;
+        hb_destroy(&hb_data);
+    }
+
+    s->line_count = line_count;
+    s->lines = av_mallocz(line_count * sizeof(TextLine));
+    s->tab_clusters = av_mallocz(s->tab_count * sizeof(uint32_t));
+    for (i = 0; i < s->tab_count; ++i) {
+        s->tab_clusters[i] = -1;
+    }
+
+    line_count = 0;
+    for (i = 0, p = textdup; 1; i++) {
+        if (*p == '\t') {
+            s->tab_clusters[tab_idx++] = i;
+            *p = ' ';
+        }
+        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_failed2;);
+continue_on_failed2:
+        if (is_newline(code) || code == 0) {
+            TextLine *cur_line = &s->lines[line_count];
+            HarfbuzzData *hb = &cur_line->hb_data;
+            cur_line->cluster_offset = line_offset;
+            shape_text_hb(s, hb, start, num_chars);
+            w64 = 0;
+            cur_min_y64 = 32000;
+            for (int t = 0; t < hb->glyph_count; ++t) {
+                uint8_t is_tab = last_tab_idx < s->tab_count &&
+                    hb->glyph_info[t].cluster == s->tab_clusters[last_tab_idx] - line_offset;
+                if (is_tab) {
+                    ++last_tab_idx;
+                }
+                ret = load_glyph(ctx, &glyph, hb->glyph_info[t].codepoint, -1, -1);
+                if (ret != 0) {
+                    break;
+                }
+                if (line_count == 0) {
+                    first_max_y64 = FFMAX(glyph->bbox.yMax, first_max_y64);
+                }
+                if (t == 0) {
+                    cur_line->offset_left64 = glyph->bbox.xMin;
+                    first_min_x64 = FFMIN(glyph->bbox.xMin, first_min_x64);
+                }
+                if (t == hb->glyph_count - 1) {
+                    w64 += glyph->bbox.xMax;
+                    last_max_x64 = FFMAX(glyph->bbox.xMax, last_max_x64);
+                    cur_line->offset_right64 = glyph->bbox.xMax;
+                } else {
+                    if (is_tab) {
+                        int size = s->blank_advance64 * s->tabsize;
+                        w64 = (w64 / size + 1) * size;
+                    } else {
+                        w64 += hb->glyph_pos[t].x_advance;
+                    }
+                }
+                cur_min_y64 = FFMIN(glyph->bbox.yMin, cur_min_y64);
+                min_y64 = FFMIN(glyph->bbox.yMin, min_y64);
+                max_y64 = FFMAX(glyph->bbox.yMax, max_y64);
+                min_x64 = FFMIN(glyph->bbox.xMin, min_x64);
+                max_x64 = FFMAX(glyph->bbox.xMax, max_x64);
+            }
+
+            if (ret == 0) {
+                cur_line->width64 = w64;
+
+                av_log(s, AV_LOG_DEBUG, "  Line: %d -- glyphs count: %d - width64: %d - offset_left64: %d - offset_right64: %d)\n",
+                    line_count, hb->glyph_count, cur_line->width64, cur_line->offset_left64, cur_line->offset_right64);
+
+                if (w64 > width64) {
+                    width64 = w64;
+                }
+                num_chars = -1;
+                start = p;
+                ++line_count;
+                line_offset = i + 1;
+            }
+        }
+
+        if (code == 0 || ret != 0) break;
+        ++num_chars;
+    }
+
+    if (ret == 0) {
+        int height64;
+        metrics->line_height64 = s->face->size->metrics.height;
+        
+        metrics->width = POS_CEIL(width64, 64);
+        height64 = (metrics->line_height64 + s->line_spacing * 64) *
+            (FFMAX(0, line_count - 1)) + first_max_y64 - cur_min_y64;
+        metrics->height = POS_CEIL(height64, 64);
+
+        metrics->offset_top64 = first_max_y64;
+        metrics->offset_right64 = last_max_x64;
+        metrics->offset_bottom64 = cur_min_y64;
+        metrics->offset_left64 = first_min_x64;
+        metrics->min_x64 = min_x64;
+        metrics->min_y64 = min_y64;
+        metrics->max_x64 = max_x64;
+        metrics->max_y64 = max_y64;
+    }
+
+    av_free(textdup);
+    return ret;
 }
 
-static int draw_text(AVFilterContext *ctx, AVFrame *frame,
-                     int width, int height)
+static int draw_text(AVFilterContext *ctx, AVFrame *frame)
 {
     DrawTextContext *s = ctx->priv;
     AVFilterLink *inlink = ctx->inputs[0];
-
-    uint32_t code = 0, prev_code = 0;
-    int x = 0, y = 0, i = 0, ret;
-    int max_text_line_w = 0, len;
-    int box_w, box_h;
-    char *text;
-    uint8_t *p;
-    int y_min = 32000, y_max = -32000;
-    int x_min = 32000, x_max = -32000;
-    FT_Vector delta;
-    Glyph *glyph = NULL, *prev_glyph = NULL;
-    Glyph dummy = { 0 };
+    int x = 0, y = 0, ret;
+    int shift_x64, shift_y64;
+    int x64, y64;
+    Glyph *glyph = NULL;
 
     time_t now = time(0);
     struct tm ltime;
@@ -1413,9 +1709,17 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame,
     FFDrawColor bordercolor;
     FFDrawColor boxcolor;
 
+    int width = frame->width;
+    int height = frame->height;
+    int rec_x = 0, rec_y = 0, rec_width = 0, rec_height = 0;
+    int is_outside = 0;
+    int last_tab_idx = 0;
+
+    TextMetrics metrics;
+
     av_bprint_clear(bp);
 
-    if(s->basetime != AV_NOPTS_VALUE)
+    if (s->basetime != AV_NOPTS_VALUE)
         now= frame->pts*av_q2d(ctx->inputs[0]->time_base) + s->basetime/1000000;
 
     switch (s->exp_mode) {
@@ -1441,13 +1745,6 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame,
 
     if (!av_bprint_is_complete(bp))
         return AVERROR(ENOMEM);
-    text = s->expanded_text.str;
-    if ((len = s->expanded_text.len) > s->nb_positions) {
-        if (!(s->positions =
-              av_realloc(s->positions, len*sizeof(*s->positions))))
-            return AVERROR(ENOMEM);
-        s->nb_positions = len;
-    }
 
     if (s->fontcolor_expr[0]) {
         /* If expression is set, evaluate and replace the static value */
@@ -1463,85 +1760,24 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame,
         ff_draw_color(&s->dc, &s->fontcolor, s->fontcolor.rgba);
     }
 
-    x = 0;
-    y = 0;
-
-    if ((ret = update_fontsize(ctx)) < 0)
+    if ((ret = update_fontsize(ctx)) < 0) {
         return ret;
-
-    /* load and cache glyphs */
-    for (i = 0, p = text; *p; i++) {
-        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_invalid;);
-continue_on_invalid:
-
-        /* get glyph */
-        dummy.code = code;
-        dummy.fontsize = s->fontsize;
-        glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
-        if (!glyph) {
-            ret = load_glyph(ctx, &glyph, code);
-            if (ret < 0)
-                return ret;
-        }
-
-        y_min = FFMIN(glyph->bbox.yMin, y_min);
-        y_max = FFMAX(glyph->bbox.yMax, y_max);
-        x_min = FFMIN(glyph->bbox.xMin, x_min);
-        x_max = FFMAX(glyph->bbox.xMax, x_max);
     }
-    s->max_glyph_h = y_max - y_min;
-    s->max_glyph_w = x_max - x_min;
-
-    /* compute and save position for each glyph */
-    glyph = NULL;
-    for (i = 0, p = text; *p; i++) {
-        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_invalid2;);
-continue_on_invalid2:
-
-        /* skip the \n in the sequence \r\n */
-        if (prev_code == '\r' && code == '\n')
-            continue;
-
-        prev_code = code;
-        if (is_newline(code)) {
-
-            max_text_line_w = FFMAX(max_text_line_w, x);
-            y += s->max_glyph_h + s->line_spacing;
-            x = 0;
-            continue;
-        }
-
-        /* get glyph */
-        prev_glyph = glyph;
-        dummy.code = code;
-        dummy.fontsize = s->fontsize;
-        glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
-
-        /* kerning */
-        if (s->use_kerning && prev_glyph && glyph->code) {
-            FT_Get_Kerning(s->face, prev_glyph->code, glyph->code,
-                           ft_kerning_default, &delta);
-            x += delta.x >> 6;
-        }
 
-        /* save position */
-        s->positions[i].x = x + glyph->bitmap_left;
-        s->positions[i].y = y - glyph->bitmap_top + y_max;
-        if (code == '\t') x  = (x / s->tabsize + 1)*s->tabsize;
-        else              x += glyph->advance;
-    }
+    measure_text(ctx, &metrics);
 
-    max_text_line_w = FFMAX(x, max_text_line_w);
+    s->max_glyph_h = POS_CEIL(metrics.max_y64 - metrics.min_y64, 64);
+    s->max_glyph_w = POS_CEIL(metrics.max_x64 - metrics.min_x64, 64);
 
-    s->var_values[VAR_TW] = s->var_values[VAR_TEXT_W] = max_text_line_w;
-    s->var_values[VAR_TH] = s->var_values[VAR_TEXT_H] = y + s->max_glyph_h;
+    s->var_values[VAR_TW] = s->var_values[VAR_TEXT_W] = metrics.width;
+    s->var_values[VAR_TH] = s->var_values[VAR_TEXT_H] = metrics.height;
 
     s->var_values[VAR_MAX_GLYPH_W] = s->max_glyph_w;
     s->var_values[VAR_MAX_GLYPH_H] = s->max_glyph_h;
-    s->var_values[VAR_MAX_GLYPH_A] = s->var_values[VAR_ASCENT ] = y_max;
-    s->var_values[VAR_MAX_GLYPH_D] = s->var_values[VAR_DESCENT] = y_min;
+    s->var_values[VAR_MAX_GLYPH_A] = s->var_values[VAR_ASCENT] = POS_CEIL(metrics.max_y64, 64);
+    s->var_values[VAR_MAX_GLYPH_D] = s->var_values[VAR_DESCENT] = POS_CEIL(metrics.min_y64, 64);
 
-    s->var_values[VAR_LINE_H] = s->var_values[VAR_LH] = s->max_glyph_h;
+    s->var_values[VAR_LINE_H] = s->var_values[VAR_LH] = metrics.line_height64 / 64.;
 
     if (s->text_source == AV_FRAME_DATA_DETECTION_BBOXES) {
         s->var_values[VAR_X] = s->x;
@@ -1559,56 +1795,142 @@ continue_on_invalid2:
     update_color_with_alpha(s, &bordercolor, s->bordercolor);
     update_color_with_alpha(s, &boxcolor   , s->boxcolor   );
 
-    box_w = max_text_line_w;
-    box_h = y + s->max_glyph_h;
+    if (s->draw_box && s->boxborderw) {
+        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = s->boxborderw;
+    } else {
+        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = 0;
+    }
 
     if (s->fix_bounds) {
-
         /* calculate footprint of text effects */
-        int boxoffset     = s->draw_box ? FFMAX(s->boxborderw, 0) : 0;
         int borderoffset  = s->borderw  ? FFMAX(s->borderw, 0) : 0;
 
-        int offsetleft = FFMAX3(boxoffset, borderoffset,
+        int offsetleft = FFMAX3(FFMAX(s->bb_left, 0), borderoffset,
                                 (s->shadowx < 0 ? FFABS(s->shadowx) : 0));
-        int offsettop = FFMAX3(boxoffset, borderoffset,
+        int offsettop = FFMAX3(FFMAX(s->bb_top, 0), borderoffset,
                                 (s->shadowy < 0 ? FFABS(s->shadowy) : 0));
-
-        int offsetright = FFMAX3(boxoffset, borderoffset,
+        int offsetright = FFMAX3(FFMAX(s->bb_right, 0), borderoffset,
                                  (s->shadowx > 0 ? s->shadowx : 0));
-        int offsetbottom = FFMAX3(boxoffset, borderoffset,
+        int offsetbottom = FFMAX3(FFMAX(s->bb_bottom, 0), borderoffset,
                                   (s->shadowy > 0 ? s->shadowy : 0));
 
-
         if (s->x - offsetleft < 0) s->x = offsetleft;
         if (s->y - offsettop < 0)  s->y = offsettop;
 
-        if (s->x + box_w + offsetright > width)
-            s->x = FFMAX(width - box_w - offsetright, 0);
-        if (s->y + box_h + offsetbottom > height)
-            s->y = FFMAX(height - box_h - offsetbottom, 0);
+        if (s->x + metrics.width + offsetright > width)
+            s->x = FFMAX(width - metrics.width - offsetright, 0);
+        if (s->y + metrics.height + offsetbottom > height)
+            s->y = FFMAX(height - metrics.height - offsetbottom, 0);
     }
 
-    /* draw box */
-    if (s->draw_box)
-        ff_blend_rectangle(&s->dc, &boxcolor,
-                           frame->data, frame->linesize, width, height,
-                           s->x - s->boxborderw, s->y - s->boxborderw,
-                           box_w + s->boxborderw * 2, box_h + s->boxborderw * 2);
+    x = 0;
+    y = 0;
+    x64 = (int)(s->x * 64.);
+    y64 = (int)(s->y * 64. + metrics.offset_top64);
+
+    for (int l = 0; l < s->line_count; ++l) {
+        TextLine *line = &s->lines[l];
+        HarfbuzzData *hb = &line->hb_data;
+        line->glyphs = av_mallocz(hb->glyph_count * sizeof(GlyphInfo));
+
+        for (int t = 0; t < hb->glyph_count; ++t) {
+            GlyphInfo *g_info = &line->glyphs[t];
+            uint8_t is_tab = last_tab_idx < s->tab_count &&
+                hb->glyph_info[t].cluster == s->tab_clusters[last_tab_idx] - line->cluster_offset;
+            int true_x, true_y;
+            if (is_tab) {
+                ++last_tab_idx;
+            }
+            true_x = x + hb->glyph_pos[t].x_offset;
+            true_y = y + hb->glyph_pos[t].y_offset;
+            shift_x64 = (((x64 + true_x) >> 4) & 0b0011) << 4;
+            shift_y64 = ((4 - (((y64 + true_y) >> 4) & 0b0011)) & 0b0011) << 4;
+
+            ret = load_glyph(ctx, &glyph, hb->glyph_info[t].codepoint, shift_x64, shift_y64);
+            if (ret != 0) {
+                return ret;
+            }
+            g_info->code = hb->glyph_info[t].codepoint;
+            g_info->x = (x64 + true_x) >> 6;
+            g_info->y = ((y64 + true_y) >> 6) + (shift_y64 > 0 ? 1 : 0);
+            g_info->shift_x64 = shift_x64;
+            g_info->shift_y64 = shift_y64;
+
+            if (!is_tab) {
+                x += hb->glyph_pos[t].x_advance;
+            } else {
+                int size = s->blank_advance64 * s->tabsize;
+                x = (x / size + 1) * size;
+            }
+            y += hb->glyph_pos[t].y_advance;
+        }
 
-    if (s->shadowx || s->shadowy) {
-        if ((ret = draw_glyphs(s, frame, width, height,
-                               &shadowcolor, s->shadowx, s->shadowy, 0)) < 0)
-            return ret;
+        y += metrics.line_height64 + s->line_spacing * 64;
+        x = 0;
     }
 
-    if (s->borderw) {
-        if ((ret = draw_glyphs(s, frame, width, height,
-                               &bordercolor, 0, 0, s->borderw)) < 0)
+    metrics.rect_x = s->x;
+    metrics.rect_y = s->y;
+    
+    s->box_width = metrics.width;
+    s->box_height = metrics.height;
+
+    if (!s->draw_box) {
+        // Create a border for the clipping region to take into account subpixel
+        // errors in text measurement and effects.
+        int borderoffset = s->borderw ? FFMAX(s->borderw, 0) : 0;
+        s->bb_left = borderoffset + (s->shadowx < 0 ? FFABS(s->shadowx) : 0) + 1;
+        s->bb_top = borderoffset + (s->shadowy < 0 ? FFABS(s->shadowy) : 0) + 1;
+        s->bb_right = borderoffset + (s->shadowx > 0 ? s->shadowx : 0) + 1;
+        s->bb_bottom = borderoffset + (s->shadowy > 0 ? s->shadowy : 0) + 1;
+    }
+
+    /* Check if the whole box is out of the frame */        
+    is_outside = metrics.rect_x - s->bb_left >= width ||
+                    metrics.rect_y - s->bb_top >= height ||
+                    metrics.rect_x + s->box_width + s->bb_right <= 0 ||
+                    metrics.rect_y + s->box_height + s->bb_bottom <= 0;
+
+    if (!is_outside) {
+        /* draw box */
+        if (s->draw_box) {
+            rec_x = metrics.rect_x - s->bb_left;
+            rec_y = metrics.rect_y - s->bb_top;
+            rec_width = s->box_width + s->bb_right + s->bb_left;
+            rec_height = s->box_height + s->bb_bottom + s->bb_top;
+            ff_blend_rectangle(&s->dc, &boxcolor,
+                frame->data, frame->linesize, width, height,
+                rec_x, rec_y, rec_width, rec_height);
+        }
+
+        if (s->shadowx || s->shadowy) {
+            if ((ret = draw_glyphs(s, frame, &shadowcolor, &metrics,
+                    s->shadowx, s->shadowy, s->borderw)) < 0) {
+                return ret;
+            }
+        }
+
+        if (s->borderw) {
+            if ((ret = draw_glyphs(s, frame, &bordercolor, &metrics,
+                    0, 0, s->borderw)) < 0) {
+                return ret;
+            }
+        }
+
+        if ((ret = draw_glyphs(s, frame, &fontcolor, &metrics, 0,
+                0, 0)) < 0) {
             return ret;
+        }
     }
-    if ((ret = draw_glyphs(s, frame, width, height,
-                           &fontcolor, 0, 0, 0)) < 0)
-        return ret;
+
+    // FREE data structures
+    for (int l = 0; l < s->line_count; ++l) {
+        TextLine *line = &s->lines[l];
+        av_freep(&line->glyphs);
+        hb_destroy(&line->hb_data);
+    }
+    av_freep(&s->lines);
+    av_freep(&s->tab_clusters);
 
     return 0;
 }
@@ -1680,14 +2002,9 @@ FF_ENABLE_DEPRECATION_WARNINGS
             s->x = bbox->x;
             s->y = bbox->y - s->fontsize;
         }
-        draw_text(ctx, frame, frame->width, frame->height);
+        draw_text(ctx, frame);
     }
 
-    av_log(ctx, AV_LOG_DEBUG, "n:%d t:%f text_w:%d text_h:%d x:%d y:%d\n",
-           (int)s->var_values[VAR_N], s->var_values[VAR_T],
-           (int)s->var_values[VAR_TEXT_W], (int)s->var_values[VAR_TEXT_H],
-           s->x, s->y);
-
     return ff_filter_frame(outlink, frame);
 }
Francesco Carusi March 14, 2023, 3:58 p.m. UTC | #14
Can I help in any way in advancing this patch?

On 03/02/2023 15:18, Francesco Carusi wrote:
>
>
> On 30/01/2023 13:19, Paul B Mahol wrote:
>> On 1/30/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>> On 28/01/2023 16:32, Paul B Mahol wrote:
>>>> On 1/28/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>> On 27/01/2023 18:31, Paul B Mahol wrote:
>>>>>> On 1/27/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>> On 26/01/2023 17:37, Paul B Mahol wrote:
>>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>>>> On 26/01/2023 14:21, Paul B Mahol wrote:
>>>>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>>>>>> The drawtext reinit command is also used in the docs as an 
>>>>>>>>>>> example
>>>>>>>>>>> for
>>>>>>>>>>> the sendcmd filter, so I thought it was fine to use commands in
>>>>>>>>>>> that
>>>>>>>>>>> way. In my opinion it is also a convenient way to modify 
>>>>>>>>>>> multiple
>>>>>>>>>>> options at the same time.
>>>>>>>>>>> Should the command match the name of a filter option instead?
>>>>>>>>>>>
>>>>>>>>>> Please do not top post.
>>>>>>>>>>
>>>>>>>>>> It is much better to use already existing options for 
>>>>>>>>>> commands that
>>>>>>>>>> is
>>>>>>>>>> more intuitive to users. Also multiple options can be set at
>>>>>>>>>> runtime,
>>>>>>>>>> there is no such limitation.
>>>>>>>>> ok, I'm going to remove the "change" command and add commands 
>>>>>>>>> that
>>>>>>>>> match
>>>>>>>>> the options that it included.
>>>>>>>> Thanks, feel free to ask questions on #ffmpeg-devel irc channel.
>>>>>>> I'm attaching the updated patch, I also updated the document at
>>>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
>>>>>>> Thanks
>>>>>> Amazing, I like demos!
>>>>>>
>>>>>> Could improve code style of newly added/changed lines?
>>>>>> For example opening { put on separate line. So code style is in sync
>>>>>> with rest of codebase.
>>>>> Sure, I'll put opening { on a new line for functions, not for control
>>>>> statements, like in the rest of the code. Is it fine?
>>>> Yes. Thanks.
>>>>
>>>>>> The commands stuff does not need to use strcmp to detect if option
>>>>>> value have been changed, you could avoid strcmp by just caching old
>>>>>> value prior to calling function the picks new values, and after that
>>>>>> just compare old with new and then if it differs call needed code.
>>>>> I'll cache the numeric values. I think that caching string values 
>>>>> is not
>>>>> the preferred solution because in addition to the strcmp needed to 
>>>>> check
>>>>> the value, it would also need a strdup to cache the previous 
>>>>> value, even
>>>>> when the command does not involve those options. Does it sound
>>>>> reasonable?
>>>> Yes.
>>> I'm attaching the patch that includes the changes we discussed.
>> space between 'for' and '('
>>
>> Do not keep old code in comments if its no longer relevant or working.
>
> Ok I added spaces between control statements (if, for, while) and '(', 
> and also cleaned up comments.
> Following Anton Khirnov suggestion I tried to split the changes into 
> multiple commits. However the first one is quite bit since it contains 
> a major change in how the filter works and cannot be split further.
> Patches attached.
>
>>>>>>>>>>> On 26/01/2023 11:50, Paul B Mahol wrote:
>>>>>>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
>>>>>>>>>>>>> Hi, I'm new to contributing to ffmpeg!
>>>>>>>>>>>>>
>>>>>>>>>>>>> I modified the drawtext filter to improve text rendering 
>>>>>>>>>>>>> and add
>>>>>>>>>>>>> some
>>>>>>>>>>>>> features. You can find a high level description of the 
>>>>>>>>>>>>> changes
>>>>>>>>>>>>> at
>>>>>>>>>>>>> this
>>>>>>>>>>>>> link:
>>>>>>>>>>>>>
>>>>>>>>>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md 
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> I'm also attaching the patch file.
>>>>>>>>>>>>> I looked for the filter maintainer to discuss about the 
>>>>>>>>>>>>> changes
>>>>>>>>>>>>> I
>>>>>>>>>>>>> made
>>>>>>>>>>>>> but it looks like there isn't any, am I correct?
>>>>>>>>>>>>>
>>>>>>>>>>>>> Please let me know if this is the right way to submit my
>>>>>>>>>>>>> contribution.
>>>>>>>>>>>> Why filter can not support normal commands for options? Like
>>>>>>>>>>>> most/all
>>>>>>>>>>>> other filters that have support for changing options values at
>>>>>>>>>>>> runtime.
>>>>>>>>>>>>
>>>>>>>>>>>> The reinit and yours added change option(s) are very
>>>>>>>>>>>> strange/inconvenient things to do.
>>>>>>>>>>>> _______________________________________________
>>>>>>>>>>>> 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".
>>>>>>>>>>>
>>>>>>>>>> _______________________________________________
>>>>>>>>>> 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".
>>>>>>>>>
>>>>>>>> _______________________________________________
>>>>>>>> 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".
>>>>> _______________________________________________
>>>>> 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".
>> _______________________________________________
>> 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".
Paul B Mahol March 14, 2023, 4:07 p.m. UTC | #15
On Tue, Mar 14, 2023 at 4:59 PM Francesco Carusi <klimklim@tiscali.it>
wrote:

> Can I help in any way in advancing this patch?
>

I will apply it if nobody objects in next 48h.


>
> On 03/02/2023 15:18, Francesco Carusi wrote:
> >
> >
> > On 30/01/2023 13:19, Paul B Mahol wrote:
> >> On 1/30/23, Francesco Carusi <klimklim@tiscali.it> wrote:
> >>> On 28/01/2023 16:32, Paul B Mahol wrote:
> >>>> On 1/28/23, Francesco Carusi <klimklim@tiscali.it> wrote:
> >>>>> On 27/01/2023 18:31, Paul B Mahol wrote:
> >>>>>> On 1/27/23, Francesco Carusi <klimklim@tiscali.it> wrote:
> >>>>>>> On 26/01/2023 17:37, Paul B Mahol wrote:
> >>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
> >>>>>>>>> On 26/01/2023 14:21, Paul B Mahol wrote:
> >>>>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
> >>>>>>>>>>> The drawtext reinit command is also used in the docs as an
> >>>>>>>>>>> example
> >>>>>>>>>>> for
> >>>>>>>>>>> the sendcmd filter, so I thought it was fine to use commands in
> >>>>>>>>>>> that
> >>>>>>>>>>> way. In my opinion it is also a convenient way to modify
> >>>>>>>>>>> multiple
> >>>>>>>>>>> options at the same time.
> >>>>>>>>>>> Should the command match the name of a filter option instead?
> >>>>>>>>>>>
> >>>>>>>>>> Please do not top post.
> >>>>>>>>>>
> >>>>>>>>>> It is much better to use already existing options for
> >>>>>>>>>> commands that
> >>>>>>>>>> is
> >>>>>>>>>> more intuitive to users. Also multiple options can be set at
> >>>>>>>>>> runtime,
> >>>>>>>>>> there is no such limitation.
> >>>>>>>>> ok, I'm going to remove the "change" command and add commands
> >>>>>>>>> that
> >>>>>>>>> match
> >>>>>>>>> the options that it included.
> >>>>>>>> Thanks, feel free to ask questions on #ffmpeg-devel irc channel.
> >>>>>>> I'm attaching the updated patch, I also updated the document at
> >>>>>>> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
> >>>>>>> Thanks
> >>>>>> Amazing, I like demos!
> >>>>>>
> >>>>>> Could improve code style of newly added/changed lines?
> >>>>>> For example opening { put on separate line. So code style is in sync
> >>>>>> with rest of codebase.
> >>>>> Sure, I'll put opening { on a new line for functions, not for control
> >>>>> statements, like in the rest of the code. Is it fine?
> >>>> Yes. Thanks.
> >>>>
> >>>>>> The commands stuff does not need to use strcmp to detect if option
> >>>>>> value have been changed, you could avoid strcmp by just caching old
> >>>>>> value prior to calling function the picks new values, and after that
> >>>>>> just compare old with new and then if it differs call needed code.
> >>>>> I'll cache the numeric values. I think that caching string values
> >>>>> is not
> >>>>> the preferred solution because in addition to the strcmp needed to
> >>>>> check
> >>>>> the value, it would also need a strdup to cache the previous
> >>>>> value, even
> >>>>> when the command does not involve those options. Does it sound
> >>>>> reasonable?
> >>>> Yes.
> >>> I'm attaching the patch that includes the changes we discussed.
> >> space between 'for' and '('
> >>
> >> Do not keep old code in comments if its no longer relevant or working.
> >
> > Ok I added spaces between control statements (if, for, while) and '(',
> > and also cleaned up comments.
> > Following Anton Khirnov suggestion I tried to split the changes into
> > multiple commits. However the first one is quite bit since it contains
> > a major change in how the filter works and cannot be split further.
> > Patches attached.
> >
> >>>>>>>>>>> On 26/01/2023 11:50, Paul B Mahol wrote:
> >>>>>>>>>>>> On 1/26/23, Francesco Carusi <klimklim@tiscali.it> wrote:
> >>>>>>>>>>>>> Hi, I'm new to contributing to ffmpeg!
> >>>>>>>>>>>>>
> >>>>>>>>>>>>> I modified the drawtext filter to improve text rendering
> >>>>>>>>>>>>> and add
> >>>>>>>>>>>>> some
> >>>>>>>>>>>>> features. You can find a high level description of the
> >>>>>>>>>>>>> changes
> >>>>>>>>>>>>> at
> >>>>>>>>>>>>> this
> >>>>>>>>>>>>> link:
> >>>>>>>>>>>>>
> >>>>>>>>>>>>>
> https://github.com/yethie/FFmpeg/blob/master/drawtext/CHANGES.md
> >>>>>>>>>>>>>
> >>>>>>>>>>>>>
> >>>>>>>>>>>>> I'm also attaching the patch file.
> >>>>>>>>>>>>> I looked for the filter maintainer to discuss about the
> >>>>>>>>>>>>> changes
> >>>>>>>>>>>>> I
> >>>>>>>>>>>>> made
> >>>>>>>>>>>>> but it looks like there isn't any, am I correct?
> >>>>>>>>>>>>>
> >>>>>>>>>>>>> Please let me know if this is the right way to submit my
> >>>>>>>>>>>>> contribution.
> >>>>>>>>>>>> Why filter can not support normal commands for options? Like
> >>>>>>>>>>>> most/all
> >>>>>>>>>>>> other filters that have support for changing options values at
> >>>>>>>>>>>> runtime.
> >>>>>>>>>>>>
> >>>>>>>>>>>> The reinit and yours added change option(s) are very
> >>>>>>>>>>>> strange/inconvenient things to do.
> >>>>>>>>>>>> _______________________________________________
> >>>>>>>>>>>> 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".
> >>>>>>>>>>>
> >>>>>>>>>> _______________________________________________
> >>>>>>>>>> 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".
> >>>>>>>>>
> >>>>>>>> _______________________________________________
> >>>>>>>> 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".
> >>>>> _______________________________________________
> >>>>> 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".
> >> _______________________________________________
> >> 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".
>
> _______________________________________________
> 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".
>
Anton Khirnov March 16, 2023, 4:52 p.m. UTC | #16
Overall this patch could use a lot more polish. I really wish you
developed it in a more review-friendly form.
The commit message should use standard form and elaborate on what
advantage do we get from this huge change, which also adds a new
dependendency.


Quoting Francesco Carusi (2023-02-03 15:18:21)
> diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c
> index 50012bb258..7a0a255c5e 100644
> --- a/libavfilter/vf_drawtext.c
> +++ b/libavfilter/vf_drawtext.c

You are adding a bunch of trailing whitespace in the file, which is
forbidden.

> @@ -1,4 +1,5 @@
>  /*
> + * Copyright (c) 2023 Francesco Carusi
>   * Copyright (c) 2011 Stefano Sabatini
>   * Copyright (c) 2010 S.N. Hemanth Meenakshisundaram
>   * Copyright (c) 2003 Gustavo Sverzut Barbieri <gsbarbieri@yahoo.com.br>
> @@ -20,6 +21,14 @@
>   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
>   */
>  
> +/*
> + * Changelog - 2023
> + *
> + * - This filter now depends on libharfbuzz for text shaping.
> + * - Glyphs position is now accurate to 1/4 pixel in both directions
> + * - The default line height is now the one defined in the font
> + */

This is not the place for a changelog, that's what commit messages and
the Changelog file are for.

> +
>  /**
>   * @file
>   * drawtext filter, based on the original vhook/drawtext.c
> @@ -72,14 +81,20 @@
>  #include FT_GLYPH_H
>  #include FT_STROKER_H
>  
> +#include <hb.h>
> +#include <hb-ft.h>
> +
> +// Ceiling operation for positive integers division
> +#define POS_CEIL(x, y) ((x)/(y) + ((x)%(y) != 0))
> +
>  static const char *const var_names[] = {
>      "dar",
>      "hsub", "vsub",
> -    "line_h", "lh",           ///< line height, same as max_glyph_h
> +    "line_h", "lh",           ///< line height
>      "main_h", "h", "H",       ///< height of the input video
>      "main_w", "w", "W",       ///< width  of the input video
> -    "max_glyph_a", "ascent",  ///< max glyph ascent
> -    "max_glyph_d", "descent", ///< min glyph descent
> +    "max_glyph_a", "ascent",  ///< max glyph ascender
> +    "max_glyph_d", "descent", ///< min glyph descender

Seems like this should not be in this patch.

>  static const AVOption drawtext_options[]= {
> -    {"fontfile",    "set font file",        OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> -    {"text",        "set text",             OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> -    {"textfile",    "set text file",        OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> -    {"fontcolor",   "set foreground color", OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
> +    {"fontfile",       "set font file",         OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> +    {"text",           "set text",              OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> +    {"textfile",       "set text file",         OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> +    {"fontcolor",      "set foreground color",  OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
>      {"fontcolor_expr", "set foreground color expression", OFFSET(fontcolor_expr), AV_OPT_TYPE_STRING, {.str=""}, 0, 0, FLAGS},
> -    {"boxcolor",    "set box color",        OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
> -    {"bordercolor", "set border color",     OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
> -    {"shadowcolor", "set shadow color",     OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
> -    {"box",         "set box",              OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0,        1       , FLAGS},
> -    {"boxborderw",  "set box border width", OFFSET(boxborderw),         AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
> -    {"line_spacing",  "set line spacing in pixels", OFFSET(line_spacing),   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX,FLAGS},
> -    {"fontsize",    "set font size",        OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0 , FLAGS},
> -    {"x",           "set x expression",     OFFSET(x_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
> -    {"y",           "set y expression",     OFFSET(y_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
> -    {"shadowx",     "set shadow x offset",  OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
> -    {"shadowy",     "set shadow y offset",  OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
> -    {"borderw",     "set border width",     OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
> -    {"tabsize",     "set tab size",         OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0,        INT_MAX , FLAGS},
> -    {"basetime",    "set base time",        OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX , FLAGS},
> +    {"boxcolor",       "set box color",         OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
> +    {"bordercolor",    "set border color",      OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
> +    {"shadowcolor",    "set shadow color",      OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
> +    {"box",            "set box",               OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0, 1, FLAGS},
> +    {"boxborderw",     "set box borders width", OFFSET(boxborderw),         AV_OPT_TYPE_INT,    {.i64=0},     0, INT_MAX, FLAGS},
> +    {"line_spacing",   "set line spacing in pixels", OFFSET(line_spacing),  AV_OPT_TYPE_INT,    {.i64=-1},    INT_MIN, INT_MAX, FLAGS},
> +    {"fontsize",       "set font size",         OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> +    {"x",              "set x expression",      OFFSET(x_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
> +    {"y",              "set y expression",      OFFSET(y_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
> +    {"shadowx",        "set shadow x offset",   OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
> +    {"shadowy",        "set shadow y offset",   OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
> +    {"borderw",        "set border width",      OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
> +    {"tabsize",        "set tab size",          OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0, INT_MAX, FLAGS},
> +    {"basetime",       "set base time",         OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX, FLAGS},
>  #if CONFIG_LIBFONTCONFIG
>      { "font",        "Font name",            OFFSET(font),               AV_OPT_TYPE_STRING, { .str = "Sans" },           .flags = FLAGS },
>  #endif
> @@ -248,15 +338,15 @@ static const AVOption drawtext_options[]= {
>          {"strftime", "set strftime expansion (deprecated)", OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_STRFTIME}, 0, 0, FLAGS, "expansion"},
>  
>      {"timecode",        "set initial timecode",             OFFSET(tc_opt_string), AV_OPT_TYPE_STRING,   {.str=NULL}, 0, 0, FLAGS},
> -    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},           0,        1, FLAGS},
> -    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
> -    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
> -    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
> -    {"reload",     "reload text file at specified frame interval", OFFSET(reload),     AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
> -    { "alpha",       "apply alpha while rendering", OFFSET(a_expr),      AV_OPT_TYPE_STRING, { .str = "1"     },          .flags = FLAGS },
> -    {"fix_bounds", "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
> -    {"start_number", "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
> -    {"text_source", "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
> +    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},    0, 1, FLAGS},
> +    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
> +    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
> +    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
> +    {"reload",          "reload text file at specified frame interval", OFFSET(reload), AV_OPT_TYPE_INT, {.i64=0},    0, INT_MAX, FLAGS},
> +    {"alpha",           "apply alpha while rendering",      OFFSET(a_expr),        AV_OPT_TYPE_STRING,   {.str = "1"}, .flags = FLAGS},
> +    {"fix_bounds",      "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
> +    {"start_number",    "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
> +    {"text_source",     "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },

Most of these changes seem cosmetic, they do not belong in this patch.

>  
>  #if CONFIG_LIBFRIBIDI
>      {"text_shaping", "attempt to shape text before drawing", OFFSET(text_shaping), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS},
> @@ -297,18 +387,24 @@ static const struct ft_error {
>  
>  #define FT_ERRMSG(e) ft_errors[e].err_msg
>  
> -typedef struct Glyph {
> -    FT_Glyph glyph;
> -    FT_Glyph border_glyph;
> -    uint32_t code;
> -    unsigned int fontsize;
> -    FT_Bitmap bitmap; ///< array holding bitmaps of font
> -    FT_Bitmap border_bitmap; ///< array holding bitmaps of font border
> -    FT_BBox bbox;
> -    int advance;
> -    int bitmap_left;
> -    int bitmap_top;
> -} Glyph;
> +
> +// Loads and (optionally) renders a glyph
> +static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code,
> +     int8_t shift_x64, int8_t shift_y64);
> +
> +// Shapes a line of text using libharfbuzz
> +static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const char* text, int textLen);
> +
> +// Performs text measurements
> +static int measure_text(AVFilterContext *ctx, TextMetrics *metrics);
> +
> +// Draws glyphs on the frame
> +static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
> +                       FFDrawColor *color, TextMetrics *metrics,
> +                       int x, int y, int borderw);
> +
> +// Draws text on the frame
> +static int draw_text(AVFilterContext *ctx, AVFrame *frame);

Why is there a need for forward declarations?

>  static int glyph_cmp(const void *key, const void *b)
>  {
> @@ -316,80 +412,9 @@ static int glyph_cmp(const void *key, const void *b)
>      int64_t diff = (int64_t)a->code - (int64_t)bb->code;
>  
>      if (diff != 0)
> -         return diff > 0 ? 1 : -1;
> +        return diff > 0 ? 1 : -1;

unrelated cosmetics

> @@ -439,7 +464,6 @@ static av_cold int update_fontsize(AVFilterContext *ctx)
>             return err;
>  
>          size = av_expr_eval(s->fontsize_pexpr, s->var_values, &s->prng);
> -

unrelated cosmetics

>          if (!isnan(size)) {
>              roundedsize = round(size);
>              // test for overflow before cast
> @@ -447,7 +471,6 @@ static av_cold int update_fontsize(AVFilterContext *ctx)
>                  av_log(ctx, AV_LOG_ERROR, "fontsize overflow\n");
>                  return AVERROR(EINVAL);
>              }
> -

unrelated cosmetics

>              fontsize = roundedsize;
>          }
>      }
> @@ -548,7 +571,7 @@ static int load_font_fontconfig(AVFilterContext *ctx)
>          goto fail;
>      }
>  
> -    av_log(ctx, AV_LOG_INFO, "Using \"%s\"\n", filename);
> +    av_log(ctx, AV_LOG_VERBOSE, "Using \"%s\"\n", filename);

unrelated cosmetics

>      if (parse_err)
>          s->default_fontsize = size + 0.5;
>  
> @@ -690,6 +713,7 @@ static int shape_text(AVFilterContext *ctx)
>      s->text = tmp;
>      len = fribidi_unicode_to_charset(FRIBIDI_CHAR_SET_UTF8,
>                                       unicodestr, len, s->text);
> +

unrelated cosmetics

>  
> +static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const char* text, int textLen) 
> +{
> +    hb->buf = hb_buffer_create();

Seems like it at least this needs to be checked for errors, maybe some
of the other calls too.

> @@ -1559,56 +1795,142 @@ continue_on_invalid2:
>      update_color_with_alpha(s, &bordercolor, s->bordercolor);
>      update_color_with_alpha(s, &boxcolor   , s->boxcolor   );
>  
> -    box_w = max_text_line_w;
> -    box_h = y + s->max_glyph_h;
> +    if (s->draw_box && s->boxborderw) {
> +        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = s->boxborderw;
> +    } else {
> +        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = 0;
> +    }
>  
>      if (s->fix_bounds) {
> -
>          /* calculate footprint of text effects */
> -        int boxoffset     = s->draw_box ? FFMAX(s->boxborderw, 0) : 0;
>          int borderoffset  = s->borderw  ? FFMAX(s->borderw, 0) : 0;
>  
> -        int offsetleft = FFMAX3(boxoffset, borderoffset,
> +        int offsetleft = FFMAX3(FFMAX(s->bb_left, 0), borderoffset,
>                                  (s->shadowx < 0 ? FFABS(s->shadowx) : 0));
> -        int offsettop = FFMAX3(boxoffset, borderoffset,
> +        int offsettop = FFMAX3(FFMAX(s->bb_top, 0), borderoffset,
>                                  (s->shadowy < 0 ? FFABS(s->shadowy) : 0));
> -
> -        int offsetright = FFMAX3(boxoffset, borderoffset,
> +        int offsetright = FFMAX3(FFMAX(s->bb_right, 0), borderoffset,
>                                   (s->shadowx > 0 ? s->shadowx : 0));
> -        int offsetbottom = FFMAX3(boxoffset, borderoffset,
> +        int offsetbottom = FFMAX3(FFMAX(s->bb_bottom, 0), borderoffset,
>                                    (s->shadowy > 0 ? s->shadowy : 0));
>  
> -
>          if (s->x - offsetleft < 0) s->x = offsetleft;
>          if (s->y - offsettop < 0)  s->y = offsettop;
>  
> -        if (s->x + box_w + offsetright > width)
> -            s->x = FFMAX(width - box_w - offsetright, 0);
> -        if (s->y + box_h + offsetbottom > height)
> -            s->y = FFMAX(height - box_h - offsetbottom, 0);
> +        if (s->x + metrics.width + offsetright > width)
> +            s->x = FFMAX(width - metrics.width - offsetright, 0);
> +        if (s->y + metrics.height + offsetbottom > height)
> +            s->y = FFMAX(height - metrics.height - offsetbottom, 0);
>      }
>  
> -    /* draw box */
> -    if (s->draw_box)
> -        ff_blend_rectangle(&s->dc, &boxcolor,
> -                           frame->data, frame->linesize, width, height,
> -                           s->x - s->boxborderw, s->y - s->boxborderw,
> -                           box_w + s->boxborderw * 2, box_h + s->boxborderw * 2);
> +    x = 0;
> +    y = 0;
> +    x64 = (int)(s->x * 64.);
> +    y64 = (int)(s->y * 64. + metrics.offset_top64);
> +
> +    for (int l = 0; l < s->line_count; ++l) {
> +        TextLine *line = &s->lines[l];
> +        HarfbuzzData *hb = &line->hb_data;
> +        line->glyphs = av_mallocz(hb->glyph_count * sizeof(GlyphInfo));
> +
> +        for (int t = 0; t < hb->glyph_count; ++t) {
> +            GlyphInfo *g_info = &line->glyphs[t];
> +            uint8_t is_tab = last_tab_idx < s->tab_count &&
> +                hb->glyph_info[t].cluster == s->tab_clusters[last_tab_idx] - line->cluster_offset;
> +            int true_x, true_y;
> +            if (is_tab) {
> +                ++last_tab_idx;
> +            }
> +            true_x = x + hb->glyph_pos[t].x_offset;
> +            true_y = y + hb->glyph_pos[t].y_offset;
> +            shift_x64 = (((x64 + true_x) >> 4) & 0b0011) << 4;
> +            shift_y64 = ((4 - (((y64 + true_y) >> 4) & 0b0011)) & 0b0011) << 4;
> +
> +            ret = load_glyph(ctx, &glyph, hb->glyph_info[t].codepoint, shift_x64, shift_y64);
> +            if (ret != 0) {
> +                return ret;
> +            }
> +            g_info->code = hb->glyph_info[t].codepoint;
> +            g_info->x = (x64 + true_x) >> 6;
> +            g_info->y = ((y64 + true_y) >> 6) + (shift_y64 > 0 ? 1 : 0);
> +            g_info->shift_x64 = shift_x64;
> +            g_info->shift_y64 = shift_y64;
> +
> +            if (!is_tab) {
> +                x += hb->glyph_pos[t].x_advance;
> +            } else {
> +                int size = s->blank_advance64 * s->tabsize;
> +                x = (x / size + 1) * size;
> +            }
> +            y += hb->glyph_pos[t].y_advance;
> +        }
>  
> -    if (s->shadowx || s->shadowy) {
> -        if ((ret = draw_glyphs(s, frame, width, height,
> -                               &shadowcolor, s->shadowx, s->shadowy, 0)) < 0)
> -            return ret;
> +        y += metrics.line_height64 + s->line_spacing * 64;
> +        x = 0;
>      }
>  
> -    if (s->borderw) {
> -        if ((ret = draw_glyphs(s, frame, width, height,
> -                               &bordercolor, 0, 0, s->borderw)) < 0)
> +    metrics.rect_x = s->x;
> +    metrics.rect_y = s->y;
> +    
> +    s->box_width = metrics.width;
> +    s->box_height = metrics.height;

What is the point of duplicating these values in both structs?
Francesco Carusi March 20, 2023, 7:41 a.m. UTC | #17
Would it be ok if I split the patch so that one is dedicated to 
cosmetics and one to the implementation?
I'll also remove the change log section and the forward declarations and 
improve the error checking.


On 16/03/2023 17:52, Anton Khirnov wrote:
> Overall this patch could use a lot more polish. I really wish you
> developed it in a more review-friendly form.
> The commit message should use standard form and elaborate on what
> advantage do we get from this huge change, which also adds a new
> dependendency.
>
>
> Quoting Francesco Carusi (2023-02-03 15:18:21)
>> diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c
>> index 50012bb258..7a0a255c5e 100644
>> --- a/libavfilter/vf_drawtext.c
>> +++ b/libavfilter/vf_drawtext.c
> You are adding a bunch of trailing whitespace in the file, which is
> forbidden.
>
>> @@ -1,4 +1,5 @@
>>   /*
>> + * Copyright (c) 2023 Francesco Carusi
>>    * Copyright (c) 2011 Stefano Sabatini
>>    * Copyright (c) 2010 S.N. Hemanth Meenakshisundaram
>>    * Copyright (c) 2003 Gustavo Sverzut Barbieri <gsbarbieri@yahoo.com.br>
>> @@ -20,6 +21,14 @@
>>    * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
>>    */
>>   
>> +/*
>> + * Changelog - 2023
>> + *
>> + * - This filter now depends on libharfbuzz for text shaping.
>> + * - Glyphs position is now accurate to 1/4 pixel in both directions
>> + * - The default line height is now the one defined in the font
>> + */
> This is not the place for a changelog, that's what commit messages and
> the Changelog file are for.
>
>> +
>>   /**
>>    * @file
>>    * drawtext filter, based on the original vhook/drawtext.c
>> @@ -72,14 +81,20 @@
>>   #include FT_GLYPH_H
>>   #include FT_STROKER_H
>>   
>> +#include <hb.h>
>> +#include <hb-ft.h>
>> +
>> +// Ceiling operation for positive integers division
>> +#define POS_CEIL(x, y) ((x)/(y) + ((x)%(y) != 0))
>> +
>>   static const char *const var_names[] = {
>>       "dar",
>>       "hsub", "vsub",
>> -    "line_h", "lh",           ///< line height, same as max_glyph_h
>> +    "line_h", "lh",           ///< line height
>>       "main_h", "h", "H",       ///< height of the input video
>>       "main_w", "w", "W",       ///< width  of the input video
>> -    "max_glyph_a", "ascent",  ///< max glyph ascent
>> -    "max_glyph_d", "descent", ///< min glyph descent
>> +    "max_glyph_a", "ascent",  ///< max glyph ascender
>> +    "max_glyph_d", "descent", ///< min glyph descender
> Seems like this should not be in this patch.
>
>>   static const AVOption drawtext_options[]= {
>> -    {"fontfile",    "set font file",        OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>> -    {"text",        "set text",             OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>> -    {"textfile",    "set text file",        OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>> -    {"fontcolor",   "set foreground color", OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
>> +    {"fontfile",       "set font file",         OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>> +    {"text",           "set text",              OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>> +    {"textfile",       "set text file",         OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>> +    {"fontcolor",      "set foreground color",  OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
>>       {"fontcolor_expr", "set foreground color expression", OFFSET(fontcolor_expr), AV_OPT_TYPE_STRING, {.str=""}, 0, 0, FLAGS},
>> -    {"boxcolor",    "set box color",        OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
>> -    {"bordercolor", "set border color",     OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
>> -    {"shadowcolor", "set shadow color",     OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
>> -    {"box",         "set box",              OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0,        1       , FLAGS},
>> -    {"boxborderw",  "set box border width", OFFSET(boxborderw),         AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
>> -    {"line_spacing",  "set line spacing in pixels", OFFSET(line_spacing),   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX,FLAGS},
>> -    {"fontsize",    "set font size",        OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0 , FLAGS},
>> -    {"x",           "set x expression",     OFFSET(x_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
>> -    {"y",           "set y expression",     OFFSET(y_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
>> -    {"shadowx",     "set shadow x offset",  OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
>> -    {"shadowy",     "set shadow y offset",  OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
>> -    {"borderw",     "set border width",     OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
>> -    {"tabsize",     "set tab size",         OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0,        INT_MAX , FLAGS},
>> -    {"basetime",    "set base time",        OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX , FLAGS},
>> +    {"boxcolor",       "set box color",         OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
>> +    {"bordercolor",    "set border color",      OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
>> +    {"shadowcolor",    "set shadow color",      OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
>> +    {"box",            "set box",               OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0, 1, FLAGS},
>> +    {"boxborderw",     "set box borders width", OFFSET(boxborderw),         AV_OPT_TYPE_INT,    {.i64=0},     0, INT_MAX, FLAGS},
>> +    {"line_spacing",   "set line spacing in pixels", OFFSET(line_spacing),  AV_OPT_TYPE_INT,    {.i64=-1},    INT_MIN, INT_MAX, FLAGS},
>> +    {"fontsize",       "set font size",         OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>> +    {"x",              "set x expression",      OFFSET(x_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
>> +    {"y",              "set y expression",      OFFSET(y_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
>> +    {"shadowx",        "set shadow x offset",   OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
>> +    {"shadowy",        "set shadow y offset",   OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
>> +    {"borderw",        "set border width",      OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
>> +    {"tabsize",        "set tab size",          OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0, INT_MAX, FLAGS},
>> +    {"basetime",       "set base time",         OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX, FLAGS},
>>   #if CONFIG_LIBFONTCONFIG
>>       { "font",        "Font name",            OFFSET(font),               AV_OPT_TYPE_STRING, { .str = "Sans" },           .flags = FLAGS },
>>   #endif
>> @@ -248,15 +338,15 @@ static const AVOption drawtext_options[]= {
>>           {"strftime", "set strftime expansion (deprecated)", OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_STRFTIME}, 0, 0, FLAGS, "expansion"},
>>   
>>       {"timecode",        "set initial timecode",             OFFSET(tc_opt_string), AV_OPT_TYPE_STRING,   {.str=NULL}, 0, 0, FLAGS},
>> -    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},           0,        1, FLAGS},
>> -    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
>> -    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
>> -    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
>> -    {"reload",     "reload text file at specified frame interval", OFFSET(reload),     AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
>> -    { "alpha",       "apply alpha while rendering", OFFSET(a_expr),      AV_OPT_TYPE_STRING, { .str = "1"     },          .flags = FLAGS },
>> -    {"fix_bounds", "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
>> -    {"start_number", "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
>> -    {"text_source", "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
>> +    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},    0, 1, FLAGS},
>> +    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
>> +    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
>> +    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
>> +    {"reload",          "reload text file at specified frame interval", OFFSET(reload), AV_OPT_TYPE_INT, {.i64=0},    0, INT_MAX, FLAGS},
>> +    {"alpha",           "apply alpha while rendering",      OFFSET(a_expr),        AV_OPT_TYPE_STRING,   {.str = "1"}, .flags = FLAGS},
>> +    {"fix_bounds",      "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
>> +    {"start_number",    "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
>> +    {"text_source",     "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
> Most of these changes seem cosmetic, they do not belong in this patch.
>
>>   
>>   #if CONFIG_LIBFRIBIDI
>>       {"text_shaping", "attempt to shape text before drawing", OFFSET(text_shaping), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS},
>> @@ -297,18 +387,24 @@ static const struct ft_error {
>>   
>>   #define FT_ERRMSG(e) ft_errors[e].err_msg
>>   
>> -typedef struct Glyph {
>> -    FT_Glyph glyph;
>> -    FT_Glyph border_glyph;
>> -    uint32_t code;
>> -    unsigned int fontsize;
>> -    FT_Bitmap bitmap; ///< array holding bitmaps of font
>> -    FT_Bitmap border_bitmap; ///< array holding bitmaps of font border
>> -    FT_BBox bbox;
>> -    int advance;
>> -    int bitmap_left;
>> -    int bitmap_top;
>> -} Glyph;
>> +
>> +// Loads and (optionally) renders a glyph
>> +static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code,
>> +     int8_t shift_x64, int8_t shift_y64);
>> +
>> +// Shapes a line of text using libharfbuzz
>> +static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const char* text, int textLen);
>> +
>> +// Performs text measurements
>> +static int measure_text(AVFilterContext *ctx, TextMetrics *metrics);
>> +
>> +// Draws glyphs on the frame
>> +static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
>> +                       FFDrawColor *color, TextMetrics *metrics,
>> +                       int x, int y, int borderw);
>> +
>> +// Draws text on the frame
>> +static int draw_text(AVFilterContext *ctx, AVFrame *frame);
> Why is there a need for forward declarations?
>
>>   static int glyph_cmp(const void *key, const void *b)
>>   {
>> @@ -316,80 +412,9 @@ static int glyph_cmp(const void *key, const void *b)
>>       int64_t diff = (int64_t)a->code - (int64_t)bb->code;
>>   
>>       if (diff != 0)
>> -         return diff > 0 ? 1 : -1;
>> +        return diff > 0 ? 1 : -1;
> unrelated cosmetics
>
>> @@ -439,7 +464,6 @@ static av_cold int update_fontsize(AVFilterContext *ctx)
>>              return err;
>>   
>>           size = av_expr_eval(s->fontsize_pexpr, s->var_values, &s->prng);
>> -
> unrelated cosmetics
>
>>           if (!isnan(size)) {
>>               roundedsize = round(size);
>>               // test for overflow before cast
>> @@ -447,7 +471,6 @@ static av_cold int update_fontsize(AVFilterContext *ctx)
>>                   av_log(ctx, AV_LOG_ERROR, "fontsize overflow\n");
>>                   return AVERROR(EINVAL);
>>               }
>> -
> unrelated cosmetics
>
>>               fontsize = roundedsize;
>>           }
>>       }
>> @@ -548,7 +571,7 @@ static int load_font_fontconfig(AVFilterContext *ctx)
>>           goto fail;
>>       }
>>   
>> -    av_log(ctx, AV_LOG_INFO, "Using \"%s\"\n", filename);
>> +    av_log(ctx, AV_LOG_VERBOSE, "Using \"%s\"\n", filename);
> unrelated cosmetics
>
>>       if (parse_err)
>>           s->default_fontsize = size + 0.5;
>>   
>> @@ -690,6 +713,7 @@ static int shape_text(AVFilterContext *ctx)
>>       s->text = tmp;
>>       len = fribidi_unicode_to_charset(FRIBIDI_CHAR_SET_UTF8,
>>                                        unicodestr, len, s->text);
>> +
> unrelated cosmetics
>
>>   
>> +static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const char* text, int textLen)
>> +{
>> +    hb->buf = hb_buffer_create();
> Seems like it at least this needs to be checked for errors, maybe some
> of the other calls too.
>
>> @@ -1559,56 +1795,142 @@ continue_on_invalid2:
>>       update_color_with_alpha(s, &bordercolor, s->bordercolor);
>>       update_color_with_alpha(s, &boxcolor   , s->boxcolor   );
>>   
>> -    box_w = max_text_line_w;
>> -    box_h = y + s->max_glyph_h;
>> +    if (s->draw_box && s->boxborderw) {
>> +        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = s->boxborderw;
>> +    } else {
>> +        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = 0;
>> +    }
>>   
>>       if (s->fix_bounds) {
>> -
>>           /* calculate footprint of text effects */
>> -        int boxoffset     = s->draw_box ? FFMAX(s->boxborderw, 0) : 0;
>>           int borderoffset  = s->borderw  ? FFMAX(s->borderw, 0) : 0;
>>   
>> -        int offsetleft = FFMAX3(boxoffset, borderoffset,
>> +        int offsetleft = FFMAX3(FFMAX(s->bb_left, 0), borderoffset,
>>                                   (s->shadowx < 0 ? FFABS(s->shadowx) : 0));
>> -        int offsettop = FFMAX3(boxoffset, borderoffset,
>> +        int offsettop = FFMAX3(FFMAX(s->bb_top, 0), borderoffset,
>>                                   (s->shadowy < 0 ? FFABS(s->shadowy) : 0));
>> -
>> -        int offsetright = FFMAX3(boxoffset, borderoffset,
>> +        int offsetright = FFMAX3(FFMAX(s->bb_right, 0), borderoffset,
>>                                    (s->shadowx > 0 ? s->shadowx : 0));
>> -        int offsetbottom = FFMAX3(boxoffset, borderoffset,
>> +        int offsetbottom = FFMAX3(FFMAX(s->bb_bottom, 0), borderoffset,
>>                                     (s->shadowy > 0 ? s->shadowy : 0));
>>   
>> -
>>           if (s->x - offsetleft < 0) s->x = offsetleft;
>>           if (s->y - offsettop < 0)  s->y = offsettop;
>>   
>> -        if (s->x + box_w + offsetright > width)
>> -            s->x = FFMAX(width - box_w - offsetright, 0);
>> -        if (s->y + box_h + offsetbottom > height)
>> -            s->y = FFMAX(height - box_h - offsetbottom, 0);
>> +        if (s->x + metrics.width + offsetright > width)
>> +            s->x = FFMAX(width - metrics.width - offsetright, 0);
>> +        if (s->y + metrics.height + offsetbottom > height)
>> +            s->y = FFMAX(height - metrics.height - offsetbottom, 0);
>>       }
>>   
>> -    /* draw box */
>> -    if (s->draw_box)
>> -        ff_blend_rectangle(&s->dc, &boxcolor,
>> -                           frame->data, frame->linesize, width, height,
>> -                           s->x - s->boxborderw, s->y - s->boxborderw,
>> -                           box_w + s->boxborderw * 2, box_h + s->boxborderw * 2);
>> +    x = 0;
>> +    y = 0;
>> +    x64 = (int)(s->x * 64.);
>> +    y64 = (int)(s->y * 64. + metrics.offset_top64);
>> +
>> +    for (int l = 0; l < s->line_count; ++l) {
>> +        TextLine *line = &s->lines[l];
>> +        HarfbuzzData *hb = &line->hb_data;
>> +        line->glyphs = av_mallocz(hb->glyph_count * sizeof(GlyphInfo));
>> +
>> +        for (int t = 0; t < hb->glyph_count; ++t) {
>> +            GlyphInfo *g_info = &line->glyphs[t];
>> +            uint8_t is_tab = last_tab_idx < s->tab_count &&
>> +                hb->glyph_info[t].cluster == s->tab_clusters[last_tab_idx] - line->cluster_offset;
>> +            int true_x, true_y;
>> +            if (is_tab) {
>> +                ++last_tab_idx;
>> +            }
>> +            true_x = x + hb->glyph_pos[t].x_offset;
>> +            true_y = y + hb->glyph_pos[t].y_offset;
>> +            shift_x64 = (((x64 + true_x) >> 4) & 0b0011) << 4;
>> +            shift_y64 = ((4 - (((y64 + true_y) >> 4) & 0b0011)) & 0b0011) << 4;
>> +
>> +            ret = load_glyph(ctx, &glyph, hb->glyph_info[t].codepoint, shift_x64, shift_y64);
>> +            if (ret != 0) {
>> +                return ret;
>> +            }
>> +            g_info->code = hb->glyph_info[t].codepoint;
>> +            g_info->x = (x64 + true_x) >> 6;
>> +            g_info->y = ((y64 + true_y) >> 6) + (shift_y64 > 0 ? 1 : 0);
>> +            g_info->shift_x64 = shift_x64;
>> +            g_info->shift_y64 = shift_y64;
>> +
>> +            if (!is_tab) {
>> +                x += hb->glyph_pos[t].x_advance;
>> +            } else {
>> +                int size = s->blank_advance64 * s->tabsize;
>> +                x = (x / size + 1) * size;
>> +            }
>> +            y += hb->glyph_pos[t].y_advance;
>> +        }
>>   
>> -    if (s->shadowx || s->shadowy) {
>> -        if ((ret = draw_glyphs(s, frame, width, height,
>> -                               &shadowcolor, s->shadowx, s->shadowy, 0)) < 0)
>> -            return ret;
>> +        y += metrics.line_height64 + s->line_spacing * 64;
>> +        x = 0;
>>       }
>>   
>> -    if (s->borderw) {
>> -        if ((ret = draw_glyphs(s, frame, width, height,
>> -                               &bordercolor, 0, 0, s->borderw)) < 0)
>> +    metrics.rect_x = s->x;
>> +    metrics.rect_y = s->y;
>> +
>> +    s->box_width = metrics.width;
>> +    s->box_height = metrics.height;
> What is the point of duplicating these values in both structs?
>
>
Paul B Mahol March 20, 2023, 8:50 a.m. UTC | #18
On Mon, Mar 20, 2023 at 8:41 AM Francesco Carusi <klimklim@tiscali.it>
wrote:

> Would it be ok if I split the patch so that one is dedicated to
> cosmetics and one to the implementation?
> I'll also remove the change log section and the forward declarations and
> improve the error checking.
>

Yes split it.


Note that reviewers here tend to review only after big time passes and are
usually nitpicking all the time.


>
> On 16/03/2023 17:52, Anton Khirnov wrote:
> > Overall this patch could use a lot more polish. I really wish you
> > developed it in a more review-friendly form.
> > The commit message should use standard form and elaborate on what
> > advantage do we get from this huge change, which also adds a new
> > dependendency.
> >
> >
> > Quoting Francesco Carusi (2023-02-03 15:18:21)
> >> diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c
> >> index 50012bb258..7a0a255c5e 100644
> >> --- a/libavfilter/vf_drawtext.c
> >> +++ b/libavfilter/vf_drawtext.c
> > You are adding a bunch of trailing whitespace in the file, which is
> > forbidden.
> >
> >> @@ -1,4 +1,5 @@
> >>   /*
> >> + * Copyright (c) 2023 Francesco Carusi
> >>    * Copyright (c) 2011 Stefano Sabatini
> >>    * Copyright (c) 2010 S.N. Hemanth Meenakshisundaram
> >>    * Copyright (c) 2003 Gustavo Sverzut Barbieri <
> gsbarbieri@yahoo.com.br>
> >> @@ -20,6 +21,14 @@
> >>    * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> 02110-1301 USA
> >>    */
> >>
> >> +/*
> >> + * Changelog - 2023
> >> + *
> >> + * - This filter now depends on libharfbuzz for text shaping.
> >> + * - Glyphs position is now accurate to 1/4 pixel in both directions
> >> + * - The default line height is now the one defined in the font
> >> + */
> > This is not the place for a changelog, that's what commit messages and
> > the Changelog file are for.
> >
> >> +
> >>   /**
> >>    * @file
> >>    * drawtext filter, based on the original vhook/drawtext.c
> >> @@ -72,14 +81,20 @@
> >>   #include FT_GLYPH_H
> >>   #include FT_STROKER_H
> >>
> >> +#include <hb.h>
> >> +#include <hb-ft.h>
> >> +
> >> +// Ceiling operation for positive integers division
> >> +#define POS_CEIL(x, y) ((x)/(y) + ((x)%(y) != 0))
> >> +
> >>   static const char *const var_names[] = {
> >>       "dar",
> >>       "hsub", "vsub",
> >> -    "line_h", "lh",           ///< line height, same as max_glyph_h
> >> +    "line_h", "lh",           ///< line height
> >>       "main_h", "h", "H",       ///< height of the input video
> >>       "main_w", "w", "W",       ///< width  of the input video
> >> -    "max_glyph_a", "ascent",  ///< max glyph ascent
> >> -    "max_glyph_d", "descent", ///< min glyph descent
> >> +    "max_glyph_a", "ascent",  ///< max glyph ascender
> >> +    "max_glyph_d", "descent", ///< min glyph descender
> > Seems like this should not be in this patch.
> >
> >>   static const AVOption drawtext_options[]= {
> >> -    {"fontfile",    "set font file",        OFFSET(fontfile),
>  AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >> -    {"text",        "set text",             OFFSET(text),
>  AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >> -    {"textfile",    "set text file",        OFFSET(textfile),
>  AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >> -    {"fontcolor",   "set foreground color", OFFSET(fontcolor.rgba),
>  AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
> >> +    {"fontfile",       "set font file",         OFFSET(fontfile),
>      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >> +    {"text",           "set text",              OFFSET(text),
>      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >> +    {"textfile",       "set text file",         OFFSET(textfile),
>      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >> +    {"fontcolor",      "set foreground color",
> OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0,
> FLAGS},
> >>       {"fontcolor_expr", "set foreground color expression",
> OFFSET(fontcolor_expr), AV_OPT_TYPE_STRING, {.str=""}, 0, 0, FLAGS},
> >> -    {"boxcolor",    "set box color",        OFFSET(boxcolor.rgba),
>   AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
> >> -    {"bordercolor", "set border color",     OFFSET(bordercolor.rgba),
>  AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
> >> -    {"shadowcolor", "set shadow color",     OFFSET(shadowcolor.rgba),
>  AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
> >> -    {"box",         "set box",              OFFSET(draw_box),
>  AV_OPT_TYPE_BOOL,   {.i64=0},     0,        1       , FLAGS},
> >> -    {"boxborderw",  "set box border width", OFFSET(boxborderw),
>  AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
> >> -    {"line_spacing",  "set line spacing in pixels",
> OFFSET(line_spacing),   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,
> INT_MAX,FLAGS},
> >> -    {"fontsize",    "set font size",        OFFSET(fontsize_expr),
>   AV_OPT_TYPE_STRING, {.str=NULL},  0, 0 , FLAGS},
> >> -    {"x",           "set x expression",     OFFSET(x_expr),
>  AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
> >> -    {"y",           "set y expression",     OFFSET(y_expr),
>  AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
> >> -    {"shadowx",     "set shadow x offset",  OFFSET(shadowx),
>   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
> >> -    {"shadowy",     "set shadow y offset",  OFFSET(shadowy),
>   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
> >> -    {"borderw",     "set border width",     OFFSET(borderw),
>   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
> >> -    {"tabsize",     "set tab size",         OFFSET(tabsize),
>   AV_OPT_TYPE_INT,    {.i64=4},     0,        INT_MAX , FLAGS},
> >> -    {"basetime",    "set base time",        OFFSET(basetime),
>  AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX , FLAGS},
> >> +    {"boxcolor",       "set box color",
>  OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0,
> FLAGS},
> >> +    {"bordercolor",    "set border color",
> OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0,
> FLAGS},
> >> +    {"shadowcolor",    "set shadow color",
> OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0,
> FLAGS},
> >> +    {"box",            "set box",               OFFSET(draw_box),
>      AV_OPT_TYPE_BOOL,   {.i64=0},     0, 1, FLAGS},
> >> +    {"boxborderw",     "set box borders width", OFFSET(boxborderw),
>      AV_OPT_TYPE_INT,    {.i64=0},     0, INT_MAX, FLAGS},
> >> +    {"line_spacing",   "set line spacing in pixels",
> OFFSET(line_spacing),  AV_OPT_TYPE_INT,    {.i64=-1},    INT_MIN, INT_MAX,
> FLAGS},
> >> +    {"fontsize",       "set font size",
>  OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >> +    {"x",              "set x expression",      OFFSET(x_expr),
>      AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
> >> +    {"y",              "set y expression",      OFFSET(y_expr),
>      AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
> >> +    {"shadowx",        "set shadow x offset",   OFFSET(shadowx),
>       AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
> >> +    {"shadowy",        "set shadow y offset",   OFFSET(shadowy),
>       AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
> >> +    {"borderw",        "set border width",      OFFSET(borderw),
>       AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
> >> +    {"tabsize",        "set tab size",          OFFSET(tabsize),
>       AV_OPT_TYPE_INT,    {.i64=4},     0, INT_MAX, FLAGS},
> >> +    {"basetime",       "set base time",         OFFSET(basetime),
>      AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX,
> FLAGS},
> >>   #if CONFIG_LIBFONTCONFIG
> >>       { "font",        "Font name",            OFFSET(font),
>    AV_OPT_TYPE_STRING, { .str = "Sans" },           .flags = FLAGS },
> >>   #endif
> >> @@ -248,15 +338,15 @@ static const AVOption drawtext_options[]= {
> >>           {"strftime", "set strftime expansion (deprecated)",
> OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_STRFTIME}, 0, 0, FLAGS,
> "expansion"},
> >>
> >>       {"timecode",        "set initial timecode",
>  OFFSET(tc_opt_string), AV_OPT_TYPE_STRING,   {.str=NULL}, 0, 0, FLAGS},
> >> -    {"tc24hmax",        "set 24 hours max (timecode only)",
> OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},           0,
> 1, FLAGS},
> >> -    {"timecode_rate",   "set rate (timecode only)",
>  OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,
> INT_MAX, FLAGS},
> >> -    {"r",               "set rate (timecode only)",
>  OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,
> INT_MAX, FLAGS},
> >> -    {"rate",            "set rate (timecode only)",
>  OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,
> INT_MAX, FLAGS},
> >> -    {"reload",     "reload text file at specified frame interval",
> OFFSET(reload),     AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
> >> -    { "alpha",       "apply alpha while rendering", OFFSET(a_expr),
>   AV_OPT_TYPE_STRING, { .str = "1"     },          .flags = FLAGS },
> >> -    {"fix_bounds", "check and fix text coords to avoid clipping",
> OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
> >> -    {"start_number", "start frame number for n/frame_num variable",
> OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
> >> -    {"text_source", "the source of text", OFFSET(text_source_string),
> AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
> >> +    {"tc24hmax",        "set 24 hours max (timecode only)",
> OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},    0, 1, FLAGS},
> >> +    {"timecode_rate",   "set rate (timecode only)",
>  OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX,
> FLAGS},
> >> +    {"r",               "set rate (timecode only)",
>  OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX,
> FLAGS},
> >> +    {"rate",            "set rate (timecode only)",
>  OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX,
> FLAGS},
> >> +    {"reload",          "reload text file at specified frame
> interval", OFFSET(reload), AV_OPT_TYPE_INT, {.i64=0},    0, INT_MAX, FLAGS},
> >> +    {"alpha",           "apply alpha while rendering",
> OFFSET(a_expr),        AV_OPT_TYPE_STRING,   {.str = "1"}, .flags = FLAGS},
> >> +    {"fix_bounds",      "check and fix text coords to avoid clipping",
> OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
> >> +    {"start_number",    "start frame number for n/frame_num variable",
> OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
> >> +    {"text_source",     "the source of text",
> OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
> > Most of these changes seem cosmetic, they do not belong in this patch.
> >
> >>
> >>   #if CONFIG_LIBFRIBIDI
> >>       {"text_shaping", "attempt to shape text before drawing",
> OFFSET(text_shaping), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS},
> >> @@ -297,18 +387,24 @@ static const struct ft_error {
> >>
> >>   #define FT_ERRMSG(e) ft_errors[e].err_msg
> >>
> >> -typedef struct Glyph {
> >> -    FT_Glyph glyph;
> >> -    FT_Glyph border_glyph;
> >> -    uint32_t code;
> >> -    unsigned int fontsize;
> >> -    FT_Bitmap bitmap; ///< array holding bitmaps of font
> >> -    FT_Bitmap border_bitmap; ///< array holding bitmaps of font border
> >> -    FT_BBox bbox;
> >> -    int advance;
> >> -    int bitmap_left;
> >> -    int bitmap_top;
> >> -} Glyph;
> >> +
> >> +// Loads and (optionally) renders a glyph
> >> +static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr,
> uint32_t code,
> >> +     int8_t shift_x64, int8_t shift_y64);
> >> +
> >> +// Shapes a line of text using libharfbuzz
> >> +static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const
> char* text, int textLen);
> >> +
> >> +// Performs text measurements
> >> +static int measure_text(AVFilterContext *ctx, TextMetrics *metrics);
> >> +
> >> +// Draws glyphs on the frame
> >> +static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
> >> +                       FFDrawColor *color, TextMetrics *metrics,
> >> +                       int x, int y, int borderw);
> >> +
> >> +// Draws text on the frame
> >> +static int draw_text(AVFilterContext *ctx, AVFrame *frame);
> > Why is there a need for forward declarations?
> >
> >>   static int glyph_cmp(const void *key, const void *b)
> >>   {
> >> @@ -316,80 +412,9 @@ static int glyph_cmp(const void *key, const void
> *b)
> >>       int64_t diff = (int64_t)a->code - (int64_t)bb->code;
> >>
> >>       if (diff != 0)
> >> -         return diff > 0 ? 1 : -1;
> >> +        return diff > 0 ? 1 : -1;
> > unrelated cosmetics
> >
> >> @@ -439,7 +464,6 @@ static av_cold int update_fontsize(AVFilterContext
> *ctx)
> >>              return err;
> >>
> >>           size = av_expr_eval(s->fontsize_pexpr, s->var_values,
> &s->prng);
> >> -
> > unrelated cosmetics
> >
> >>           if (!isnan(size)) {
> >>               roundedsize = round(size);
> >>               // test for overflow before cast
> >> @@ -447,7 +471,6 @@ static av_cold int update_fontsize(AVFilterContext
> *ctx)
> >>                   av_log(ctx, AV_LOG_ERROR, "fontsize overflow\n");
> >>                   return AVERROR(EINVAL);
> >>               }
> >> -
> > unrelated cosmetics
> >
> >>               fontsize = roundedsize;
> >>           }
> >>       }
> >> @@ -548,7 +571,7 @@ static int load_font_fontconfig(AVFilterContext
> *ctx)
> >>           goto fail;
> >>       }
> >>
> >> -    av_log(ctx, AV_LOG_INFO, "Using \"%s\"\n", filename);
> >> +    av_log(ctx, AV_LOG_VERBOSE, "Using \"%s\"\n", filename);
> > unrelated cosmetics
> >
> >>       if (parse_err)
> >>           s->default_fontsize = size + 0.5;
> >>
> >> @@ -690,6 +713,7 @@ static int shape_text(AVFilterContext *ctx)
> >>       s->text = tmp;
> >>       len = fribidi_unicode_to_charset(FRIBIDI_CHAR_SET_UTF8,
> >>                                        unicodestr, len, s->text);
> >> +
> > unrelated cosmetics
> >
> >>
> >> +static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const
> char* text, int textLen)
> >> +{
> >> +    hb->buf = hb_buffer_create();
> > Seems like it at least this needs to be checked for errors, maybe some
> > of the other calls too.
> >
> >> @@ -1559,56 +1795,142 @@ continue_on_invalid2:
> >>       update_color_with_alpha(s, &bordercolor, s->bordercolor);
> >>       update_color_with_alpha(s, &boxcolor   , s->boxcolor   );
> >>
> >> -    box_w = max_text_line_w;
> >> -    box_h = y + s->max_glyph_h;
> >> +    if (s->draw_box && s->boxborderw) {
> >> +        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left =
> s->boxborderw;
> >> +    } else {
> >> +        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = 0;
> >> +    }
> >>
> >>       if (s->fix_bounds) {
> >> -
> >>           /* calculate footprint of text effects */
> >> -        int boxoffset     = s->draw_box ? FFMAX(s->boxborderw, 0) : 0;
> >>           int borderoffset  = s->borderw  ? FFMAX(s->borderw, 0) : 0;
> >>
> >> -        int offsetleft = FFMAX3(boxoffset, borderoffset,
> >> +        int offsetleft = FFMAX3(FFMAX(s->bb_left, 0), borderoffset,
> >>                                   (s->shadowx < 0 ? FFABS(s->shadowx) :
> 0));
> >> -        int offsettop = FFMAX3(boxoffset, borderoffset,
> >> +        int offsettop = FFMAX3(FFMAX(s->bb_top, 0), borderoffset,
> >>                                   (s->shadowy < 0 ? FFABS(s->shadowy) :
> 0));
> >> -
> >> -        int offsetright = FFMAX3(boxoffset, borderoffset,
> >> +        int offsetright = FFMAX3(FFMAX(s->bb_right, 0), borderoffset,
> >>                                    (s->shadowx > 0 ? s->shadowx : 0));
> >> -        int offsetbottom = FFMAX3(boxoffset, borderoffset,
> >> +        int offsetbottom = FFMAX3(FFMAX(s->bb_bottom, 0), borderoffset,
> >>                                     (s->shadowy > 0 ? s->shadowy : 0));
> >>
> >> -
> >>           if (s->x - offsetleft < 0) s->x = offsetleft;
> >>           if (s->y - offsettop < 0)  s->y = offsettop;
> >>
> >> -        if (s->x + box_w + offsetright > width)
> >> -            s->x = FFMAX(width - box_w - offsetright, 0);
> >> -        if (s->y + box_h + offsetbottom > height)
> >> -            s->y = FFMAX(height - box_h - offsetbottom, 0);
> >> +        if (s->x + metrics.width + offsetright > width)
> >> +            s->x = FFMAX(width - metrics.width - offsetright, 0);
> >> +        if (s->y + metrics.height + offsetbottom > height)
> >> +            s->y = FFMAX(height - metrics.height - offsetbottom, 0);
> >>       }
> >>
> >> -    /* draw box */
> >> -    if (s->draw_box)
> >> -        ff_blend_rectangle(&s->dc, &boxcolor,
> >> -                           frame->data, frame->linesize, width, height,
> >> -                           s->x - s->boxborderw, s->y - s->boxborderw,
> >> -                           box_w + s->boxborderw * 2, box_h +
> s->boxborderw * 2);
> >> +    x = 0;
> >> +    y = 0;
> >> +    x64 = (int)(s->x * 64.);
> >> +    y64 = (int)(s->y * 64. + metrics.offset_top64);
> >> +
> >> +    for (int l = 0; l < s->line_count; ++l) {
> >> +        TextLine *line = &s->lines[l];
> >> +        HarfbuzzData *hb = &line->hb_data;
> >> +        line->glyphs = av_mallocz(hb->glyph_count * sizeof(GlyphInfo));
> >> +
> >> +        for (int t = 0; t < hb->glyph_count; ++t) {
> >> +            GlyphInfo *g_info = &line->glyphs[t];
> >> +            uint8_t is_tab = last_tab_idx < s->tab_count &&
> >> +                hb->glyph_info[t].cluster ==
> s->tab_clusters[last_tab_idx] - line->cluster_offset;
> >> +            int true_x, true_y;
> >> +            if (is_tab) {
> >> +                ++last_tab_idx;
> >> +            }
> >> +            true_x = x + hb->glyph_pos[t].x_offset;
> >> +            true_y = y + hb->glyph_pos[t].y_offset;
> >> +            shift_x64 = (((x64 + true_x) >> 4) & 0b0011) << 4;
> >> +            shift_y64 = ((4 - (((y64 + true_y) >> 4) & 0b0011)) &
> 0b0011) << 4;
> >> +
> >> +            ret = load_glyph(ctx, &glyph, hb->glyph_info[t].codepoint,
> shift_x64, shift_y64);
> >> +            if (ret != 0) {
> >> +                return ret;
> >> +            }
> >> +            g_info->code = hb->glyph_info[t].codepoint;
> >> +            g_info->x = (x64 + true_x) >> 6;
> >> +            g_info->y = ((y64 + true_y) >> 6) + (shift_y64 > 0 ? 1 :
> 0);
> >> +            g_info->shift_x64 = shift_x64;
> >> +            g_info->shift_y64 = shift_y64;
> >> +
> >> +            if (!is_tab) {
> >> +                x += hb->glyph_pos[t].x_advance;
> >> +            } else {
> >> +                int size = s->blank_advance64 * s->tabsize;
> >> +                x = (x / size + 1) * size;
> >> +            }
> >> +            y += hb->glyph_pos[t].y_advance;
> >> +        }
> >>
> >> -    if (s->shadowx || s->shadowy) {
> >> -        if ((ret = draw_glyphs(s, frame, width, height,
> >> -                               &shadowcolor, s->shadowx, s->shadowy,
> 0)) < 0)
> >> -            return ret;
> >> +        y += metrics.line_height64 + s->line_spacing * 64;
> >> +        x = 0;
> >>       }
> >>
> >> -    if (s->borderw) {
> >> -        if ((ret = draw_glyphs(s, frame, width, height,
> >> -                               &bordercolor, 0, 0, s->borderw)) < 0)
> >> +    metrics.rect_x = s->x;
> >> +    metrics.rect_y = s->y;
> >> +
> >> +    s->box_width = metrics.width;
> >> +    s->box_height = metrics.height;
> > What is the point of duplicating these values in both structs?
> >
> >
>
> _______________________________________________
> 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".
>
Francesco Carusi May 26, 2023, 12:19 p.m. UTC | #19
I added a patch specifically to include cosmetic changes. I also removed 
the change log section, the forward declarations and improved error 
checking.
The changes to the configure file are in a dedicated patch (the last 
one: 0009).


On 20/03/2023 09:50, Paul B Mahol wrote:
> On Mon, Mar 20, 2023 at 8:41 AM Francesco Carusi <klimklim@tiscali.it>
> wrote:
>
>> Would it be ok if I split the patch so that one is dedicated to
>> cosmetics and one to the implementation?
>> I'll also remove the change log section and the forward declarations and
>> improve the error checking.
>>
> Yes split it.
>
>
> Note that reviewers here tend to review only after big time passes and are
> usually nitpicking all the time.
>
>
>> On 16/03/2023 17:52, Anton Khirnov wrote:
>>> Overall this patch could use a lot more polish. I really wish you
>>> developed it in a more review-friendly form.
>>> The commit message should use standard form and elaborate on what
>>> advantage do we get from this huge change, which also adds a new
>>> dependendency.
>>>
>>>
>>> Quoting Francesco Carusi (2023-02-03 15:18:21)
>>>> diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c
>>>> index 50012bb258..7a0a255c5e 100644
>>>> --- a/libavfilter/vf_drawtext.c
>>>> +++ b/libavfilter/vf_drawtext.c
>>> You are adding a bunch of trailing whitespace in the file, which is
>>> forbidden.
>>>
>>>> @@ -1,4 +1,5 @@
>>>>    /*
>>>> + * Copyright (c) 2023 Francesco Carusi
>>>>     * Copyright (c) 2011 Stefano Sabatini
>>>>     * Copyright (c) 2010 S.N. Hemanth Meenakshisundaram
>>>>     * Copyright (c) 2003 Gustavo Sverzut Barbieri <
>> gsbarbieri@yahoo.com.br>
>>>> @@ -20,6 +21,14 @@
>>>>     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
>> 02110-1301 USA
>>>>     */
>>>>
>>>> +/*
>>>> + * Changelog - 2023
>>>> + *
>>>> + * - This filter now depends on libharfbuzz for text shaping.
>>>> + * - Glyphs position is now accurate to 1/4 pixel in both directions
>>>> + * - The default line height is now the one defined in the font
>>>> + */
>>> This is not the place for a changelog, that's what commit messages and
>>> the Changelog file are for.
>>>
>>>> +
>>>>    /**
>>>>     * @file
>>>>     * drawtext filter, based on the original vhook/drawtext.c
>>>> @@ -72,14 +81,20 @@
>>>>    #include FT_GLYPH_H
>>>>    #include FT_STROKER_H
>>>>
>>>> +#include <hb.h>
>>>> +#include <hb-ft.h>
>>>> +
>>>> +// Ceiling operation for positive integers division
>>>> +#define POS_CEIL(x, y) ((x)/(y) + ((x)%(y) != 0))
>>>> +
>>>>    static const char *const var_names[] = {
>>>>        "dar",
>>>>        "hsub", "vsub",
>>>> -    "line_h", "lh",           ///< line height, same as max_glyph_h
>>>> +    "line_h", "lh",           ///< line height
>>>>        "main_h", "h", "H",       ///< height of the input video
>>>>        "main_w", "w", "W",       ///< width  of the input video
>>>> -    "max_glyph_a", "ascent",  ///< max glyph ascent
>>>> -    "max_glyph_d", "descent", ///< min glyph descent
>>>> +    "max_glyph_a", "ascent",  ///< max glyph ascender
>>>> +    "max_glyph_d", "descent", ///< min glyph descender
>>> Seems like this should not be in this patch.
>>>
>>>>    static const AVOption drawtext_options[]= {
>>>> -    {"fontfile",    "set font file",        OFFSET(fontfile),
>>   AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>>>> -    {"text",        "set text",             OFFSET(text),
>>   AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>>>> -    {"textfile",    "set text file",        OFFSET(textfile),
>>   AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>>>> -    {"fontcolor",   "set foreground color", OFFSET(fontcolor.rgba),
>>   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
>>>> +    {"fontfile",       "set font file",         OFFSET(fontfile),
>>       AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>>>> +    {"text",           "set text",              OFFSET(text),
>>       AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>>>> +    {"textfile",       "set text file",         OFFSET(textfile),
>>       AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>>>> +    {"fontcolor",      "set foreground color",
>> OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0,
>> FLAGS},
>>>>        {"fontcolor_expr", "set foreground color expression",
>> OFFSET(fontcolor_expr), AV_OPT_TYPE_STRING, {.str=""}, 0, 0, FLAGS},
>>>> -    {"boxcolor",    "set box color",        OFFSET(boxcolor.rgba),
>>    AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
>>>> -    {"bordercolor", "set border color",     OFFSET(bordercolor.rgba),
>>   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
>>>> -    {"shadowcolor", "set shadow color",     OFFSET(shadowcolor.rgba),
>>   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
>>>> -    {"box",         "set box",              OFFSET(draw_box),
>>   AV_OPT_TYPE_BOOL,   {.i64=0},     0,        1       , FLAGS},
>>>> -    {"boxborderw",  "set box border width", OFFSET(boxborderw),
>>   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
>>>> -    {"line_spacing",  "set line spacing in pixels",
>> OFFSET(line_spacing),   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,
>> INT_MAX,FLAGS},
>>>> -    {"fontsize",    "set font size",        OFFSET(fontsize_expr),
>>    AV_OPT_TYPE_STRING, {.str=NULL},  0, 0 , FLAGS},
>>>> -    {"x",           "set x expression",     OFFSET(x_expr),
>>   AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
>>>> -    {"y",           "set y expression",     OFFSET(y_expr),
>>   AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
>>>> -    {"shadowx",     "set shadow x offset",  OFFSET(shadowx),
>>    AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
>>>> -    {"shadowy",     "set shadow y offset",  OFFSET(shadowy),
>>    AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
>>>> -    {"borderw",     "set border width",     OFFSET(borderw),
>>    AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
>>>> -    {"tabsize",     "set tab size",         OFFSET(tabsize),
>>    AV_OPT_TYPE_INT,    {.i64=4},     0,        INT_MAX , FLAGS},
>>>> -    {"basetime",    "set base time",        OFFSET(basetime),
>>   AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX , FLAGS},
>>>> +    {"boxcolor",       "set box color",
>>   OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0,
>> FLAGS},
>>>> +    {"bordercolor",    "set border color",
>> OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0,
>> FLAGS},
>>>> +    {"shadowcolor",    "set shadow color",
>> OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0,
>> FLAGS},
>>>> +    {"box",            "set box",               OFFSET(draw_box),
>>       AV_OPT_TYPE_BOOL,   {.i64=0},     0, 1, FLAGS},
>>>> +    {"boxborderw",     "set box borders width", OFFSET(boxborderw),
>>       AV_OPT_TYPE_INT,    {.i64=0},     0, INT_MAX, FLAGS},
>>>> +    {"line_spacing",   "set line spacing in pixels",
>> OFFSET(line_spacing),  AV_OPT_TYPE_INT,    {.i64=-1},    INT_MIN, INT_MAX,
>> FLAGS},
>>>> +    {"fontsize",       "set font size",
>>   OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
>>>> +    {"x",              "set x expression",      OFFSET(x_expr),
>>       AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
>>>> +    {"y",              "set y expression",      OFFSET(y_expr),
>>       AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
>>>> +    {"shadowx",        "set shadow x offset",   OFFSET(shadowx),
>>        AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
>>>> +    {"shadowy",        "set shadow y offset",   OFFSET(shadowy),
>>        AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
>>>> +    {"borderw",        "set border width",      OFFSET(borderw),
>>        AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
>>>> +    {"tabsize",        "set tab size",          OFFSET(tabsize),
>>        AV_OPT_TYPE_INT,    {.i64=4},     0, INT_MAX, FLAGS},
>>>> +    {"basetime",       "set base time",         OFFSET(basetime),
>>       AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX,
>> FLAGS},
>>>>    #if CONFIG_LIBFONTCONFIG
>>>>        { "font",        "Font name",            OFFSET(font),
>>     AV_OPT_TYPE_STRING, { .str = "Sans" },           .flags = FLAGS },
>>>>    #endif
>>>> @@ -248,15 +338,15 @@ static const AVOption drawtext_options[]= {
>>>>            {"strftime", "set strftime expansion (deprecated)",
>> OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_STRFTIME}, 0, 0, FLAGS,
>> "expansion"},
>>>>        {"timecode",        "set initial timecode",
>>   OFFSET(tc_opt_string), AV_OPT_TYPE_STRING,   {.str=NULL}, 0, 0, FLAGS},
>>>> -    {"tc24hmax",        "set 24 hours max (timecode only)",
>> OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},           0,
>> 1, FLAGS},
>>>> -    {"timecode_rate",   "set rate (timecode only)",
>>   OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,
>> INT_MAX, FLAGS},
>>>> -    {"r",               "set rate (timecode only)",
>>   OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,
>> INT_MAX, FLAGS},
>>>> -    {"rate",            "set rate (timecode only)",
>>   OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,
>> INT_MAX, FLAGS},
>>>> -    {"reload",     "reload text file at specified frame interval",
>> OFFSET(reload),     AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
>>>> -    { "alpha",       "apply alpha while rendering", OFFSET(a_expr),
>>    AV_OPT_TYPE_STRING, { .str = "1"     },          .flags = FLAGS },
>>>> -    {"fix_bounds", "check and fix text coords to avoid clipping",
>> OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
>>>> -    {"start_number", "start frame number for n/frame_num variable",
>> OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
>>>> -    {"text_source", "the source of text", OFFSET(text_source_string),
>> AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
>>>> +    {"tc24hmax",        "set 24 hours max (timecode only)",
>> OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},    0, 1, FLAGS},
>>>> +    {"timecode_rate",   "set rate (timecode only)",
>>   OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX,
>> FLAGS},
>>>> +    {"r",               "set rate (timecode only)",
>>   OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX,
>> FLAGS},
>>>> +    {"rate",            "set rate (timecode only)",
>>   OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX,
>> FLAGS},
>>>> +    {"reload",          "reload text file at specified frame
>> interval", OFFSET(reload), AV_OPT_TYPE_INT, {.i64=0},    0, INT_MAX, FLAGS},
>>>> +    {"alpha",           "apply alpha while rendering",
>> OFFSET(a_expr),        AV_OPT_TYPE_STRING,   {.str = "1"}, .flags = FLAGS},
>>>> +    {"fix_bounds",      "check and fix text coords to avoid clipping",
>> OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
>>>> +    {"start_number",    "start frame number for n/frame_num variable",
>> OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
>>>> +    {"text_source",     "the source of text",
>> OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
>>> Most of these changes seem cosmetic, they do not belong in this patch.
>>>
>>>>    #if CONFIG_LIBFRIBIDI
>>>>        {"text_shaping", "attempt to shape text before drawing",
>> OFFSET(text_shaping), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS},
>>>> @@ -297,18 +387,24 @@ static const struct ft_error {
>>>>
>>>>    #define FT_ERRMSG(e) ft_errors[e].err_msg
>>>>
>>>> -typedef struct Glyph {
>>>> -    FT_Glyph glyph;
>>>> -    FT_Glyph border_glyph;
>>>> -    uint32_t code;
>>>> -    unsigned int fontsize;
>>>> -    FT_Bitmap bitmap; ///< array holding bitmaps of font
>>>> -    FT_Bitmap border_bitmap; ///< array holding bitmaps of font border
>>>> -    FT_BBox bbox;
>>>> -    int advance;
>>>> -    int bitmap_left;
>>>> -    int bitmap_top;
>>>> -} Glyph;
>>>> +
>>>> +// Loads and (optionally) renders a glyph
>>>> +static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr,
>> uint32_t code,
>>>> +     int8_t shift_x64, int8_t shift_y64);
>>>> +
>>>> +// Shapes a line of text using libharfbuzz
>>>> +static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const
>> char* text, int textLen);
>>>> +
>>>> +// Performs text measurements
>>>> +static int measure_text(AVFilterContext *ctx, TextMetrics *metrics);
>>>> +
>>>> +// Draws glyphs on the frame
>>>> +static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
>>>> +                       FFDrawColor *color, TextMetrics *metrics,
>>>> +                       int x, int y, int borderw);
>>>> +
>>>> +// Draws text on the frame
>>>> +static int draw_text(AVFilterContext *ctx, AVFrame *frame);
>>> Why is there a need for forward declarations?
>>>
>>>>    static int glyph_cmp(const void *key, const void *b)
>>>>    {
>>>> @@ -316,80 +412,9 @@ static int glyph_cmp(const void *key, const void
>> *b)
>>>>        int64_t diff = (int64_t)a->code - (int64_t)bb->code;
>>>>
>>>>        if (diff != 0)
>>>> -         return diff > 0 ? 1 : -1;
>>>> +        return diff > 0 ? 1 : -1;
>>> unrelated cosmetics
>>>
>>>> @@ -439,7 +464,6 @@ static av_cold int update_fontsize(AVFilterContext
>> *ctx)
>>>>               return err;
>>>>
>>>>            size = av_expr_eval(s->fontsize_pexpr, s->var_values,
>> &s->prng);
>>>> -
>>> unrelated cosmetics
>>>
>>>>            if (!isnan(size)) {
>>>>                roundedsize = round(size);
>>>>                // test for overflow before cast
>>>> @@ -447,7 +471,6 @@ static av_cold int update_fontsize(AVFilterContext
>> *ctx)
>>>>                    av_log(ctx, AV_LOG_ERROR, "fontsize overflow\n");
>>>>                    return AVERROR(EINVAL);
>>>>                }
>>>> -
>>> unrelated cosmetics
>>>
>>>>                fontsize = roundedsize;
>>>>            }
>>>>        }
>>>> @@ -548,7 +571,7 @@ static int load_font_fontconfig(AVFilterContext
>> *ctx)
>>>>            goto fail;
>>>>        }
>>>>
>>>> -    av_log(ctx, AV_LOG_INFO, "Using \"%s\"\n", filename);
>>>> +    av_log(ctx, AV_LOG_VERBOSE, "Using \"%s\"\n", filename);
>>> unrelated cosmetics
>>>
>>>>        if (parse_err)
>>>>            s->default_fontsize = size + 0.5;
>>>>
>>>> @@ -690,6 +713,7 @@ static int shape_text(AVFilterContext *ctx)
>>>>        s->text = tmp;
>>>>        len = fribidi_unicode_to_charset(FRIBIDI_CHAR_SET_UTF8,
>>>>                                         unicodestr, len, s->text);
>>>> +
>>> unrelated cosmetics
>>>
>>>> +static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const
>> char* text, int textLen)
>>>> +{
>>>> +    hb->buf = hb_buffer_create();
>>> Seems like it at least this needs to be checked for errors, maybe some
>>> of the other calls too.
>>>
>>>> @@ -1559,56 +1795,142 @@ continue_on_invalid2:
>>>>        update_color_with_alpha(s, &bordercolor, s->bordercolor);
>>>>        update_color_with_alpha(s, &boxcolor   , s->boxcolor   );
>>>>
>>>> -    box_w = max_text_line_w;
>>>> -    box_h = y + s->max_glyph_h;
>>>> +    if (s->draw_box && s->boxborderw) {
>>>> +        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left =
>> s->boxborderw;
>>>> +    } else {
>>>> +        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = 0;
>>>> +    }
>>>>
>>>>        if (s->fix_bounds) {
>>>> -
>>>>            /* calculate footprint of text effects */
>>>> -        int boxoffset     = s->draw_box ? FFMAX(s->boxborderw, 0) : 0;
>>>>            int borderoffset  = s->borderw  ? FFMAX(s->borderw, 0) : 0;
>>>>
>>>> -        int offsetleft = FFMAX3(boxoffset, borderoffset,
>>>> +        int offsetleft = FFMAX3(FFMAX(s->bb_left, 0), borderoffset,
>>>>                                    (s->shadowx < 0 ? FFABS(s->shadowx) :
>> 0));
>>>> -        int offsettop = FFMAX3(boxoffset, borderoffset,
>>>> +        int offsettop = FFMAX3(FFMAX(s->bb_top, 0), borderoffset,
>>>>                                    (s->shadowy < 0 ? FFABS(s->shadowy) :
>> 0));
>>>> -
>>>> -        int offsetright = FFMAX3(boxoffset, borderoffset,
>>>> +        int offsetright = FFMAX3(FFMAX(s->bb_right, 0), borderoffset,
>>>>                                     (s->shadowx > 0 ? s->shadowx : 0));
>>>> -        int offsetbottom = FFMAX3(boxoffset, borderoffset,
>>>> +        int offsetbottom = FFMAX3(FFMAX(s->bb_bottom, 0), borderoffset,
>>>>                                      (s->shadowy > 0 ? s->shadowy : 0));
>>>>
>>>> -
>>>>            if (s->x - offsetleft < 0) s->x = offsetleft;
>>>>            if (s->y - offsettop < 0)  s->y = offsettop;
>>>>
>>>> -        if (s->x + box_w + offsetright > width)
>>>> -            s->x = FFMAX(width - box_w - offsetright, 0);
>>>> -        if (s->y + box_h + offsetbottom > height)
>>>> -            s->y = FFMAX(height - box_h - offsetbottom, 0);
>>>> +        if (s->x + metrics.width + offsetright > width)
>>>> +            s->x = FFMAX(width - metrics.width - offsetright, 0);
>>>> +        if (s->y + metrics.height + offsetbottom > height)
>>>> +            s->y = FFMAX(height - metrics.height - offsetbottom, 0);
>>>>        }
>>>>
>>>> -    /* draw box */
>>>> -    if (s->draw_box)
>>>> -        ff_blend_rectangle(&s->dc, &boxcolor,
>>>> -                           frame->data, frame->linesize, width, height,
>>>> -                           s->x - s->boxborderw, s->y - s->boxborderw,
>>>> -                           box_w + s->boxborderw * 2, box_h +
>> s->boxborderw * 2);
>>>> +    x = 0;
>>>> +    y = 0;
>>>> +    x64 = (int)(s->x * 64.);
>>>> +    y64 = (int)(s->y * 64. + metrics.offset_top64);
>>>> +
>>>> +    for (int l = 0; l < s->line_count; ++l) {
>>>> +        TextLine *line = &s->lines[l];
>>>> +        HarfbuzzData *hb = &line->hb_data;
>>>> +        line->glyphs = av_mallocz(hb->glyph_count * sizeof(GlyphInfo));
>>>> +
>>>> +        for (int t = 0; t < hb->glyph_count; ++t) {
>>>> +            GlyphInfo *g_info = &line->glyphs[t];
>>>> +            uint8_t is_tab = last_tab_idx < s->tab_count &&
>>>> +                hb->glyph_info[t].cluster ==
>> s->tab_clusters[last_tab_idx] - line->cluster_offset;
>>>> +            int true_x, true_y;
>>>> +            if (is_tab) {
>>>> +                ++last_tab_idx;
>>>> +            }
>>>> +            true_x = x + hb->glyph_pos[t].x_offset;
>>>> +            true_y = y + hb->glyph_pos[t].y_offset;
>>>> +            shift_x64 = (((x64 + true_x) >> 4) & 0b0011) << 4;
>>>> +            shift_y64 = ((4 - (((y64 + true_y) >> 4) & 0b0011)) &
>> 0b0011) << 4;
>>>> +
>>>> +            ret = load_glyph(ctx, &glyph, hb->glyph_info[t].codepoint,
>> shift_x64, shift_y64);
>>>> +            if (ret != 0) {
>>>> +                return ret;
>>>> +            }
>>>> +            g_info->code = hb->glyph_info[t].codepoint;
>>>> +            g_info->x = (x64 + true_x) >> 6;
>>>> +            g_info->y = ((y64 + true_y) >> 6) + (shift_y64 > 0 ? 1 :
>> 0);
>>>> +            g_info->shift_x64 = shift_x64;
>>>> +            g_info->shift_y64 = shift_y64;
>>>> +
>>>> +            if (!is_tab) {
>>>> +                x += hb->glyph_pos[t].x_advance;
>>>> +            } else {
>>>> +                int size = s->blank_advance64 * s->tabsize;
>>>> +                x = (x / size + 1) * size;
>>>> +            }
>>>> +            y += hb->glyph_pos[t].y_advance;
>>>> +        }
>>>>
>>>> -    if (s->shadowx || s->shadowy) {
>>>> -        if ((ret = draw_glyphs(s, frame, width, height,
>>>> -                               &shadowcolor, s->shadowx, s->shadowy,
>> 0)) < 0)
>>>> -            return ret;
>>>> +        y += metrics.line_height64 + s->line_spacing * 64;
>>>> +        x = 0;
>>>>        }
>>>>
>>>> -    if (s->borderw) {
>>>> -        if ((ret = draw_glyphs(s, frame, width, height,
>>>> -                               &bordercolor, 0, 0, s->borderw)) < 0)
>>>> +    metrics.rect_x = s->x;
>>>> +    metrics.rect_y = s->y;
>>>> +
>>>> +    s->box_width = metrics.width;
>>>> +    s->box_height = metrics.height;
>>> What is the point of duplicating these values in both structs?
>>>
>>>
>> _______________________________________________
>> 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".
From 90c4a9d817ca72ee3c5d12ba5211079d3d497be5 Mon Sep 17 00:00:00 2001
From: yethie <klimklim@tiscali.it>
Date: Mon, 15 May 2023 17:00:30 +0200
Subject: [PATCH 1/9] cosmetics

---
 libavfilter/vf_drawtext.c | 89 +++++++++++++++++++--------------------
 1 file changed, 44 insertions(+), 45 deletions(-)

diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c
index 71ab851462..0c4db3ec53 100644
--- a/libavfilter/vf_drawtext.c
+++ b/libavfilter/vf_drawtext.c
@@ -78,8 +78,8 @@ static const char *const var_names[] = {
     "line_h", "lh",           ///< line height, same as max_glyph_h
     "main_h", "h", "H",       ///< height of the input video
     "main_w", "w", "W",       ///< width  of the input video
-    "max_glyph_a", "ascent",  ///< max glyph ascent
-    "max_glyph_d", "descent", ///< min glyph descent
+    "max_glyph_a", "ascent",  ///< max glyph ascender
+    "max_glyph_d", "descent", ///< min glyph descender
     "max_glyph_h",            ///< max glyph height
     "max_glyph_w",            ///< max glyph width
     "n",                      ///< number of frame
@@ -161,7 +161,7 @@ typedef struct DrawTextContext {
     int exp_mode;                   ///< expansion mode to use for the text
     int reinit;                     ///< tells if the filter is being reinited
 #if CONFIG_LIBFONTCONFIG
-    uint8_t *font;              ///< font to be used
+    uint8_t *font;                  ///< font to be used
 #endif
     uint8_t *fontfile;              ///< font to be used
     uint8_t *text;                  ///< text to be drawn
@@ -227,25 +227,25 @@ typedef struct DrawTextContext {
 #define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM
 
 static const AVOption drawtext_options[]= {
-    {"fontfile",    "set font file",        OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"text",        "set text",             OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"textfile",    "set text file",        OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"fontcolor",   "set foreground color", OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
+    {"fontfile",       "set font file",         OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"text",           "set text",              OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"textfile",       "set text file",         OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"fontcolor",      "set foreground color",  OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
     {"fontcolor_expr", "set foreground color expression", OFFSET(fontcolor_expr), AV_OPT_TYPE_STRING, {.str=""}, 0, 0, FLAGS},
-    {"boxcolor",    "set box color",        OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
-    {"bordercolor", "set border color",     OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
-    {"shadowcolor", "set shadow color",     OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
-    {"box",         "set box",              OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0,        1       , FLAGS},
-    {"boxborderw",  "set box border width", OFFSET(boxborderw),         AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"line_spacing",  "set line spacing in pixels", OFFSET(line_spacing),   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX,FLAGS},
-    {"fontsize",    "set font size",        OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0 , FLAGS},
-    {"x",           "set x expression",     OFFSET(x_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
-    {"y",           "set y expression",     OFFSET(y_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
-    {"shadowx",     "set shadow x offset",  OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"shadowy",     "set shadow y offset",  OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"borderw",     "set border width",     OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"tabsize",     "set tab size",         OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0,        INT_MAX , FLAGS},
-    {"basetime",    "set base time",        OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX , FLAGS},
+    {"boxcolor",       "set box color",         OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
+    {"bordercolor",    "set border color",      OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
+    {"shadowcolor",    "set shadow color",      OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
+    {"box",            "set box",               OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0, 1, FLAGS},
+    {"boxborderw",     "set box borders width", OFFSET(boxborderw),         AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
+    {"line_spacing",   "set line spacing in pixels", OFFSET(line_spacing),  AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
+    {"fontsize",       "set font size",         OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"x",              "set x expression",      OFFSET(x_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
+    {"y",              "set y expression",      OFFSET(y_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
+    {"shadowx",        "set shadow x offset",   OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
+    {"shadowy",        "set shadow y offset",   OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
+    {"borderw",        "set border width",      OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
+    {"tabsize",        "set tab size",          OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0, INT_MAX, FLAGS},
+    {"basetime",       "set base time",         OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX, FLAGS},
 #if CONFIG_LIBFONTCONFIG
     { "font",        "Font name",            OFFSET(font),               AV_OPT_TYPE_STRING, { .str = "Sans" },           .flags = FLAGS },
 #endif
@@ -256,15 +256,15 @@ static const AVOption drawtext_options[]= {
         {"strftime", "set strftime expansion (deprecated)", OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_STRFTIME}, 0, 0, FLAGS, "expansion"},
 
     {"timecode",        "set initial timecode",             OFFSET(tc_opt_string), AV_OPT_TYPE_STRING,   {.str=NULL}, 0, 0, FLAGS},
-    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},           0,        1, FLAGS},
-    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"reload",     "reload text file at specified frame interval", OFFSET(reload),     AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
-    { "alpha",       "apply alpha while rendering", OFFSET(a_expr),      AV_OPT_TYPE_STRING, { .str = "1"     },          .flags = FLAGS },
-    {"fix_bounds", "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
-    {"start_number", "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
-    {"text_source", "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
+    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},    0, 1, FLAGS},
+    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"reload",          "reload text file at specified frame interval", OFFSET(reload), AV_OPT_TYPE_INT, {.i64=0},    0, INT_MAX, FLAGS},
+    {"alpha",           "apply alpha while rendering",      OFFSET(a_expr),        AV_OPT_TYPE_STRING,   {.str = "1"}, .flags = FLAGS},
+    {"fix_bounds",      "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
+    {"start_number",    "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
+    {"text_source",     "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
 
 #if CONFIG_LIBFRIBIDI
     {"text_shaping", "attempt to shape text before drawing", OFFSET(text_shaping), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS},
@@ -324,9 +324,9 @@ static int glyph_cmp(const void *key, const void *b)
     int64_t diff = (int64_t)a->code - (int64_t)bb->code;
 
     if (diff != 0)
-         return diff > 0 ? 1 : -1;
+        return diff > 0 ? 1 : -1;
     else
-         return FFDIFFSIGN((int64_t)a->fontsize, (int64_t)bb->fontsize);
+        return FFDIFFSIGN((int64_t)a->fontsize, (int64_t)bb->fontsize);
 }
 
 /**
@@ -447,7 +447,6 @@ static av_cold int update_fontsize(AVFilterContext *ctx)
            return err;
 
         size = av_expr_eval(s->fontsize_pexpr, s->var_values, &s->prng);
-
         if (!isnan(size)) {
             roundedsize = round(size);
             // test for overflow before cast
@@ -455,7 +454,6 @@ static av_cold int update_fontsize(AVFilterContext *ctx)
                 av_log(ctx, AV_LOG_ERROR, "fontsize overflow\n");
                 return AVERROR(EINVAL);
             }
-
             fontsize = roundedsize;
         }
     }
@@ -556,7 +554,7 @@ static int load_font_fontconfig(AVFilterContext *ctx)
         goto fail;
     }
 
-    av_log(ctx, AV_LOG_INFO, "Using \"%s\"\n", filename);
+    av_log(ctx, AV_LOG_VERBOSE, "Using \"%s\"\n", filename);
     if (parse_err)
         s->default_fontsize = size + 0.5;
 
@@ -698,6 +696,7 @@ static int shape_text(AVFilterContext *ctx)
     s->text = tmp;
     len = fribidi_unicode_to_charset(FRIBIDI_CHAR_SET_UTF8,
                                      unicodestr, len, s->text);
+
     ret = 0;
 
 out:
@@ -888,15 +887,15 @@ static int config_input(AVFilterLink *inlink)
     ff_draw_color(&s->dc, &s->bordercolor, s->bordercolor.rgba);
     ff_draw_color(&s->dc, &s->boxcolor,    s->boxcolor.rgba);
 
-    s->var_values[VAR_w]     = s->var_values[VAR_W]     = s->var_values[VAR_MAIN_W] = inlink->w;
-    s->var_values[VAR_h]     = s->var_values[VAR_H]     = s->var_values[VAR_MAIN_H] = inlink->h;
-    s->var_values[VAR_SAR]   = inlink->sample_aspect_ratio.num ? av_q2d(inlink->sample_aspect_ratio) : 1;
-    s->var_values[VAR_DAR]   = (double)inlink->w / inlink->h * s->var_values[VAR_SAR];
-    s->var_values[VAR_HSUB]  = 1 << s->dc.hsub_max;
-    s->var_values[VAR_VSUB]  = 1 << s->dc.vsub_max;
-    s->var_values[VAR_X]     = NAN;
-    s->var_values[VAR_Y]     = NAN;
-    s->var_values[VAR_T]     = NAN;
+    s->var_values[VAR_w]    = s->var_values[VAR_W] = s->var_values[VAR_MAIN_W] = inlink->w;
+    s->var_values[VAR_h]    = s->var_values[VAR_H] = s->var_values[VAR_MAIN_H] = inlink->h;
+    s->var_values[VAR_SAR]  = inlink->sample_aspect_ratio.num ? av_q2d(inlink->sample_aspect_ratio) : 1;
+    s->var_values[VAR_DAR]  = (double)inlink->w / inlink->h * s->var_values[VAR_SAR];
+    s->var_values[VAR_HSUB] = 1 << s->dc.hsub_max;
+    s->var_values[VAR_VSUB] = 1 << s->dc.vsub_max;
+    s->var_values[VAR_X]    = NAN;
+    s->var_values[VAR_Y]    = NAN;
+    s->var_values[VAR_T]    = NAN;
 
     av_lfg_init(&s->prng, av_get_random_seed());
 
@@ -1423,7 +1422,7 @@ static int draw_text(AVFilterContext *ctx, AVFrame *frame,
 
     av_bprint_clear(bp);
 
-    if(s->basetime != AV_NOPTS_VALUE)
+    if (s->basetime != AV_NOPTS_VALUE)
         now= frame->pts*av_q2d(ctx->inputs[0]->time_base) + s->basetime/1000000;
 
     switch (s->exp_mode) {
Paul B Mahol June 2, 2023, 5:12 p.m. UTC | #20
On Fri, May 26, 2023 at 2:19 PM Francesco Carusi <klimklim@tiscali.it>
wrote:

> I added a patch specifically to include cosmetic changes. I also removed
> the change log section, the forward declarations and improved error
> checking.
> The changes to the configure file are in a dedicated patch (the last
> one: 0009).
>
>
Will carefully review and apply whole set if time permits.


>
> On 20/03/2023 09:50, Paul B Mahol wrote:
> > On Mon, Mar 20, 2023 at 8:41 AM Francesco Carusi <klimklim@tiscali.it>
> > wrote:
> >
> >> Would it be ok if I split the patch so that one is dedicated to
> >> cosmetics and one to the implementation?
> >> I'll also remove the change log section and the forward declarations and
> >> improve the error checking.
> >>
> > Yes split it.
> >
> >
> > Note that reviewers here tend to review only after big time passes and
> are
> > usually nitpicking all the time.
> >
> >
> >> On 16/03/2023 17:52, Anton Khirnov wrote:
> >>> Overall this patch could use a lot more polish. I really wish you
> >>> developed it in a more review-friendly form.
> >>> The commit message should use standard form and elaborate on what
> >>> advantage do we get from this huge change, which also adds a new
> >>> dependendency.
> >>>
> >>>
> >>> Quoting Francesco Carusi (2023-02-03 15:18:21)
> >>>> diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c
> >>>> index 50012bb258..7a0a255c5e 100644
> >>>> --- a/libavfilter/vf_drawtext.c
> >>>> +++ b/libavfilter/vf_drawtext.c
> >>> You are adding a bunch of trailing whitespace in the file, which is
> >>> forbidden.
> >>>
> >>>> @@ -1,4 +1,5 @@
> >>>>    /*
> >>>> + * Copyright (c) 2023 Francesco Carusi
> >>>>     * Copyright (c) 2011 Stefano Sabatini
> >>>>     * Copyright (c) 2010 S.N. Hemanth Meenakshisundaram
> >>>>     * Copyright (c) 2003 Gustavo Sverzut Barbieri <
> >> gsbarbieri@yahoo.com.br>
> >>>> @@ -20,6 +21,14 @@
> >>>>     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> >> 02110-1301 USA
> >>>>     */
> >>>>
> >>>> +/*
> >>>> + * Changelog - 2023
> >>>> + *
> >>>> + * - This filter now depends on libharfbuzz for text shaping.
> >>>> + * - Glyphs position is now accurate to 1/4 pixel in both directions
> >>>> + * - The default line height is now the one defined in the font
> >>>> + */
> >>> This is not the place for a changelog, that's what commit messages and
> >>> the Changelog file are for.
> >>>
> >>>> +
> >>>>    /**
> >>>>     * @file
> >>>>     * drawtext filter, based on the original vhook/drawtext.c
> >>>> @@ -72,14 +81,20 @@
> >>>>    #include FT_GLYPH_H
> >>>>    #include FT_STROKER_H
> >>>>
> >>>> +#include <hb.h>
> >>>> +#include <hb-ft.h>
> >>>> +
> >>>> +// Ceiling operation for positive integers division
> >>>> +#define POS_CEIL(x, y) ((x)/(y) + ((x)%(y) != 0))
> >>>> +
> >>>>    static const char *const var_names[] = {
> >>>>        "dar",
> >>>>        "hsub", "vsub",
> >>>> -    "line_h", "lh",           ///< line height, same as max_glyph_h
> >>>> +    "line_h", "lh",           ///< line height
> >>>>        "main_h", "h", "H",       ///< height of the input video
> >>>>        "main_w", "w", "W",       ///< width  of the input video
> >>>> -    "max_glyph_a", "ascent",  ///< max glyph ascent
> >>>> -    "max_glyph_d", "descent", ///< min glyph descent
> >>>> +    "max_glyph_a", "ascent",  ///< max glyph ascender
> >>>> +    "max_glyph_d", "descent", ///< min glyph descender
> >>> Seems like this should not be in this patch.
> >>>
> >>>>    static const AVOption drawtext_options[]= {
> >>>> -    {"fontfile",    "set font file",        OFFSET(fontfile),
> >>   AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >>>> -    {"text",        "set text",             OFFSET(text),
> >>   AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >>>> -    {"textfile",    "set text file",        OFFSET(textfile),
> >>   AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >>>> -    {"fontcolor",   "set foreground color", OFFSET(fontcolor.rgba),
> >>   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
> >>>> +    {"fontfile",       "set font file",         OFFSET(fontfile),
> >>       AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >>>> +    {"text",           "set text",              OFFSET(text),
> >>       AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >>>> +    {"textfile",       "set text file",         OFFSET(textfile),
> >>       AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
> >>>> +    {"fontcolor",      "set foreground color",
> >> OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0,
> >> FLAGS},
> >>>>        {"fontcolor_expr", "set foreground color expression",
> >> OFFSET(fontcolor_expr), AV_OPT_TYPE_STRING, {.str=""}, 0, 0, FLAGS},
> >>>> -    {"boxcolor",    "set box color",        OFFSET(boxcolor.rgba),
> >>    AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
> >>>> -    {"bordercolor", "set border color",     OFFSET(bordercolor.rgba),
> >>   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
> >>>> -    {"shadowcolor", "set shadow color",     OFFSET(shadowcolor.rgba),
> >>   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
> >>>> -    {"box",         "set box",              OFFSET(draw_box),
> >>   AV_OPT_TYPE_BOOL,   {.i64=0},     0,        1       , FLAGS},
> >>>> -    {"boxborderw",  "set box border width", OFFSET(boxborderw),
> >>   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
> >>>> -    {"line_spacing",  "set line spacing in pixels",
> >> OFFSET(line_spacing),   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,
> >> INT_MAX,FLAGS},
> >>>> -    {"fontsize",    "set font size",        OFFSET(fontsize_expr),
> >>    AV_OPT_TYPE_STRING, {.str=NULL},  0, 0 , FLAGS},
> >>>> -    {"x",           "set x expression",     OFFSET(x_expr),
> >>   AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
> >>>> -    {"y",           "set y expression",     OFFSET(y_expr),
> >>   AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
> >>>> -    {"shadowx",     "set shadow x offset",  OFFSET(shadowx),
> >>    AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
> >>>> -    {"shadowy",     "set shadow y offset",  OFFSET(shadowy),
> >>    AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
> >>>> -    {"borderw",     "set border width",     OFFSET(borderw),
> >>    AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
> >>>> -    {"tabsize",     "set tab size",         OFFSET(tabsize),
> >>    AV_OPT_TYPE_INT,    {.i64=4},     0,        INT_MAX , FLAGS},
> >>>> -    {"basetime",    "set base time",        OFFSET(basetime),
> >>   AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX ,
> FLAGS},
> >>>> +    {"boxcolor",       "set box color",
> >>   OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0,
> >> FLAGS},
> >>>> +    {"bordercolor",    "set border color",
> >> OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0,
> >> FLAGS},
> >>>> +    {"shadowcolor",    "set shadow color",
> >> OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0,
> >> FLAGS},
> >>>> +    {"box",            "set box",               OFFSET(draw_box),
> >>       AV_OPT_TYPE_BOOL,   {.i64=0},     0, 1, FLAGS},
> >>>> +    {"boxborderw",     "set box borders width", OFFSET(boxborderw),
> >>       AV_OPT_TYPE_INT,    {.i64=0},     0, INT_MAX, FLAGS},
> >>>> +    {"line_spacing",   "set line spacing in pixels",
> >> OFFSET(line_spacing),  AV_OPT_TYPE_INT,    {.i64=-1},    INT_MIN,
> INT_MAX,
> >> FLAGS},
> >>>> +    {"fontsize",       "set font size",
> >>   OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0,
> FLAGS},
> >>>> +    {"x",              "set x expression",      OFFSET(x_expr),
> >>       AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
> >>>> +    {"y",              "set y expression",      OFFSET(y_expr),
> >>       AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
> >>>> +    {"shadowx",        "set shadow x offset",   OFFSET(shadowx),
> >>        AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
> >>>> +    {"shadowy",        "set shadow y offset",   OFFSET(shadowy),
> >>        AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
> >>>> +    {"borderw",        "set border width",      OFFSET(borderw),
> >>        AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
> >>>> +    {"tabsize",        "set tab size",          OFFSET(tabsize),
> >>        AV_OPT_TYPE_INT,    {.i64=4},     0, INT_MAX, FLAGS},
> >>>> +    {"basetime",       "set base time",         OFFSET(basetime),
> >>       AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX,
> >> FLAGS},
> >>>>    #if CONFIG_LIBFONTCONFIG
> >>>>        { "font",        "Font name",            OFFSET(font),
> >>     AV_OPT_TYPE_STRING, { .str = "Sans" },           .flags = FLAGS },
> >>>>    #endif
> >>>> @@ -248,15 +338,15 @@ static const AVOption drawtext_options[]= {
> >>>>            {"strftime", "set strftime expansion (deprecated)",
> >> OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_STRFTIME}, 0, 0, FLAGS,
> >> "expansion"},
> >>>>        {"timecode",        "set initial timecode",
> >>   OFFSET(tc_opt_string), AV_OPT_TYPE_STRING,   {.str=NULL}, 0, 0,
> FLAGS},
> >>>> -    {"tc24hmax",        "set 24 hours max (timecode only)",
> >> OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},           0,
> >> 1, FLAGS},
> >>>> -    {"timecode_rate",   "set rate (timecode only)",
> >>   OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,
> >> INT_MAX, FLAGS},
> >>>> -    {"r",               "set rate (timecode only)",
> >>   OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,
> >> INT_MAX, FLAGS},
> >>>> -    {"rate",            "set rate (timecode only)",
> >>   OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,
> >> INT_MAX, FLAGS},
> >>>> -    {"reload",     "reload text file at specified frame interval",
> >> OFFSET(reload),     AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
> >>>> -    { "alpha",       "apply alpha while rendering", OFFSET(a_expr),
> >>    AV_OPT_TYPE_STRING, { .str = "1"     },          .flags = FLAGS },
> >>>> -    {"fix_bounds", "check and fix text coords to avoid clipping",
> >> OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
> >>>> -    {"start_number", "start frame number for n/frame_num variable",
> >> OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
> >>>> -    {"text_source", "the source of text", OFFSET(text_source_string),
> >> AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
> >>>> +    {"tc24hmax",        "set 24 hours max (timecode only)",
> >> OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},    0, 1, FLAGS},
> >>>> +    {"timecode_rate",   "set rate (timecode only)",
> >>   OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX,
> >> FLAGS},
> >>>> +    {"r",               "set rate (timecode only)",
> >>   OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX,
> >> FLAGS},
> >>>> +    {"rate",            "set rate (timecode only)",
> >>   OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX,
> >> FLAGS},
> >>>> +    {"reload",          "reload text file at specified frame
> >> interval", OFFSET(reload), AV_OPT_TYPE_INT, {.i64=0},    0, INT_MAX,
> FLAGS},
> >>>> +    {"alpha",           "apply alpha while rendering",
> >> OFFSET(a_expr),        AV_OPT_TYPE_STRING,   {.str = "1"}, .flags =
> FLAGS},
> >>>> +    {"fix_bounds",      "check and fix text coords to avoid
> clipping",
> >> OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
> >>>> +    {"start_number",    "start frame number for n/frame_num
> variable",
> >> OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
> >>>> +    {"text_source",     "the source of text",
> >> OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1,
> FLAGS },
> >>> Most of these changes seem cosmetic, they do not belong in this patch.
> >>>
> >>>>    #if CONFIG_LIBFRIBIDI
> >>>>        {"text_shaping", "attempt to shape text before drawing",
> >> OFFSET(text_shaping), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS},
> >>>> @@ -297,18 +387,24 @@ static const struct ft_error {
> >>>>
> >>>>    #define FT_ERRMSG(e) ft_errors[e].err_msg
> >>>>
> >>>> -typedef struct Glyph {
> >>>> -    FT_Glyph glyph;
> >>>> -    FT_Glyph border_glyph;
> >>>> -    uint32_t code;
> >>>> -    unsigned int fontsize;
> >>>> -    FT_Bitmap bitmap; ///< array holding bitmaps of font
> >>>> -    FT_Bitmap border_bitmap; ///< array holding bitmaps of font
> border
> >>>> -    FT_BBox bbox;
> >>>> -    int advance;
> >>>> -    int bitmap_left;
> >>>> -    int bitmap_top;
> >>>> -} Glyph;
> >>>> +
> >>>> +// Loads and (optionally) renders a glyph
> >>>> +static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr,
> >> uint32_t code,
> >>>> +     int8_t shift_x64, int8_t shift_y64);
> >>>> +
> >>>> +// Shapes a line of text using libharfbuzz
> >>>> +static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const
> >> char* text, int textLen);
> >>>> +
> >>>> +// Performs text measurements
> >>>> +static int measure_text(AVFilterContext *ctx, TextMetrics *metrics);
> >>>> +
> >>>> +// Draws glyphs on the frame
> >>>> +static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
> >>>> +                       FFDrawColor *color, TextMetrics *metrics,
> >>>> +                       int x, int y, int borderw);
> >>>> +
> >>>> +// Draws text on the frame
> >>>> +static int draw_text(AVFilterContext *ctx, AVFrame *frame);
> >>> Why is there a need for forward declarations?
> >>>
> >>>>    static int glyph_cmp(const void *key, const void *b)
> >>>>    {
> >>>> @@ -316,80 +412,9 @@ static int glyph_cmp(const void *key, const void
> >> *b)
> >>>>        int64_t diff = (int64_t)a->code - (int64_t)bb->code;
> >>>>
> >>>>        if (diff != 0)
> >>>> -         return diff > 0 ? 1 : -1;
> >>>> +        return diff > 0 ? 1 : -1;
> >>> unrelated cosmetics
> >>>
> >>>> @@ -439,7 +464,6 @@ static av_cold int update_fontsize(AVFilterContext
> >> *ctx)
> >>>>               return err;
> >>>>
> >>>>            size = av_expr_eval(s->fontsize_pexpr, s->var_values,
> >> &s->prng);
> >>>> -
> >>> unrelated cosmetics
> >>>
> >>>>            if (!isnan(size)) {
> >>>>                roundedsize = round(size);
> >>>>                // test for overflow before cast
> >>>> @@ -447,7 +471,6 @@ static av_cold int update_fontsize(AVFilterContext
> >> *ctx)
> >>>>                    av_log(ctx, AV_LOG_ERROR, "fontsize overflow\n");
> >>>>                    return AVERROR(EINVAL);
> >>>>                }
> >>>> -
> >>> unrelated cosmetics
> >>>
> >>>>                fontsize = roundedsize;
> >>>>            }
> >>>>        }
> >>>> @@ -548,7 +571,7 @@ static int load_font_fontconfig(AVFilterContext
> >> *ctx)
> >>>>            goto fail;
> >>>>        }
> >>>>
> >>>> -    av_log(ctx, AV_LOG_INFO, "Using \"%s\"\n", filename);
> >>>> +    av_log(ctx, AV_LOG_VERBOSE, "Using \"%s\"\n", filename);
> >>> unrelated cosmetics
> >>>
> >>>>        if (parse_err)
> >>>>            s->default_fontsize = size + 0.5;
> >>>>
> >>>> @@ -690,6 +713,7 @@ static int shape_text(AVFilterContext *ctx)
> >>>>        s->text = tmp;
> >>>>        len = fribidi_unicode_to_charset(FRIBIDI_CHAR_SET_UTF8,
> >>>>                                         unicodestr, len, s->text);
> >>>> +
> >>> unrelated cosmetics
> >>>
> >>>> +static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const
> >> char* text, int textLen)
> >>>> +{
> >>>> +    hb->buf = hb_buffer_create();
> >>> Seems like it at least this needs to be checked for errors, maybe some
> >>> of the other calls too.
> >>>
> >>>> @@ -1559,56 +1795,142 @@ continue_on_invalid2:
> >>>>        update_color_with_alpha(s, &bordercolor, s->bordercolor);
> >>>>        update_color_with_alpha(s, &boxcolor   , s->boxcolor   );
> >>>>
> >>>> -    box_w = max_text_line_w;
> >>>> -    box_h = y + s->max_glyph_h;
> >>>> +    if (s->draw_box && s->boxborderw) {
> >>>> +        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left =
> >> s->boxborderw;
> >>>> +    } else {
> >>>> +        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = 0;
> >>>> +    }
> >>>>
> >>>>        if (s->fix_bounds) {
> >>>> -
> >>>>            /* calculate footprint of text effects */
> >>>> -        int boxoffset     = s->draw_box ? FFMAX(s->boxborderw, 0) :
> 0;
> >>>>            int borderoffset  = s->borderw  ? FFMAX(s->borderw, 0) : 0;
> >>>>
> >>>> -        int offsetleft = FFMAX3(boxoffset, borderoffset,
> >>>> +        int offsetleft = FFMAX3(FFMAX(s->bb_left, 0), borderoffset,
> >>>>                                    (s->shadowx < 0 ?
> FFABS(s->shadowx) :
> >> 0));
> >>>> -        int offsettop = FFMAX3(boxoffset, borderoffset,
> >>>> +        int offsettop = FFMAX3(FFMAX(s->bb_top, 0), borderoffset,
> >>>>                                    (s->shadowy < 0 ?
> FFABS(s->shadowy) :
> >> 0));
> >>>> -
> >>>> -        int offsetright = FFMAX3(boxoffset, borderoffset,
> >>>> +        int offsetright = FFMAX3(FFMAX(s->bb_right, 0), borderoffset,
> >>>>                                     (s->shadowx > 0 ? s->shadowx :
> 0));
> >>>> -        int offsetbottom = FFMAX3(boxoffset, borderoffset,
> >>>> +        int offsetbottom = FFMAX3(FFMAX(s->bb_bottom, 0),
> borderoffset,
> >>>>                                      (s->shadowy > 0 ? s->shadowy :
> 0));
> >>>>
> >>>> -
> >>>>            if (s->x - offsetleft < 0) s->x = offsetleft;
> >>>>            if (s->y - offsettop < 0)  s->y = offsettop;
> >>>>
> >>>> -        if (s->x + box_w + offsetright > width)
> >>>> -            s->x = FFMAX(width - box_w - offsetright, 0);
> >>>> -        if (s->y + box_h + offsetbottom > height)
> >>>> -            s->y = FFMAX(height - box_h - offsetbottom, 0);
> >>>> +        if (s->x + metrics.width + offsetright > width)
> >>>> +            s->x = FFMAX(width - metrics.width - offsetright, 0);
> >>>> +        if (s->y + metrics.height + offsetbottom > height)
> >>>> +            s->y = FFMAX(height - metrics.height - offsetbottom, 0);
> >>>>        }
> >>>>
> >>>> -    /* draw box */
> >>>> -    if (s->draw_box)
> >>>> -        ff_blend_rectangle(&s->dc, &boxcolor,
> >>>> -                           frame->data, frame->linesize, width,
> height,
> >>>> -                           s->x - s->boxborderw, s->y -
> s->boxborderw,
> >>>> -                           box_w + s->boxborderw * 2, box_h +
> >> s->boxborderw * 2);
> >>>> +    x = 0;
> >>>> +    y = 0;
> >>>> +    x64 = (int)(s->x * 64.);
> >>>> +    y64 = (int)(s->y * 64. + metrics.offset_top64);
> >>>> +
> >>>> +    for (int l = 0; l < s->line_count; ++l) {
> >>>> +        TextLine *line = &s->lines[l];
> >>>> +        HarfbuzzData *hb = &line->hb_data;
> >>>> +        line->glyphs = av_mallocz(hb->glyph_count *
> sizeof(GlyphInfo));
> >>>> +
> >>>> +        for (int t = 0; t < hb->glyph_count; ++t) {
> >>>> +            GlyphInfo *g_info = &line->glyphs[t];
> >>>> +            uint8_t is_tab = last_tab_idx < s->tab_count &&
> >>>> +                hb->glyph_info[t].cluster ==
> >> s->tab_clusters[last_tab_idx] - line->cluster_offset;
> >>>> +            int true_x, true_y;
> >>>> +            if (is_tab) {
> >>>> +                ++last_tab_idx;
> >>>> +            }
> >>>> +            true_x = x + hb->glyph_pos[t].x_offset;
> >>>> +            true_y = y + hb->glyph_pos[t].y_offset;
> >>>> +            shift_x64 = (((x64 + true_x) >> 4) & 0b0011) << 4;
> >>>> +            shift_y64 = ((4 - (((y64 + true_y) >> 4) & 0b0011)) &
> >> 0b0011) << 4;
> >>>> +
> >>>> +            ret = load_glyph(ctx, &glyph,
> hb->glyph_info[t].codepoint,
> >> shift_x64, shift_y64);
> >>>> +            if (ret != 0) {
> >>>> +                return ret;
> >>>> +            }
> >>>> +            g_info->code = hb->glyph_info[t].codepoint;
> >>>> +            g_info->x = (x64 + true_x) >> 6;
> >>>> +            g_info->y = ((y64 + true_y) >> 6) + (shift_y64 > 0 ? 1 :
> >> 0);
> >>>> +            g_info->shift_x64 = shift_x64;
> >>>> +            g_info->shift_y64 = shift_y64;
> >>>> +
> >>>> +            if (!is_tab) {
> >>>> +                x += hb->glyph_pos[t].x_advance;
> >>>> +            } else {
> >>>> +                int size = s->blank_advance64 * s->tabsize;
> >>>> +                x = (x / size + 1) * size;
> >>>> +            }
> >>>> +            y += hb->glyph_pos[t].y_advance;
> >>>> +        }
> >>>>
> >>>> -    if (s->shadowx || s->shadowy) {
> >>>> -        if ((ret = draw_glyphs(s, frame, width, height,
> >>>> -                               &shadowcolor, s->shadowx, s->shadowy,
> >> 0)) < 0)
> >>>> -            return ret;
> >>>> +        y += metrics.line_height64 + s->line_spacing * 64;
> >>>> +        x = 0;
> >>>>        }
> >>>>
> >>>> -    if (s->borderw) {
> >>>> -        if ((ret = draw_glyphs(s, frame, width, height,
> >>>> -                               &bordercolor, 0, 0, s->borderw)) < 0)
> >>>> +    metrics.rect_x = s->x;
> >>>> +    metrics.rect_y = s->y;
> >>>> +
> >>>> +    s->box_width = metrics.width;
> >>>> +    s->box_height = metrics.height;
> >>> What is the point of duplicating these values in both structs?
> >>>
> >>>
> >> _______________________________________________
> >> 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".
> _______________________________________________
> 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".
>
Paul B Mahol Sept. 11, 2023, 10:01 a.m. UTC | #21
See this bug report:
https://trac.ffmpeg.org/ticket/10531
diff mbox series

Patch

diff --git a/configure b/configure
index 6e88c32223..8c2c434c66 100755
--- a/configure
+++ b/configure
@@ -235,6 +235,7 @@  External library support:
   --enable-libfontconfig   enable libfontconfig, useful for drawtext filter [no]
   --enable-libfreetype     enable libfreetype, needed for drawtext filter [no]
   --enable-libfribidi      enable libfribidi, improves drawtext filter [no]
+  --enable-libharfbuzz     enable libharfbuzz, needed for drawtext filter [no]
   --enable-libglslang      enable GLSL->SPIRV compilation via libglslang [no]
   --enable-libgme          enable Game Music Emu via libgme [no]
   --enable-libgsm          enable GSM de/encoding via libgsm [no]
@@ -1818,6 +1819,7 @@  EXTERNAL_LIBRARY_LIST="
     libfontconfig
     libfreetype
     libfribidi
+    libharfbuzz
     libglslang
     libgme
     libgsm
@@ -3657,7 +3659,7 @@  dilation_opencl_filter_deps="opencl"
 dnn_classify_filter_select="dnn"
 dnn_detect_filter_select="dnn"
 dnn_processing_filter_select="dnn"
-drawtext_filter_deps="libfreetype"
+drawtext_filter_deps="libfreetype libharfbuzz"
 drawtext_filter_suggest="libfontconfig libfribidi"
 elbg_filter_deps="avcodec"
 eq_filter_deps="gpl"
@@ -6577,6 +6579,7 @@  enabled fontconfig        && enable libfontconfig
 enabled libfontconfig     && require_pkg_config libfontconfig fontconfig "fontconfig/fontconfig.h" FcInit
 enabled libfreetype       && require_pkg_config libfreetype freetype2 "ft2build.h FT_FREETYPE_H" FT_Init_FreeType
 enabled libfribidi        && require_pkg_config libfribidi fribidi fribidi.h fribidi_version_info
+enabled libharfbuzz       && require_pkg_config libharfbuzz harfbuzz hb.h hb_buffer_create
 enabled libglslang && { check_lib spirv_compiler glslang/Include/glslang_c_interface.h glslang_initialize_process \
                             -lglslang -lMachineIndependent -lOSDependent -lHLSL -lOGLCompiler -lGenericCodeGen \
                             -lSPVRemapper -lSPIRV -lSPIRV-Tools-opt -lSPIRV-Tools -lpthread -lstdc++ -lm ||
diff --git a/doc/filters.texi b/doc/filters.texi
index 60e4d113a1..ea2e4f0f4e 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -12016,7 +12016,7 @@  Draw a text string or text from a specified file on top of a video, using the
 libfreetype library.
 
 To enable compilation of this filter, you need to configure FFmpeg with
-@code{--enable-libfreetype}.
+@code{--enable-libfreetype} and @code{--enable-libharfbuzz}.
 To enable default font fallback and the @var{font} option you need to
 configure FFmpeg with @code{--enable-libfontconfig}.
 To enable the @var{text_shaping} option, you need to configure FFmpeg with
@@ -12033,9 +12033,27 @@  Used to draw a box around text using the background color.
 The value must be either 1 (enable) or 0 (disable).
 The default value of @var{box} is 0.
 
+@item boxw
+Set the width of the box to be drawn around text.
+The default value of @var{boxw} is computed automatically to match the text width
+
+@item boxh
+Set the height of the box to be drawn around text.
+The default value of @var{boxh} is computed automatically to match the text height
+
 @item boxborderw
 Set the width of the border to be drawn around the box using @var{boxcolor}.
-The default value of @var{boxborderw} is 0.
+The value must be specified using one of the following formats:
+@itemize @bullet
+@item @code{boxborderw=10} set the width of all the borders to 10
+@item @code{boxborderw=10|20} set the width of the top and bottom borders to 10
+    and the width of the left and right borders to 20
+@item @code{boxborderw=10|20|30} set the width of the top border to 10, the width
+    of the bottom border to 30 and the width of the left and right borders to 20
+@item @code{boxborderw=10|20|30|40} set the borders width to 10 (top), 20 (right),
+    30 (bottom), 40 (left)
+@end itemize
+The default value of @var{boxborderw} is "0".
 
 @item boxcolor
 The color to be used for drawing box around text. For the syntax of this
@@ -12044,8 +12062,22 @@  option, check the @ref{color syntax,,"Color" section in the ffmpeg-utils manual,
 The default value of @var{boxcolor} is "white".
 
 @item line_spacing
-Set the line spacing in pixels of the border to be drawn around the box using @var{box}.
-The default value of @var{line_spacing} is 0.
+Set the line spacing in pixels. The default value of @var{line_spacing} is 0.
+
+@item text_align
+Set the vertical and horizontal alignment of the text with respect to the box boundaries.
+The value must contain exactly two letters, one for the vertical alignment (T=top,
+M=middle, B=bottom) and one for the horizontal alignment (L=left, C=center, R=right).
+
+@item y_align
+Specify what the @var{y} value is referred to. Possible values are:
+@itemize @bullet
+@item @code{text} the top of the highest glyph of the first text line is placed at @var{y}
+@item @code{baseline} the baseline of the first text line is placed at @var{y}
+@item @code{font} the baseline of the first text line is placed at @var{y} plus the
+    ascent (in pixels) defined in the font metrics
+@end itemize
+The default value of @var{y_align} is "text" for backward compatibility.
 
 @item borderw
 Set the width of the border to be drawn around the text using @var{bordercolor}.
@@ -12054,7 +12086,6 @@  The default value of @var{borderw} is 0.
 @item bordercolor
 Set the color to be used for drawing border around text. For the syntax of this
 option, check the @ref{color syntax,,"Color" section in the ffmpeg-utils manual,ffmpeg-utils}.
-
 The default value of @var{bordercolor} is "black".
 
 @item expansion
@@ -12152,10 +12183,6 @@  values. The default value for both is "0".
 The starting frame number for the n/frame_num variable. The default value
 is "0".
 
-@item tabsize
-The size in number of spaces to use for rendering the tab.
-Default value is 4.
-
 @item timecode
 Set the initial timecode representation in "hh:mm:ss[:;.]ff"
 format. It can be used with or without text parameter. @var{timecode_rate}
@@ -12253,6 +12280,18 @@  contained in the rendered text, it is equivalent to @var{ascent} -
 maximum glyph width, that is the maximum width for all the glyphs
 contained in the rendered text
 
+@item font_a
+the ascent size defined in the font metrics
+
+@item font_d
+the descent size defined in the font metrics
+
+@item top_a
+the maximum ascender of the glyphs of the first text line
+
+@item bottom_d
+the maximum descender of the glyphs of the last text line
+
 @item n
 the number of input frame, starting from 0
 
@@ -12417,6 +12456,33 @@  Full filter invocation with sendcmd would look like this:
 @example
 sendcmd=c='56.0 drawtext reinit fontsize=56\:fontcolor=green\:text=Hello\\ World'
 @end example
+
+@item change
+Alter a subset of the existing filter parameters. Using this command results in a
+faster execution of the filter rather than using the 'reinit' command. The syntax
+for the argument is the same as for filter invocation, the following parameters
+can be altered:
+@itemize @bullet
+@item x
+@item y
+@item alpha
+@item fontsize
+@item fontcolor
+@item boxcolor
+@item bordercolor
+@item shadowcolor
+@item fontcolor_expr
+@item box
+@item boxw
+@item boxh
+@item boxborderw
+@item line_spacing
+@item text_align
+@item shadowx
+@item shadowy
+@item borderw
+@end itemize
+
 @end table
 
 If the entire argument can't be parsed or applied as valid values then the filter will
@@ -12543,6 +12609,9 @@  For more information about fontconfig, check:
 For more information about libfribidi, check:
 @url{http://fribidi.org/}.
 
+For more information about libharfbuzz, check:
+@url{https://github.com/harfbuzz/harfbuzz}.
+
 @section edgedetect
 
 Detect and draw edges. The filter uses the Canny Edge Detection algorithm.
diff --git a/libavfilter/vf_drawtext.c b/libavfilter/vf_drawtext.c
index 50012bb258..9c2c2a4efd 100644
--- a/libavfilter/vf_drawtext.c
+++ b/libavfilter/vf_drawtext.c
@@ -1,4 +1,5 @@ 
 /*
+ * Copyright (c) 2023 Francesco Carusi
  * Copyright (c) 2011 Stefano Sabatini
  * Copyright (c) 2010 S.N. Hemanth Meenakshisundaram
  * Copyright (c) 2003 Gustavo Sverzut Barbieri <gsbarbieri@yahoo.com.br>
@@ -20,6 +21,27 @@ 
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  */
 
+/*
+ * Changelog - 2023
+ *
+ * - This filter now depends on libharfbuzz for text shaping.
+ * - Glyphs position is now accurate to 1/4 pixel in both directions
+ * - The size of the background box can now be forced with the boxw
+ *   and boxh parameters
+ * - Text can be aligned horizontally (top, middle, bottom) and vertically
+ *   (left, center, right) relative to the background box
+ * - The default line height is now the one defined in the font
+ * - The new y_align parameter specifies if the user provided y value is
+ *   referred to the top of the text, to the font baseline or to the
+ *   top of the font.
+ * - The boxborderw parameter can now contain a different value for each border
+ *   (e.g. boxborderw=top|right|bottom|left)
+ * - The "change" command can be used to modify a subset of the filter parameters.
+ *   Using the "change" command is much faster than using the "reinit" command.
+ * - The following new variables can be used in the x and y expressions:
+ *   font_a, font_d, top_a, bottom_d
+ */
+
 /**
  * @file
  * drawtext filter, based on the original vhook/drawtext.c
@@ -72,16 +94,26 @@ 
 #include FT_GLYPH_H
 #include FT_STROKER_H
 
+#include <hb.h>
+#include <hb-ft.h>
+
+// Ceiling operation for positive integers division
+#define POS_CEIL(x, y) ((x)/(y) + ((x)%(y) != 0))
+
 static const char *const var_names[] = {
     "dar",
     "hsub", "vsub",
-    "line_h", "lh",           ///< line height, same as max_glyph_h
+    "line_h", "lh",           ///< line height
     "main_h", "h", "H",       ///< height of the input video
     "main_w", "w", "W",       ///< width  of the input video
-    "max_glyph_a", "ascent",  ///< max glyph ascent
-    "max_glyph_d", "descent", ///< min glyph descent
+    "max_glyph_a", "ascent",  ///< max glyph ascender
+    "max_glyph_d", "descent", ///< min glyph descender
     "max_glyph_h",            ///< max glyph height
     "max_glyph_w",            ///< max glyph width
+    "font_a",                 ///< font-defined ascent
+    "font_d",                 ///< font-defined descent
+    "top_a",                  ///< max glyph ascender of the top line
+    "bottom_d",               ///< max glyph descender of the bottom line
     "n",                      ///< number of frame
     "sar",
     "t",                      ///< timestamp expressed in seconds
@@ -125,6 +157,10 @@  enum var_name {
     VAR_MAX_GLYPH_D, VAR_DESCENT,
     VAR_MAX_GLYPH_H,
     VAR_MAX_GLYPH_W,
+    VAR_FONT_A,
+    VAR_FONT_D,
+    VAR_TOP_A,
+    VAR_BOTTOM_D,
     VAR_N,
     VAR_SAR,
     VAR_T,
@@ -148,12 +184,84 @@  enum expansion_mode {
     EXP_STRFTIME,
 };
 
+enum y_alignment {
+    YA_TEXT,
+    YA_BASELINE,
+    YA_FONT,
+};
+
+typedef struct HarfbuzzData {
+    hb_buffer_t* buf;
+    hb_font_t* font;
+    unsigned int glyph_count;
+    hb_glyph_info_t* glyph_info;
+    hb_glyph_position_t* glyph_pos;
+} HarfbuzzData;
+
+/** Information about a single glyph in a text line */
+typedef struct GlyphInfo {
+    uint32_t code;                  ///< the glyph code point
+    int x;                          ///< the x position of the glyph
+    int y;                          ///< the y position of the glyph
+    int shift_x64;                  ///< the horizontal shift of the glyph in 26.6 units
+    int shift_y64;                  ///< the vertical shift of the glyph in 26.6 units
+} GlyphInfo;
+
+// Information about a single line of text
+typedef struct TextLine {
+    int offset_left64;              ///< offset between the origin and
+                                    ///  the leftmost pixel of the first glyph
+    int offset_right64;             ///< maximum offset between the origin and
+                                    ///  the rightmost pixel of the last glyph
+    int width64;                    ///< width of the line
+    HarfbuzzData hb_data;           ///< libharfbuzz data of this text line
+    GlyphInfo* glyphs;              ///< array of glyphs in this text line
+    int cluster_offset;             ///< the offset at which this line begins
+} TextLine;
+
+/** A glyph as loaded and rendered using libfreetype */
+typedef struct Glyph {
+    FT_Glyph glyph;
+    FT_Glyph border_glyph;
+    uint32_t code;
+    unsigned int fontsize;
+    /** Glyph bitmaps with 1/4 pixel precision in both directions */
+    FT_BitmapGlyph bglyph[16];
+    /** Outlined glyph bitmaps with 1/4 pixel precision in both directions */
+    FT_BitmapGlyph border_bglyph[16];
+    FT_BBox bbox;
+} Glyph;
+
+/** Global text metrics */
+typedef struct TextMetrics {
+    int offset_top64;               ///< ascender amount of the first line (in 26.6 units)
+    int offset_bottom64;            ///< descender amount of the last line (in 26.6 units)
+    int offset_left64;              ///< maximum offset between the origin and
+                                    ///  the leftmost pixel of the first glyph
+                                    ///  of each line (in 26.6 units)
+    int offset_right64;             ///< maximum offset between the origin and
+                                    ///  the rightmost pixel of the last glyph
+                                    ///  of each line (in 26.6 units)
+    int line_height64;              ///< the font-defined line height
+    int width;                      ///< width of the longest line - ceil(width64/64)
+    int height;                     ///< total height of the text - ceil(height64/64)
+
+    int min_y64;                    ///< minimum value of bbox.yMin among glyphs (in 26.6 units)
+    int max_y64;                    ///< maximum value of bbox.yMax among glyphs (in 26.6 units)
+    int min_x64;                    ///< minimum value of bbox.xMin among glyphs (in 26.6 units)
+    int max_x64;                    ///< maximum value of bbox.xMax among glyphs (in 26.6 units)
+
+    // Position and size of the background box (without borders)
+    int rect_x;                     ///< x position of the box
+    int rect_y;                     ///< y position of the box
+} TextMetrics;
+
 typedef struct DrawTextContext {
     const AVClass *class;
     int exp_mode;                   ///< expansion mode to use for the text
     int reinit;                     ///< tells if the filter is being reinited
 #if CONFIG_LIBFONTCONFIG
-    uint8_t *font;              ///< font to be used
+    uint8_t *font;                  ///< font to be used
 #endif
     uint8_t *fontfile;              ///< font to be used
     uint8_t *text;                  ///< text to be drawn
@@ -161,11 +269,9 @@  typedef struct DrawTextContext {
     uint8_t *fontcolor_expr;        ///< fontcolor expression to evaluate
     AVBPrint expanded_fontcolor;    ///< used to contain the expanded fontcolor spec
     int ft_load_flags;              ///< flags used for loading fonts, see FT_LOAD_*
-    FT_Vector *positions;           ///< positions for each element in the text
-    size_t nb_positions;            ///< number of elements of positions array
     char *textfile;                 ///< file with text to be drawn
-    int x;                          ///< x position to start drawing text
-    int y;                          ///< y position to start drawing text
+    double x;                       ///< x position to start drawing text
+    double y;                       ///< y position to start drawing text
     int max_glyph_w;                ///< max glyph width
     int max_glyph_h;                ///< max glyph height
     int shadowx, shadowy;
@@ -177,8 +283,14 @@  typedef struct DrawTextContext {
 
     int line_spacing;               ///< lines spacing in pixels
     short int draw_box;             ///< draw box around text - true or false
-    int boxborderw;                 ///< box border width
-    int use_kerning;                ///< font kerning is used - true/false
+    char* boxborderw;               ///< box border width (padding)
+                                    ///  allowed formats: "all", "vert|oriz", "top|right|bottom|left"
+    int bb_top;                     ///< the size of the top box border
+    int bb_right;                   ///< the size of the right box border
+    int bb_bottom;                  ///< the size of the bottom box border
+    int bb_left;                    ///< the size of the left box border
+    int box_width;                  ///< the width of box
+    int box_height;                 ///< the height of box
     int tabsize;                    ///< tab size
     int fix_bounds;                 ///< do we let it go out of frame bounds - t/f
 
@@ -213,31 +325,45 @@  typedef struct DrawTextContext {
     int text_shaping;               ///< 1 to shape the text before drawing it
 #endif
     AVDictionary *metadata;
+
+    int boxw;                       ///< the value of the boxw parameter
+    int boxh;                       ///< the value of the boxh parameter
+    uint8_t *text_align;            ///< the horizontal and vertical text alignment
+    int y_align;                    ///< the value of the y_align parameter
+
+    TextLine *lines;                ///< computed information about text lines
+    int line_count;                 ///< the number of text lines
+    uint32_t *tab_clusters;         ///< the position of tab characters in the text
+    int tab_count;                  ///< the number of tab characters
+    int blank_advance64;            ///< the size of the space character
 } DrawTextContext;
 
 #define OFFSET(x) offsetof(DrawTextContext, x)
 #define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM
 
 static const AVOption drawtext_options[]= {
-    {"fontfile",    "set font file",        OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"text",        "set text",             OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"textfile",    "set text file",        OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
-    {"fontcolor",   "set foreground color", OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
+    {"fontfile",       "set font file",         OFFSET(fontfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"text",           "set text",              OFFSET(text),               AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"textfile",       "set text file",         OFFSET(textfile),           AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"fontcolor",      "set foreground color",  OFFSET(fontcolor.rgba),     AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
     {"fontcolor_expr", "set foreground color expression", OFFSET(fontcolor_expr), AV_OPT_TYPE_STRING, {.str=""}, 0, 0, FLAGS},
-    {"boxcolor",    "set box color",        OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
-    {"bordercolor", "set border color",     OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
-    {"shadowcolor", "set shadow color",     OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
-    {"box",         "set box",              OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0,        1       , FLAGS},
-    {"boxborderw",  "set box border width", OFFSET(boxborderw),         AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"line_spacing",  "set line spacing in pixels", OFFSET(line_spacing),   AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX,FLAGS},
-    {"fontsize",    "set font size",        OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0 , FLAGS},
-    {"x",           "set x expression",     OFFSET(x_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
-    {"y",           "set y expression",     OFFSET(y_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
-    {"shadowx",     "set shadow x offset",  OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"shadowy",     "set shadow y offset",  OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"borderw",     "set border width",     OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN,  INT_MAX , FLAGS},
-    {"tabsize",     "set tab size",         OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0,        INT_MAX , FLAGS},
-    {"basetime",    "set base time",        OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX , FLAGS},
+    {"boxcolor",       "set box color",         OFFSET(boxcolor.rgba),      AV_OPT_TYPE_COLOR,  {.str="white"}, 0, 0, FLAGS},
+    {"bordercolor",    "set border color",      OFFSET(bordercolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
+    {"shadowcolor",    "set shadow color",      OFFSET(shadowcolor.rgba),   AV_OPT_TYPE_COLOR,  {.str="black"}, 0, 0, FLAGS},
+    {"box",            "set box",               OFFSET(draw_box),           AV_OPT_TYPE_BOOL,   {.i64=0},     0, 1, FLAGS},
+    {"boxborderw",     "set box borders width", OFFSET(boxborderw),         AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
+    {"line_spacing",   "set line spacing in pixels", OFFSET(line_spacing), AV_OPT_TYPE_INT,    {.i64=-1},    INT_MIN, INT_MAX, FLAGS},
+    {"fontsize",       "set font size",         OFFSET(fontsize_expr),      AV_OPT_TYPE_STRING, {.str=NULL},  0, 0, FLAGS},
+    {"text_align",     "set text alignment",    OFFSET(text_align),         AV_OPT_TYPE_STRING, {.str="TL"},  0, 0, FLAGS},
+    {"x",              "set x expression",      OFFSET(x_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
+    {"y",              "set y expression",      OFFSET(y_expr),             AV_OPT_TYPE_STRING, {.str="0"},   0, 0, FLAGS},
+    {"boxw",           "set box width",         OFFSET(boxw),               AV_OPT_TYPE_INT,    {.i64=0},     0, INT_MAX, FLAGS},
+    {"boxh",           "set box height",        OFFSET(boxh),               AV_OPT_TYPE_INT,    {.i64=0},     0, INT_MAX, FLAGS},
+    {"shadowx",        "set shadow x offset",   OFFSET(shadowx),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
+    {"shadowy",        "set shadow y offset",   OFFSET(shadowy),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
+    {"borderw",        "set border width",      OFFSET(borderw),            AV_OPT_TYPE_INT,    {.i64=0},     INT_MIN, INT_MAX, FLAGS},
+    {"tabsize",        "set tab size",          OFFSET(tabsize),            AV_OPT_TYPE_INT,    {.i64=4},     0, INT_MAX, FLAGS},
+    {"basetime",       "set base time",         OFFSET(basetime),           AV_OPT_TYPE_INT64,  {.i64=AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX, FLAGS},
 #if CONFIG_LIBFONTCONFIG
     { "font",        "Font name",            OFFSET(font),               AV_OPT_TYPE_STRING, { .str = "Sans" },           .flags = FLAGS },
 #endif
@@ -246,17 +372,21 @@  static const AVOption drawtext_options[]= {
         {"none",     "set no expansion",                    OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_NONE},     0, 0, FLAGS, "expansion"},
         {"normal",   "set normal expansion",                OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_NORMAL},   0, 0, FLAGS, "expansion"},
         {"strftime", "set strftime expansion (deprecated)", OFFSET(exp_mode), AV_OPT_TYPE_CONST, {.i64=EXP_STRFTIME}, 0, 0, FLAGS, "expansion"},
+    {"y_align",   "set the y alignment",    OFFSET(y_align), AV_OPT_TYPE_INT,  {.i64=YA_TEXT}, 0, 2, FLAGS, "y_align"},
+        {"text",     "y is referred to the top of the first text line", OFFSET(y_align), AV_OPT_TYPE_CONST, {.i64=YA_TEXT},     0, 0, FLAGS, "y_align"},
+        {"baseline", "y is referred to the baseline of the first line", OFFSET(y_align), AV_OPT_TYPE_CONST, {.i64=YA_BASELINE}, 0, 0, FLAGS, "y_align"},
+        {"font",     "y is referred to the font defined line metrics",  OFFSET(y_align), AV_OPT_TYPE_CONST, {.i64=YA_FONT},     0, 0, FLAGS, "y_align"},
 
     {"timecode",        "set initial timecode",             OFFSET(tc_opt_string), AV_OPT_TYPE_STRING,   {.str=NULL}, 0, 0, FLAGS},
-    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},           0,        1, FLAGS},
-    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},           0,  INT_MAX, FLAGS},
-    {"reload",     "reload text file at specified frame interval", OFFSET(reload),     AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
-    { "alpha",       "apply alpha while rendering", OFFSET(a_expr),      AV_OPT_TYPE_STRING, { .str = "1"     },          .flags = FLAGS },
-    {"fix_bounds", "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
-    {"start_number", "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
-    {"text_source", "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
+    {"tc24hmax",        "set 24 hours max (timecode only)", OFFSET(tc24hmax),      AV_OPT_TYPE_BOOL,     {.i64=0},    0, 1, FLAGS},
+    {"timecode_rate",   "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"r",               "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"rate",            "set rate (timecode only)",         OFFSET(tc_rate),       AV_OPT_TYPE_RATIONAL, {.dbl=0},    0, INT_MAX, FLAGS},
+    {"reload",          "reload text file at specified frame interval", OFFSET(reload), AV_OPT_TYPE_INT, {.i64=0},    0, INT_MAX, FLAGS},
+    {"alpha",           "apply alpha while rendering",      OFFSET(a_expr),        AV_OPT_TYPE_STRING,   {.str = "1"}, .flags = FLAGS},
+    {"fix_bounds",      "check and fix text coords to avoid clipping", OFFSET(fix_bounds), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS},
+    {"start_number",    "start frame number for n/frame_num variable", OFFSET(start_number), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS},
+    {"text_source",     "the source of text", OFFSET(text_source_string), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 1, FLAGS },
 
 #if CONFIG_LIBFRIBIDI
     {"text_shaping", "attempt to shape text before drawing", OFFSET(text_shaping), AV_OPT_TYPE_BOOL, {.i64=1}, 0, 1, FLAGS},
@@ -297,18 +427,24 @@  static const struct ft_error {
 
 #define FT_ERRMSG(e) ft_errors[e].err_msg
 
-typedef struct Glyph {
-    FT_Glyph glyph;
-    FT_Glyph border_glyph;
-    uint32_t code;
-    unsigned int fontsize;
-    FT_Bitmap bitmap; ///< array holding bitmaps of font
-    FT_Bitmap border_bitmap; ///< array holding bitmaps of font border
-    FT_BBox bbox;
-    int advance;
-    int bitmap_left;
-    int bitmap_top;
-} Glyph;
+
+// Loads and (optionally) renders a glyph
+static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code,
+     int8_t shift_x64, int8_t shift_y64);
+
+// Shapes a line of text using libharfbuzz
+static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const char* text, int textLen);
+
+// Performs text measurements
+static int measure_text(AVFilterContext *ctx, TextMetrics *metrics);
+
+// Draws glyphs on the frame
+static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
+                       FFDrawColor *color, TextMetrics *metrics,
+                       int x, int y, int borderw);
+
+// Draws text on the frame
+static int draw_text(AVFilterContext *ctx, AVFrame *frame);
 
 static int glyph_cmp(const void *key, const void *b)
 {
@@ -316,80 +452,9 @@  static int glyph_cmp(const void *key, const void *b)
     int64_t diff = (int64_t)a->code - (int64_t)bb->code;
 
     if (diff != 0)
-         return diff > 0 ? 1 : -1;
+        return diff > 0 ? 1 : -1;
     else
-         return FFDIFFSIGN((int64_t)a->fontsize, (int64_t)bb->fontsize);
-}
-
-/**
- * Load glyphs corresponding to the UTF-32 codepoint code.
- */
-static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code)
-{
-    DrawTextContext *s = ctx->priv;
-    FT_BitmapGlyph bitmapglyph;
-    Glyph *glyph;
-    struct AVTreeNode *node = NULL;
-    int ret;
-
-    /* load glyph into s->face->glyph */
-    if (FT_Load_Char(s->face, code, s->ft_load_flags))
-        return AVERROR(EINVAL);
-
-    glyph = av_mallocz(sizeof(*glyph));
-    if (!glyph) {
-        ret = AVERROR(ENOMEM);
-        goto error;
-    }
-    glyph->code  = code;
-    glyph->fontsize = s->fontsize;
-
-    if (FT_Get_Glyph(s->face->glyph, &glyph->glyph)) {
-        ret = AVERROR(EINVAL);
-        goto error;
-    }
-    if (s->borderw) {
-        glyph->border_glyph = glyph->glyph;
-        if (FT_Glyph_StrokeBorder(&glyph->border_glyph, s->stroker, 0, 0) ||
-            FT_Glyph_To_Bitmap(&glyph->border_glyph, FT_RENDER_MODE_NORMAL, 0, 1)) {
-            ret = AVERROR_EXTERNAL;
-            goto error;
-        }
-        bitmapglyph = (FT_BitmapGlyph) glyph->border_glyph;
-        glyph->border_bitmap = bitmapglyph->bitmap;
-    }
-    if (FT_Glyph_To_Bitmap(&glyph->glyph, FT_RENDER_MODE_NORMAL, 0, 1)) {
-        ret = AVERROR_EXTERNAL;
-        goto error;
-    }
-    bitmapglyph = (FT_BitmapGlyph) glyph->glyph;
-
-    glyph->bitmap      = bitmapglyph->bitmap;
-    glyph->bitmap_left = bitmapglyph->left;
-    glyph->bitmap_top  = bitmapglyph->top;
-    glyph->advance     = s->face->glyph->advance.x >> 6;
-
-    /* measure text height to calculate text_height (or the maximum text height) */
-    FT_Glyph_Get_CBox(glyph->glyph, ft_glyph_bbox_pixels, &glyph->bbox);
-
-    /* cache the newly created glyph */
-    if (!(node = av_tree_node_alloc())) {
-        ret = AVERROR(ENOMEM);
-        goto error;
-    }
-    av_tree_insert(&s->glyphs, glyph, glyph_cmp, &node);
-
-    if (glyph_ptr)
-        *glyph_ptr = glyph;
-    return 0;
-
-error:
-    if (glyph)
-        av_freep(&glyph->glyph);
-
-    av_freep(&glyph);
-    av_freep(&node);
-    return ret;
+        return FFDIFFSIGN((int64_t)a->fontsize, (int64_t)bb->fontsize);
 }
 
 static av_cold int set_fontsize(AVFilterContext *ctx, unsigned int fontsize)
@@ -439,7 +504,6 @@  static av_cold int update_fontsize(AVFilterContext *ctx)
            return err;
 
         size = av_expr_eval(s->fontsize_pexpr, s->var_values, &s->prng);
-
         if (!isnan(size)) {
             roundedsize = round(size);
             // test for overflow before cast
@@ -447,7 +511,6 @@  static av_cold int update_fontsize(AVFilterContext *ctx)
                 av_log(ctx, AV_LOG_ERROR, "fontsize overflow\n");
                 return AVERROR(EINVAL);
             }
-
             fontsize = roundedsize;
         }
     }
@@ -548,7 +611,7 @@  static int load_font_fontconfig(AVFilterContext *ctx)
         goto fail;
     }
 
-    av_log(ctx, AV_LOG_INFO, "Using \"%s\"\n", filename);
+    av_log(ctx, AV_LOG_VERBOSE, "Using \"%s\"\n", filename);
     if (parse_err)
         s->default_fontsize = size + 0.5;
 
@@ -690,6 +753,7 @@  static int shape_text(AVFilterContext *ctx)
     s->text = tmp;
     len = fribidi_unicode_to_charset(FRIBIDI_CHAR_SET_UTF8,
                                      unicodestr, len, s->text);
+
     ret = 0;
 
 out:
@@ -711,11 +775,39 @@  static enum AVFrameSideDataType text_source_string_parse(const char *text_source
     }
 }
 
+// Convert a string formatted as "n1|n2|...|nN" into an integer array
+static int string_to_array(const char* source, int* result, int result_size) {
+    int counter = 0, size = strlen(source) + 1;
+    char *saveptr, *curval, *dup = av_malloc(size);
+    av_strlcpy(dup, source, size);
+    if(result_size > 0 && (curval = av_strtok(dup, "|", &saveptr))) {
+        do {
+            if(counter == result_size) {
+                break;
+            }
+            result[counter++] = atoi(curval);
+        } while((curval = av_strtok(NULL, "|", &saveptr)) && counter < result_size);
+    }
+    av_free(dup);
+    return counter;
+}
+
+static int validate_text_align(char* text_align) {
+    int err = 0;
+    if(strlen(text_align) != 2
+        || strchr("LCRTMB", text_align[0]) == NULL || strchr("LCRTMB", text_align[1]) == NULL
+        || (strchr("TMB", text_align[0]) != NULL && strchr("LCR", text_align[1]) == NULL)
+        || (strchr("LCR", text_align[0]) != NULL && strchr("TMB", text_align[1]) == NULL)) {
+        err = AVERROR(EINVAL);
+    }
+
+    return err;
+}
+
 static av_cold int init(AVFilterContext *ctx)
 {
     int err;
     DrawTextContext *s = ctx->priv;
-    Glyph *glyph;
 
     av_expr_free(s->fontsize_pexpr);
     s->fontsize_pexpr = NULL;
@@ -728,6 +820,8 @@  static av_cold int init(AVFilterContext *ctx)
         return AVERROR(EINVAL);
     }
 
+//    init_text(ctx);
+
     if (s->textfile) {
         if (s->text) {
             av_log(ctx, AV_LOG_ERROR,
@@ -777,6 +871,14 @@  static av_cold int init(AVFilterContext *ctx)
         return AVERROR(EINVAL);
     }
 
+    if((err = validate_text_align(s->text_align))) {
+        av_log(ctx, AV_LOG_ERROR,
+               "The value provided for parameter 'text_align' is not valid,\n");
+        av_log(ctx, AV_LOG_ERROR,
+               "please specify a two characters string containing only one letter for horizontal alignment ('LCR') and one for vertical alignment ('TMB')\n");
+        return err;
+    }
+
 #if CONFIG_LIBFRIBIDI
     if (s->text_shaping)
         if ((err = shape_text(ctx)) < 0)
@@ -795,26 +897,19 @@  static av_cold int init(AVFilterContext *ctx)
     if ((err = update_fontsize(ctx)) < 0)
         return err;
 
+    // Always init the stroker, may be needed if borderw is set via the "change" command
+    if (FT_Stroker_New(s->library, &s->stroker)) {
+        av_log(ctx, AV_LOG_ERROR, "Coult not init FT stroker\n");
+        return AVERROR_EXTERNAL;
+    }
+
     if (s->borderw) {
-        if (FT_Stroker_New(s->library, &s->stroker)) {
-            av_log(ctx, AV_LOG_ERROR, "Coult not init FT stroker\n");
-            return AVERROR_EXTERNAL;
-        }
         FT_Stroker_Set(s->stroker, s->borderw << 6, FT_STROKER_LINECAP_ROUND,
                        FT_STROKER_LINEJOIN_ROUND, 0);
     }
 
-    s->use_kerning = FT_HAS_KERNING(s->face);
-
     /* load the fallback glyph with code 0 */
-    load_glyph(ctx, NULL, 0);
-
-    /* set the tabsize in pixels */
-    if ((err = load_glyph(ctx, &glyph, ' ')) < 0) {
-        av_log(ctx, AV_LOG_ERROR, "Could not set tabsize.\n");
-        return err;
-    }
-    s->tabsize *= glyph->advance;
+    load_glyph(ctx, NULL, 0, 0, 0);
 
     if (s->exp_mode == EXP_STRFTIME &&
         (strchr(s->text, '%') || strchr(s->text, '\\')))
@@ -837,6 +932,14 @@  static int glyph_enu_free(void *opaque, void *elem)
 
     FT_Done_Glyph(glyph->glyph);
     FT_Done_Glyph(glyph->border_glyph);
+    for(int t = 0; t < 16; ++t) {
+        if(glyph->bglyph[t] != NULL) {
+            FT_Done_Glyph((FT_Glyph)glyph->bglyph[t]);
+        }
+        if(glyph->border_bglyph[t] != NULL) {
+            FT_Done_Glyph((FT_Glyph)glyph->border_bglyph[t]);
+        }
+    }
     av_free(elem);
     return 0;
 }
@@ -852,9 +955,6 @@  static av_cold void uninit(AVFilterContext *ctx)
 
     s->x_pexpr = s->y_pexpr = s->a_pexpr = s->fontsize_pexpr = NULL;
 
-    av_freep(&s->positions);
-    s->nb_positions = 0;
-
     av_tree_enumerate(s->glyphs, NULL, NULL, glyph_enu_free);
     av_tree_destroy(s->glyphs);
     s->glyphs = NULL;
@@ -880,15 +980,15 @@  static int config_input(AVFilterLink *inlink)
     ff_draw_color(&s->dc, &s->bordercolor, s->bordercolor.rgba);
     ff_draw_color(&s->dc, &s->boxcolor,    s->boxcolor.rgba);
 
-    s->var_values[VAR_w]     = s->var_values[VAR_W]     = s->var_values[VAR_MAIN_W] = inlink->w;
-    s->var_values[VAR_h]     = s->var_values[VAR_H]     = s->var_values[VAR_MAIN_H] = inlink->h;
-    s->var_values[VAR_SAR]   = inlink->sample_aspect_ratio.num ? av_q2d(inlink->sample_aspect_ratio) : 1;
-    s->var_values[VAR_DAR]   = (double)inlink->w / inlink->h * s->var_values[VAR_SAR];
-    s->var_values[VAR_HSUB]  = 1 << s->dc.hsub_max;
-    s->var_values[VAR_VSUB]  = 1 << s->dc.vsub_max;
-    s->var_values[VAR_X]     = NAN;
-    s->var_values[VAR_Y]     = NAN;
-    s->var_values[VAR_T]     = NAN;
+    s->var_values[VAR_w]    = s->var_values[VAR_W] = s->var_values[VAR_MAIN_W] = inlink->w;
+    s->var_values[VAR_h]    = s->var_values[VAR_H] = s->var_values[VAR_MAIN_H] = inlink->h;
+    s->var_values[VAR_SAR]  = inlink->sample_aspect_ratio.num ? av_q2d(inlink->sample_aspect_ratio) : 1;
+    s->var_values[VAR_DAR]  = (double)inlink->w / inlink->h * s->var_values[VAR_SAR];
+    s->var_values[VAR_HSUB] = 1 << s->dc.hsub_max;
+    s->var_values[VAR_VSUB] = 1 << s->dc.vsub_max;
+    s->var_values[VAR_X]    = NAN;
+    s->var_values[VAR_Y]    = NAN;
+    s->var_values[VAR_T]    = NAN;
 
     av_lfg_init(&s->prng, av_get_random_seed());
 
@@ -948,8 +1048,100 @@  static int command(AVFilterContext *ctx, const char *cmd, const char *arg, char
 
         ctx->priv = new;
         return config_input(ctx->inputs[0]);
-    } else
+    } else if (!strcmp(cmd, "change")) {
+        char *key, *value;
+        const char *argv = arg;
+        new = av_mallocz(sizeof(DrawTextContext));
+        if (!new)
+            return AVERROR(ENOMEM);
+        new->class = &drawtext_class;
+        ctx->priv = new;
+        ret = av_set_options_string(ctx, arg, "=", ":");
+        if (ret < 0) {
+            ctx->priv = old;
+            goto fail;
+        }
+        do {
+            int err = av_opt_get_key_value(&argv, "=", ":", 0, &key, &value);
+            if(err == AVERROR(EINVAL)) {
+                break;
+            } else if(err >= 0) {
+                if(strcmp(key, "text") == 0) {
+//                    init_text(ctx);
+                    FFSWAP(uint8_t*, new->text, old->text);
+                } else if(strcmp(key, "x") == 0) {
+                    FFSWAP(char*, new->x_expr, old->x_expr);
+                    FFSWAP(AVExpr*, new->x_pexpr, old->x_pexpr);
+                } else if(strcmp(key, "y") == 0) {
+                    FFSWAP(char*, new->y_expr, old->y_expr);
+                    FFSWAP(AVExpr*, new->y_pexpr, old->y_pexpr);
+                } else if(strcmp(key, "alpha") == 0) {
+                    FFSWAP(char*, new->a_expr, old->a_expr);
+                    FFSWAP(AVExpr*, new->a_pexpr, old->a_pexpr);
+                } else if(strcmp(key, "fontsize") == 0) {
+                    FFSWAP(char*, new->fontsize_expr, old->fontsize_expr);
+                    FFSWAP(AVExpr*, new->fontsize_pexpr, old->fontsize_pexpr);
+                } else if(strcmp(key, "fontcolor") == 0) {
+                    old->fontcolor = new->fontcolor;
+                } else if(strcmp(key, "boxcolor") == 0) {
+                    old->boxcolor = new->boxcolor;
+                } else if(strcmp(key, "bordercolor") == 0) {
+                    old->bordercolor = new->bordercolor;
+                } else if(strcmp(key, "shadowcolor") == 0) {
+                    old->shadowcolor = new->shadowcolor;
+                } else if(strcmp(key, "fontcolor_expr") == 0) {
+                    FFSWAP(uint8_t*, new->fontcolor_expr, old->fontcolor_expr);
+                } else if(strcmp(key, "box") == 0) {
+                    old->draw_box = new->draw_box;
+                } else if(strcmp(key, "boxw") == 0) {
+                    old->boxw = new->boxw;
+                } else if(strcmp(key, "boxh") == 0) {
+                    old->boxh = new->boxh;
+                } else if(strcmp(key, "boxborderw") == 0) {
+                    FFSWAP(char*, new->boxborderw, old->boxborderw);
+                } else if(strcmp(key, "line_spacing") == 0) {
+                    old->line_spacing = new->line_spacing;
+                } else if(strcmp(key, "text_align") == 0) {
+                    if(!validate_text_align(value)) {
+                        FFSWAP(uint8_t*, new->text_align, old->text_align);
+                    } else {
+                        av_log(ctx, AV_LOG_ERROR,
+                            "Ignoring invalid parameter value '%s' for 'text_align'\n", value);
+                    }
+                } else if(strcmp(key, "shadowx") == 0) {
+                    old->shadowx = new->shadowx;
+                } else if(strcmp(key, "shadowy") == 0) {
+                    old->shadowy = new->shadowy;
+                } else if(strcmp(key, "borderw") == 0) {
+                    old->borderw = new->borderw;
+                    if(old->borderw) {
+                        FT_Stroker_Set(old->stroker, old->borderw << 6, FT_STROKER_LINECAP_ROUND,
+                                    FT_STROKER_LINEJOIN_ROUND, 0);
+                    }
+                } else {
+                    av_log(ctx, AV_LOG_ERROR, "Option '%s' cannot be modified with the 'change' command, use 'reinit' instead\n", key);
+                }
+//                av_log(ctx, AV_LOG_DEBUG, "option: %s %s\n", key, value);
+                av_free(key);
+                av_free(value);
+                if(*argv == ':') {
+                    ++argv;
+                }
+            } else {
+                ctx->priv = old;
+                av_opt_free(new);
+                ret = err;
+                goto fail;
+            }
+        } while(1);
+        uninit(ctx);
+        ctx->priv = old;
+        av_opt_free(new);
+        av_freep(&new);
+        return config_input(ctx->inputs[0]);
+    } else {
         return AVERROR(ENOSYS);
+    }
 
 fail:
     av_log(ctx, AV_LOG_ERROR, "Failed to process command. Continuing with existing parameters.\n");
@@ -1318,91 +1510,422 @@  static int expand_text(AVFilterContext *ctx, char *text, AVBPrint *bp)
     return 0;
 }
 
+static void update_color_with_alpha(DrawTextContext *s, FFDrawColor *color, const FFDrawColor incolor)
+{
+    *color = incolor;
+    color->rgba[3] = (color->rgba[3] * s->alpha) / 255;
+    ff_draw_color(&s->dc, color, color->rgba);
+}
+
+static void update_alpha(DrawTextContext *s)
+{
+    double alpha = av_expr_eval(s->a_pexpr, s->var_values, &s->prng);
+
+    if (isnan(alpha))
+        return;
+
+    if (alpha >= 1.0)
+        s->alpha = 255;
+    else if (alpha <= 0)
+        s->alpha = 0;
+    else
+        s->alpha = 256 * alpha;
+}
+
+static inline int get_subpixel_idx(int shift_x64, int shift_y64) {
+    int idx = (shift_x64 >> 2) + (shift_y64 >> 4);
+    return idx;
+}
+
+static int load_glyph(AVFilterContext *ctx, Glyph **glyph_ptr, uint32_t code, int8_t shift_x64, int8_t shift_y64)
+{
+    DrawTextContext *s = ctx->priv;
+    Glyph dummy = { 0 };
+    Glyph *glyph;
+    FT_Vector shift;
+    struct AVTreeNode *node = NULL;
+    int ret = 0;
+
+    /* get glyph */
+    dummy.code = code;
+    dummy.fontsize = s->fontsize;
+    glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
+    if(!glyph) {
+//        av_log(ctx, AV_LOG_DEBUG, "Glyph: %d not cached -> loading...\n", code);
+        if (FT_Load_Glyph(s->face, code, s->ft_load_flags)) {
+            return AVERROR(EINVAL);
+        }
+        glyph = av_mallocz(sizeof(*glyph));
+        if (!glyph) {
+            ret = AVERROR(ENOMEM);
+            goto error;
+        }
+        glyph->code  = code;
+        glyph->fontsize = s->fontsize;
+        if (FT_Get_Glyph(s->face->glyph, &glyph->glyph)) {
+            ret = AVERROR(EINVAL);
+            goto error;
+        }
+        if (s->borderw) {
+//            av_log(ctx, AV_LOG_DEBUG, "Stroking glyph: %d\n", code);
+            glyph->border_glyph = glyph->glyph;
+            if (FT_Glyph_StrokeBorder(&glyph->border_glyph, s->stroker, 0, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+        }
+        /* measure text height to calculate text_height (or the maximum text height) */
+        FT_Glyph_Get_CBox(glyph->glyph, FT_GLYPH_BBOX_SUBPIXELS, &glyph->bbox);
+
+        /* cache the newly created glyph */
+        if (!(node = av_tree_node_alloc())) {
+            ret = AVERROR(ENOMEM);
+            goto error;
+        }
+        av_tree_insert(&s->glyphs, glyph, glyph_cmp, &node);
+    } else {
+        if(s->borderw && !glyph->border_glyph) {
+            glyph->border_glyph = glyph->glyph;
+            if (FT_Glyph_StrokeBorder(&glyph->border_glyph, s->stroker, 0, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+        }
+    }
+
+    // Check if a bitmap is needed
+    if(shift_x64 >= 0 && shift_y64 >= 0) {
+        // Get the bitmap subpixel index (0 -> 15)
+        int idx = get_subpixel_idx(shift_x64, shift_y64);
+
+        if(!glyph->bglyph[idx]) {
+            FT_Glyph tmp_glyph = glyph->glyph;
+            // av_log(ctx, AV_LOG_DEBUG, "Rendering bitmap [%d] for glyph: %d\n", idx, code);
+            shift.x = shift_x64;
+            shift.y = shift_y64;
+            if(FT_Glyph_To_Bitmap(&tmp_glyph, FT_RENDER_MODE_NORMAL, &shift, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+            glyph->bglyph[idx] = (FT_BitmapGlyph)tmp_glyph;
+            if(glyph->bglyph[idx]->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) {
+                av_log(ctx, AV_LOG_ERROR, "Monocromatic (1bpp) fonts are not supported.\n");
+                ret = AVERROR(EINVAL);
+                goto error;
+            }
+        }
+        if (s->borderw && !glyph->border_bglyph[idx]) {
+            FT_Glyph tmp_glyph = glyph->border_glyph;
+            // av_log(ctx, AV_LOG_DEBUG, "Rendering border bitmap [%d] for glyph: %d\n", idx, code);
+            if(FT_Glyph_To_Bitmap(&tmp_glyph, FT_RENDER_MODE_NORMAL, &shift, 0)) {
+                ret = AVERROR_EXTERNAL;
+                goto error;
+            }
+            glyph->border_bglyph[idx] = (FT_BitmapGlyph)tmp_glyph;
+        }
+    }
+    if(glyph_ptr) {
+        *glyph_ptr = glyph;
+    }
+    return 0;
+
+error:
+    if (glyph && glyph->glyph)
+        FT_Done_Glyph(glyph->glyph);
+
+    av_freep(&glyph);
+    av_freep(&node);
+    return ret;
+}
+
 static int draw_glyphs(DrawTextContext *s, AVFrame *frame,
-                       int width, int height,
                        FFDrawColor *color,
+                       TextMetrics *metrics,
                        int x, int y, int borderw)
 {
-    char *text = s->expanded_text.str;
-    uint32_t code = 0;
-    int i, x1, y1;
-    uint8_t *p;
-    Glyph *glyph = NULL;
+    int g, l, x1, y1, w1, h1, idx;
+    int dx = 0, dy = 0, pdx = 0;
+    GlyphInfo *info;
+    Glyph dummy = { 0 }, *glyph;
+    FT_Bitmap bitmap;
+    FT_BitmapGlyph b_glyph;
+    uint8_t j_center = 0, j_right = 0, j_middle = 0, j_bottom = 0;
+    int line_w, offset_y = 0;
+    int clip_x = 0, clip_y = 0;
+
+    j_center = strstr(s->text_align, "C") > 0;
+    j_right = strstr(s->text_align, "R") > 0;
+    j_middle = strstr(s->text_align, "M") > 0;
+    j_bottom = strstr(s->text_align, "B") > 0;
+    // av_log(s, AV_LOG_DEBUG, "Outer rectangle - w: %d, h: %d\n", s->box_width, s->box_height);
+    // av_log(s, AV_LOG_DEBUG, "Text position: %s\n", s->text_align);
+
+    if(j_middle) {
+        offset_y = (s->box_height - metrics->height) / 2;
+    } else if(j_bottom) {
+        offset_y = s->box_height - metrics->height;
+    }
 
-    for (i = 0, p = text; *p; i++) {
-        FT_Bitmap bitmap;
-        Glyph dummy = { 0 };
-        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_invalid;);
-continue_on_invalid:
+    clip_x = FFMIN(metrics->rect_x + s->box_width + s->bb_right, frame->width);
+    clip_y = FFMIN(metrics->rect_y + s->box_height + s->bb_bottom, frame->height);
+
+    // av_log(s, AV_LOG_DEBUG, "Drawing text at (%d, %d) (clip_x: %d, clip_y: %d)\n",
+    //     x, y, clip_x, clip_y);
+
+    for(l = 0; l < s->line_count; ++l) {
+        TextLine *line = &s->lines[l];
+        line_w = POS_CEIL(line->width64, 64);
+        for(g = 0; g < line->hb_data.glyph_count; ++g) {
+            info = &line->glyphs[g];
+            dummy.fontsize = s->fontsize;
+            dummy.code = info->code;
+            glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
+            if(!glyph) {
+                return AVERROR(EINVAL);
+            }
 
-        /* skip new line chars, just go to new line */
-        if (code == '\n' || code == '\r' || code == '\t')
-            continue;
+            idx = get_subpixel_idx(info->shift_x64, info->shift_y64);
+            b_glyph = borderw ? glyph->border_bglyph[idx] : glyph->bglyph[idx];
+            bitmap = b_glyph->bitmap;
+            x1 = x + info->x + b_glyph->left;
+            y1 = y + info->y - b_glyph->top + offset_y;
+            w1 = bitmap.width;
+            h1 = bitmap.rows;
+
+            if(j_center) {
+                x1 += (s->box_width - line_w) / 2;
+            } else if(j_right) {
+                x1 += s->box_width - line_w;
+            }
 
-        dummy.code = code;
-        dummy.fontsize = s->fontsize;
-        glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
+            // Offset of the glyph's bitmap in the visible region
+            dx = dy = 0;
+            if(x1 < metrics->rect_x - s->bb_left) {
+                dx = metrics->rect_x - s->bb_left - x1;
+                x1 = metrics->rect_x - s->bb_left;
+            }
+            if(y1 < metrics->rect_y - s->bb_top) {
+                dy = metrics->rect_y - s->bb_top - y1;
+                y1 = metrics->rect_y - s->bb_top;
+            }
 
-        bitmap = borderw ? glyph->border_bitmap : glyph->bitmap;
+            // check if the glyph is empty or out of the clipping region
+            if(dx >= w1 || dy >= h1 || x1 >= clip_x || y1 >= clip_y) {
+                // av_log(s, AV_LOG_DEBUG, "Glyph (code: %d - line: %d - glyph: %d - dx: %d - wx: %d) is empty or out of the clipping region\n",
+                //     info->code, l, g, dx, w1);
+                // av_log(s, AV_LOG_DEBUG, "Glyph %d -- dx: %d, dy: %d, x1: %d, y1: %d, w1: %d, h1: %d\n",
+                //     info->code, dx, dy, x1, y1, w1, h1);
+                continue;
+            }
 
-        if (glyph->bitmap.pixel_mode != FT_PIXEL_MODE_MONO &&
-            glyph->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY)
-            return AVERROR(EINVAL);
+            pdx = dx + dy * bitmap.pitch;
+            w1 = FFMIN(clip_x - x1, w1 - dx);
+            h1 = FFMIN(clip_y - y1, h1 - dy);
 
-        x1 = s->positions[i].x+s->x+x - borderw;
-        y1 = s->positions[i].y+s->y+y - borderw;
+            // av_log(s, AV_LOG_DEBUG, "Drawing glyph %d[%d] (line: %d num: %d) at (%d, %d) (info.x: %d, info.y: %d)\n",
+            //     info->code, idx, l, g, x1, y1, info->x, info->y);
 
-        ff_blend_mask(&s->dc, color,
-                      frame->data, frame->linesize, width, height,
-                      bitmap.buffer, bitmap.pitch,
-                      bitmap.width, bitmap.rows,
-                      bitmap.pixel_mode == FT_PIXEL_MODE_MONO ? 0 : 3,
-                      0, x1, y1);
+            ff_blend_mask(&s->dc, color, frame->data, frame->linesize, clip_x, clip_y,
+                bitmap.buffer + pdx, bitmap.pitch, w1, h1, 3, 0, x1, y1);
+        }
     }
 
     return 0;
 }
 
+static void shape_text_hb(DrawTextContext *s, HarfbuzzData* hb, const char* text, int textLen) {
+    hb->buf = hb_buffer_create();
+    hb_buffer_set_direction(hb->buf, HB_DIRECTION_LTR);
+    hb_buffer_set_script(hb->buf, HB_SCRIPT_LATIN);
+    hb_buffer_set_language(hb->buf, hb_language_from_string("en", -1));
+    hb_buffer_guess_segment_properties(hb->buf);
+    hb->font = hb_ft_font_create(s->face, NULL);
+    hb_ft_font_set_funcs(hb->font);
+    hb_buffer_add_utf8(hb->buf, text, textLen, 0, -1);
+    hb_shape(hb->font, hb->buf, NULL, 0);
+    hb->glyph_info = hb_buffer_get_glyph_infos(hb->buf, &hb->glyph_count);
+    hb->glyph_pos = hb_buffer_get_glyph_positions(hb->buf, &hb->glyph_count);
+}
 
-static void update_color_with_alpha(DrawTextContext *s, FFDrawColor *color, const FFDrawColor incolor)
-{
-    *color = incolor;
-    color->rgba[3] = (color->rgba[3] * s->alpha) / 255;
-    ff_draw_color(&s->dc, color, color->rgba);
+static void hb_destroy(HarfbuzzData *hb) {
+    hb_buffer_destroy(hb->buf);
+    hb_font_destroy(hb->font);
+    hb->buf = NULL;
+    hb->font = NULL;
+    hb->glyph_info = NULL;
+    hb->glyph_pos = NULL;
 }
 
-static void update_alpha(DrawTextContext *s)
-{
-    double alpha = av_expr_eval(s->a_pexpr, s->var_values, &s->prng);
+static int measure_text(AVFilterContext *ctx, TextMetrics *metrics) {
+    DrawTextContext *s = ctx->priv;
+    char* text = s->expanded_text.str;
+    char *textdup = av_strdup(text), *start = textdup;
+    int num_chars = 0;
+    int width64 = 0, w64 = 0, height64 = 0;
+    int cur_min_y64 = 0, first_max_y64 = -32000;
+    int first_min_x64 = 32000, last_max_x64 = -32000;
+    int min_y64 = 32000, max_y64 = -32000, min_x64 = 32000, max_x64 = -32000;
+    int line_count = 0;
+    uint32_t code = 0;
+    Glyph *glyph = NULL;
 
-    if (isnan(alpha))
-        return;
+    int i, tab_idx = 0, last_tab_idx = 0, line_offset = 0;
+    char* p;
+    int ret = 0;
+
+    // Count the lines and the tab characters
+    s->tab_count = 0;
+    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) {
+            ++line_count;
+            if(code == 0) {
+                break;
+            }
+        } else if(code == '\t') {
+            ++s->tab_count;
+        }
+    }
 
-    if (alpha >= 1.0)
-        s->alpha = 255;
-    else if (alpha <= 0)
-        s->alpha = 0;
-    else
-        s->alpha = 256 * alpha;
+    // Evaluate the width of the space character if needed to replace tabs
+    if(s->tab_count > 0 && !s->blank_advance64) {
+        HarfbuzzData hb_data;
+        shape_text_hb(s, &hb_data, " ", 1);
+        s->blank_advance64 = hb_data.glyph_pos[0].x_advance;
+        hb_destroy(&hb_data);
+    }
+
+    s->line_count = line_count;
+    s->lines = av_mallocz(line_count * sizeof(TextLine));
+    s->tab_clusters = av_mallocz(s->tab_count * sizeof(uint32_t));
+    for(i = 0; i < s->tab_count; ++i) {
+        s->tab_clusters[i] = -1;
+    }
+
+    // av_log(s, AV_LOG_DEBUG, "Starting text measurement...\n");
+    line_count = 0;
+    for (i = 0, p = textdup; 1; i++) {
+        if(*p == '\t') {
+            s->tab_clusters[tab_idx++] = i;
+            *p = ' ';
+        }
+        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_failed2;);
+continue_on_failed2:
+        if(is_newline(code) || code == 0) {
+            TextLine *cur_line = &s->lines[line_count];
+            HarfbuzzData *hb = &cur_line->hb_data;
+            cur_line->cluster_offset = line_offset;
+            shape_text_hb(s, hb, start, num_chars);
+            w64 = 0;
+            cur_min_y64 = 32000;
+            for(int t = 0; t < hb->glyph_count; ++t) {
+                uint8_t is_tab = last_tab_idx < s->tab_count &&
+                    hb->glyph_info[t].cluster == s->tab_clusters[last_tab_idx] - line_offset;
+                if(is_tab) {
+                    ++last_tab_idx;
+                }
+                ret = load_glyph(ctx, &glyph, hb->glyph_info[t].codepoint, -1, -1);
+                if(ret != 0) {
+                    break;
+                }
+                if(line_count == 0) {
+                    first_max_y64 = FFMAX(glyph->bbox.yMax, first_max_y64);
+                }
+                if(t == 0) {
+                    // TODO (OFFSET LEFT)
+                    // w64 += glyph->bbox.xMin;
+                    cur_line->offset_left64 = glyph->bbox.xMin;
+                    first_min_x64 = FFMIN(glyph->bbox.xMin, first_min_x64);
+                }
+                if(t == hb->glyph_count - 1) {
+                    w64 += glyph->bbox.xMax;
+                    last_max_x64 = FFMAX(glyph->bbox.xMax, last_max_x64);
+                    cur_line->offset_right64 = glyph->bbox.xMax;
+                } else {
+                    if(is_tab) {
+                        int size = s->blank_advance64 * s->tabsize;
+                        w64 = (w64 / size + 1) * size;
+                    } else {
+                        w64 += hb->glyph_pos[t].x_advance;
+                    }
+                }
+                cur_min_y64 = FFMIN(glyph->bbox.yMin, cur_min_y64);
+                min_y64 = FFMIN(glyph->bbox.yMin, min_y64);
+                max_y64 = FFMAX(glyph->bbox.yMax, max_y64);
+                min_x64 = FFMIN(glyph->bbox.xMin, min_x64);
+                max_x64 = FFMAX(glyph->bbox.xMax, max_x64);
+
+                // av_log(s, AV_LOG_DEBUG, "    Glyph: %d -- yMin: %ld -- yMax: %ld -- xMin: %ld -- xMax: %ld\n",
+                //     hb->glyph_info[t].codepoint, glyph->bbox.yMin, glyph->bbox.yMax, glyph->bbox.xMin, glyph->bbox.xMax);
+                // av_log(s, AV_LOG_DEBUG, "      min_y64: %d -- max_y64: %d -- min_x64: %d -- max_x64: %d\n",
+                //     min_y64, max_y64, min_x64, max_x64);
+            }
+
+            if(ret == 0) {
+                // TODO (OFFSET LEFT)
+                // cur_line->width64 = w64 - cur_line->offset_left64;
+                cur_line->width64 = w64;
+
+                av_log(s, AV_LOG_DEBUG, "  Line: %d -- glyphs count: %d - width64: %d - offset_left64: %d - offset_right64: %d)\n",
+                    line_count, hb->glyph_count, cur_line->width64, cur_line->offset_left64, cur_line->offset_right64);
+
+                if(w64 > width64) {
+                    width64 = w64;
+                }
+                num_chars = -1;
+                start = p;
+                ++line_count;
+                line_offset = i + 1;
+            }
+        }
+
+        if(code == 0 || ret != 0) break;
+        ++num_chars;
+    }
+
+    if(ret == 0) {
+        metrics->line_height64 = s->face->size->metrics.height;
+        height64 = (metrics->line_height64 + s->line_spacing * 64) *
+            (FFMAX(0, line_count - 1)) + first_max_y64 - cur_min_y64;
+        // TODO (LEFT OFFSET) 
+        // metrics->width = POS_CEIL(width64 - first_min_x64, 64);
+        metrics->width = POS_CEIL(width64, 64);
+        if(s->y_align == YA_FONT) {
+            metrics->height = POS_CEIL(metrics->line_height64 * line_count, 64);
+        } else {
+            metrics->height = POS_CEIL(height64, 64);
+        }
+        metrics->offset_top64 = first_max_y64;
+        metrics->offset_right64 = last_max_x64;
+        metrics->offset_bottom64 = cur_min_y64;
+        metrics->offset_left64 = first_min_x64;
+        metrics->min_x64 = min_x64;
+        metrics->min_y64 = min_y64;
+        metrics->max_x64 = max_x64;
+        metrics->max_y64 = max_y64;
+
+        // av_log(s, AV_LOG_DEBUG, "  Text: width: %d | height: %d\n", metrics->width, metrics->height);
+        // av_log(s, AV_LOG_DEBUG, "      off_t64: %d | off_r64: %d | off_b64: %d | off_l64: %d\n",
+        //     metrics->offset_top64, metrics->offset_right64, metrics->offset_bottom64, metrics->offset_left64);
+        // av_log(s, AV_LOG_DEBUG, "      min_x64: %d | min_y64: %d | max_x64: %d | max_y64: %d\n",
+        //     metrics->min_x64, metrics->min_y64, metrics->max_x64, metrics->max_y64);
+        // av_log(s, AV_LOG_DEBUG, "Text measurement completed\n");
+    }
+
+    av_free(textdup);
+    return ret;
 }
 
-static int draw_text(AVFilterContext *ctx, AVFrame *frame,
-                     int width, int height)
+static int draw_text(AVFilterContext *ctx, AVFrame *frame)
 {
     DrawTextContext *s = ctx->priv;
     AVFilterLink *inlink = ctx->inputs[0];
-
-    uint32_t code = 0, prev_code = 0;
-    int x = 0, y = 0, i = 0, ret;
-    int max_text_line_w = 0, len;
-    int box_w, box_h;
-    char *text;
-    uint8_t *p;
-    int y_min = 32000, y_max = -32000;
-    int x_min = 32000, x_max = -32000;
-    FT_Vector delta;
-    Glyph *glyph = NULL, *prev_glyph = NULL;
-    Glyph dummy = { 0 };
+    int x = 0, y = 0, ret;
+    int shift_x64, shift_y64;
+    int x64, y64;
+    int offset_left = 0;
+    Glyph *glyph = NULL;
 
     time_t now = time(0);
     struct tm ltime;
@@ -1413,6 +1936,17 @@  static int draw_text(AVFilterContext *ctx, AVFrame *frame,
     FFDrawColor bordercolor;
     FFDrawColor boxcolor;
 
+    int width = frame->width;
+    int height = frame->height;
+    int rec_x = 0, rec_y = 0, rec_width = 0, rec_height = 0;
+    int is_outside = 0;
+    int last_tab_idx = 0;
+
+    TextMetrics metrics;
+
+    // av_log(s, AV_LOG_DEBUG, "ascend: %ld descent: %ld height: %ld\n",
+    //     s->face->size->metrics.ascender, s->face->size->metrics.descender, s->face->size->metrics.height);
+
     av_bprint_clear(bp);
 
     if(s->basetime != AV_NOPTS_VALUE)
@@ -1441,13 +1975,6 @@  static int draw_text(AVFilterContext *ctx, AVFrame *frame,
 
     if (!av_bprint_is_complete(bp))
         return AVERROR(ENOMEM);
-    text = s->expanded_text.str;
-    if ((len = s->expanded_text.len) > s->nb_positions) {
-        if (!(s->positions =
-              av_realloc(s->positions, len*sizeof(*s->positions))))
-            return AVERROR(ENOMEM);
-        s->nb_positions = len;
-    }
 
     if (s->fontcolor_expr[0]) {
         /* If expression is set, evaluate and replace the static value */
@@ -1463,85 +1990,28 @@  static int draw_text(AVFilterContext *ctx, AVFrame *frame,
         ff_draw_color(&s->dc, &s->fontcolor, s->fontcolor.rgba);
     }
 
-    x = 0;
-    y = 0;
-
-    if ((ret = update_fontsize(ctx)) < 0)
+    if ((ret = update_fontsize(ctx)) < 0) {
         return ret;
-
-    /* load and cache glyphs */
-    for (i = 0, p = text; *p; i++) {
-        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_invalid;);
-continue_on_invalid:
-
-        /* get glyph */
-        dummy.code = code;
-        dummy.fontsize = s->fontsize;
-        glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
-        if (!glyph) {
-            ret = load_glyph(ctx, &glyph, code);
-            if (ret < 0)
-                return ret;
-        }
-
-        y_min = FFMIN(glyph->bbox.yMin, y_min);
-        y_max = FFMAX(glyph->bbox.yMax, y_max);
-        x_min = FFMIN(glyph->bbox.xMin, x_min);
-        x_max = FFMAX(glyph->bbox.xMax, x_max);
     }
-    s->max_glyph_h = y_max - y_min;
-    s->max_glyph_w = x_max - x_min;
-
-    /* compute and save position for each glyph */
-    glyph = NULL;
-    for (i = 0, p = text; *p; i++) {
-        GET_UTF8(code, *p ? *p++ : 0, code = 0xfffd; goto continue_on_invalid2;);
-continue_on_invalid2:
-
-        /* skip the \n in the sequence \r\n */
-        if (prev_code == '\r' && code == '\n')
-            continue;
-
-        prev_code = code;
-        if (is_newline(code)) {
-
-            max_text_line_w = FFMAX(max_text_line_w, x);
-            y += s->max_glyph_h + s->line_spacing;
-            x = 0;
-            continue;
-        }
-
-        /* get glyph */
-        prev_glyph = glyph;
-        dummy.code = code;
-        dummy.fontsize = s->fontsize;
-        glyph = av_tree_find(s->glyphs, &dummy, glyph_cmp, NULL);
-
-        /* kerning */
-        if (s->use_kerning && prev_glyph && glyph->code) {
-            FT_Get_Kerning(s->face, prev_glyph->code, glyph->code,
-                           ft_kerning_default, &delta);
-            x += delta.x >> 6;
-        }
 
-        /* save position */
-        s->positions[i].x = x + glyph->bitmap_left;
-        s->positions[i].y = y - glyph->bitmap_top + y_max;
-        if (code == '\t') x  = (x / s->tabsize + 1)*s->tabsize;
-        else              x += glyph->advance;
-    }
+    measure_text(ctx, &metrics);
 
-    max_text_line_w = FFMAX(x, max_text_line_w);
+    s->max_glyph_h = POS_CEIL(metrics.max_y64 - metrics.min_y64, 64);
+    s->max_glyph_w = POS_CEIL(metrics.max_x64 - metrics.min_x64, 64);
 
-    s->var_values[VAR_TW] = s->var_values[VAR_TEXT_W] = max_text_line_w;
-    s->var_values[VAR_TH] = s->var_values[VAR_TEXT_H] = y + s->max_glyph_h;
+    s->var_values[VAR_TW] = s->var_values[VAR_TEXT_W] = metrics.width;
+    s->var_values[VAR_TH] = s->var_values[VAR_TEXT_H] = metrics.height;
 
     s->var_values[VAR_MAX_GLYPH_W] = s->max_glyph_w;
     s->var_values[VAR_MAX_GLYPH_H] = s->max_glyph_h;
-    s->var_values[VAR_MAX_GLYPH_A] = s->var_values[VAR_ASCENT ] = y_max;
-    s->var_values[VAR_MAX_GLYPH_D] = s->var_values[VAR_DESCENT] = y_min;
+    s->var_values[VAR_MAX_GLYPH_A] = s->var_values[VAR_ASCENT] = POS_CEIL(metrics.max_y64, 64);
+    s->var_values[VAR_FONT_A] = s->face->size->metrics.ascender / 64;
+    s->var_values[VAR_MAX_GLYPH_D] = s->var_values[VAR_DESCENT] = POS_CEIL(metrics.min_y64, 64);
+    s->var_values[VAR_FONT_D] = -s->face->size->metrics.descender / 64;
 
-    s->var_values[VAR_LINE_H] = s->var_values[VAR_LH] = s->max_glyph_h;
+    s->var_values[VAR_TOP_A] = POS_CEIL(metrics.offset_top64, 64);
+    s->var_values[VAR_BOTTOM_D] = POS_CEIL(metrics.offset_bottom64, 64);
+    s->var_values[VAR_LINE_H] = s->var_values[VAR_LH] = metrics.line_height64 / 64.;
 
     if (s->text_source == AV_FRAME_DATA_DETECTION_BBOXES) {
         s->var_values[VAR_X] = s->x;
@@ -1559,56 +2029,176 @@  continue_on_invalid2:
     update_color_with_alpha(s, &bordercolor, s->bordercolor);
     update_color_with_alpha(s, &boxcolor   , s->boxcolor   );
 
-    box_w = max_text_line_w;
-    box_h = y + s->max_glyph_h;
+    if (s->draw_box && s->boxborderw) {
+        int bbsize[4];
+        int count;
+        count = string_to_array(s->boxborderw, bbsize, 4);
+        if(count == 1) {
+            s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = bbsize[0];
+        } else if(count == 2) {
+            s->bb_top = s->bb_bottom = bbsize[0];
+            s->bb_right = s->bb_left = bbsize[1];
+        } else if(count == 3) {
+            s->bb_top = bbsize[0];
+            s->bb_right = s->bb_left = bbsize[1];
+            s->bb_bottom = bbsize[2];
+        } else if(count == 4) {
+            s->bb_top = bbsize[0];
+            s->bb_right = bbsize[1];
+            s->bb_bottom = bbsize[2];
+            s->bb_left = bbsize[3];
+        }
+    } else {
+        s->bb_top = s->bb_right = s->bb_bottom = s->bb_left = 0;
+    }
 
     if (s->fix_bounds) {
-
         /* calculate footprint of text effects */
-        int boxoffset     = s->draw_box ? FFMAX(s->boxborderw, 0) : 0;
         int borderoffset  = s->borderw  ? FFMAX(s->borderw, 0) : 0;
 
-        int offsetleft = FFMAX3(boxoffset, borderoffset,
+        int offsetleft = FFMAX3(FFMAX(s->bb_left, 0), borderoffset,
                                 (s->shadowx < 0 ? FFABS(s->shadowx) : 0));
-        int offsettop = FFMAX3(boxoffset, borderoffset,
+        int offsettop = FFMAX3(FFMAX(s->bb_top, 0), borderoffset,
                                 (s->shadowy < 0 ? FFABS(s->shadowy) : 0));
-
-        int offsetright = FFMAX3(boxoffset, borderoffset,
+        int offsetright = FFMAX3(FFMAX(s->bb_right, 0), borderoffset,
                                  (s->shadowx > 0 ? s->shadowx : 0));
-        int offsetbottom = FFMAX3(boxoffset, borderoffset,
+        int offsetbottom = FFMAX3(FFMAX(s->bb_bottom, 0), borderoffset,
                                   (s->shadowy > 0 ? s->shadowy : 0));
 
-
         if (s->x - offsetleft < 0) s->x = offsetleft;
         if (s->y - offsettop < 0)  s->y = offsettop;
 
-        if (s->x + box_w + offsetright > width)
-            s->x = FFMAX(width - box_w - offsetright, 0);
-        if (s->y + box_h + offsetbottom > height)
-            s->y = FFMAX(height - box_h - offsetbottom, 0);
+        if (s->x + metrics.width + offsetright > width)
+            s->x = FFMAX(width - metrics.width - offsetright, 0);
+        if (s->y + metrics.height + offsetbottom > height)
+            s->y = FFMAX(height - metrics.height - offsetbottom, 0);
     }
 
-    /* draw box */
-    if (s->draw_box)
-        ff_blend_rectangle(&s->dc, &boxcolor,
-                           frame->data, frame->linesize, width, height,
-                           s->x - s->boxborderw, s->y - s->boxborderw,
-                           box_w + s->boxborderw * 2, box_h + s->boxborderw * 2);
+    x = 0;
+    y = 0;
+    x64 = (int)(s->x * 64.);
+    if(s->y_align == YA_FONT) {
+        y64 = (int)(s->y * 64. + s->face->size->metrics.ascender);
+    } else if(s->y_align == YA_BASELINE) {
+        y64 = (int)(s->y * 64.);
+    } else {
+        y64 = (int)(s->y * 64. + metrics.offset_top64);
+    }
 
-    if (s->shadowx || s->shadowy) {
-        if ((ret = draw_glyphs(s, frame, width, height,
-                               &shadowcolor, s->shadowx, s->shadowy, 0)) < 0)
-            return ret;
+    for(int l = 0; l < s->line_count; ++l) {
+        TextLine *line = &s->lines[l];
+        HarfbuzzData *hb = &line->hb_data;
+        line->glyphs = av_mallocz(hb->glyph_count * sizeof(GlyphInfo));
+
+        for(int t = 0; t < hb->glyph_count; ++t) {
+            GlyphInfo *g_info = &line->glyphs[t];
+            uint8_t is_tab = last_tab_idx < s->tab_count &&
+                hb->glyph_info[t].cluster == s->tab_clusters[last_tab_idx] - line->cluster_offset;
+            int true_x, true_y;
+            if(is_tab) {
+                ++last_tab_idx;
+            }
+            true_x = x + hb->glyph_pos[t].x_offset;
+            true_y = y + hb->glyph_pos[t].y_offset;
+            shift_x64 = (((x64 + true_x) >> 4) & 0b0011) << 4;
+            shift_y64 = ((4 - (((y64 + true_y) >> 4) & 0b0011)) & 0b0011) << 4;
+
+            ret = load_glyph(ctx, &glyph, hb->glyph_info[t].codepoint, shift_x64, shift_y64);
+            if (ret != 0) {
+                return ret;
+            }
+            g_info->code = hb->glyph_info[t].codepoint;
+            g_info->x = (x64 + true_x) >> 6;
+            g_info->y = ((y64 + true_y) >> 6) + (shift_y64 > 0 ? 1 : 0);
+            g_info->shift_x64 = shift_x64;
+            g_info->shift_y64 = shift_y64;
+
+            if(!is_tab) {
+                x += hb->glyph_pos[t].x_advance;
+            } else {
+                int size = s->blank_advance64 * s->tabsize;
+                x = (x / size + 1) * size;
+            }
+            y += hb->glyph_pos[t].y_advance;
+        }
+
+        y += metrics.line_height64 + s->line_spacing * 64;
+        x = 0;
     }
 
-    if (s->borderw) {
-        if ((ret = draw_glyphs(s, frame, width, height,
-                               &bordercolor, 0, 0, s->borderw)) < 0)
+// TODO (LEFT OFFSET)
+//    offset_left = metrics.offset_left64 / 64;
+    offset_left = 0;
+    metrics.rect_x = s->x;
+    if(s->y_align == YA_BASELINE) {
+        metrics.rect_y = s->y - metrics.offset_top64 / 64;
+    } else {
+        metrics.rect_y = s->y;
+    }
+    
+    s->box_width = s->boxw == 0 ? metrics.width : s->boxw;
+    s->box_height = s->boxh == 0 ? metrics.height : s->boxh;
+
+    if(!s->draw_box) {
+        // Create a border for the clipping region to take into account subpixel
+        // errors in text measurement and effects.
+        int borderoffset = s->borderw ? FFMAX(s->borderw, 0) : 0;
+        s->bb_left = borderoffset + (s->shadowx < 0 ? FFABS(s->shadowx) : 0) + 1;
+        s->bb_top = borderoffset + (s->shadowy < 0 ? FFABS(s->shadowy) : 0) + 1;
+        s->bb_right = borderoffset + (s->shadowx > 0 ? s->shadowx : 0) + 1;
+        s->bb_bottom = borderoffset + (s->shadowy > 0 ? s->shadowy : 0) + 1;
+    }
+
+    /* Check if the whole box is out of the frame */        
+    is_outside = metrics.rect_x - s->bb_left >= width ||
+                    metrics.rect_y - s->bb_top >= height ||
+                    metrics.rect_x + s->box_width + s->bb_right <= 0 ||
+                    metrics.rect_y + s->box_height + s->bb_bottom <= 0;
+
+    if(!is_outside) {
+        /* draw box */
+        if (s->draw_box) {
+            rec_x = metrics.rect_x - s->bb_left;
+            rec_y = metrics.rect_y - s->bb_top;
+            rec_width = s->box_width + s->bb_right + s->bb_left;
+            rec_height = s->box_height + s->bb_bottom + s->bb_top;
+            // av_log(s, AV_LOG_DEBUG, "rect_x: %d -> bb_left: %d\n",
+            //     metrics.rect_x, s->bb_left);
+            // av_log(s, AV_LOG_DEBUG, "Rect -> (x: %d - y: %d - dx: %d - dy: %d)\n",
+            //     rec_x, rec_y, rec_width, rec_height);
+            ff_blend_rectangle(&s->dc, &boxcolor,
+                frame->data, frame->linesize, width, height,
+                rec_x, rec_y, rec_width, rec_height);
+        }
+
+        if (s->shadowx || s->shadowy) {
+            if ((ret = draw_glyphs(s, frame, &shadowcolor, &metrics,
+                    s->shadowx - offset_left, s->shadowy, s->borderw)) < 0) {
+                return ret;
+            }
+        }
+
+        if (s->borderw) {
+            if ((ret = draw_glyphs(s, frame, &bordercolor, &metrics,
+                    -offset_left, 0, s->borderw)) < 0) {
+                return ret;
+            }
+        }
+
+        if ((ret = draw_glyphs(s, frame, &fontcolor, &metrics, -offset_left,
+                0, 0)) < 0) {
             return ret;
+        }
     }
-    if ((ret = draw_glyphs(s, frame, width, height,
-                           &fontcolor, 0, 0, 0)) < 0)
-        return ret;
+
+    // FREE data structures
+    for(int l = 0; l < s->line_count; ++l) {
+        TextLine *line = &s->lines[l];
+        av_freep(&line->glyphs);
+        hb_destroy(&line->hb_data);
+    }
+    av_freep(&s->lines);
+    av_freep(&s->tab_clusters);
 
     return 0;
 }
@@ -1680,13 +2270,13 @@  FF_ENABLE_DEPRECATION_WARNINGS
             s->x = bbox->x;
             s->y = bbox->y - s->fontsize;
         }
-        draw_text(ctx, frame, frame->width, frame->height);
+        draw_text(ctx, frame);
     }
 
-    av_log(ctx, AV_LOG_DEBUG, "n:%d t:%f text_w:%d text_h:%d x:%d y:%d\n",
-           (int)s->var_values[VAR_N], s->var_values[VAR_T],
-           (int)s->var_values[VAR_TEXT_W], (int)s->var_values[VAR_TEXT_H],
-           s->x, s->y);
+    // av_log(ctx, AV_LOG_DEBUG, "n:%d t:%f text_w:%d text_h:%d x:%.2f y:%.2f\n",
+    //        (int)s->var_values[VAR_N], s->var_values[VAR_T],
+    //        (int)s->var_values[VAR_TEXT_W], (int)s->var_values[VAR_TEXT_H],
+    //        s->x, s->y);
 
     return ff_filter_frame(outlink, frame);
 }