Skip to content

Commit

Permalink
Backport PR matplotlib#29024: Fix saving animations to transparent fo…
Browse files Browse the repository at this point in the history
…rmats
  • Loading branch information
tacaswell authored and meeseeksmachine committed Nov 1, 2024
1 parent 0e44670 commit d9d9528
Showing 1 changed file with 47 additions and 10 deletions.
57 changes: 47 additions & 10 deletions lib/matplotlib/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ def frame_size(self):
w, h = self.fig.get_size_inches()
return int(w * self.dpi), int(h * self.dpi)

def _supports_transparency(self):
"""
Whether this writer supports transparency.
Writers may consult output file type and codec to determine this at runtime.
"""
return False

@abc.abstractmethod
def grab_frame(self, **savefig_kwargs):
"""
Expand Down Expand Up @@ -468,6 +476,9 @@ def finish(self):

@writers.register('pillow')
class PillowWriter(AbstractMovieWriter):
def _supports_transparency(self):
return True

@classmethod
def isAvailable(cls):
return True
Expand Down Expand Up @@ -503,11 +514,26 @@ class FFMpegBase:
_exec_key = 'animation.ffmpeg_path'
_args_key = 'animation.ffmpeg_args'

def _supports_transparency(self):
suffix = Path(self.outfile).suffix
if suffix in {'.apng', '.avif', '.gif', '.webm', '.webp'}:
return True
# This list was found by going through `ffmpeg -codecs` for video encoders,
# running them with _support_transparency() forced to True, and checking that
# the "Pixel format" in Kdenlive included alpha. Note this is not a guarantee
# that transparency will work; you may also need to pass `-pix_fmt`, but we
# trust the user has done so if they are asking for these formats.
return self.codec in {
'apng', 'avrp', 'bmp', 'cfhd', 'dpx', 'ffv1', 'ffvhuff', 'gif', 'huffyuv',
'jpeg2000', 'ljpeg', 'png', 'prores', 'prores_aw', 'prores_ks', 'qtrle',
'rawvideo', 'targa', 'tiff', 'utvideo', 'v408', }

@property
def output_args(self):
args = []
if Path(self.outfile).suffix == '.gif':
self.codec = 'gif'
suffix = Path(self.outfile).suffix
if suffix in {'.apng', '.avif', '.gif', '.webm', '.webp'}:
self.codec = suffix[1:]
else:
args.extend(['-vcodec', self.codec])
extra_args = (self.extra_args if self.extra_args is not None
Expand All @@ -518,11 +544,17 @@ def output_args(self):
# macOS). Also fixes internet explorer. This is as of 2015/10/29.
if self.codec == 'h264' and '-pix_fmt' not in extra_args:
args.extend(['-pix_fmt', 'yuv420p'])
# For GIF, we're telling FFMPEG to split the video stream, to generate
# For GIF, we're telling FFmpeg to split the video stream, to generate
# a palette, and then use it for encoding.
elif self.codec == 'gif' and '-filter_complex' not in extra_args:
args.extend(['-filter_complex',
'split [a][b];[a] palettegen [p];[b][p] paletteuse'])
# For AVIF, we're telling FFmpeg to split the video stream, extract the alpha,
# in order to place it in a secondary stream, as needed by AVIF-in-FFmpeg.
elif self.codec == 'avif' and '-filter_complex' not in extra_args:
args.extend(['-filter_complex',
'split [rgb][rgba]; [rgba] alphaextract [alpha]',
'-map', '[rgb]', '-map', '[alpha]'])
if self.bitrate > 0:
args.extend(['-b', '%dk' % self.bitrate]) # %dk: bitrate in kbps.
for k, v in self.metadata.items():
Expand Down Expand Up @@ -610,6 +642,10 @@ class ImageMagickBase:
_exec_key = 'animation.convert_path'
_args_key = 'animation.convert_args'

def _supports_transparency(self):
suffix = Path(self.outfile).suffix
return suffix in {'.apng', '.avif', '.gif', '.webm', '.webp'}

def _args(self):
# ImageMagick does not recognize "raw".
fmt = "rgba" if self.frame_format == "raw" else self.frame_format
Expand Down Expand Up @@ -1045,22 +1081,23 @@ def func(current_frame: int, total_frames: int) -> Any
# since GUI widgets are gone. Either need to remove extra code to
# allow for this non-existent use case or find a way to make it work.

facecolor = savefig_kwargs.get('facecolor',
mpl.rcParams['savefig.facecolor'])
if facecolor == 'auto':
facecolor = self._fig.get_facecolor()

def _pre_composite_to_white(color):
r, g, b, a = mcolors.to_rgba(color)
return a * np.array([r, g, b]) + 1 - a

savefig_kwargs['facecolor'] = _pre_composite_to_white(facecolor)
savefig_kwargs['transparent'] = False # just to be safe!
# canvas._is_saving = True makes the draw_event animation-starting
# callback a no-op; canvas.manager = None prevents resizing the GUI
# widget (both are likewise done in savefig()).
with (writer.saving(self._fig, filename, dpi),
cbook._setattr_cm(self._fig.canvas, _is_saving=True, manager=None)):
if not writer._supports_transparency():
facecolor = savefig_kwargs.get('facecolor',
mpl.rcParams['savefig.facecolor'])
if facecolor == 'auto':
facecolor = self._fig.get_facecolor()
savefig_kwargs['facecolor'] = _pre_composite_to_white(facecolor)
savefig_kwargs['transparent'] = False # just to be safe!

for anim in all_anim:
anim._init_draw() # Clear the initial frame
frame_number = 0
Expand Down

0 comments on commit d9d9528

Please sign in to comment.