1

A script takes a single word 3 to 11 characters long, draws it with a custom font and overlays on top of a video. The word should fit a certain width.

The animation involves pan and zoom that the word follows. Maybe this compositing job suits smth like After Effects, but the task is ffmpeg implementation. I made it work with the shortest 3-character long sample word and the longest 11-character one.

drawtext filter generates a transparent png with big margins around the text. zoompan filter does the zooming afterwards.

I determined the font size for 3- and 11-char sample words and tried linear interpolation to get font size, depending on the word length. This approach fails with some words, as the font has variable width letters, and "MMM" vs. "III" differs n times in width.

Is there a way in ffmpeg to fit a variable-width text to fixed dimensions?

Maybe some filter to detect a least-sized rectangle with non-transparent pixels?

Plan-B works fine: bash function that calculates word length depending on each letter width in the particular font. Had to measure each letter width in Photoshop in pixels at 100pt font size.

Still wondering if there is a way ffmpeg could scale font size text to fit some fixed width.

  • Were you able to find any FFmpeg command option to solve this problem? Is bash function mentioned in Plan-B working for you? Can you share that bash function here? I am also facing the similar issue and not able to find a solution yet. – Raju Mar 26 '18 at 18:20
  • @Raju I added my answer with the solution and the code. – Serge Mar 27 '18 at 6:14
2

There's no way to force the drawtext filter to fit the text in a certain width.

What you can do, is get the rendered text dimensions in a first pass and then run drawtext accordingly in 2nd pass, or crop and scale the output using the values from first pass.

For first pass,

ffmpeg -i video -vf drawtext=fontfile=path/to/font:fontsize=FinalSize:text='Lorem Ipsor' \
                   :x=0+0*print(tw):y=0+0*print(th) -vframes 1 -f null -

This will print the text at the top-left. The print statements will output

84.000000
14.000000

The first value is tw - the width, and the 2nd th - the height, in pixels.

0

My solution that works well was to manually measure every letter width. I typed letters in Photoshop at a relatvely large font size to minimize rounding effect of pixel size and measured widths in pixels.

Given the word I determine its length and calculate the scale factor to make it fit required length.

Function that calculates the word width for the Cyrilllic alphabet and a paricular typeface:

# Given string calculate its length based on the font metrics.
# Letter widhts measured in pixels at 100pt font size in Photoshop.
function textWidth {
    local word=$1
    local varname=$2

    local widths=(73 59 65 52 80 56 56 105 66 73 73 75 76 102 69 78 70 62 67 64 74 86 76 81 66 90 102 75 84 63 60 105 70)
    local alpha="абвгдеёжзийклмнопрстуфхцчшщъыьэюя"
    local total=0
    local i

    strindex() { 
        local x="${1%%$2*}"
        local length=${widths["${#x}"]}
        total=$(( length > 0 ? total + length : total + 74 )) 
    }

    for i in $(seq 1 ${#word}); do
        strindex $alpha ${word:i-1:1}
    done

    eval $varname="\"$total\""
}
  • Hey, @Serge thanks a ton. I will update you how it goes. Have a good day ahead. – Raju Mar 27 '18 at 7:07
  • I would need it for English alphabets and few supported special symbols, I will have to recalculate the width I suppose. Also, I may have to extend this feature to a different language in future. – Raju Mar 27 '18 at 7:21
0

I call FFMPEG from python and this worked with me:

from PIL import ImageFont

translation_font = ImageFont.truetype(
    "SourceCodePro-Regular.otf", size=55, encoding="unic")
w, _ = translation_font.getsize(" ".join([line, word]))

Which will put the width of the text (rendered with the given settings) in w.

Based on that you can find the first word that makes the text get outside of the screen and insert a new line before it

def fit_text(string: str, frame_width):
    split_line = [x.strip() for x in string.split()]
    lines = ""
    w = 0
    line_num = 0
    line = ""
    for word in split_line:
        # Make a test
        w, _ = translation_font.getsize(" ".join([line, word]))
        # If it exceeds the frame width, add a new line
        if w > (frame_width - (2 * 6)):  # Leave 6px margin on each side
            lines += line.strip() + "\n"
            line = ""

        line += word + " "

    lines += line.strip()  # Append leftover words
    return lines

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.