diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index a0236a6956cb..47f2f0f9515b 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -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): """ @@ -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 @@ -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 @@ -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(): @@ -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 @@ -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