8000 Merge branch 'feature/17' into feature/18 · Powercoder64/ffmpeg-python@35cd113 · GitHub
[go: up one dir, main page]

Skip to content

Commit 35cd113

Browse files
committed
Merge branch 'feature/17' into feature/18
2 parents 2d6b0d4 + 7b2d8b6 commit 35cd113

File tree

5 files changed

+257
-14
lines changed

5 files changed

+257
-14
lines changed

ffmpeg/_filters.py

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
from __future__ import unicode_literals
22

3-
from .nodes import (
4-
FilterNode,
5-
filter_operator,
6-
)
3+
from .nodes import FilterNode, filter_operator
4+
from ._utils import escape_chars
75

86

97
@filter_operator()
@@ -179,6 +177,148 @@ def drawbox(stream, x, y, width, height, color, thickness=None, **kwargs):
179177
return FilterNode(stream, drawbox.__name__, args=[x, y, width, height, color], kwargs=kwargs).stream()
180178

181179

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

ffmpeg/_run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def _get_filter_spec(node, outgoing_edge_map, stream_name_map):
5959
outgoing_edges = get_outgoing_edges(node, outgoing_edge_map)
6060
inputs = [stream_name_map[edge.upstream_node, edge.upstream_label] for edge in incoming_edges]
6161
outputs = [stream_name_map[edge.upstream_node, edge.upstream_label] for edge in outgoing_edges]
62-
filter_spec = '{}{}{}'.format(''.join(inputs), node._get_filter(), ''.join(outputs))
62+
filter_spec = '{}{}{}'.format(''.join(inputs), node._get_filter(outgoing_edges), ''.join(outputs))
6363
return filter_spec
6464

6565

ffmpeg/_utils.py

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

33
from builtins import str
44
from past.builtins import basestring
@@ -29,3 +29,15 @@ def get_hash(item):
2929

3030
def get_hash_int(item):
3131
return int(get_hash(item), base=16)
32+
33+
34+
def escape_chars(text, chars):
35+
"""Helper function to escape uncomfortable characters."""
36+
text = str(text)
37+
chars = list(set(chars))
38+
if '\\' in chars:
39+
chars.remove('\\')
40+
chars.insert(0, '\\')
41+
for ch in chars:
42+
text = text.replace(ch, '\\' + ch)
43+
return text

ffmpeg/nodes.py

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

33
from .dag import KwargReprNode
4-
from ._utils import get_hash_int
4+
from ._utils import escape_chars, get_hash_int
55
from builtins import object
66
import os
77

@@ -148,14 +148,29 @@ def __init__(self, stream_spec, name, max_inputs=1, args=[], kwargs={}):
148148
kwargs=kwargs
149149
)
150150

151-
def _get_filter(self):
152-
params_text = self.name
153-
arg_params = ['{}'.format(arg) for arg in self.args]
154-
kwarg_params = ['{}={}'.format(k, self.kwargs[k]) for k in sorted(self.kwargs)]
151+
"""FilterNode"""
152+
def _get_filter(self, outgoing_edges):
153+
args = self.args
154+
kwargs = self.kwargs
155+
if self.name == 'split':
156+
args = [len(outgoing_edges)]
157+
158+
out_args = [escape_chars(x, '\\\'=:') for x in args]
159+
out_kwargs = {}
160+
for k, v in kwargs.items():
161+
k = escape_chars(k, '\\\'=:')
162+
v = escape_chars(v, '\\\'=:')
163+
out_kwargs[k] = v
164+
165+
arg_params = [escape_chars(v, '\\\'=:') for v in out_args]
166+
kwarg_params = ['{}={}'.format(k, out_kwargs[k]) for k in sorted(out_kwargs)]
155167
params = arg_params + kwarg_params
168+
169+
params_text = escape_chars(self.name, '\\\'=:')
170+
156171
if params:
157172
params_text += '={}'.format(':'.join(params))
158-
return params_text
173+
return escape_chars(params_text, '\\\'[],;')
159174

160175

161176
class OutputNode(Node):

ffmpeg/tests/test_ffmpeg.py

Lines changed: 78 additions & 2 deletions
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__)
@@ -17,6 +19,13 @@
1719
subprocess.check_call(['ffmpeg', '-version'])
1820

1921

