passagemath-plot 10.6.31rc3__cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of passagemath-plot might be problematic. Click here for more details.

Files changed (82) hide show
  1. passagemath_plot-10.6.31rc3.dist-info/METADATA +172 -0
  2. passagemath_plot-10.6.31rc3.dist-info/RECORD +82 -0
  3. passagemath_plot-10.6.31rc3.dist-info/WHEEL +6 -0
  4. passagemath_plot-10.6.31rc3.dist-info/top_level.txt +2 -0
  5. passagemath_plot.libs/libgfortran-83c28eba.so.5.0.0 +0 -0
  6. passagemath_plot.libs/libgsl-cda90e79.so.28.0.0 +0 -0
  7. passagemath_plot.libs/libopenblasp-r0-6dcb67f9.3.29.so +0 -0
  8. passagemath_plot.libs/libquadmath-2284e583.so.0.0.0 +0 -0
  9. sage/all__sagemath_plot.py +15 -0
  10. sage/ext_data/threejs/animation.css +195 -0
  11. sage/ext_data/threejs/animation.html +85 -0
  12. sage/ext_data/threejs/animation.js +273 -0
  13. sage/ext_data/threejs/fat_lines.js +48 -0
  14. sage/ext_data/threejs/threejs-version.txt +1 -0
  15. sage/ext_data/threejs/threejs_template.html +597 -0
  16. sage/interfaces/all__sagemath_plot.py +1 -0
  17. sage/interfaces/gnuplot.py +196 -0
  18. sage/interfaces/jmoldata.py +208 -0
  19. sage/interfaces/povray.py +56 -0
  20. sage/plot/all.py +42 -0
  21. sage/plot/animate.py +1796 -0
  22. sage/plot/arc.py +504 -0
  23. sage/plot/arrow.py +671 -0
  24. sage/plot/bar_chart.py +205 -0
  25. sage/plot/bezier_path.py +400 -0
  26. sage/plot/circle.py +435 -0
  27. sage/plot/colors.py +1606 -0
  28. sage/plot/complex_plot.cpython-314-x86_64-linux-gnu.so +0 -0
  29. sage/plot/complex_plot.pyx +1446 -0
  30. sage/plot/contour_plot.py +1792 -0
  31. sage/plot/density_plot.py +318 -0
  32. sage/plot/disk.py +373 -0
  33. sage/plot/ellipse.py +375 -0
  34. sage/plot/graphics.py +3580 -0
  35. sage/plot/histogram.py +354 -0
  36. sage/plot/hyperbolic_arc.py +404 -0
  37. sage/plot/hyperbolic_polygon.py +416 -0
  38. sage/plot/hyperbolic_regular_polygon.py +296 -0
  39. sage/plot/line.py +626 -0
  40. sage/plot/matrix_plot.py +629 -0
  41. sage/plot/misc.py +509 -0
  42. sage/plot/multigraphics.py +1294 -0
  43. sage/plot/plot.py +4183 -0
  44. sage/plot/plot3d/all.py +23 -0
  45. sage/plot/plot3d/base.cpython-314-x86_64-linux-gnu.so +0 -0
  46. sage/plot/plot3d/base.pxd +12 -0
  47. sage/plot/plot3d/base.pyx +3378 -0
  48. sage/plot/plot3d/implicit_plot3d.py +659 -0
  49. sage/plot/plot3d/implicit_surface.cpython-314-x86_64-linux-gnu.so +0 -0
  50. sage/plot/plot3d/implicit_surface.pyx +1453 -0
  51. sage/plot/plot3d/index_face_set.cpython-314-x86_64-linux-gnu.so +0 -0
  52. sage/plot/plot3d/index_face_set.pxd +32 -0
  53. sage/plot/plot3d/index_face_set.pyx +1873 -0
  54. sage/plot/plot3d/introduction.py +131 -0
  55. sage/plot/plot3d/list_plot3d.py +649 -0
  56. sage/plot/plot3d/parametric_plot3d.py +1130 -0
  57. sage/plot/plot3d/parametric_surface.cpython-314-x86_64-linux-gnu.so +0 -0
  58. sage/plot/plot3d/parametric_surface.pxd +12 -0
  59. sage/plot/plot3d/parametric_surface.pyx +893 -0
  60. sage/plot/plot3d/platonic.py +601 -0
  61. sage/plot/plot3d/plot3d.py +1442 -0
  62. sage/plot/plot3d/plot_field3d.py +162 -0
  63. sage/plot/plot3d/point_c.pxi +148 -0
  64. sage/plot/plot3d/revolution_plot3d.py +309 -0
  65. sage/plot/plot3d/shapes.cpython-314-x86_64-linux-gnu.so +0 -0
  66. sage/plot/plot3d/shapes.pxd +22 -0
  67. sage/plot/plot3d/shapes.pyx +1382 -0
  68. sage/plot/plot3d/shapes2.py +1512 -0
  69. sage/plot/plot3d/tachyon.py +1779 -0
  70. sage/plot/plot3d/texture.py +453 -0
  71. sage/plot/plot3d/transform.cpython-314-x86_64-linux-gnu.so +0 -0
  72. sage/plot/plot3d/transform.pxd +21 -0
  73. sage/plot/plot3d/transform.pyx +268 -0
  74. sage/plot/plot3d/tri_plot.py +589 -0
  75. sage/plot/plot_field.py +362 -0
  76. sage/plot/point.py +624 -0
  77. sage/plot/polygon.py +562 -0
  78. sage/plot/primitive.py +249 -0
  79. sage/plot/scatter_plot.py +199 -0
  80. sage/plot/step.py +85 -0
  81. sage/plot/streamline_plot.py +328 -0
  82. sage/plot/text.py +432 -0
