It… is… appearing… one… word… at… a… time
Context
I use Blender VSE to make videos, but I want to make the tedious bits less so. I’ve already made it easier to quickly set a text sequence’s properties (colour, location, size, duration) via hotkey; and last time I mooted the feature of appearing text. For example:
Testing Out a Prototype
We start out by defining a new operator:
class SEQUENCER_OT_split_to_appearing_words(TextSequenceAction): """Split the text in a text sequence to several text sequences The words should appear one after another in both time and space""" bl_label = "Convert to appearing words" bl_idname = "sequencer.split_to_appearing_words" bl_options = {'REGISTER', 'UNDO'}
We do some basic sanity checking — are we operating on one text sequence, does it have more than one word — and then get to the meat of the operation.
Splitting in Time
Simply put, we want to split in both time and space. The first, splitting in time, is relatively simple with a few obvious options for moving subsequent words’ start time (frame_start
) by: i) a fixed offset, ii) by an equally-distributed offset which is based on the duration of the ‘parent’ sequence minus some amount for the final word to be shown for a sensible amount of time, or iii) one of the previous two options modulated by the length of the words with shorter words having a shorter gap after them than longer words.
The first is slightly easier than the rest so we’ll do that for our test / proof of concept.
# main body of work: ts_words = sequence.text.split(" ") for i, word in enumerate(ts_words): # new_strip = bpy.ops.sequencer.effect_strip_add( # replace_sel=False, # Set times for new strip new_strip = scene.sequence_editor.sequences.new_effect( name=f"split_word_{i}", type='TEXT', frame_start=int(sequence.frame_start+(i*OFFSET)), frame_end=int(sequence.frame_final_end), channel=int(sequence.channel+1+i), )
Oddly, we have to cast to int
for setting start/end frames and channels, despite the attribute being a float
. The two commented lines are from another addon I use to add outlines to text in VSE, though I believe that method is deprecated.
It works grand:
Although the new text sequence needs to have the font details etc set, so it may be better to use the VSE operator duplicate
or duplicate_move
.
Splitting in Space
This is the trickier of the two. We want words to appear beside each other, as they would in a regular sentence. That means we need to manage their horizontal distribution, ie change the x coordinate. The difficulty is in figuring out how much to move it by.
Again, there are a couple of obvious approaches: i) use a heuristic-based ‘best guess’ at how much to move the next word based on the length of the current word, ii) figure out the precise width of the present word, and move by that much plus some fixed margin.
Of the options, the latter is preferable! The former is something I’ve done before, albeit from the opposite direction: figuring out what size to make text so that it all fits on one line / within the video frame width for making image-based subtitles/captions for use with shotcut. There’s also an old QA on SO asking for a heuristic for estimating text width; but the top answer notes that the preferred approach is to use the framework to get the exact dimension!
Unfortunately, the way to do that in Blender isn’t obvious, so I asked if it was possible to do over on blenderartists. A reply from @testure pointed me in the direction of blf. In that module there is a function for getting the dimensions of text (blf.dimensions
). Awesome!
Or so I thought. Unfortunately, blf works with font ids, which are returned by the blf.load()
method. In their answer, testure mentioned that the first three are Blender default fonts, so work from the list of bpy.data.fonts
and add 3 to the index. That gave me odd results, so I wrote a method to walk text sequences and the font id and print metrics:
class SEQUENCER_OT_debug_font_sizing(bpy.types.Operator): """Print font sizing info""" bl_label = "Show font sizing info" bl_idname = "sequencer.debug_font_sizing" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): """Ensure we're in the VSE with at least one sequence selected""" return (context.scene and context.scene.sequence_editor and context.selected_editable_sequences is not None) def execute(self, context): """Loop through sequences, printing info about font and text""" import blf OUTFILE = "/tmp/blender_font_infos.txt" render_rez_x = bpy.data.scenes["Scene"].render.resolution_x with open(OUTFILE, "w", encoding="UTF-8") as fh: # bpy.data.fonts fh.write(f"There are {len(bpy.data.fonts)} fonts in bpy.data.fonts:\n") for bpyfont in bpy.data.fonts: fh.write(f"\t{bpyfont}\n") fh.write("*"*72 + "\n\n\n") # sequences fh.write(f"There are {len(context.selected_editable_sequences)} sequences\n\n") for seq in context.selected_editable_sequences: closest_diff = render_rez_x closest_width = 0 closest_id = 0 fh.write(f"With sequence '{seq.name}':\n") fh.write(f"\tSequence text: '{seq.text}\'\n") fh.write(f"\tSequence font: '{seq.font}'\n") fh.write(f"\tSequence size: '{seq.font_size}'\n") for fid in range(0, 50): blf.size(fid, seq.font_size) w, h = blf.dimensions(fid, seq.text) if w == 0 and h == 0: continue fh.write(f"\t\tfid: {fid}\tw: {w}\th: {h}\n") difference = abs(render_rez_x - w) if difference < closest_diff: closest_diff = difference closest_width = w closest_id = fid fh.write(f"\nClosest candidate:fid={closest_id} at {closest_width} px.\n") fh.write(f"Font id - 25 ({closest_id - 25}) to bpy.data.fonts: {bpy.data.fonts[closest_id-25].name}\n") fh.write("-"*72 + "\n\n") return {'FINISHED'}
I created a few sequences with text sized to the width of the output (1920px) so I could figure out which font was which from the return values from blf.dimensions
:
That gave me some data to work with, reproduced in part here:
There are 5 fonts in bpy.data.fonts: <bpy_struct, VectorFont("Agency FB Regular") at 0x7fdcd5f27108> <bpy_struct, VectorFont("Poiret One Regular") at 0x7fdcd5f1ef08> <bpy_struct, VectorFont("Roboto Slab Bold") at 0x7fdcb2b64208> <bpy_struct, VectorFont("Robson Thin") at 0x7fdcd5f08208> <bpy_struct, VectorFont("VCR OSD Mono Regular") at 0x7fdcd5f77b08> ************************************************************************ There are 3 sequences With sequence 'test strip 1': Sequence text: 'An example sentence to split' Sequence font: '<bpy_struct, VectorFont("Poiret One Regular") at 0x7fdcd5f1ef08>' Sequence size: '157.552001953125' fid: 0 w: 2281.0 h: 153.0 fid: 1 w: 2632.0 h: 154.0 fid: 2 w: 2632.0 h: 154.0 fid: 3 w: 5040.0 h: 152.0 fid: 4 w: 2092.0 h: 163.0 fid: 5 w: 2884.0 h: 153.0 fid: 6 w: 2244.0 h: 153.0 fid: 7 w: 2244.0 h: 153.0 fid: 8 w: 2244.0 h: 153.0 fid: 9 w: 2244.0 h: 153.0 fid: 10 w: 2244.0 h: 153.0 fid: 11 w: 2244.0 h: 153.0 fid: 12 w: 2272.0 h: 153.0 fid: 13 w: 2244.0 h: 153.0 fid: 14 w: 2109.0 h: 158.0 fid: 15 w: 2244.0 h: 153.0 fid: 16 w: 2264.0 h: 153.0 fid: 17 w: 2276.0 h: 153.0 fid: 18 w: 2109.0 h: 158.0 fid: 19 w: 2244.0 h: 153.0 fid: 20 w: 2109.0 h: 158.0 fid: 21 w: 2244.0 h: 153.0 fid: 22 w: 2244.0 h: 153.0 fid: 23 w: 2260.0 h: 153.0 fid: 24 w: 2244.0 h: 153.0 fid: 25 w: 2150.0 h: 152.0 fid: 26 w: 1916.0 h: 153.0 fid: 27 w: 1403.0 h: 150.0 Closest candidate:fid=26 at 1916.0 px. Font id - 25 (1) to bpy.data.fonts: Poiret One Regular ------------------------------------------------------------------------
It seems in my case there are 25 fonts before the custom ones I used in the sequences. Hmm. The text sequence itself does have a .font
property (yay!); but that is a VectorFont
, which doesn’t seem to have anything that maps to the blf
-related font id (boo!).
The source for the blf API doesn’t show much promise, but I will keep looking, waiting for a reply over at BA, and if all else fails I can either use the heuristic/estimate method, or patch Blender to expose the font data I need (!!).
While it doesn’t help me release the feature, in the meantime for my own purposes I can proceed with a fontid offset of 25 to add to the index from bpy.data.fonts
and see how that works.
Pingback: [solved] Why is my Blender addon panel property read only? – Rob's Blog