22+
def test_escape_chars():
23+
assert ffmpeg._utils.escape_chars('a:b', ':') == 'a\:b'
24< F438 /code>+
assert ffmpeg._utils.escape_chars('a\\:b', ':\\') == 'a\\\\\\:b'
25+
assert ffmpeg._utils.escape_chars('a:b,c[d]e%{}f\'g\'h\\i', '\\\':,[]%') == 'a\\:b\\,c\\[d\\]e\\%{}f\\\'g\\\'h\\\\i'
26+
assert ffmpeg._utils.escape_chars(123, ':\\') == '123'
27+
28+
2029
def test_fluent_equality():
2130
base1 = ffmpeg.input('dummy1.mp4')
2231
base2 = ffmpeg.input('dummy1.mp4')
@@ -122,7 +131,7 @@ def test_get_args_complex_filter():
122131
'-i', TEST_OVERLAY_FILE,
123132
'-filter_complex',
124133
'[0]vflip[s0];' \
125-
'[s0]split[s1][s2];' \
134+
'[s0]split=2[s1][s2];' \
126135
'[s1]trim=end_frame=20:start_frame=10[s3];' \
127136
'[s2]trim=end_frame=40:start_frame=30[s4];' \
128137
'[s3][s4]concat=n=2[s5];' \
@@ -134,6 +143,73 @@ def test_get_args_complex_filter():
134143
]
135144

136145

146+
def test_filter_normal_arg_escape():
147+
"""Test string escaping of normal filter args (e.g. ``font`` param of ``drawtext`` filter)."""
148+
def _get_drawtext_font_repr(font):
149+
"""Build a command-line arg using drawtext ``font`` param and extract the ``-filter_complex`` arg."""
150+
args = (ffmpeg
151+
.input('in')
152+
.drawtext('test', font='a{}b'.format(font))
153+
.output('out')
154+
.get_args()
155+
)
156+
assert args[:3] == ['-i', 'in', '-filter_complex']
157+
assert args[4:] == ['-map', '[s0]', 'out']
158+
match = re.match(r'\[0\]drawtext=font=a((.|\n)*)b:text=test\[s0\]', args[3], re.MULTILINE)
159+
assert match is not None, 'Invalid -filter_complex arg: {!r}'.format(args[3])
160+
return match.group(1)
161+
162+
expected_backslash_counts = {
163+
'x': 0,
164+
'\'': 3,
165+
'\\': 3,
166+
'%&# 10000 39;: 0,
167+
':': 2,
168+
',': 1,
169+
'[': 1,
170+
']': 1,
171+
'=': 2,
172+
'\n': 0,
173+
}
174+
for ch, expected_backslash_count in expected_backslash_counts.items():
175+
expected = '{}{}'.format('\\' * expected_backslash_count, ch)
176+
actual = _get_drawtext_font_repr(ch)
177+
assert expected == actual
178+
179+
180+
def test_filter_text_arg_str_escape():
181+
"""Test string escaping of normal filter args (e.g. ``text`` param of ``drawtext`` filter)."""
182+
def _get_drawtext_text_repr(text):
183+
"""Build a command-line arg using drawtext ``text`` param and extract the ``-filter_complex`` arg."""
184+
args = (ffmpeg
185+
.input('in')
186+
.drawtext('a{}b'.format(text))
187+
.output('out')
188+
.get_args()
189+
)
190+
assert args[:3] == ['-i', 'in', '-filter_complex']
191+
assert args[4:] == ['-map', '[s0]', 'out']
192+
match = re.match(r'\[0\]drawtext=text=a((.|\n)*)b\[s0\]', args[3], re.MULTILINE)
193+
assert match is not None, 'Invalid -filter_complex arg: {!r}'.format(args[3])
194+
return match.group(1)
195+
196+
expected_backslash_counts = {
197+
'x': 0,
198+
'\'': 7,
199+
'\\': 7,
200+
'%': 4,
201+
':': 2,
202+
',': 1,
203+
'[': 1,
204+
']': 1,
205+
'=': 2,
206+
'\n': 0,
207+
}
208+
for ch, expected_backslash_count in expected_backslash_counts.items():
209+
expected = '{}{}'.format('\\' * expected_backslash_count, ch)
210+
actual = _get_drawtext_text_repr(ch)
211+
assert expected == actual
212+
137213

138214
#def test_version():
139215
# subprocess.check_call(['ffmpeg', '-version'])

0 commit comments

Comments
 (0)
0