Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 6a8211e

Browse filesBrowse files
authored
Merge pull request #29024 from QuLogic/animation-fixes
Fix saving animations to transparent formats
2 parents 8f3b029 + 3fa8b10 commit 6a8211e
Copy full SHA for 6a8211e

File tree

Expand file treeCollapse file tree

1 file changed

+47
-10
lines changed
Filter options
Expand file treeCollapse file tree

1 file changed

+47
-10
lines changed

‎lib/matplotlib/animation.py

Copy file name to clipboardExpand all lines: lib/matplotlib/animation.py
+47-10Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ def frame_size(self):
178178
w, h = self.fig.get_size_inches()
179179
return int(w * self.dpi), int(h * self.dpi)
180180

181+
def _supports_transparency(self):
182+
"""
183+
Whether this writer supports transparency.
184+
185+
Writers may consult output file type and codec to determine this at runtime.
186+
"""
187+
return False
188+
181189
@abc.abstractmethod
182190
def grab_frame(self, **savefig_kwargs):
183191
"""
@@ -468,6 +476,9 @@ def finish(self):
468476

469477
@writers.register('pillow')
470478
class PillowWriter(AbstractMovieWriter):
479+
def _supports_transparency(self):
480+
return True
481+
471482
@classmethod
472483
def isAvailable(cls):
473484
return True
@@ -503,11 +514,26 @@ class FFMpegBase:
503514
_exec_key = 'animation.ffmpeg_path'
504515
_args_key = 'animation.ffmpeg_args'
505516

517+
def _supports_transparency(self):
518+
suffix = Path(self.outfile).suffix
519+
if suffix in {'.apng', '.avif', '.gif', '.webm', '.webp'}:
520+
return True
521+
# This list was found by going through `ffmpeg -codecs` for video encoders,
522+
# running them with _support_transparency() forced to True, and checking that
523+
# the "Pixel format" in Kdenlive included alpha. Note this is not a guarantee
524+
# that transparency will work; you may also need to pass `-pix_fmt`, but we
525+
# trust the user has done so if they are asking for these formats.
526+
return self.codec in {
527+
'apng', 'avrp', 'bmp', 'cfhd', 'dpx', 'ffv1', 'ffvhuff', 'gif', 'huffyuv',
528+
'jpeg2000', 'ljpeg', 'png', 'prores', 'prores_aw', 'prores_ks', 'qtrle',
529+
'rawvideo', 'targa', 'tiff', 'utvideo', 'v408', }
530+
506531
@property
507532
def output_args(self):
508533
args = []
509-
if Path(self.outfile).suffix == '.gif':
510-
self.codec = 'gif'
534+
suffix = Path(self.outfile).suffix
535+
if suffix in {'.apng', '.avif', '.gif', '.webm', '.webp'}:
536+
self.codec = suffix[1:]
511537
else:
512538
args.extend(['-vcodec', self.codec])
513539
extra_args = (self.extra_args if self.extra_args is not None
@@ -518,11 +544,17 @@ def output_args(self):
518544
# macOS). Also fixes internet explorer. This is as of 2015/10/29.
519545
if self.codec == 'h264' and '-pix_fmt' not in extra_args:
520546
args.extend(['-pix_fmt', 'yuv420p'])
521-
# For GIF, we're telling FFMPEG to split the video stream, to generate
547+
# For GIF, we're telling FFmpeg to split the video stream, to generate
522548
# a palette, and then use it for encoding.
523549
elif self.codec == 'gif' and '-filter_complex' not in extra_args:
524550
args.extend(['-filter_complex',
525551
'split [a][b];[a] palettegen [p];[b][p] paletteuse'])
552+
# For AVIF, we're telling FFmpeg to split the video stream, extract the alpha,
553+
# in order to place it in a secondary stream, as needed by AVIF-in-FFmpeg.
554+
elif self.codec == 'avif' and '-filter_complex' not in extra_args:
555+
args.extend(['-filter_complex',
556+
'split [rgb][rgba]; [rgba] alphaextract [alpha]',
557+
'-map', '[rgb]', '-map', '[alpha]'])
526558
if self.bitrate > 0:
527559
args.extend(['-b', '%dk' % self.bitrate]) # %dk: bitrate in kbps.
528560
for k, v in self.metadata.items():
@@ -610,6 +642,10 @@ class ImageMagickBase:
610642
_exec_key = 'animation.convert_path'
611643
_args_key = 'animation.convert_args'
612644

645+
def _supports_transparency(self):
646+
suffix = Path(self.outfile).suffix
647+
return suffix in {'.apng', '.avif', '.gif', '.webm', '.webp'}
648+
613649
def _args(self):
614650
# ImageMagick does not recognize "raw".
615651
fmt = "rgba" if self.frame_format == "raw" else self.frame_format
@@ -1045,22 +1081,23 @@ def func(current_frame: int, total_frames: int) -> Any
10451081
# since GUI widgets are gone. Either need to remove extra code to
10461082
# allow for this non-existent use case or find a way to make it work.
10471083

1048-
facecolor = savefig_kwargs.get('facecolor',
1049-
mpl.rcParams['savefig.facecolor'])
1050-
if facecolor == 'auto':
1051-
facecolor = self._fig.get_facecolor()
1052-
10531084
def _pre_composite_to_white(color):
10541085
r, g, b, a = mcolors.to_rgba(color)
10551086
return a * np.array([r, g, b]) + 1 - a
10561087

1057-
savefig_kwargs['facecolor'] = _pre_composite_to_white(facecolor)
1058-
savefig_kwargs['transparent'] = False # just to be safe!
10591088
# canvas._is_saving = True makes the draw_event animation-starting
10601089
# callback a no-op; canvas.manager = None prevents resizing the GUI
10611090
# widget (both are likewise done in savefig()).
10621091
with (writer.saving(self._fig, filename, dpi),
10631092
cbook._setattr_cm(self._fig.canvas, _is_saving=True, manager=None)):
1093+
if not writer._supports_transparency():
1094+
facecolor = savefig_kwargs.get('facecolor',
1095+
mpl.rcParams['savefig.facecolor'])
1096+
if facecolor == 'auto':
1097+
facecolor = self._fig.get_facecolor()
1098+
savefig_kwargs['facecolor'] = _pre_composite_to_white(facecolor)
1099+
savefig_kwargs['transparent'] = False # just to be safe!
1100+
10641101
for anim in all_anim:
10651102
anim._init_draw() # Clear the initial frame
10661103
frame_number = 0

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.