Skip to content

Fix saving animations to transparent formats #29024

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 1, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@

@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 @@
_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 @@
# 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',

Check warning on line 555 in lib/matplotlib/animation.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/animation.py#L555

Added line #L555 was not covered by tests
'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 @@
_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 @@
# 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
Loading