8000 Merge pull request #25 from Depaulicious/escapeargs · Powercoder64/ffmpeg-python@17e9e46 · GitHub
[go: up one dir, main page]

Skip to content
8000

Commit 17e9e46

Browse files
authored
Merge pull request kkroening#25 from Depaulicious/escapeargs
Escape terminator characters in filter arguments
2 parents a986cbe + fbf01f2 commit 17e9e46

File tree

4 files changed

+249
-9
lines changed

4 files changed

+249
-9
lines changed

ffmpeg/_filters.py

Lines changed: 145 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from __future__ import unicode_literals
2-
from .nodes import (
3-
FilterNode,
4-
operator,
5-
)
2+
3+
from .nodes import FilterNode, operator
4+
from ._utils import escape_chars
65

76

87
@operator()
@@ -182,6 +181,148 @@ def drawbox(parent_node, x, y, width, height, color, thickness=None, **kwargs):
182181
return filter_(parent_node, drawbox.__name__, x, y, width, height, color, **kwargs)
183182

184183

184+
@operator()
185+
def drawtext(parent_node, text=None, x=0, y=0, escape_text=True, **kwargs):
186+
"""Draw a text string or text from a specified file on top of a video, using the libfreetype library.
187+
188+
To enable compilation of this filter, you need to configure FFmpeg with ``--enable-libfreetype``. To enable default
189+
font fallback and the font option you need to configure FFmpeg with ``--enable-libfontconfig``. To enable the
190+
text_shaping option, you need to configure FFmpeg with ``--enable-libfribidi``.
191+
192+
Args:
193+
box: Used to draw a box around text using the background color. The value must be either 1 (enable) or 0
194+
(disable). The default value of box is 0.
195+
boxborderw: Set the width of the border to be drawn around the box using boxcolor. The default value of
196+
boxborderw is 0.
197+
boxcolor: The color to be used for drawing box around text. For the syntax of this option, check the "Color"
198+
section in the ffmpeg-utils manual. The default value of boxcolor is "white".
199+
line_spacing: Set the line spacing in pixels of the border to be drawn around the box using box. The default
200+
value of line_spacing is 0.
201+
borderw: Set the width of the border to be drawn around the text using bordercolor. The default value of
202+
borderw is 0.
203+
bordercolor: Set the color to be used for drawing border around text. For the syntax of this option, check the
204+
"Color" section in the ffmpeg-utils manual. The default value of bordercolor is "black".
205+
expansion: Select how the text is expanded. Can be either none, strftime (deprecated) or normal (default). See
206+
the Text expansion section below for details.
207+
basetime: Set a start time for the count. Value is in microseconds. Only applied in the deprecated strftime
208+
expansion mode. To emulate in normal expansion mode use the pts function, supplying the start time (in
209+
seconds) as the second argument.
210+
fix_bounds: If true, check and fix text coords to avoid clipping.
211+
fontcolor: The color to be used for drawing fonts. For the syntax of this option, check the "Color" section in
212+
the ffmpeg-utils manual. The default value of fontcolor is "black".
213+
fontcolor_expr: String which is expanded the same way as text to obtain dynamic fontcolor value. By default
214+
this option has empty value and is not processed. When this option is set, it overrides fontcolor option.
215+
font: The font family to be used for drawing text. By default Sans.
216+
fontfile: The font file to be used for drawing text. The path must be included. This parameter is mandatory if
217+
the fontconfig support is disabled.
218+
alpha: Draw the text applying alpha blending. The value can be a number between 0.0 and 1.0. The expression
219+
accepts the same variables x, y as well. The default value is 1. Please see fontcolor_expr.
220+
fontsize: The font size to be used for drawing text. The default value of fontsize is 16.
221+
text_shaping: If set to 1, attempt to shape the text (for example, reverse the order of right-to-left text and
222+
join Arabic characters) before drawing it. Otherwise, just draw the text exactly as given. By default 1 (if
223+
supported).
224+
ft_load_flags: The flags to be used for loading the fonts. The flags map the corresponding flags supported by
225+
libfreetype, and are a combination of the following values:
226+
227+
* ``default``
228+
* ``no_scale``
229+
* ``no_hinting``
230+
* ``render``
231+
* ``no_bitmap``
232+
* ``vertical_layout``
233+
* ``force_autohint``
234+
* ``crop_bitmap``
235+
* ``pedantic``
236+
* ``ignore_global_advance_width``
237+
* ``no_recurse``
238+
* ``ignore_transform``
239+
* ``monochrome``
240+
* ``linear_design``
241+
* ``no_autohint``
242+
243+
Default value is "default". For more information consult the documentation for the FT_LOAD_* libfreetype
244+
flags.
245+
shadowcolor: The color to be used for drawing a shadow behind the drawn text. For the syntax of this option,
246+
check the "Color" section in the ffmpeg-utils manual. The default value of shadowcolor is "black".
247+
shadowx: The x offset for the text shadow position with respect to the position of the text. It can be either
248+
positive or negative values. The default value is "0".
249+
shadowy: The y offset for the text shadow position with respect to the position of the text. It can be either
250+
positive or negative values. The default value is "0".
251+
start_number: The starting frame number for the n/frame_num variable. The default value is "0".
252+
tabsize: The size in number of spaces to use for rendering the tab. Default value is 4.
253+
timecode: Set the initial timecode representation in "hh:mm:ss[:;.]ff" format. It can be used with or without
254+
text parameter. timecode_rate option must be specified.
255+
rate: Set the timecode frame rate (timecode only).
256+
timecode_rate: Alias for ``rate``.
257+
r: Alias for ``rate``.
258+
tc24hmax: If set to 1, the output of the timecode option will wrap around at 24 hours. Default is 0 (disabled).
259+
text: The text string to be drawn. The text must be a sequence of UTF-8 encoded characters. This parameter is
260+
mandatory if no file is specified with the parameter textfile.
261+
textfile: A text file containing text to be drawn. The text must be a sequence of UTF-8 encoded characters.
262+
This parameter is mandatory if no text string is specified with the parameter text. If both text and
263+
textfile are specified, an error is thrown.
264+
reload: If set to 1, the textfile will be reloaded before each frame. Be sure to update it atomically, or it
265+
may be read partially, or even fail.
266+
x: The expression which specifies the offset where text will be drawn within the video frame. It is relative to
267+
the left border of the output image. The default value is "0".
268+
y: The expression which specifies the offset where text will be drawn within the video frame. It is relative to
269+
the top border of the output image. The default value is "0". See below for the list of accepted constants
270+
and functions.
271+
272+
Expression constants:
273+
The parameters for x and y are expressions containing the following constants and functions:
274+
dar: input display aspect ratio, it is the same as ``(w / h) * sar``
275+
hsub: horizontal chroma subsample values. For example for the pixel format "yuv422p" hsub is 2 and vsub
276+
is 1.
277+
vsub: vertical chroma subsample values. For example for the pixel format "yuv422p" hsub is 2 and vsub
278+
is 1.
279+
line_h: the height of each text line
280+
lh: Alias for ``line_h``.
281+
main_h: the input height
282+
h: Alias for ``main_h``.
283+
H: Alias for ``main_h``.
284+
main_w: the input width
285+
w: Alias for ``main_w``.
286+
W: Alias for ``main_w``.
287+
ascent: the maximum distance from the baseline to the highest/upper grid coordinate used to place a
288+
glyph outline point, for all the rendered glyphs. It is a positive value, due to the grid's
289+
orientation with the Y axis upwards.
290+
max_glyph_a: Alias for ``ascent``.
291+
descent: the maximum distance from the baseline to the lowest grid coordinate used to place a glyph
292+
outline point, for all the rendered glyphs. This is a negative value, due to the grid's
293+
orientation, with the Y axis upwards.
294+
max_glyph_d: Alias for ``descent``.
295+
max_glyph_h: maximum glyph height, that is the maximum height for all the glyphs contained in the
296+
rendered text, it is equivalent to ascent - descent.
297+
max_glyph_w: maximum glyph width, that is the maximum width for all the glyphs contained in the
298+
rendered text
299+
n: the number of input frame, starting from 0
300+
rand(min, max): return a random number included between min and max
301+
sar: The input sample aspect ratio.
302+
t: timestamp expressed in seconds, NAN if the input timestamp is unknown
303+
text_h: the height of the rendered text
304+
th: Alias for ``text_h``.
305+
text_w: the width of the rendered text
306+
tw: Alias for ``text_w``.
307+
x: the x offset coordinates where the text is drawn.
308+
y: the y offset coordinates where the text is drawn.
309+
310+
These parameters allow the x and y expressions to refer each other, so you can for example specify
311+
``y=x/dar``.
312+
313+
Official documentation: `drawtext <https://ffmpeg.org/ffmpeg-filters.html#drawtext>`__
314+
"""
315+
if text is not None:
316+
if escape_text:
317+
text = escape_chars(text, '\\\'%')
318+
kwargs['text'] = text
319+
if x != 0:
320+
kwargs['x'] = x
321+
if y != 0:
322+
kwargs['y'] = y
323+
return filter_(parent_node, drawtext.__name__, **kwargs)
324+
325+
185326
@operator()
186327
def concat(*parent_nodes, **kwargs):
187328
"""Concatenate audio and video streams, joining them together one after the other.

ffmpeg/_utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from builtins import str
2+
3+
4+
def escape_chars(text, chars):
5+
"""Helper function to escape uncomfortable characters."""
6+
text = str(text)
7+
chars = list(set(chars))
8+
if '\\' in chars:
9+
chars.remove('\\')
10+
chars.insert(0, '\\')
11+
for ch in chars:
12+
text = text.replace(ch, '\\' + ch)
13+
return text

ffmpeg/nodes.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import unicode_literals
22

3+
from ._utils import escape_chars
34
from builtins import object
45
import hashlib
56
import json
@@ -50,13 +51,21 @@ def __init__(self, name, *args, **kwargs):
5051
class FilterNode(Node):
5152
"""FilterNode"""
5253
def _get_filter(self):
53-
params_text = self._name
54-
arg_params = ['{}'.format(arg) for arg in self._args]
55-
kwarg_params = ['{}={}'.format(k, self._kwargs[k]) for k in sorted(self._kwargs)]
54+
args = [escape_chars(x, '\\\'=:') for x in self._args]
55+
kwargs = {}
56+
for k, v in self._kwargs.items():
57+
k = escape_chars(k, '\\\'=:')
58+
v = escape_chars(v, '\\\'=:')
59+
kwargs[k] = v
60+
61+
arg_params = [escape_chars(v, '\\\'=:') for v in args]
62+
kwarg_params = ['{}={}'.format(k, kwargs[k]) for k in sorted(kwargs)]
5663
params = arg_params + kwarg_params
64+
65+
params_text = escape_chars(self._name, '\\\'=:')
5766
if params:
5867
params_text += '={}'.format(':'.join(params))
59-
return params_text
68+
return escape_chars(params_text, '\\\'[],;')
6069

6170

6271
class OutputNode(Node):

ffmpeg/tests/test_ffmpeg.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from __future__ import unicode_literals
2+
23
import ffmpeg
34
import os
45
import pytest
5-
import subprocess
66
import random
7+
import re
8+
import subprocess
79

810

911
TEST_DIR = os.path.dirname(__file__)
@@ -16,6 +18,13 @@
1618
subprocess.check_call(['ffmpeg', '-version'])
1719

1820

21+
def test_escape_chars():
22+
assert ffmpeg._utils.escape_chars('a:b', ':') == 'a\:b'
23+
assert ffmpeg._utils.escape_chars('a\\:b', ' F438 ;:\\') == 'a\\\\\\:b'
24+
assert ffmpeg._utils.escape_chars('a:b,c[d]e%{}f\'g\'h\\i', '\\\':,[]%') == 'a\\:b\\,c\\[d\\]e\\%{}f\\\'g\\\'h\\\\i'
25+
assert ffmpeg._utils.escape_chars(123, ':\\') == '123'
26+
27+
1928
def test_fluent_equality():
2029
base1 = ffmpeg.input('dummy1.mp4')
2130
base2 = ffmpeg.input('dummy1.mp4')
@@ -119,6 +128,74 @@ def test_get_args_complex_filter():
119128
]
120129

121130

131+
def test_filter_normal_arg_escape():
132+
"""Test string escaping of normal filter args (e.g. ``font`` param of ``drawtext`` filter)."""
133+
def _get_drawtext_font_repr(font):
134+
"""Build a command-line arg using drawtext ``font`` param and extract the ``-filter_complex`` arg."""
135+
args = (ffmpeg
136+
.input('in')
137+
.drawtext('test', font='a{}b'.format(font))
138+
.output('out')
139+
.get_args()
140+
)
141+
assert args[:3] == ['-i', 'in', '-filter_complex']
142+
assert args[4:] == ['-map', '[v0]', 'out']
143+
match = re.match(r'\[0\]drawtext=font=a((.|\n)*)b:text=test\[v0\]', args[3], re.MULTILINE)
144+
assert match is not None, 'Invalid -filter_complex arg: {!r}'.format(args[3])
145+
return match.group(1)
146+
147+
expected_backslash_counts = {
148+
'x': 0,
149+
'\'': 3,
150+
'\\': 3,
151+
'%': 0,
152+
':': 2,
153+
',': 1,
154+
'[': 1,
155+
']': 1,
156+
'=': 2,
157+
'\n': 0,
158+
}
159+
for ch, expected_backslash_count in expected_backslash_counts.items():
160+
expected = '{}{}'.format('\\' * expected_backslash_count, ch)
161+
actual = _get_drawtext_font_repr(ch)
162+
assert expected == actual
163+
164+
165+
def test_filter_text_arg_str_escape():
166+
"""Test string escaping of normal filter args (e.g. ``text`` param of ``drawtext`` filter)."""
167+
def _get_drawtext_text_repr(text):
168+
"""Build a command-line arg using drawtext ``text`` param and extract the ``-filter_complex`` arg."""
169+
args = (ffmpeg
170+
.input('in')
171+
.drawtext('a{}b'.format(text))
172+
.output('out')
173+
.get_args()
174+
)
175+
assert args[:3] == ['-i', 'in', '-filter_complex']
176+
assert args[4:] == ['-map', '[v0]', 'out']
177+
match = re.match(r'\[0\]drawtext=text=a((.|\n)*)b\[v0\]', args[3], re.MULTILINE)
178+
assert match is not None, 'Invalid -filter_complex arg: {!r}'.format(args[3])
179+
return match.group(1)
180+
181+
expected_backslash_counts = {
182+
'x': 0,
183+
'\'': 7,
184+
'\\': 7,
185+
'%': 4,
186+
':': 2,
187+
',': 1,
188+
'[': 1,
189+
']': 1,
190+
'=': 2,
191+
'\n': 0,
192+
}
193+
for ch, expected_backslash_count in expected_backslash_counts.items():
194+
expected = '{}{}'.format('\\' * expected_backslash_count, ch)
195+
actual = _get_drawtext_text_repr(ch)
196+
assert expected == actual
197+
198+
122199
#def test_version():
123200
# subprocess.check_call(['ffmpeg', '-version'])
124201

0 commit comments

Comments
 (0)
0