@@ -178,6 +178,14 @@ def frame_size(self):
178
178
w , h = self .fig .get_size_inches ()
179
179
return int (w * self .dpi ), int (h * self .dpi )
180
180
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
+
181
189
@abc .abstractmethod
182
190
def grab_frame (self , ** savefig_kwargs ):
183
191
"""
@@ -468,6 +476,9 @@ def finish(self):
468
476
469
477
@writers .register ('pillow' )
470
478
class PillowWriter (AbstractMovieWriter ):
479
+ def _supports_transparency (self ):
480
+ return True
481
+
471
482
@classmethod
472
483
def isAvailable (cls ):
473
484
return True
@@ -503,11 +514,26 @@ class FFMpegBase:
503
514
_exec_key = 'animation.ffmpeg_path'
504
515
_args_key = 'animation.ffmpeg_args'
505
516
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'
10000
, 'bmp' , 'cfhd' , 'dpx' , 'ffv1' , 'ffvhuff' , 'gif' , 'huffyuv' ,
528
+ 'jpeg2000' , 'ljpeg' , 'png' , 'prores' , 'prores_aw' , 'prores_ks' , 'qtrle' ,
529
+ 'rawvideo' , 'targa' , 'tiff' , 'utvideo' , 'v408' , }
530
+
506
531
@property
507
532
def output_args (self ):
508
533
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 :]
511
537
else :
512
538
args .extend (['-vcodec' , self .codec ])
513
539
extra_args = (self .extra_args if self .extra_args is not None
@@ -518,11 +544,17 @@ def output_args(self):
518
544
# macOS). Also fixes internet explorer. This is as of 2015/10/29.
519
545
if self .codec == 'h264' and '-pix_fmt' not in extra_args :
520
546
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
522
548
# a palette, and then use it for encoding.
523
549
elif self .codec == 'gif' and '-filter_complex' not in extra_args :
524
550
args .extend (['-filter_complex' ,
525
551
'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]' ])
526
558
if self .bitrate > 0 :
527
559
args .extend (['-b' , '%dk' % self .bitrate ]) # %dk: bitrate in kbps.
528
560
for k , v in self .metadata .items ():
@@ -610,6 +642,10 @@ class ImageMagickBase:
610
642
_exec_key = 'animation.convert_path'
611
643
_args_key = 'animation.convert_args'
612
644
645
+ def _supports_transparency (self ):
646
+ suffix = Path (self .outfile ).suffix
647
+ return suffix in {'.apng' , '.avif' , '.gif' , '.webm' , '.webp' }
648
+
613
649
def _args (self ):
614
650
# ImageMagick does not recognize "raw".
615
651
fmt = "rgba" if self .frame_format == "raw" else self .frame_format
@@ -1045,22 +1081,23 @@ def func(current_frame: int, total_frames: int) -> Any
1045
1081
# since GUI widgets are gone. Either need to remove extra code to
1046
1082
# allow for this non-existent use case or find a way to make it work.
1047
1083
1048
- facecolor = savefig_kwargs .get ('facecolor' ,
1049
- mpl .rcParams ['savefig.facecolor' ])
1050
- if facecolor == 'auto' :
1051
- facecolor = self ._fig .get_facecolor ()
1052
-
1053
1084
def _pre_composite_to_white (color ):
1054
1085
r , g , b , a = mcolors .to_rgba (color )
1055
1086
return a * np .array ([r , g , b ]) + 1 - a
1056
1087
1057
- savefig_kwargs ['facecolor' ] = _pre_composite_to_white (facecolor )
1058
- savefig_kwargs ['transparent' ] = False # just to be safe!
1059
1088
# canvas._is_saving = True makes the draw_event animation-starting
1060
1089
# callback a no-op; canvas.manager = None prevents resizing the GUI
1061
1090
# widget (both are likewise done in savefig()).
1062
1091
with (writer .saving (self ._fig , filename , dpi ),
1063
1092
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
+
1064
1101
for anim in all_anim :
1065
1102
anim ._init_draw () # Clear the initial frame
1066
1103
frame_number = 0
0 commit comments