Skip to content
This repository was archived by the owner on Feb 1, 2023. It is now read-only.

Commit 8585f89

Browse files
author
Release Manager
committed
Trac #33092: ffmpeg/imagemagick features need feature checks
We have optional tests that make use of these packages, for example: {{{ sage: a.show(format="webm", iterations=1) # optional -- ffmpeg }}} {{{ sage: with open(td + 'wave.gif', 'rb') as f: print(b'!\xf9\x04\x08\x14\x00' in f.read()) # optional -- ImageMagick }}} These tests are run whenever the corresponding "feature" is available, but the feature checks look only for the `convert` and `ffmpeg` programs. Both imagemagick and ffmpeg can be built without support for (say) webm files, making the tests above fail. To avoid spurious failures, the features should test for the necessary file format support, likely in the `is_functional()` method. URL: https://trac.sagemath.org/33092 Reported by: mjo Ticket author(s): Sébastien Labbé Reviewer(s): Julian Rüth, Michael Orlitzky
2 parents 8ea92d5 + 1678c7f commit 8585f89

File tree

5 files changed

+192
-29
lines changed

5 files changed

+192
-29
lines changed

build/pkgs/configure/checksums.ini

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
tarball=configure-VERSION.tar.gz
2-
sha1=82db5472ac5d29950254ec45b1f448dbc4f8e9f0
3-
md5=92c171e5c495c2c2f601d49f89cdaa36
4-
cksum=3258581436
2+
sha1=01b3caa0498cbafb629c30907795fcae451f24c3
3+
md5=39943e883800854e8f277fc7fae36768
4+
cksum=158191402
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ad7e1731c0dd97d79084a0c3baa0022ba4cfff69
1+
2076fd57a487a367d7845a1ae0fbf99cf33400de

src/sage/features/ffmpeg.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
# https://www.gnu.org/licenses/
1313
# ****************************************************************************
1414

15-
from . import Executable
16-
15+
from . import Executable, FeatureTestResult
1716

1817
class FFmpeg(Executable):
1918
r"""
@@ -37,6 +36,81 @@ def __init__(self):
3736
spkg="ffmpeg",
3837
url="https://www.ffmpeg.org/")
3938

39+
def is_functional(self):
40+
r"""
41+
Return whether command ``ffmpeg`` in the path is functional.
42+
43+
EXAMPLES::
44+
45+
sage: from sage.features.ffmpeg import FFmpeg
46+
sage: FFmpeg().is_functional() # optional - ffmpeg
47+
FeatureTestResult('ffmpeg', True)
48+
49+
"""
50+
# Create the content of 1-pixel png file
51+
content = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x00\x00\x00\x00:~\x9bU\x00\x00\x00\nIDATx\x9cc`\x00\x00\x00\x02\x00\x01H\xaf\xa4q\x00\x00\x00\x00IEND\xaeB`\x82'
52+
53+
# NOTE:
54+
#
55+
# This is how the above content of a 1 pixel PNG was created::
56+
#
57+
# sage: import numpy as np
58+
# sage: from PIL import Image
59+
# sage: image = Image.fromarray(np.array([[100]], dtype=np.uint8))
60+
# sage: image.save('file.png')
61+
# sage: with open('file.png', 'rb') as f:
62+
# ....: content = f.read()
63+
64+
# create a png file with the content
65+
from sage.misc.temporary_file import tmp_filename
66+
base_filename_png = tmp_filename(ext='.png')
67+
with open(base_filename_png, 'wb') as f:
68+
f.write(content)
69+
70+
# Set up filenames
71+
import os
72+
base, filename_png = os.path.split(base_filename_png)
73+
filename, _png = os.path.splitext(filename_png)
74+
75+
# Setting a list of commands (taken from sage/plot/animate.py)
76+
# The `-nostdin` is needed to avoid the command to hang, see
77+
# https://stackoverflow.com/questions/16523746/ffmpeg-hangs-when-run-in-background
78+
commands = []
79+
for ext in ['.avi', '.flv', '.gif', '.mkv', '.mov', #'.mpg',
80+
'.mp4', '.ogg', '.ogv', '.webm', '.wmv']:
81+
82+
cmd = ['ffmpeg', '-nostdin', '-y', '-f', 'image2', '-r', '5',
83+
'-i', filename_png, '-pix_fmt', 'rgb24', '-loop', '0',
84+
filename + ext]
85+
commands.append(cmd)
86+
87+
for ext in ['.avi', '.flv', '.gif', '.mkv', '.mov', '.mpg',
88+
'.mp4', '.ogg', '.ogv', '.webm', '.wmv']:
89+
90+
cmd = ['ffmpeg', '-nostdin', '-y', '-f', 'image2', '-i',
91+
filename_png, filename + ext]
92+
commands.append(cmd)
93+
94+
# Running the commands and reporting any issue encountered
95+
from subprocess import run
96+
for cmd in commands:
97+
result = run(cmd, cwd=base, capture_output=True, text=True)
98+
99+
# If an error occurred, return False
100+
if result.returncode:
101+
return FeatureTestResult(self, False, reason='Running command "{}" '
102+
'returned non-zero exit status "{}" with stderr '
103+
'"{}" and stdout "{}".'.format(result.args,
104+
result.returncode,
105+
result.stderr.strip(),
106+
result.stdout.strip()))
107+
108+
# If necessary, run more tests here
109+
# ...
110+
111+
# The command seems functional
112+
return FeatureTestResult(self, True)
113+
40114

41115
def all_features():
42116
return [FFmpeg()]

src/sage/features/imagemagick.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,86 @@
1717
# https://www.gnu.org/licenses/
1818
# ****************************************************************************
1919

20-
from . import Executable
20+
from . import Executable, FeatureTestResult
2121
from .join_feature import JoinFeature
2222

23+
class Convert(Executable):
24+
r"""
25+
A :class:`~sage.features.Feature` describing the presence of ``convert``
26+
27+
EXAMPLES::
28+
29+
sage: from sage.features.imagemagick import Convert
30+
sage: Convert().is_present() # optional - imagemagick
31+
FeatureTestResult('convert', True)
32+
"""
33+
def __init__(self):
34+
r"""
35+
TESTS::
36+
37+
sage: from sage.features.imagemagick import Convert
38+
sage: isinstance(Convert(), Convert)
39+
True
40+
"""
41+
Executable.__init__(self, "convert", executable="convert")
42+
43+
def is_functional(self):
44+
r"""
45+
Return whether command ``convert`` in the path is functional.
46+
47+
EXAMPLES::
48+
49+
sage: from sage.features.imagemagick import Convert
50+
sage: Convert().is_functional() # optional - imagemagick
51+
FeatureTestResult('convert', True)
52+
53+
"""
54+
# Create the content of 1-pixel png file
55+
content = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x00\x00\x00\x00:~\x9bU\x00\x00\x00\nIDATx\x9cc`\x00\x00\x00\x02\x00\x01H\xaf\xa4q\x00\x00\x00\x00IEND\xaeB`\x82'
56+
57+
# NOTE:
58+
#
59+
# This is how the above content of a 1 pixel PNG was created::
60+
#
61+
# sage: import numpy as np
62+
# sage: from PIL import Image
63+
# sage: image = Image.fromarray(np.array([[100]], dtype=np.uint8))
64+
# sage: image.save('file.png')
65+
# sage: with open('file.png', 'rb') as f:
66+
# ....: content = f.read()
67+
68+
# create a png file with the content
69+
from sage.misc.temporary_file import tmp_filename
70+
base_filename_png = tmp_filename(ext='.png')
71+
with open(base_filename_png, 'wb') as f:
72+
f.write(content)
73+
74+
# Set up filenames
75+
import os
76+
base, filename_png = os.path.split(base_filename_png)
77+
filename, _png = os.path.splitext(filename_png)
78+
filename_gif = filename + '.gif'
79+
80+
# running command convert (taken from sage/plot/animate.py)
81+
from subprocess import run
82+
cmd = ['convert', '-dispose', 'Background', '-delay', '20',
83+
'-loop', '0', filename_png, filename_gif]
84+
result = run(cmd, cwd=base, capture_output=True, text=True)
85+
86+
# If an error occurred, return False
87+
if result.returncode:
88+
return FeatureTestResult(self, False, reason='Running command "{}" '
89+
'returned non-zero exit status "{}" with stderr '
90+
'"{}" and stdout "{}".'.format(result.args,
91+
result.returncode,
92+
result.stderr.strip(),
93+
result.stdout.strip()))
94+
95+
# If necessary, run more tests here
96+
# ...
97+
98+
# The command seems functional
99+
return FeatureTestResult(self, True)
23100

24101
class ImageMagick(JoinFeature):
25102
r"""
@@ -33,6 +110,8 @@ class ImageMagick(JoinFeature):
33110
sage: from sage.features.imagemagick import ImageMagick
34111
sage: ImageMagick().is_present() # optional - imagemagick
35112
FeatureTestResult('imagemagick', True)
113+
sage: ImageMagick().is_functional() # optional - imagemagick
114+
FeatureTestResult('imagemagick', True)
36115
"""
37116
def __init__(self):
38117
r"""
@@ -43,10 +122,9 @@ def __init__(self):
43122
True
44123
"""
45124
JoinFeature.__init__(self, "imagemagick",
46-
[Executable("convert", executable="convert")],
125+
[Convert()],
47126
spkg="imagemagick",
48127
url="https://www.imagemagick.org/")
49128

50-
51129
def all_features():
52130
return [ImageMagick()]

src/sage/plot/animate.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,10 @@ def png(self, dir=None):
445445
``None``; in this case, a temporary directory will be
446446
created for storing the frames.
447447
448+
OUTPUT:
449+
450+
Absolute path to the directory containing the PNG images
451+
448452
EXAMPLES::
449453
450454
sage: a = animate([plot(x^2 + n) for n in range(4)], ymin=0, ymax=4)
@@ -558,7 +562,7 @@ def gif(self, delay=20, savefile=None, iterations=0, show_path=False,
558562
sage: td = tmp_dir()
559563
sage: a.gif() # not tested
560564
sage: a.gif(savefile=td + 'my_animation.gif', delay=35, iterations=3) # optional -- ImageMagick
561-
sage: with open(td + 'my_animation.gif', 'rb') as f: print(b'\x21\xf9\x04\x08\x23\x00' in f.read()) # optional -- ImageMagick
565+
sage: with open(td + 'my_animation.gif', 'rb') as f: print(b'GIF8' in f.read()) # optional -- ImageMagick
562566
True
563567
sage: a.gif(savefile=td + 'my_animation.gif', show_path=True) # optional -- ImageMagick
564568
Animation saved to .../my_animation.gif.
@@ -654,22 +658,31 @@ def _gif_from_imagemagick(self, savefile=None, show_path=False,
654658
savefile += '.gif'
655659
savefile = os.path.abspath(savefile)
656660

657-
d = self.png()
658-
cmd = ( 'cd "%s"; sage-native-execute convert -dispose Background '
659-
'-delay %s -loop %s *.png "%s"' ) % ( d, int(delay),
660-
int(iterations), savefile )
661-
from subprocess import check_call, CalledProcessError
662-
try:
663-
check_call(cmd, shell=True)
664-
if show_path:
665-
print("Animation saved to file %s." % savefile)
666-
except (CalledProcessError, OSError):
661+
# running the command
662+
directory = self.png()
663+
cmd = ['sage-native-execute', 'convert', '-dispose', 'Background',
664+
'-delay', '%s' % int(delay), '-loop', '%s' % int(iterations),
665+
'*.png', savefile]
666+
from subprocess import run
667+
result = run(cmd, cwd=directory, capture_output=True, text=True)
668+
669+
# If a problem with the command occurs, print the log before
670+
# raising an error (more verbose than result.check_returncode())
671+
if result.returncode:
672+
print('Command "{}" returned non-zero exit status "{}" '
673+
'(with stderr "{}" and stdout "{}").'.format(result.args,
674+
result.returncode,
675+
result.stderr.strip(),
676+
result.stdout.strip()))
667677
raise OSError("Error: Cannot generate GIF animation. "
668-
"Verify that convert (ImageMagick) or ffmpeg is "
669-
"installed, and that the objects passed to the "
670-
"animate command can be saved in PNG image format. "
671-
"See www.imagemagick.org and www.ffmpeg.org for "
672-
"more information.")
678+
"The convert command (ImageMagick) is present but does "
679+
"not seem to be functional. Verify that the objects "
680+
"passed to the animate command can be saved in PNG "
681+
"image format. "
682+
"See www.imagemagick.org more information.")
683+
684+
if show_path:
685+
print("Animation saved to file %s." % savefile)
673686

674687
def _rich_repr_(self, display_manager, **kwds):
675688
"""
@@ -1082,14 +1095,12 @@ def save(self, filename=None, show_path=False, use_ffmpeg=False, **kwds):
10821095
GIF image (see :trac:`18176`)::
10831096
10841097
sage: a.save(td + 'wave.gif') # optional -- ImageMagick
1085-
sage: with open(td + 'wave.gif', 'rb') as f: print(b'!\xf9\x04\x08\x14\x00' in f.read()) # optional -- ImageMagick
1098+
sage: with open(td + 'wave.gif', 'rb') as f: print(b'GIF8' in f.read()) # optional -- ImageMagick
10861099
True
10871100
sage: with open(td + 'wave.gif', 'rb') as f: print(b'!\xff\x0bNETSCAPE2.0\x03\x01\x00\x00\x00' in f.read()) # optional -- ImageMagick
10881101
True
10891102
sage: a.save(td + 'wave.gif', delay=35) # optional -- ImageMagick
1090-
sage: with open(td + 'wave.gif', 'rb') as f: print(b'!\xf9\x04\x08\x14\x00' in f.read()) # optional -- ImageMagick
1091-
False
1092-
sage: with open(td + 'wave.gif', 'rb') as f: print(b'!\xf9\x04\x08\x23\x00' in f.read()) # optional -- ImageMagick
1103+
sage: with open(td + 'wave.gif', 'rb') as f: print(b'GIF8' in f.read()) # optional -- ImageMagick
10931104
True
10941105
sage: a.save(td + 'wave.gif', iterations=3) # optional -- ImageMagick
10951106
sage: with open(td + 'wave.gif', 'rb') as f: print(b'!\xff\x0bNETSCAPE2.0\x03\x01\x00\x00\x00' in f.read()) # optional -- ImageMagick

0 commit comments

Comments
 (0)