Skip to content

Commit e4c274f

Browse files
committed
anim: Only pre-composite formats that are opaque
On video formats like `.mp4`, it makes sense to do our own pre-compositing to white. The animation backends would otherwise do it themselves, and tend to just make semi-transparent pixels either fully transparent or fully opaque before placing on white. So doing it ourselves corrects that error. However, formts like `.gif`, or `.webm` _do_ support transparency, and we shouldn't do the compositing then. Fixes matplotlib#27173
1 parent 93fd6a7 commit e4c274f

File tree

1 file changed

+42
-8
lines changed

1 file changed

+42
-8
lines changed

lib/matplotlib/animation.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,15 @@ def _adjust_frame_size(self):
295295
_log.debug('frame size in pixels is %s x %s', *self.frame_size)
296296
return w, h
297297

298+
@property
299+
def _supports_transparency(self):
300+
"""
301+
Whether this writer supports transparenct.
302+
303+
Writers may consult output file type and codec to determine this at runtime.
304+
"""
305+
return False
306+
298307
def setup(self, fig, outfile, dpi=None):
299308
# docstring inherited
300309
super().setup(fig, outfile, dpi=dpi)
@@ -468,6 +477,10 @@ def finish(self):
468477

469478
@writers.register('pillow')
470479
class PillowWriter(AbstractMovieWriter):
480+
@property
481+
def _supports_transparency(self):
482+
return True
483+
471484
@classmethod
472485
def isAvailable(cls):
473486
return True
@@ -503,6 +516,15 @@ class FFMpegBase:
503516
_exec_key = 'animation.ffmpeg_path'
504517
_args_key = 'animation.ffmpeg_args'
505518

519+
@property
520+
def _supports_transparency(self):
521+
suffix = Path(self.outfile).suffix
522+
if suffix in {'.apng', '.avif', '.gif', '.webm', '.webp'}:
523+
return True
524+
elif suffix == '.mov':
525+
return self.codec == 'png'
526+
return False
527+
506528
@property
507529
def output_args(self):
508530
args = []
@@ -519,11 +541,17 @@ def output_args(self):
519541
# macOS). Also fixes internet explorer. This is as of 2015/10/29.
520542
if self.codec == 'h264' and '-pix_fmt' not in extra_args:
521543
args.extend(['-pix_fmt', 'yuv420p'])
522-
# For GIF, we're telling FFMPEG to split the video stream, to generate
544+
# For GIF, we're telling FFmpeg to split the video stream, to generate
523545
# a palette, and then use it for encoding.
524546
elif self.codec == 'gif' and '-filter_complex' not in extra_args:
525547
args.extend(['-filter_complex',
526548
'split [a][b];[a] palettegen [p];[b][p] paletteuse'])
549+
# For AVIF, we're telling FFmpeg to split the video stream, extract the alpha,
550+
# in order to place it in a secondary stream, as needed by AVIF-in-FFmpeg.
551+
elif self.codec == 'avif' and '-filter_complex' not in extra_args:
552+
args.extend(['-filter_complex',
553+
'split [rgb][rgba]; [rgba] alphaextract [alpha]',
554+
'-map', '[rgb]', '-map', '[alpha]'])
527555
if self.bitrate > 0:
528556
args.extend(['-b', '%dk' % self.bitrate]) # %dk: bitrate in kbps.
529557
for k, v in self.metadata.items():
@@ -611,6 +639,11 @@ class ImageMagickBase:
611639
_exec_key = 'animation.convert_path'
612640
_args_key = 'animation.convert_args'
613641

642+
@property
643+
def _supports_transparency(self):
644+
suffix = Path(self.outfile).suffix
645+
return suffix in {'.apng', '.avif', '.gif', '.webm', '.webp'}
646+
614647
def _args(self):
615648
# ImageMagick does not recognize "raw".
616649
fmt = "rgba" if self.frame_format == "raw" else self.frame_format
@@ -1046,22 +1079,23 @@ def func(current_frame: int, total_frames: int) -> Any
10461079
# since GUI widgets are gone. Either need to remove extra code to
10471080
# allow for this non-existent use case or find a way to make it work.
10481081

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

1058-
savefig_kwargs['facecolor'] = _pre_composite_to_white(facecolor)
1059-
savefig_kwargs['transparent'] = False # just to be safe!
10601086
# canvas._is_saving = True makes the draw_event animation-starting
10611087
# callback a no-op; canvas.manager = None prevents resizing the GUI
10621088
# widget (both are likewise done in savefig()).
10631089
with (writer.saving(self._fig, filename, dpi),
10641090
cbook._setattr_cm(self._fig.canvas, _is_saving=True, manager=None)):
1091+
if not getattr(writer, '_supports_transparency', False):
1092+
facecolor = savefig_kwargs.get('facecolor',
1093+
mpl.rcParams['savefig.facecolor'])
1094+
if facecolor == 'auto':
1095+
facecolor = self._fig.get_facecolor()
1096+
savefig_kwargs['facecolor'] = _pre_composite_to_white(facecolor)
1097+
savefig_kwargs['transparent'] = False # just to be safe!
1098+
10651099
for anim in all_anim:
10661100
anim._init_draw() # Clear the initial frame
10671101
frame_number = 0

0 commit comments

Comments
 (0)