sage/plot/animate.py ADDED
@@ -0,0 +1,1796 @@
1
+ # sage_setup: distribution = sagemath-plot
2
+ # sage.doctest: needs sage.symbolic
3
+ r"""
4
+ Animated plots
5
+
6
+ Animations are generated from a list (or other iterable) of graphics
7
+ objects.
8
+ Images are produced by calling the ``save_image`` method on each input
9
+ object, creating a sequence of PNG files.
10
+ These are then assembled to various target formats using different
11
+ tools.
12
+ In particular, the ``magick/convert`` program from ImageMagick_ can be used to
13
+ generate an animated GIF file.
14
+ FFmpeg_ (with the command line program ``ffmpeg``) provides support for
15
+ various video formats, but also an alternative method of generating
16
+ animated GIFs.
17
+ For `browsers which support it`_, APNG_ can be used as another
18
+ alternative which works without any extra dependencies.
19
+
20
+ .. WARNING::
21
+
22
+ Note that ``ImageMagick`` and ``FFmpeg`` are not included with Sage, and
23
+ must be installed by the user. On unix systems, type ``which
24
+ magick`` at a command prompt to see if ``magick`` (part of the
25
+ ``ImageMagick`` suite) is installed. If it is, you will be given its
26
+ location. Similarly, you can check for ``ffmpeg`` with ``which
27
+ ffmpeg``. See the websites of ImageMagick_ or FFmpeg_ for
28
+ installation instructions.
29
+
30
+ EXAMPLES:
31
+
32
+ The sine function::
33
+
34
+ sage: x = SR.var("x")
35
+ sage: sines = [plot(c*sin(x), (-2*pi,2*pi), color=Color(c,0,0), ymin=-1, ymax=1)
36
+ ....: for c in sxrange(0,1,.2)]
37
+ sage: a = animate(sines)
38
+ sage: print(a)
39
+ Animation with 5 frames
40
+ sage: a.show() # long time # optional -- ImageMagick
41
+
42
+ Animate using FFmpeg_ instead of ImageMagick::
43
+
44
+ sage: a.show(use_ffmpeg=True) # long time # optional -- FFmpeg
45
+
46
+ Animate as an APNG_::
47
+
48
+ sage: a.apng(show_path=True) # long time
49
+ Animation saved to ....png.
50
+
51
+ An animated :class:`sage.plot.multigraphics.GraphicsArray` of rotating ellipses::
52
+
53
+ sage: E = animate((graphics_array([[ellipse((0,0), a, b, angle=t, xmin=-3, xmax=3)
54
+ ....: + circle((0,0), 3, color='blue')
55
+ ....: for a in range(1,3)]
56
+ ....: for b in range(2,4)])
57
+ ....: for t in sxrange(0, pi/4, .15)))
58
+ sage: str(E) # animations produced from a generator do not have a known length
59
+ 'Animation with unknown number of frames'
60
+ sage: E.show() # long time # optional -- ImageMagick
61
+
62
+ A simple animation of a circle shooting up to the right::
63
+
64
+ sage: c = animate([circle((i,i), 1 - 1/(i+1), hue=i/10)
65
+ ....: for i in srange(0, 2, 0.2)],
66
+ ....: xmin=0, ymin=0, xmax=2, ymax=2, figsize=[2,2])
67
+ sage: c.show() # long time # optional -- ImageMagick
68
+
69
+
70
+ Animations of 3d objects::
71
+
72
+ sage: s,t = SR.var("s,t")
73
+ sage: def sphere_and_plane(x):
74
+ ....: return (sphere((0,0,0), 1, color='red', opacity=.5)
75
+ ....: + parametric_plot3d([t,x,s], (s,-1,1), (t,-1,1),
76
+ ....: color='green', opacity=.7))
77
+ sage: sp = animate([sphere_and_plane(x)
78
+ ....: for x in sxrange(-1, 1, .3)])
79
+ sage: sp[0] # first frame
80
+ Graphics3d Object
81
+ sage: sp[-1] # last frame
82
+ Graphics3d Object
83
+ sage: sp.show() # long time # optional -- ImageMagick
84
+
85
+ sage: (x,y,z) = SR.var("x,y,z")
86
+ sage: def frame(t):
87
+ ....: return implicit_plot3d((x^2 + y^2 + z^2),
88
+ ....: (x, -2, 2), (y, -2, 2), (z, -2, 2),
89
+ ....: plot_points=60, contour=[1,3,5],
90
+ ....: region=lambda x,y,z: x<=t or y>=t or z<=t)
91
+ sage: a = animate([frame(t) for t in srange(.01, 1.5, .2)])
92
+ sage: a[0] # long time
93
+ Graphics3d Object
94
+ sage: a.show() # long time # optional -- ImageMagick
95
+
96
+ If the input objects do not have a ``save_image`` method, then the
97
+ animation object attempts to make an image by calling its internal
98
+ method :meth:`sage.plot.animate.Animation.make_image`. This is
99
+ illustrated by the following example::
100
+
101
+ sage: t = SR.var("t")
102
+ sage: a = animate((sin(c*pi*t) for c in sxrange(1, 2, .2)))
103
+ sage: a.show() # long time # optional -- ImageMagick
104
+
105
+
106
+ AUTHORS:
107
+
108
+ - William Stein
109
+ - John Palmieri
110
+ - Niles Johnson (2013-12): Expand to animate more graphics objects
111
+ - Martin von Gagern (2014-12): Added APNG support
112
+ - Joshua Campbell (2020): interactive animation via Three.js viewer
113
+
114
+ REFERENCES:
115
+
116
+ - `ImageMagick <https://www.imagemagick.org>`_
117
+ - `FFmpeg <https://www.ffmpeg.org>`_
118
+ - `APNG <https://wiki.mozilla.org/APNG_Specification>`_
119
+ - `browsers which support it <https://caniuse.com/#feat=apng>`_
120
+ """
121
+
122
+ ############################################################################
123
+ # Copyright (C) 2007 William Stein <wstein@gmail.com>
124
+ # Distributed under the terms of the GNU General Public License (GPL)
125
+ # https://www.gnu.org/licenses/
126
+ ############################################################################
127
+
128
+ import builtins
129
+ import os
130
+ import shlex
131
+ import struct
132
+ import zlib
133
+
134
+ from sage.misc.fast_methods import WithEqualityById
135
+ from sage.structure.sage_object import SageObject
136
+ from sage.misc.temporary_file import tmp_dir, tmp_filename
137
+ from . import plot
138
+
139
+
140
+ def animate(frames, **kwds):
141
+ r"""
142
+ Animate a list of frames by creating a
143
+ :class:`sage.plot.animate.Animation` object.
144
+
145
+ EXAMPLES::
146
+
147
+ sage: t = SR.var("t")
148
+ sage: a = animate((cos(c*pi*t) for c in sxrange(1, 2, .2)))
149
+ sage: a.show() # long time # optional -- ImageMagick
150
+
151
+ See also :mod:`sage.plot.animate` for more examples.
152
+ """
153
+ return Animation(frames, **kwds)
154
+
155
+
156
+ class Animation(WithEqualityById, SageObject):
157
+ r"""
158
+ Return an animation of a sequence of plots of objects.
159
+
160
+ INPUT:
161
+
162
+ - ``v`` -- iterable of Sage objects; these should preferably be
163
+ graphics objects, but if they aren't, then :meth:`make_image` is
164
+ called on them.
165
+
166
+ - ``xmin``, ``xmax``, ``ymin``, ``ymax`` -- the ranges of the x and y axes
167
+
168
+ - ``**kwds`` -- all additional inputs are passed onto the rendering
169
+ command. E.g., use ``figsize`` to adjust the resolution and aspect
170
+ ratio.
171
+
172
+ EXAMPLES::
173
+
174
+ sage: x = SR.var("x")
175
+ sage: a = animate([sin(x + float(k)) for k in srange(0, 2*pi, 0.3)],
176
+ ....: xmin=0, xmax=2*pi, figsize=[2,1])
177
+ sage: print(a)
178
+ Animation with 21 frames
179
+ sage: print(a[:5])
180
+ Animation with 5 frames
181
+ sage: a.show() # long time # optional -- ImageMagick
182
+ sage: a[:5].show() # long time # optional -- ImageMagick
183
+
184
+ The :meth:`show` method takes arguments to specify the
185
+ delay between frames (measured in hundredths of a second, default
186
+ value 20) and the number of iterations (default: 0, which
187
+ means to iterate forever). To iterate 4 times with half a second
188
+ between each frame::
189
+
190
+ sage: a.show(delay=50, iterations=4) # long time # optional -- ImageMagick
191
+
192
+ An animation of drawing a parabola::
193
+
194
+ sage: step = 0.1
195
+ sage: L = Graphics()
196
+ sage: v = []
197
+ sage: for i in srange(0, 1, step):
198
+ ....: L += line([(i,i^2),(i+step,(i+step)^2)], rgbcolor=(1,0,0), thickness=2)
199
+ ....: v.append(L)
200
+ sage: a = animate(v, xmin=0, ymin=0)
201
+ sage: a.show() # long time # optional -- ImageMagick
202
+ sage: show(L)
203
+
204
+ TESTS:
205
+
206
+ This illustrates that :issue:`2066` is fixed (setting axes
207
+ ranges when an endpoint is 0)::
208
+
209
+ sage: animate([plot(sin, -1,1)], xmin=0, ymin=0)._kwds['xmin']
210
+ 0
211
+
212
+ We check that :issue:`7981` is fixed::
213
+
214
+ sage: x = SR.var("x")
215
+ sage: a = animate([plot(sin(x + float(k)), (0, 2*pi), ymin=-5, ymax=5)
216
+ ....: for k in srange(0,2*pi,0.3)])
217
+ sage: a.show() # long time # optional -- ImageMagick
218
+
219
+ Do not convert input iterator to a list, but ensure that
220
+ the frame count is known after rendering the frames::
221
+
222
+ sage: x = SR.var("x")
223
+ sage: a = animate((plot(x^p, (x,0,2)) for p in sxrange(1,2,.1)))
224
+ sage: str(a)
225
+ 'Animation with unknown number of frames'
226
+ sage: a.png() # long time
227
+ '.../'
228
+ sage: len(a) # long time
229
+ 10
230
+ sage: a._frames
231
+ <generator object ...
232
+
233
+ sage: from sage.plot.animate import Animation
234
+ sage: hash(Animation()) # random
235
+ 140658972348064
236
+ """
237
+ def __init__(self, v=None, **kwds):
238
+ r"""
239
+ Return an animation of a sequence of plots of objects. See
240
+ documentation of :func:`animate` for more details and
241
+ examples.
242
+
243
+ EXAMPLES::
244
+
245
+ sage: x = SR.var("x")
246
+ sage: a = animate([sin(x + float(k)) for k in srange(0,2*pi,0.3)], # indirect doctest
247
+ ....: xmin=0, xmax=2*pi, figsize=[2,1])
248
+ sage: print(a)
249
+ Animation with 21 frames
250
+ sage: a.show() # long time # optional -- ImageMagick
251
+ """
252
+ self._frames = v
253
+ self._kwds = kwds
254
+
255
+ def _combine_kwds(self, *kwds_tuple):
256
+ """
257
+ Return a dictionary which is a combination of the all the
258
+ dictionaries in kwds_tuple. This also does the appropriate thing
259
+ for taking the mins and maxes of all of the x/y mins/maxes.
260
+
261
+ EXAMPLES::
262
+
263
+ sage: a = animate([plot(sin, -1,1)], xmin=0, ymin=0)
264
+ sage: kwds1 = {'a':1, 'b':2, 'xmin':2, 'xmax':5}
265
+ sage: kwds2 = {'b':3, 'xmin':0, 'xmax':4}
266
+ sage: kwds = a._combine_kwds(kwds1, kwds2)
267
+ sage: list(sorted(kwds.items()))
268
+ [('a', 1), ('b', 3), ('xmax', 5), ('xmin', 0)]
269
+
270
+ Test that the bug reported in :issue:`12107` has been fixed::
271
+
272
+ sage: kwds3 = {}
273
+ sage: kwds4 = {'b':3, 'xmin':0, 'xmax':4}
274
+ sage: a._combine_kwds(kwds3, kwds4)['xmin']
275
+ 0
276
+ """
277
+ new_kwds = {}
278
+
279
+ for kwds in kwds_tuple:
280
+ new_kwds.update(kwds)
281
+
282
+ for name in ['xmin', 'xmax', 'ymin', 'ymax']:
283
+ values = [v for v in [kwds.get(name, None) for kwds in kwds_tuple] if v is not None]
284
+ if values:
285
+ new_kwds[name] = getattr(builtins, name[1:])(values)
286
+ return new_kwds
287
+
288
+ def __getitem__(self, i):
289
+ """
290
+ Get a frame from an animation or
291
+ slice this animation returning a subanimation.
292
+
293
+ EXAMPLES::
294
+
295
+ sage: a = animate([circle((i,-i), 1-1/(i+1), hue=i/10) for i in srange(0,2,0.2)],
296
+ ....: xmin=0,ymin=-2,xmax=2,ymax=0,figsize=[2,2])
297
+ sage: print(a)
298
+ Animation with 10 frames
299
+ sage: a.show() # long time # optional -- ImageMagick
300
+ sage: frame2 = a[2] # indirect doctest
301
+ sage: frame2.show()
302
+ sage: print(a[3:7]) # indirect doctest
303
+ Animation with 4 frames
304
+ sage: a[3:7].show() # long time # optional -- ImageMagick
305
+ """
306
+ if isinstance(i, slice):
307
+ return Animation(self._frames[i], **self._kwds)
308
+ else:
309
+ return self._frames[i]
310
+
311
+ def _repr_(self):
312
+ """
313
+ Print representation for an animation.
314
+
315
+ EXAMPLES::
316
+
317
+ sage: a = animate([circle((i,-i), 1-1/(i+1), hue=i/10) for i in srange(0,2,0.2)],
318
+ ....: xmin=0,ymin=-2,xmax=2,ymax=0,figsize=[2,2])
319
+ sage: print(a)
320
+ Animation with 10 frames
321
+ sage: a._repr_()
322
+ 'Animation with 10 frames'
323
+ """
324
+ try:
325
+ num = len(self)
326
+ except TypeError:
327
+ num = "unknown number of"
328
+ return "Animation with %s frames" % num
329
+
330
+ def __add__(self, other):
331
+ """
332
+ Add two animations. This has the effect of superimposing the two
333
+ animations frame-by-frame.
334
+
335
+ EXAMPLES::
336
+
337
+ sage: a = animate([line([(0,0),(1,i)],hue=0/3) for i in range(2)],
338
+ ....: xmin=0, ymin=0, xmax=1, ymax=1,
339
+ ....: figsize=[1,1], axes=False)
340
+ sage: print(a)
341
+ Animation with 2 frames
342
+ sage: a.show() # optional -- ImageMagick
343
+ sage: b = animate([line([(0,0),(i,1)],hue=2/3) for i in range(2)],
344
+ ....: xmin=0, ymin=0, xmax=1, ymax=1,
345
+ ....: figsize=[1,1], axes=False)
346
+ sage: print(b)
347
+ Animation with 2 frames
348
+ sage: b.show() # optional -- ImageMagick
349
+ sage: s = a+b # indirect doctest
350
+ sage: print(s)
351
+ Animation with 2 frames
352
+ sage: len(a), len(b)
353
+ (2, 2)
354
+ sage: len(s)
355
+ 2
356
+ sage: s.show() # optional -- ImageMagick
357
+ """
358
+ if not isinstance(other, Animation):
359
+ other = Animation(other)
360
+
361
+ kwds = self._combine_kwds(self._kwds, other._kwds)
362
+
363
+ #Combine the frames
364
+ m = max(len(self), len(other))
365
+ frames = [a+b for a,b in zip(self._frames, other._frames)]
366
+ frames += self._frames[m:] + other._frames[m:]
367
+
368
+ return Animation(frames, **kwds)
369
+
370
+ def __mul__(self, other):
371
+ """
372
+ Multiply two animations. This has the effect of appending the two
373
+ animations (the second comes after the first).
374
+
375
+ EXAMPLES::
376
+
377
+ sage: a = animate([line([(0,0),(1,i)],hue=0/3) for i in range(2)],
378
+ ....: xmin=0, ymin=0, xmax=1, ymax=1,
379
+ ....: figsize=[1,1], axes=False)
380
+ sage: print(a)
381
+ Animation with 2 frames
382
+ sage: a.show() # optional -- ImageMagick
383
+ sage: b = animate([line([(0,0),(i,1)],hue=2/3) for i in range(2)],
384
+ ....: xmin=0, ymin=0, xmax=1, ymax=1,
385
+ ....: figsize=[1,1], axes=False)
386
+ sage: print(b)
387
+ Animation with 2 frames
388
+ sage: b.show() # optional -- ImageMagick
389
+ sage: p = a*b # indirect doctest
390
+ sage: len(a), len(b)
391
+ (2, 2)
392
+ sage: len(p)
393
+ 4
394
+ sage: print(p)
395
+ Animation with 4 frames
396
+ sage: p.show() # optional -- ImageMagick
397
+ """
398
+ if not isinstance(other, Animation):
399
+ other = Animation(other)
400
+
401
+ kwds = self._combine_kwds(self._kwds, other._kwds)
402
+
403
+ return Animation(self._frames + other._frames, **kwds)
404
+
405
+ def __len__(self):
406
+ """
407
+ Length of ``self``.
408
+
409
+ EXAMPLES::
410
+
411
+ sage: a = animate([circle((i,0),1,thickness=20*i) for i in srange(0,2,0.4)],
412
+ ....: xmin=0, ymin=-1, xmax=3, ymax=1, figsize=[2,1], axes=False)
413
+ sage: len(a)
414
+ 5
415
+ """
416
+ try:
417
+ return self._num_frames
418
+ except AttributeError:
419
+ return len(self._frames)
420
+
421
+ def make_image(self, frame, filename, **kwds):
422
+ r"""
423
+ Given a frame which has no ``save_image()`` method, make a graphics
424
+ object and save it as an image with the given filename. By default, this is
425
+ :meth:`sage.plot.plot.plot`. To make animations of other objects,
426
+ override this method in a subclass.
427
+
428
+ EXAMPLES::
429
+
430
+ sage: from sage.plot.animate import Animation
431
+ sage: class MyAnimation(Animation):
432
+ ....: def make_image(self, frame, filename, **kwds):
433
+ ....: P = parametric_plot(frame[0], frame[1], **frame[2])
434
+ ....: P.save_image(filename, **kwds)
435
+
436
+ sage: t = SR.var("t")
437
+ sage: x = lambda t: cos(t)
438
+ sage: y = lambda n,t: sin(t)/n
439
+ sage: B = MyAnimation([([x(t), y(i+1,t)], (t,0,1),
440
+ ....: {'color':Color((1,0,i/4)), 'aspect_ratio':1, 'ymax':1})
441
+ ....: for i in range(4)])
442
+
443
+ sage: d = B.png(); v = os.listdir(d); v.sort(); v # long time
444
+ ['00000000.png', '00000001.png', '00000002.png', '00000003.png']
445
+ sage: B.show() # not tested
446
+
447
+ sage: class MyAnimation(Animation):
448
+ ....: def make_image(self, frame, filename, **kwds):
449
+ ....: G = frame.plot()
450
+ ....: G.set_axes_range(floor(G.xmin()), ceil(G.xmax()),
451
+ ....: floor(G.ymin()), ceil(G.ymax()))
452
+ ....: G.save_image(filename, **kwds)
453
+
454
+ sage: B = MyAnimation([graphs.CompleteGraph(n)
455
+ ....: for n in range(7,11)], figsize=5)
456
+ sage: d = B.png()
457
+ sage: v = os.listdir(d); v.sort(); v
458
+ ['00000000.png', '00000001.png', '00000002.png', '00000003.png']
459
+ sage: B.show() # not tested
460
+ """
461
+ p = plot.plot(frame, **kwds)
462
+ p.save_image(filename)
463
+
464
+ def png(self, dir=None):
465
+ r"""
466
+ Render PNG images of the frames in this animation, saving them
467
+ in ``dir``. Return the absolute path to that directory. If
468
+ the frames have been previously rendered and ``dir`` is
469
+ ``None``, just return the directory in which they are stored.
470
+
471
+ When ``dir`` is other than ``None``, force re-rendering of
472
+ frames.
473
+
474
+ INPUT:
475
+
476
+ - ``dir`` -- (default: ``None``) directory in which to store frames; in
477
+ this case, a temporary directory will be created for storing the frames
478
+
479
+ OUTPUT: absolute path to the directory containing the PNG images
480
+
481
+ EXAMPLES::
482
+
483
+ sage: x = SR.var("x")
484
+ sage: a = animate([plot(x^2 + n) for n in range(4)], ymin=0, ymax=4)
485
+ sage: d = a.png(); v = os.listdir(d); v.sort(); v # long time
486
+ ['00000000.png', '00000001.png', '00000002.png', '00000003.png']
487
+ """
488
+ if dir is None:
489
+ try:
490
+ return self._png_dir
491
+ except AttributeError:
492
+ pass
493
+ dir = tmp_dir()
494
+ i = 0
495
+ for frame in self._frames:
496
+ filename = '%s/%08d.png' % (dir,i)
497
+ try:
498
+ save_image = frame.save_image
499
+ except AttributeError:
500
+ self.make_image(frame, filename, **self._kwds)
501
+ else:
502
+ save_image(filename, **self._kwds)
503
+ i += 1
504
+ self._num_frames = i
505
+ self._png_dir = dir
506
+ return dir
507
+
508
+ def graphics_array(self, ncols=3):
509
+ r"""
510
+ Return a :class:`sage.plot.multigraphics.GraphicsArray` with plots of the
511
+ frames of this animation, using the given number of columns.
512
+ The frames must be acceptable inputs for
513
+ :class:`sage.plot.multigraphics.GraphicsArray`.
514
+
515
+ EXAMPLES::
516
+
517
+ sage: # needs sage.schemes
518
+ sage: E = EllipticCurve('37a')
519
+ sage: v = [E.change_ring(GF(p)).plot(pointsize=30)
520
+ ....: for p in [97, 101, 103]]
521
+ sage: a = animate(v, xmin=0, ymin=0, axes=False)
522
+ sage: print(a)
523
+ Animation with 3 frames
524
+ sage: a.show() # optional -- ImageMagick
525
+
526
+ Modify the default arrangement of array::
527
+
528
+ sage: g = a.graphics_array(); print(g) # needs sage.schemes
529
+ Graphics Array of size 1 x 3
530
+ sage: g.show(figsize=[6,3]) # needs sage.schemes
531
+
532
+ Specify different arrangement of array and save it with a given file name::
533
+
534
+ sage: g = a.graphics_array(ncols=2); print(g) # needs sage.schemes
535
+ Graphics Array of size 2 x 2
536
+ sage: f = tmp_filename(ext='.png'); print(f) # needs sage.schemes
537
+ ...png
538
+ sage: g.save(f) # needs sage.schemes
539
+
540
+ Frames can be specified as a generator too; it is internally converted to a list::
541
+
542
+ sage: t = SR.var("t")
543
+ sage: b = animate((plot(sin(c*pi*t)) for c in sxrange(1,2,.2)))
544
+ sage: g = b.graphics_array()
545
+ sage: print(g)
546
+ Graphics Array of size 2 x 3
547
+ """
548
+ ncols = int(ncols)
549
+ frame_list = list(self._frames)
550
+ n = len(frame_list)
551
+ nrows, rem = divmod(n,ncols)
552
+ if rem > 0:
553
+ nrows += 1
554
+ return plot.graphics_array(frame_list, nrows, ncols)
555
+
556
+ def gif(self, delay=20, savefile=None, iterations=0, show_path=False,
557
+ use_ffmpeg=False):
558
+ r"""
559
+ Return an animated gif composed from rendering the graphics
560
+ objects in ``self``.
561
+
562
+ This method will only work if either (a) the ImageMagick
563
+ software suite is installed, i.e., you have the ``magick/convert``
564
+ command or (b) ``ffmpeg`` is installed. See the web sites of
565
+ ImageMagick_ and FFmpeg_ for more details. By default, this
566
+ produces the gif using Imagemagick if it is present. If this
567
+ can't find ImageMagick or if ``use_ffmpeg`` is True, then it
568
+ uses ``ffmpeg`` instead.
569
+
570
+ INPUT:
571
+
572
+ - ``delay`` -- (default: 20) delay in hundredths of a
573
+ second between frames
574
+
575
+ - ``savefile`` -- file that the animated gif gets saved
576
+ to
577
+
578
+ - ``iterations`` -- integer (default: 0); number of
579
+ iterations of animation. If 0, loop forever.
580
+
581
+ - ``show_path`` -- boolean (default: ``False``); if True,
582
+ print the path to the saved file
583
+
584
+ - ``use_ffmpeg`` -- boolean (default: ``False``); if ``True``, use
585
+ 'ffmpeg' by default instead of ImageMagick
586
+
587
+ If ``savefile`` is not specified: in notebook mode, display the
588
+ animation; otherwise, save it to a default file name.
589
+
590
+ EXAMPLES::
591
+
592
+ sage: x = SR.var("x")
593
+ sage: a = animate([sin(x + float(k))
594
+ ....: for k in srange(0,2*pi,0.7)],
595
+ ....: xmin=0, xmax=2*pi, ymin=-1, ymax=1, figsize=[2,1])
596
+ sage: td = tmp_dir()
597
+ sage: a.gif() # not tested
598
+ sage: a.gif(savefile=td + 'my_animation.gif', # long time # optional -- ImageMagick
599
+ ....: delay=35, iterations=3)
600
+ sage: with open(td + 'my_animation.gif', 'rb') as f: # long time # optional -- ImageMagick
601
+ ....: print(b'GIF8' in f.read())
602
+ True
603
+ sage: a.gif(savefile=td + 'my_animation.gif', # long time # optional -- ImageMagick
604
+ ....: show_path=True)
605
+ Animation saved to .../my_animation.gif.
606
+ sage: a.gif(savefile=td + 'my_animation_2.gif', # long time # optional -- FFmpeg
607
+ ....: show_path=True, use_ffmpeg=True)
608
+ Animation saved to .../my_animation_2.gif.
609
+
610
+ .. NOTE::
611
+
612
+ If neither ffmpeg nor ImageMagick is installed, you will
613
+ get an error message like this::
614
+
615
+ Error: Neither ImageMagick nor ffmpeg appears to be installed. Saving an
616
+ animation to a GIF file or displaying an animation requires one of these
617
+ packages, so please install one of them and try again.
618
+
619
+ See www.imagemagick.org and www.ffmpeg.org for more information.
620
+ """
621
+ from sage.features.imagemagick import ImageMagick
622
+ from sage.features.ffmpeg import FFmpeg
623
+
624
+ if not ImageMagick().is_present() and not FFmpeg().is_present():
625
+ raise OSError("Error: Neither ImageMagick nor ffmpeg appear to "
626
+ "be installed. Saving an animation to a GIF file or "
627
+ "displaying an animation requires one of these "
628
+ "packages, so please install one of them and try "
629
+ "again. See www.imagemagick.org and www.ffmpeg.org "
630
+ "for more information.")
631
+
632
+ if use_ffmpeg or not ImageMagick().is_present():
633
+ self.ffmpeg(savefile=savefile, show_path=show_path,
634
+ output_format='.gif', delay=delay,
635
+ iterations=iterations)
636
+ else:
637
+ self._gif_from_imagemagick(savefile=savefile, show_path=show_path,
638
+ delay=delay, iterations=iterations)
639
+
640
+ def _gif_from_imagemagick(self, savefile=None, show_path=False,
641
+ delay=20, iterations=0):
642
+ r"""
643
+ Return a movie showing an animation composed from rendering
644
+ the frames in ``self``.
645
+
646
+ This method will only work if ``imagemagick`` is installed (command
647
+ ``magick`` or ``convert``). See https://www.imagemagick.org for information
648
+ about ``imagemagick``.
649
+
650
+ INPUT:
651
+
652
+ - ``savefile`` -- file that the mpeg gets saved to
653
+
654
+ .. warning::
655
+
656
+ This will overwrite ``savefile`` if it already exists.
657
+
658
+ - ``show_path`` -- boolean (default: ``False``); if ``True``,
659
+ print the path to the saved file
660
+
661
+ - ``delay`` -- (default: 20) delay in hundredths of a
662
+ second between frames
663
+
664
+ - ``iterations`` -- integer (default: 0); number of iterations
665
+ of animation. If 0, loop forever.
666
+
667
+ If ``savefile`` is not specified: in notebook mode, display
668
+ the animation; otherwise, save it to a default file name. Use
669
+ :func:`sage.misc.verbose.set_verbose` with ``level=1`` to see
670
+ additional output.
671
+
672
+ EXAMPLES::
673
+
674
+ sage: x = SR.var("x")
675
+ sage: a = animate([sin(x + float(k)) for k in srange(0,2*pi,0.7)],
676
+ ....: xmin=0, xmax=2*pi, ymin=-1, ymax=1, figsize=[2,1])
677
+ sage: td = tmp_dir()
678
+ sage: a._gif_from_imagemagick(savefile=td + 'new.gif') # long time # optional -- ImageMagick
679
+
680
+ .. NOTE::
681
+
682
+ If imagemagick is not installed, you will get an error message
683
+ like this::
684
+
685
+ FeatureNotPresentError: imagemagick is not available.
686
+ Executable 'magick' not found on PATH.
687
+ Further installation instructions might be available at
688
+ https://www.imagemagick.org/.
689
+ """
690
+ from sage.features.imagemagick import ImageMagick, Magick
691
+ ImageMagick().require()
692
+
693
+ if not savefile:
694
+ savefile = tmp_filename(ext='.gif')
695
+ if not savefile.endswith('.gif'):
696
+ savefile += '.gif'
697
+ savefile = os.path.abspath(savefile)
698
+
699
+ # running the command
700
+ directory = self.png()
701
+ cmd = [Magick().executable, '-dispose', 'Background',
702
+ '-delay', '%s' % int(delay), '-loop', '%s' % int(iterations),
703
+ '*.png', savefile]
704
+ from subprocess import run
705
+ result = run(cmd, cwd=directory, capture_output=True, text=True)
706
+
707
+ # If a problem with the command occurs, print the log before
708
+ # raising an error (more verbose than result.check_returncode())
709
+ if result.returncode:
710
+ print('Command "{}" returned nonzero exit status "{}" '
711
+ '(with stderr "{}" and stdout "{}").'.format(result.args,
712
+ result.returncode,
713
+ result.stderr.strip(),
714
+ result.stdout.strip()))
715
+ raise OSError("Error: Cannot generate GIF animation. "
716
+ "The magick/convert command (ImageMagick) is present but does "
717
+ "not seem to be functional. Verify that the objects "
718
+ "passed to the animate command can be saved in PNG "
719
+ "image format. "
720
+ "See www.imagemagick.org more information.")
721
+
722
+ if show_path:
723
+ print("Animation saved to file %s." % savefile)
724
+
725
+ def _rich_repr_(self, display_manager, **kwds):
726
+ """
727
+ Rich Output Magic Method.
728
+
729
+ See :mod:`sage.repl.rich_output` for details.
730
+
731
+ EXAMPLES::
732
+
733
+ sage: x = SR.var("x")
734
+ sage: a = animate([plot(x^2 + n) for n in range(2)], ymin=0, ymax=4)
735
+ sage: from sage.repl.rich_output import get_display_manager
736
+ sage: dm = get_display_manager()
737
+ sage: a._rich_repr_(dm) # optional -- ImageMagick
738
+ OutputImageGif container
739
+ """
740
+
741
+ iterations = kwds.get('iterations', 0)
742
+ loop = (iterations == 0)
743
+
744
+ t = display_manager.types
745
+ supported = display_manager.supported_output()
746
+ format = kwds.pop("format", None)
747
+ if format is None:
748
+ if t.OutputImageGif in supported:
749
+ format = "gif"
750
+ else:
751
+ return # No supported format could be guessed
752
+ suffix = None
753
+ outputType = None
754
+ if format == "gif":
755
+ outputType = t.OutputImageGif
756
+ suffix = ".gif"
757
+ if format == "ogg":
758
+ outputType = t.OutputVideoOgg
759
+ if format == "webm":
760
+ outputType = t.OutputVideoWebM
761
+ if format == "mp4":
762
+ outputType = t.OutputVideoMp4
763
+ if format == "flash":
764
+ outputType = t.OutputVideoFlash
765
+ if format == "matroska":
766
+ outputType = t.OutputVideoMatroska
767
+ if format == "avi":
768
+ outputType = t.OutputVideoAvi
769
+ if format == "wmv":
770
+ outputType = t.OutputVideoWmv
771
+ if format == "quicktime":
772
+ outputType = t.OutputVideoQuicktime
773
+ if format is None:
774
+ raise ValueError("Unknown video format")
775
+ if outputType not in supported:
776
+ return # Sorry, requested format is not supported
777
+ if suffix is not None:
778
+ return display_manager.graphics_from_save(
779
+ self.save, kwds, suffix, outputType)
780
+
781
+ # Now we save for OutputVideoBase
782
+ filename = tmp_filename(ext=outputType.ext)
783
+ self.save(filename, **kwds)
784
+ from sage.repl.rich_output.buffer import OutputBuffer
785
+ buf = OutputBuffer.from_file(filename)
786
+ return outputType(buf, loop=loop)
787
+
788
+ def show(self, delay=None, iterations=None, **kwds):
789
+ r"""
790
+ Show this animation immediately.
791
+
792
+ This method attempts to display the graphics immediately,
793
+ without waiting for the currently running code (if any) to
794
+ return to the command line. Be careful, calling it from within
795
+ a loop will potentially launch a large number of external
796
+ viewer programs.
797
+
798
+ INPUT:
799
+
800
+ - ``delay`` -- (default: 20) delay in hundredths of a
801
+ second between frames
802
+
803
+ - ``iterations`` -- integer (default: 0); number of
804
+ iterations of animation. If 0, loop forever.
805
+
806
+ - ``format`` -- (default: gif) format to use for output.
807
+ Currently supported formats are: gif,
808
+ ogg, webm, mp4, flash, matroska, avi, wmv, quicktime.
809
+
810
+ OUTPUT:
811
+
812
+ This method does not return anything. Use :meth:`save` if you
813
+ want to save the figure as an image.
814
+
815
+ .. NOTE::
816
+
817
+ Currently this is done using an animated gif, though this
818
+ could change in the future. This requires that either
819
+ ffmpeg or the ImageMagick suite (in particular, the
820
+ ``magick/convert`` command) is installed.
821
+
822
+ See also the :meth:`ffmpeg` method.
823
+
824
+ EXAMPLES::
825
+
826
+ sage: x = SR.var("x")
827
+ sage: a = animate([sin(x + float(k))
828
+ ....: for k in srange(0,2*pi,0.7)],
829
+ ....: xmin=0, xmax=2*pi, figsize=[2,1])
830
+ sage: a.show() # long time # optional -- ImageMagick
831
+
832
+ The preceding will loop the animation forever. If you want to show
833
+ only three iterations instead::
834
+
835
+ sage: a.show(iterations=3) # long time # optional -- ImageMagick
836
+
837
+ To put a half-second delay between frames::
838
+
839
+ sage: a.show(delay=50) # long time # optional -- ImageMagick
840
+
841
+ You can also make use of the HTML5 video element in the Sage notebook::
842
+
843
+ sage: # long time, optional -- FFmpeg
844
+ sage: a.show(format='ogg')
845
+ sage: a.show(format='webm')
846
+ sage: a.show(format='mp4')
847
+ sage: a.show(format='webm', iterations=1)
848
+
849
+ Other backends may support other file formats as well::
850
+
851
+ sage: # long time, optional -- FFmpeg
852
+ sage: a.show(format='flash')
853
+ sage: a.show(format='matroska')
854
+ sage: a.show(format='avi')
855
+ sage: a.show(format='wmv')
856
+ sage: a.show(format='quicktime')
857
+
858
+ TESTS:
859
+
860
+ Use of positional parameters is discouraged, will likely get
861
+ deprecated, but should still work for the time being::
862
+
863
+ sage: a.show(50, 3) # long time # optional -- ImageMagick
864
+
865
+ .. NOTE::
866
+
867
+ If you don't have ffmpeg or ImageMagick installed, you will
868
+ get an error message like this::
869
+
870
+ Error: Neither ImageMagick nor ffmpeg appears to be installed. Saving an
871
+ animation to a GIF file or displaying an animation requires one of these
872
+ packages, so please install one of them and try again.
873
+
874
+ See www.imagemagick.org and www.ffmpeg.org for more information.
875
+ """
876
+
877
+ # Positional parameters for the sake of backwards compatibility
878
+ if delay is not None:
879
+ kwds.setdefault("delay", delay)
880
+ if iterations is not None:
881
+ kwds.setdefault("iterations", iterations)
882
+
883
+ from sage.repl.rich_output import get_display_manager
884
+ dm = get_display_manager()
885
+ dm.display_immediately(self, **kwds)
886
+
887
+ def ffmpeg(self, savefile=None, show_path=False, output_format=None,
888
+ ffmpeg_options='', delay=None, iterations=0, pix_fmt='rgb24'):
889
+ r"""
890
+ Return a movie showing an animation composed from rendering
891
+ the frames in ``self``.
892
+
893
+ This method will only work if ``ffmpeg`` is installed. See
894
+ https://www.ffmpeg.org for information about ``ffmpeg``.
895
+
896
+ INPUT:
897
+
898
+ - ``savefile`` -- file that the mpeg gets saved to
899
+
900
+ .. warning::
901
+
902
+ This will overwrite ``savefile`` if it already exists.
903
+
904
+ - ``show_path`` -- boolean (default: ``False``); if ``True``,
905
+ print the path to the saved file
906
+
907
+ - ``output_format`` -- string (default: ``None``); format and
908
+ suffix to use for the video. This may be ``'mpg'``, ``'mpeg'``,
909
+ ``'avi'``, ``'gif'``, or any other format that ``ffmpeg`` can handle.
910
+ If this is ``None`` and the user specifies ``savefile`` with a
911
+ suffix, say ``savefile='animation.avi'``, try to determine the
912
+ format (``'avi'`` in this case) from that file name. If no file
913
+ is specified or if the suffix cannot be determined, ``'mpg'`` is
914
+ used.
915
+
916
+ - ``ffmpeg_options`` -- string (default: ``''``); this string is
917
+ passed directly to ffmpeg
918
+
919
+ - ``delay`` -- integer (default: ``None``); delay in hundredths of a
920
+ second between frames. The framerate is 100/delay.
921
+ This is not supported for mpeg files: for mpegs, the frame
922
+ rate is always 25 fps.
923
+
924
+ - ``iterations`` -- integer (default: 0); number of iterations
925
+ of animation. If 0, loop forever. This is only supported
926
+ for animated gif output and requires ``ffmpeg`` version 0.9 or
927
+ later. For older versions, set ``iterations=None``.
928
+
929
+ - ``pix_fmt`` -- string (default: ``'rgb24'``); used only for gif
930
+ output. Different values such as 'rgb8' or 'pal8' may be
931
+ necessary depending on how ffmpeg was installed. Set
932
+ ``pix_fmt=None`` to disable this option.
933
+
934
+ If ``savefile`` is not specified: in notebook mode, display
935
+ the animation; otherwise, save it to a default file name. Use
936
+ :func:`sage.misc.verbose.set_verbose` with ``level=1`` to see
937
+ additional output.
938
+
939
+ EXAMPLES::
940
+
941
+ sage: x = SR.var("x")
942
+ sage: a = animate([sin(x + float(k))
943
+ ....: for k in srange(0, 2*pi, 0.7)],
944
+ ....: xmin=0, xmax=2*pi, ymin=-1, ymax=1, figsize=[2,1])
945
+ sage: td = tmp_dir()
946
+ sage: a.ffmpeg(savefile=td + 'new.mpg') # long time # optional -- FFmpeg
947
+ sage: a.ffmpeg(savefile=td + 'new.avi') # long time # optional -- FFmpeg
948
+ sage: a.ffmpeg(savefile=td + 'new.gif') # long time # optional -- FFmpeg
949
+ sage: a.ffmpeg(savefile=td + 'new.mpg', show_path=True) # long time # optional -- FFmpeg
950
+ Animation saved to .../new.mpg.
951
+
952
+ .. NOTE::
953
+
954
+ If ffmpeg is not installed, you will get an error message
955
+ like this::
956
+
957
+ FeatureNotPresentError: ffmpeg is not available.
958
+ Executable 'ffmpeg' not found on PATH.
959
+ Further installation instructions might be available at https://www.ffmpeg.org/.
960
+
961
+ TESTS::
962
+
963
+ sage: a.ffmpeg(output_format='gif',delay=30,iterations=5) # long time # optional -- FFmpeg
964
+ """
965
+ from sage.features.ffmpeg import FFmpeg
966
+ FFmpeg().require()
967
+
968
+ if savefile is None:
969
+ if output_format is None:
970
+ output_format = '.mpg'
971
+ else:
972
+ if output_format[0] != '.':
973
+ output_format = '.'+output_format
974
+ savefile = tmp_filename(ext=output_format)
975
+ else:
976
+ if output_format is None:
977
+ suffix = os.path.splitext(savefile)[1]
978
+ if len(suffix) > 0:
979
+ output_format = suffix
980
+ else:
981
+ output_format = '.mpg'
982
+ if not savefile.endswith(output_format):
983
+ savefile += output_format
984
+ early_options = ''
985
+ if output_format == '.gif':
986
+ # We try to set reasonable options for gif output.
987
+ #
988
+ # Older versions of ffmpeg (before 0.9, summer 2011)
989
+ # use the option -loop_output instead of -loop.
990
+ # Setting iterations=None is a way of preventing sage
991
+ # from adding the -loop option. A separate
992
+ # -loop_output option can be added with the
993
+ # ffmpeg_options argument.
994
+ if iterations is not None:
995
+ loop_cmd = f'-loop {iterations} '
996
+ else:
997
+ loop_cmd = ''
998
+ # A pix_fmt value is required for some but not all
999
+ # ffmpeg installations. Setting pix_fmt=None will
1000
+ # prevent sage from adding this option, and it may be
1001
+ # controlled separately through ffmpeg_options.
1002
+ if pix_fmt is not None:
1003
+ pix_fmt_cmd = f'-pix_fmt {pix_fmt} '
1004
+ else:
1005
+ pix_fmt_cmd = ''
1006
+ ffmpeg_options += f' {pix_fmt_cmd}{loop_cmd}'
1007
+ if delay is not None and output_format != '.mpeg' and output_format != '.mpg':
1008
+ early_options += ' -r %s ' % int(100/delay)
1009
+ savefile = os.path.abspath(savefile)
1010
+ pngdir = self.png()
1011
+ pngs = os.path.join(pngdir, "%08d.png")
1012
+ # For ffmpeg, it seems that some options, like '-g ... -r
1013
+ # ...', need to come before the input file names, while
1014
+ # some options, like '-pix_fmt rgb24', need to come
1015
+ # afterwards. Hence 'early_options' and 'ffmpeg_options'
1016
+ # The `-nostdin` is needed to avoid the command to hang, see
1017
+ # https://stackoverflow.com/questions/16523746/ffmpeg-hangs-when-run-in-background
1018
+ cmd = 'cd {}; {} -nostdin -y -f image2 {} -i {} {} {}'.format(
1019
+ shlex.quote(pngdir), shlex.quote(FFmpeg().absolute_filename()),
1020
+ early_options, shlex.quote(pngs), ffmpeg_options, shlex.quote(savefile))
1021
+ from subprocess import check_call, CalledProcessError, PIPE
1022
+ try:
1023
+ if sage.misc.verbose.get_verbose() > 0:
1024
+ set_stderr = None
1025
+ else:
1026
+ set_stderr = PIPE
1027
+ sage.misc.verbose.verbose("Executing '%s'" % cmd,level=1)
1028
+ sage.misc.verbose.verbose("\n---- ffmpeg output below ----\n")
1029
+ check_call(cmd, shell=True, stderr=set_stderr)
1030
+ if show_path:
1031
+ print("Animation saved to file %s." % savefile)
1032
+ except (CalledProcessError, OSError):
1033
+ print("Error running ffmpeg.")
1034
+ raise
1035
+
1036
+ def apng(self, savefile=None, show_path=False, delay=20, iterations=0):
1037
+ r"""
1038
+ Create an animated PNG composed from rendering the graphics
1039
+ objects in ``self``. Return the absolute path to that file.
1040
+
1041
+ Notice that not all web browsers are capable of displaying APNG
1042
+ files, though they should still present the first frame of the
1043
+ animation as a fallback.
1044
+
1045
+ The generated file is not optimized, so it may be quite large.
1046
+
1047
+ Input:
1048
+
1049
+ - ``delay`` -- (default: 20) delay in hundredths of a
1050
+ second between frames
1051
+
1052
+ - ``savefile`` -- file that the animated gif gets saved
1053
+ to
1054
+
1055
+ - ``iterations`` -- integer (default: 0); number of
1056
+ iterations of animation. If 0, loop forever.
1057
+
1058
+ - ``show_path`` -- boolean (default: ``False``); if True,
1059
+ print the path to the saved file
1060
+
1061
+ EXAMPLES::
1062
+
1063
+ sage: x = SR.var("x")
1064
+ sage: a = animate([sin(x + float(k))
1065
+ ....: for k in srange(0,2*pi,0.7)],
1066
+ ....: xmin=0, xmax=2*pi, figsize=[2,1])
1067
+ sage: dir = tmp_dir()
1068
+ sage: a.apng(show_path=True) # long time
1069
+ Animation saved to ....png.
1070
+ sage: a.apng(savefile=dir + 'my_animation.png', delay=35, iterations=3) # long time
1071
+ sage: a.apng(savefile=dir + 'my_animation.png', show_path=True) # long time
1072
+ Animation saved to .../my_animation.png.
1073
+
1074
+ If the individual frames have different sizes, an error will be raised::
1075
+
1076
+ sage: a = animate([plot(sin(x), (x, 0, k))
1077
+ ....: for k in range(1,4)],
1078
+ ....: ymin=-1, ymax=1, aspect_ratio=1, figsize=[2,1])
1079
+ sage: a.apng() # long time
1080
+ Traceback (most recent call last):
1081
+ ...
1082
+ ValueError: Chunk IHDR mismatch
1083
+
1084
+ TESTS::
1085
+
1086
+ sage: a = animate([])
1087
+ sage: a.apng(show_path=True)
1088
+ Animation saved to file ....png.
1089
+ """
1090
+ pngdir = self.png()
1091
+ if savefile is None:
1092
+ savefile = tmp_filename(ext='.png')
1093
+ with open(savefile, "wb") as out:
1094
+ apng = APngAssembler(
1095
+ out, len(self),
1096
+ delay=delay, num_plays=iterations)
1097
+ for i in range(len(self)):
1098
+ png = os.path.join(pngdir, "%08d.png" % i)
1099
+ apng.add_frame(png)
1100
+ if show_path:
1101
+ print("Animation saved to file %s." % savefile)
1102
+
1103
+ def save(self, filename=None, show_path=False, use_ffmpeg=False, **kwds):
1104
+ r"""
1105
+ Save this animation.
1106
+
1107
+ INPUT:
1108
+
1109
+ - ``filename`` -- (default: ``None``) name of save file
1110
+
1111
+ - ``show_path`` -- boolean (default: ``False``); if True,
1112
+ print the path to the saved file
1113
+
1114
+ - ``use_ffmpeg`` -- boolean (default: ``False``); if ``True``, use
1115
+ 'ffmpeg' by default instead of ImageMagick when creating GIF files
1116
+
1117
+ If filename is ``None``, then in notebook mode, display the
1118
+ animation; otherwise, save the animation to a GIF file. If
1119
+ filename ends in '.html', save an :meth:`interactive` version of
1120
+ the animation to an HTML file that uses the Three.js viewer. If
1121
+ filename ends in '.sobj', save to an sobj file. Otherwise,
1122
+ try to determine the format from the filename extension
1123
+ ('.mpg', '.gif', '.avi', etc.). If the format cannot be
1124
+ determined, default to GIF.
1125
+
1126
+ For GIF files, either ffmpeg or the ImageMagick suite must be
1127
+ installed. For other movie formats, ffmpeg must be installed.
1128
+ sobj and HTML files can be saved with no extra software installed.
1129
+
1130
+ EXAMPLES::
1131
+
1132
+ sage: x = SR.var("x")
1133
+ sage: a = animate([sin(x + float(k))
1134
+ ....: for k in srange(0, 2*pi, 0.7)],
1135
+ ....: xmin=0, xmax=2*pi, ymin=-1, ymax=1, figsize=[2,1])
1136
+ sage: td = tmp_dir()
1137
+ sage: a.save() # not tested
1138
+ sage: a.save(td + 'wave.gif') # long time # optional -- ImageMagick
1139
+ sage: a.save(td + 'wave.gif', show_path=True) # long time # optional -- ImageMagick
1140
+ Animation saved to file .../wave.gif.
1141
+ sage: a.save(td + 'wave.avi', show_path=True) # long time # optional -- FFmpeg
1142
+ Animation saved to file .../wave.avi.
1143
+ sage: a.save(td + 'wave0.sobj')
1144
+ sage: a.save(td + 'wave1.sobj', show_path=True)
1145
+ Animation saved to file .../wave1.sobj.
1146
+ sage: a.save(td + 'wave0.html', online=True)
1147
+ sage: a.save(td + 'wave1.html', show_path=True, online=True)
1148
+ Animation saved to file .../wave1.html.
1149
+
1150
+ TESTS:
1151
+
1152
+ Ensure that we can pass delay and iteration count to the saved
1153
+ GIF image (see :issue:`18176`)::
1154
+
1155
+ sage: # long time, optional -- ImageMagick
1156
+ sage: a.save(td + 'wave.gif')
1157
+ sage: with open(td + 'wave.gif', 'rb') as f:
1158
+ ....: print(b'GIF8' in f.read())
1159
+ True
1160
+ sage: with open(td + 'wave.gif', 'rb') as f:
1161
+ ....: print(b'!\xff\x0bNETSCAPE2.0\x03\x01\x00\x00\x00' in f.read())
1162
+ True
1163
+ sage: a.save(td + 'wave.gif', delay=35)
1164
+ sage: with open(td + 'wave.gif', 'rb') as f:
1165
+ ....: print(b'GIF8' in f.read())
1166
+ True
1167
+ sage: a.save(td + 'wave.gif', iterations=3)
1168
+ sage: with open(td + 'wave.gif', 'rb') as f:
1169
+ ....: print(b'!\xff\x0bNETSCAPE2.0\x03\x01\x00\x00\x00' in f.read())
1170
+ False
1171
+ sage: with open(td + 'wave.gif', 'rb') as f:
1172
+ ....: check1 = b'!\xff\x0bNETSCAPE2.0\x03\x01\x02\x00\x00'
1173
+ ....: check2 = b'!\xff\x0bNETSCAPE2.0\x03\x01\x03\x00\x00'
1174
+ ....: data = f.read()
1175
+ ....: print(check1 in data or check2 in data)
1176
+ True
1177
+ """
1178
+ if filename is None:
1179
+ suffix = '.gif'
1180
+ else:
1181
+ suffix = os.path.splitext(filename)[1]
1182
+ if not suffix:
1183
+ suffix = '.gif'
1184
+
1185
+ if filename is None or suffix == '.gif':
1186
+ self.gif(savefile=filename, show_path=show_path,
1187
+ use_ffmpeg=use_ffmpeg, **kwds)
1188
+ elif suffix == '.sobj':
1189
+ SageObject.save(self, filename)
1190
+ if show_path:
1191
+ print("Animation saved to file %s." % filename)
1192
+ elif suffix == '.html':
1193
+ self.interactive(**kwds).save(filename)
1194
+ if show_path:
1195
+ print("Animation saved to file %s." % filename)
1196
+ else:
1197
+ self.ffmpeg(savefile=filename, show_path=show_path, **kwds)
1198
+
1199
+ def interactive(self, **kwds):
1200
+ r"""
1201
+ Create an interactive depiction of the animation.
1202
+
1203
+ INPUT:
1204
+
1205
+ - ``**kwds`` -- any of the viewing options accepted by show() are valid
1206
+ as keyword arguments to this function and they will behave in the same
1207
+ way. Those that are animation-related and recognized by the Three.js
1208
+ viewer are: ``animate``, ``animation_controls``, ``auto_play``,
1209
+ ``delay``, and ``loop``.
1210
+
1211
+ OUTPUT: a 3D graphics object which, by default, will use the Three.js viewer
1212
+
1213
+ EXAMPLES::
1214
+
1215
+ sage: x = SR.var("x")
1216
+ sage: frames = [point3d((sin(x), cos(x), x))
1217
+ ....: for x in (0, pi/16, .., 2*pi)]
1218
+ sage: animate(frames).interactive(online=True)
1219
+ Graphics3d Object
1220
+
1221
+ Works with frames that are 2D or 3D graphics objects or convertible to
1222
+ 2D or 3D graphics objects via a ``plot`` or ``plot3d`` method::
1223
+
1224
+ sage: frames = [dodecahedron(), circle(center=(0, 0), radius=1), x^2]
1225
+ sage: animate(frames).interactive(online=True, delay=100)
1226
+ Graphics3d Object
1227
+
1228
+ .. SEEALSO::
1229
+
1230
+ :ref:`threejs_viewer`
1231
+ """
1232
+ from sage.plot.plot3d.base import Graphics3d, KeyframeAnimationGroup
1233
+ # Attempt to convert frames to Graphics3d objects.
1234
+ g3d_frames = []
1235
+ for i, frame in enumerate(self._frames):
1236
+ if not isinstance(frame, Graphics3d):
1237
+ try:
1238
+ frame = frame.plot3d()
1239
+ except (AttributeError, TypeError):
1240
+ try:
1241
+ frame = frame.plot().plot3d()
1242
+ except (AttributeError, TypeError):
1243
+ frame = None
1244
+ if not isinstance(frame, Graphics3d):
1245
+ raise TypeError(f"Could not convert frame {i} to Graphics3d")
1246
+ g3d_frames.append(frame)
1247
+ # Give preference to this method's keyword arguments over those provided
1248
+ # to animate or the constructor.
1249
+ kwds = dict(self._kwds, **kwds)
1250
+ # Three.js is the only viewer that supports animation at present.
1251
+ if 'viewer' not in kwds:
1252
+ kwds['viewer'] = 'threejs'
1253
+ return KeyframeAnimationGroup(g3d_frames, **kwds)
1254
+
1255
+
1256
+ class APngAssembler:
1257
+ r"""
1258
+ Build an APNG_ (Animated PNG) from a sequence of PNG files.
1259
+ This is used by the :meth:`sage.plot.animate.Animation.apng` method.
1260
+
1261
+ This code is quite simple; it does little more than copying chunks
1262
+ from input PNG files to the output file. There is no optimization
1263
+ involved. This does not depend on external programs or libraries.
1264
+
1265
+ INPUT:
1266
+
1267
+ - ``out`` -- a file opened for binary writing to which the data
1268
+ will be written
1269
+
1270
+ - ``num_frames`` -- the number of frames in the animation
1271
+
1272
+ - ``num_plays`` -- how often to iterate, 0 means infinitely
1273
+
1274
+ - ``delay`` -- numerator of the delay fraction in seconds
1275
+
1276
+ - ``delay_denominator`` -- denominator of the delay in seconds
1277
+
1278
+ EXAMPLES::
1279
+
1280
+ sage: from sage.plot.animate import APngAssembler
1281
+ sage: x = SR.var("x")
1282
+ sage: def assembleAPNG():
1283
+ ....: a = animate([sin(x + float(k)) for k in srange(0,2*pi,0.7)],
1284
+ ....: xmin=0, xmax=2*pi, figsize=[2,1])
1285
+ ....: pngdir = a.png()
1286
+ ....: outfile = sage.misc.temporary_file.tmp_filename(ext='.png')
1287
+ ....: with open(outfile, "wb") as f:
1288
+ ....: apng = APngAssembler(f, len(a))
1289
+ ....: for i in range(len(a)):
1290
+ ....: png = os.path.join(pngdir, "{:08d}.png".format(i))
1291
+ ....: apng.add_frame(png, delay=10*i + 10)
1292
+ ....: return outfile
1293
+ sage: assembleAPNG() # long time
1294
+ '...png'
1295
+ """
1296
+ magic = b"\x89PNG\x0d\x0a\x1a\x0a"
1297
+ mustmatch = frozenset([b"IHDR", b"PLTE", b"bKGD", b"cHRM", b"gAMA",
1298
+ b"pHYs", b"sBIT", b"tRNS"])
1299
+
1300
+ def __init__(self, out, num_frames,
1301
+ num_plays=0, delay=200, delay_denominator=100):
1302
+ r"""
1303
+ Initialize for creation of an APNG file.
1304
+ """
1305
+ self._last_seqno = -1
1306
+ self._idx = 0
1307
+ self._first = True
1308
+ self.out = out
1309
+ self.num_frames = num_frames
1310
+ self.num_plays = num_plays
1311
+ self.default_delay_numerator = delay
1312
+ self.default_delay_denominator = delay_denominator
1313
+ self._matchref = {}
1314
+ self.out.write(self.magic)
1315
+
1316
+ def add_frame(self, pngfile, delay=None, delay_denominator=None):
1317
+ r"""
1318
+ Add a single frame to the APNG file.
1319
+
1320
+ INPUT:
1321
+
1322
+ - ``pngfile`` -- file name of the PNG file with data for this frame
1323
+
1324
+ - ``delay`` -- numerator of the delay fraction in seconds
1325
+
1326
+ - ``delay_denominator`` -- denominator of the delay in seconds
1327
+
1328
+ If the delay is not specified, the default from the constructor
1329
+ applies.
1330
+
1331
+ TESTS::
1332
+
1333
+ sage: from sage.plot.animate import APngAssembler
1334
+ sage: from io import BytesIO
1335
+ sage: buf = BytesIO()
1336
+ sage: apng = APngAssembler(buf, 2)
1337
+ sage: fn = APngAssembler._testData("input1", True)
1338
+ sage: apng.add_frame(fn, delay=0x567, delay_denominator=0x1234)
1339
+ sage: fn = APngAssembler._testData("input2", True)
1340
+ sage: apng.add_frame(fn)
1341
+ sage: len(buf.getvalue())
1342
+ 217
1343
+ sage: buf.getvalue() == APngAssembler._testData("anim12", False)
1344
+ True
1345
+ sage: apng.add_frame(fn)
1346
+ Traceback (most recent call last):
1347
+ ...
1348
+ RuntimeError: Already reached the declared number of frames
1349
+ """
1350
+ if self._idx == self.num_frames:
1351
+ raise RuntimeError("Already reached the declared number of frames")
1352
+ self.delay_numerator = self.default_delay_numerator
1353
+ self.delay_denominator = self.default_delay_denominator
1354
+ self._actl_written = False
1355
+ self._fctl_written = False
1356
+ if delay is not None:
1357
+ self.delay_numerator = delay
1358
+ if delay_denominator is not None:
1359
+ self.delay_denominator = delay_denominator
1360
+ self._add_png(pngfile)
1361
+ self._idx += 1
1362
+ if self._idx == self.num_frames:
1363
+ self._chunk(b"IEND", b"")
1364
+
1365
+ def set_default(self, pngfile):
1366
+ r"""
1367
+ Add a default image for the APNG file.
1368
+
1369
+ This image is used as a fallback in case some application does
1370
+ not understand the APNG format. This method must be called
1371
+ prior to any calls to the ``add_frame`` method, if it is called
1372
+ at all. If it is not called, then the first frame of the
1373
+ animation will be the default.
1374
+
1375
+ INPUT:
1376
+
1377
+ - ``pngfile`` -- file name of the PNG file with data
1378
+ for the default image
1379
+
1380
+ TESTS::
1381
+
1382
+ sage: from sage.plot.animate import APngAssembler
1383
+ sage: from io import BytesIO
1384
+ sage: buf = BytesIO()
1385
+ sage: apng = APngAssembler(buf, 1)
1386
+ sage: fn = APngAssembler._testData("input1", True)
1387
+ sage: apng.set_default(fn)
1388
+ sage: fn = APngAssembler._testData("input2", True)
1389
+ sage: apng.add_frame(fn, delay=0x567, delay_denominator=0x1234)
1390
+ sage: len(buf.getvalue())
1391
+ 179
1392
+ sage: buf.getvalue() == APngAssembler._testData("still1anim2", False)
1393
+ True
1394
+ sage: apng.add_frame(fn)
1395
+ Traceback (most recent call last):
1396
+ ...
1397
+ RuntimeError: Already reached the declared number of frames
1398
+ """
1399
+ if self._idx != 0:
1400
+ raise RuntimeError("Default image must precede all animation frames")
1401
+ self._actl_written = False
1402
+ self._fctl_written = True
1403
+ self._add_png(pngfile)
1404
+
1405
+ def _add_png(self, pngfile):
1406
+ r"""
1407
+ Add data from one PNG still image.
1408
+
1409
+ TESTS::
1410
+
1411
+ sage: from sage.plot.animate import APngAssembler
1412
+ sage: APngAssembler._testCase1("_add_png", reads=False)
1413
+ enter _add_png('...png')
1414
+ write _current_chunk = (...'\x00\x00\x00\r',...'IHDR',...'\x00\x00\x00\x03\x00\x00\x00\x02\x08\x00\x00\x00\x00',...'\xb8\x1f9\xc6')
1415
+ call _copy() -> None
1416
+ call _first_IHDR(...'\x00\x00\x00\x03\x00\x00\x00\x02\x08\x00\x00\x00\x00') -> None
1417
+ write _current_chunk = (...'\x00\x00\x00\x04',...'gAMA',...'\x00\x01\x86\xa0',...'1\xe8\x96_')
1418
+ call _copy() -> None
1419
+ write _current_chunk = (...'\x00\x00\x00\x07',...'tIME',...'\x07\xde\x06\x1b\x0b&$',...'\x1f0z\xd5')
1420
+ write _current_chunk = (...'\x00\x00\x00\x08',...'IDAT',...'img1data',...'\xce\x8aI\x99')
1421
+ call _first_IDAT(...'img1data') -> None
1422
+ write _current_chunk = (...'\x00\x00\x00\x00',...'IEND',...'',...'\xaeB`\x82')
1423
+ write _first = False
1424
+ exit _add_png -> None
1425
+ enter _add_png('...png')
1426
+ write _current_chunk = (...'\x00\x00\x00\r',...'IHDR',...'\x00\x00\x00\x03\x00\x00\x00\x02\x08\x00\x00\x00\x00',...'\xb8\x1f9\xc6')
1427
+ write _current_chunk = (...'\x00\x00\x00\x04',...'gAMA',...'\x00\x01\x86\xa0',...'1\xe8\x96_')
1428
+ write _current_chunk = (...'\x00\x00\x00\x04',...'IDAT',...'img2',...'\x0ei\xab\x1d')
1429
+ call _next_IDAT(...'img2') -> None
1430
+ write _current_chunk = (...'\x00\x00\x00\x04',...'IDAT',...'data',...'f\x94\xcbx')
1431
+ call _next_IDAT(...'data') -> None
1432
+ write _current_chunk = (...'\x00\x00\x00\x00',...'IEND',...'',...'\xaeB`\x82')
1433
+ write _first = False
1434
+ exit _add_png -> None
1435
+ """
1436
+ with open(pngfile, 'rb') as png:
1437
+ if png.read(8) != self.magic:
1438
+ raise ValueError(f"{pngfile} is not a PNG file")
1439
+ while True:
1440
+ chead = png.read(8)
1441
+ if len(chead) == 0:
1442
+ break
1443
+ clen, ctype = struct.unpack(">L4s", chead)
1444
+ cdata = png.read(clen)
1445
+ ccrc = png.read(4)
1446
+ utype = ctype.decode("ascii")
1447
+ self._current_chunk = (chead[:4], ctype, cdata, ccrc)
1448
+ if ctype in self.mustmatch:
1449
+ ref = self._matchref.get(ctype)
1450
+ if ref is None:
1451
+ self._matchref[ctype] = cdata
1452
+ self._copy()
1453
+ else:
1454
+ if cdata != ref:
1455
+ raise ValueError(f"Chunk {utype} mismatch")
1456
+ met = ("_first_" if self._first else "_next_") + utype
1457
+ try:
1458
+ met = getattr(self, met)
1459
+ except AttributeError:
1460
+ pass
1461
+ else:
1462
+ met(cdata)
1463
+ self._first = False
1464
+
1465
+ def _seqno(self):
1466
+ r"""
1467
+ Generate next sequence number.
1468
+
1469
+ TESTS::
1470
+
1471
+ sage: from sage.plot.animate import APngAssembler
1472
+ sage: from io import BytesIO
1473
+ sage: buf = BytesIO()
1474
+ sage: apng = APngAssembler(buf, 1)
1475
+ sage: apng._seqno() == b'\x00\x00\x00\x00'
1476
+ True
1477
+ sage: apng._seqno() == b'\x00\x00\x00\x01'
1478
+ True
1479
+ sage: apng._seqno() == b'\x00\x00\x00\x02'
1480
+ True
1481
+ """
1482
+ self._last_seqno += 1
1483
+ return struct.pack(">L", self._last_seqno)
1484
+
1485
+ def _first_IHDR(self, data):
1486
+ r"""
1487
+ Remember image size.
1488
+
1489
+ TESTS::
1490
+
1491
+ sage: from sage.plot.animate import APngAssembler
1492
+ sage: APngAssembler._testCase1("_first_IHDR")
1493
+ enter _first_IHDR(...'\x00\x00\x00\x03\x00\x00\x00\x02\x08\x00\x00\x00\x00')
1494
+ write width = 3
1495
+ write height = 2
1496
+ exit _first_IHDR -> None
1497
+ """
1498
+ w, h, d, ctype, comp, filt, ilace = struct.unpack(">2L5B", data)
1499
+ self.width = w
1500
+ self.height = h
1501
+
1502
+ def _first_IDAT(self, data):
1503
+ r"""
1504
+ Write acTL and fcTL, then copy as IDAT.
1505
+
1506
+ TESTS::
1507
+
1508
+ sage: from sage.plot.animate import APngAssembler
1509
+ sage: APngAssembler._testCase1("_first_IDAT")
1510
+ enter _first_IDAT(...'img1data')
1511
+ call _actl() -> None
1512
+ call _fctl() -> None
1513
+ call _copy() -> None
1514
+ exit _first_IDAT -> None
1515
+ """
1516
+ self._actl()
1517
+ self._fctl()
1518
+ self._copy()
1519
+
1520
+ def _next_IDAT(self, data):
1521
+ r"""
1522
+ Write fcTL, then convert to fdAT.
1523
+
1524
+ TESTS::
1525
+
1526
+ sage: from sage.plot.animate import APngAssembler
1527
+ sage: APngAssembler._testCase1("_next_IDAT")
1528
+ enter _next_IDAT(...'img2')
1529
+ call _fctl() -> None
1530
+ call _seqno() -> ...'\x00\x00\x00\x02'
1531
+ call _chunk(...'fdAT', ...'\x00\x00\x00\x02img2') -> None
1532
+ exit _next_IDAT -> None
1533
+ enter _next_IDAT(...'data')
1534
+ call _fctl() -> None
1535
+ call _seqno() -> ...'\x00\x00\x00\x03'
1536
+ call _chunk(...'fdAT', ...'\x00\x00\x00\x03data') -> None
1537
+ exit _next_IDAT -> None
1538
+ """
1539
+ self._fctl()
1540
+ maxlen = 0x7ffffffb
1541
+ while len(data) > maxlen:
1542
+ self._chunk(b"fdAT", self._seqno() + data[:maxlen])
1543
+ data = data[maxlen:]
1544
+ self._chunk(b"fdAT", self._seqno() + data)
1545
+
1546
+ def _copy(self):
1547
+ r"""
1548
+ Copy an existing chunk without modification.
1549
+
1550
+ TESTS::
1551
+
1552
+ sage: from sage.plot.animate import APngAssembler
1553
+ sage: APngAssembler._testCase1("_copy")
1554
+ enter _copy()
1555
+ read _current_chunk = (...'\x00\x00\x00\r',...'IHDR',...'\x00\x00\x00\x03\x00\x00\x00\x02\x08\x00\x00\x00\x00',...'\xb8\x1f9\xc6')
1556
+ read out = <_io.BytesIO... at ...
1557
+ read out = <_io.BytesIO... at ...
1558
+ read out = <_io.BytesIO... at ...
1559
+ read out = <_io.BytesIO... at ...
1560
+ exit _copy -> None
1561
+ enter _copy()
1562
+ read _current_chunk = (...'\x00\x00\x00\x04',...'gAMA',...'\x00\x01\x86\xa0',...'1\xe8\x96_')
1563
+ ...
1564
+ read _current_chunk = (...'\x00\x00\x00\x08',...'IDAT',...'img1data',...'\xce\x8aI\x99')
1565
+ ...
1566
+ exit _copy -> None
1567
+ """
1568
+ for d in self._current_chunk:
1569
+ self.out.write(d)
1570
+
1571
+ def _actl(self):
1572
+ r"""
1573
+ Write animation control data (acTL).
1574
+
1575
+ TESTS::
1576
+
1577
+ sage: from sage.plot.animate import APngAssembler
1578
+ sage: APngAssembler._testCase1("_actl")
1579
+ enter _actl()
1580
+ read _actl_written = False
1581
+ read num_frames = 2
1582
+ read num_plays = 0
1583
+ call _chunk(...'acTL',...'\x00\x00\x00\x02\x00\x00\x00\x00') -> None
1584
+ write _actl_written = True
1585
+ exit _actl -> None
1586
+ """
1587
+ if self._actl_written:
1588
+ return
1589
+ data = struct.pack(">2L", self.num_frames, self.num_plays)
1590
+ self._chunk(b"acTL", data)
1591
+ self._actl_written = True
1592
+
1593
+ def _fctl(self):
1594
+ r"""
1595
+ Write frame control data (fcTL).
1596
+
1597
+ TESTS::
1598
+
1599
+ sage: from sage.plot.animate import APngAssembler
1600
+ sage: APngAssembler._testCase1("_fctl")
1601
+ enter _fctl()
1602
+ read _fctl_written = False
1603
+ read width = 3
1604
+ read height = 2
1605
+ read delay_numerator = 1383
1606
+ read delay_denominator = 4660
1607
+ call _seqno() -> ...'\x00\x00\x00\x00'
1608
+ call _chunk(...'fcTL', ...'\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x05g\x124\x01\x00') -> None
1609
+ write _fctl_written = True
1610
+ exit _fctl -> None
1611
+ enter _fctl()
1612
+ read _fctl_written = False
1613
+ read width = 3
1614
+ read height = 2
1615
+ read delay_numerator = 200
1616
+ read delay_denominator = 100
1617
+ call _seqno() -> ...'\x00\x00\x00\x01'
1618
+ call _chunk(...'fcTL', ...'\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc8\x00d\x01\x00') -> None
1619
+ write _fctl_written = True
1620
+ exit _fctl -> None
1621
+ enter _fctl()
1622
+ read _fctl_written = True
1623
+ exit _fctl -> None
1624
+ """
1625
+ if self._fctl_written:
1626
+ return
1627
+ data = struct.pack(
1628
+ ">4L2H2B",
1629
+ self.width, self.height, 0, 0,
1630
+ self.delay_numerator, self.delay_denominator,
1631
+ 1, 0)
1632
+ self._chunk(b"fcTL", self._seqno() + data)
1633
+ self._fctl_written = True
1634
+
1635
+ def _chunk(self, ctype, cdata):
1636
+ r"""
1637
+ Write a new (or modified) chunk of data.
1638
+
1639
+ TESTS::
1640
+
1641
+ sage: from sage.plot.animate import APngAssembler
1642
+ sage: from io import BytesIO
1643
+ sage: buf = BytesIO()
1644
+ sage: apng = APngAssembler(buf, 1)
1645
+ sage: buf.getvalue() == b'\x89PNG\r\n\x1a\n'
1646
+ True
1647
+ sage: apng._chunk(b"abcd", b"efgh")
1648
+ sage: buf.getvalue() == b'\x89PNG\r\n\x1a\n\x00\x00\x00\x04abcdefgh\xae\xef*P'
1649
+ True
1650
+ """
1651
+ ccrc = struct.pack(">L", zlib.crc32(ctype + cdata) & 0xffffffff)
1652
+ clen = struct.pack(">L", len(cdata))
1653
+ for d in [clen, ctype, cdata, ccrc]:
1654
+ self.out.write(d)
1655
+
1656
+ @classmethod
1657
+ def _hex2bin(cls, h):
1658
+ r"""
1659
+ Convert hex data to binary.
1660
+
1661
+ This is a helper method used for testing.
1662
+ Most data is given as lower-case hex digits,
1663
+ possibly intermixed with whitespace.
1664
+ A dot causes the next four bytes to be copied verbatim
1665
+ even if they look like hex digits. This is used for chunk types.
1666
+ Other characters which are not hex digits are passed verbatim.
1667
+
1668
+ EXAMPLES::
1669
+
1670
+ sage: from sage.plot.animate import APngAssembler
1671
+ sage: h2b = APngAssembler._hex2bin
1672
+ sage: h2b("0123") == b"\x01\x23"
1673
+ True
1674
+ sage: h2b(" 01 \n 23 ") == b"\x01\x23"
1675
+ True
1676
+ sage: h2b(".abcdef") == b"abcd\xef"
1677
+ True
1678
+ sage: h2b("PNG") == b"PNG"
1679
+ True
1680
+ """
1681
+ b = []
1682
+ while h:
1683
+ if h[0] in ' \n': # ignore whitespace
1684
+ h = h[1:]
1685
+ elif h[0] in '0123456789abcdef': # hex byte
1686
+ b.append(int(h[:2], 16))
1687
+ h = h[2:]
1688
+ elif h[0] == '.': # for chunk type
1689
+ b.extend(ord(h[i]) for i in range(1, 5))
1690
+ h = h[5:]
1691
+ else: # for PNG magic
1692
+ b.append(ord(h[0]))
1693
+ h = h[1:]
1694
+
1695
+ return bytes(b)
1696
+
1697
+ @classmethod
1698
+ def _testData(cls, name, asFile):
1699
+ r"""
1700
+ Retrieve data for test cases.
1701
+
1702
+ INPUT:
1703
+
1704
+ - ``name`` -- the name of the file content.
1705
+
1706
+ - ``asFile`` -- whether to return a binary string of the named data
1707
+ or the path of a file containing that data
1708
+
1709
+ EXAMPLES::
1710
+
1711
+ sage: from sage.plot.animate import APngAssembler
1712
+ sage: APngAssembler._testData("input1", False).startswith(b'\x89PNG')
1713
+ True
1714
+ sage: APngAssembler._testData("input2", True)
1715
+ '...png'
1716
+ """
1717
+ data = {
1718
+
1719
+ # Input 1: one PNG image, except the data makes no real sense
1720
+ "input1": """89 PNG 0d0a1a0a
1721
+ 0000000d.IHDR 00000003000000020800000000 b81f39c6
1722
+ 00000004.gAMA 000186a0 31e8965f
1723
+ 00000007.tIME 07de061b0b2624 1f307ad5
1724
+ 00000008.IDAT 696d673164617461 ce8a4999
1725
+ 00000000.IEND ae426082""",
1726
+
1727
+ # Input 2: slightly different, data in two chunks
1728
+ "input2": """89 PNG 0d0a1a0a
1729
+ 0000000d.IHDR 00000003000000020800000000 b81f39c6
1730
+ 00000004.gAMA 000186a0 31e8965f
1731
+ 00000004.IDAT 696d6732 0e69ab1d
1732
+ 00000004.IDAT 64617461 6694cb78
1733
+ 00000000.IEND ae426082""",
1734
+
1735
+ # Expected output 1: both images as frames of an animation
1736
+ "anim12": """89 PNG 0d0a1a0a
1737
+ 0000000d.IHDR 00000003000000020800000000 b81f39c6
1738
+ 00000004.gAMA 000186a0 31e8965f
1739
+ 00000008.acTL 0000000200000000 f38d9370
1740
+ 0000001a.fcTL 000000000000000300000002
1741
+ 0000000000000000056712340100 b4f729c9
1742
+ 00000008.IDAT 696d673164617461 ce8a4999
1743
+ 0000001a.fcTL 000000010000000300000002
1744
+ 000000000000000000c800640100 1b92eb4d
1745
+ 00000008.fdAT 00000002696d6732 9cfb89a3
1746
+ 00000008.fdAT 0000000364617461 c966c076
1747
+ 00000000.IEND ae426082""",
1748
+
1749
+ # Expected output 2: first image as fallback, second as animation
1750
+ "still1anim2": """89 PNG 0d0a1a0a
1751
+ 0000000d.IHDR 00000003000000020800000000 b81f39c6
1752
+ 00000004.gAMA 000186a0 31e8965f
1753
+ 00000008.acTL 0000000100000000 b42de9a0
1754
+ 00000008.IDAT 696d673164617461 ce8a4999
1755
+ 0000001a.fcTL 000000000000000300000002
1756
+ 0000000000000000056712340100 b4f729c9
1757
+ 00000008.fdAT 00000001696d6732 db5bf373
1758
+ 00000008.fdAT 0000000264617461 f406e9c6
1759
+ 00000000.IEND ae426082""",
1760
+
1761
+ }
1762
+ d = cls._hex2bin(data[name])
1763
+ if asFile:
1764
+ from sage.misc.temporary_file import tmp_filename
1765
+ fn = tmp_filename(ext='.png')
1766
+ with open(fn, 'wb') as f:
1767
+ f.write(d)
1768
+ return fn
1769
+ return d
1770
+
1771
+ @classmethod
1772
+ def _testCase1(cls, methodToTrace=None, **kwds):
1773
+ r"""
1774
+ Run common test case.
1775
+
1776
+ This test case is one animation of two frames.
1777
+ The named method (if not None) will be traced during execution.
1778
+ This will demonstrate the role of each method in the doctests.
1779
+
1780
+ TESTS::
1781
+
1782
+ sage: from sage.plot.animate import APngAssembler
1783
+ sage: APngAssembler._testCase1()
1784
+ """
1785
+ from sage.doctest.fixtures import trace_method
1786
+ from io import BytesIO
1787
+ buf = BytesIO()
1788
+ apng = cls(buf, 2)
1789
+ if methodToTrace is not None:
1790
+ trace_method(apng, methodToTrace, **kwds)
1791
+ apng.add_frame(cls._testData("input1", True),
1792
+ delay=0x567, delay_denominator=0x1234)
1793
+ apng.add_frame(cls._testData("input2", True))
1794
+ out = buf.getvalue()
1795
+ assert len(out) == 217
1796
+ assert out == cls._testData("anim12", False)