@@ -514,11 +514,23 @@ def _build_ui(self) -> None:
514514 value = self .args_imagebuilder ["horizontal_pixels" ],
515515 layout = ipywidgets .Layout (width = '200px' ),
516516 )
517+ self .w_oversampling = ipywidgets .Dropdown (
518+ description = "oversample" ,
519+ options = [1 , 2 , 3 , 4 ],
520+ value = 1 ,
521+ layout = ipywidgets .Layout (width = '140px' ),
522+ )
523+ self .w_oversampling_mode = ipywidgets .Dropdown (
524+ description = "avg" ,
525+ options = ["linear_mean" , "db_mean" ],
526+ value = "linear_mean" ,
527+ layout = ipywidgets .Layout (width = '170px' ),
528+ )
517529
518530 # Observe global controls for rebuild
519531 for widget in [self .w_stack , self .w_stack_step , self .w_mp_cores ,
520532 self .w_stack_linear , self .w_wci_value , self .w_wci_render ,
521- self .w_horizontal_pixels ]:
533+ self .w_horizontal_pixels , self . w_oversampling , self . w_oversampling_mode ]:
522534 widget .observe (self ._on_global_param_change , names = "value" )
523535
524536 # Fix/unfix view buttons
@@ -600,10 +612,21 @@ def _build_ui(self) -> None:
600612 )
601613 self .w_video_format = ipywidgets .Dropdown (
602614 description = "format" ,
603- options = ["mp4" , "gif" , "webm" , "avi" ],
615+ options = ["mp4" , "gif" , "avif" , " webm" , "avi" ],
604616 value = "mp4" ,
605617 layout = ipywidgets .Layout (width = '130px' ),
606618 )
619+ self .w_video_quality = ipywidgets .IntSlider (
620+ value = 50 ,
621+ min = 1 ,
622+ max = 100 ,
623+ step = 1 ,
624+ description = "quality" ,
625+ tooltip = "Compression quality for AVIF (1=smallest, 100=best)" ,
626+ layout = ipywidgets .Layout (width = '200px' ),
627+ )
628+ self .w_video_quality .layout .display = 'none' # hidden unless avif selected
629+ self .w_video_format .observe (self ._on_video_format_change , names = 'value' )
607630 self .w_video_filename = ipywidgets .Text (
608631 value = "wci_video" ,
609632 description = "filename" ,
@@ -760,7 +783,7 @@ def _assemble_layout(self) -> None:
760783 tab_render = ipywidgets .VBox ([
761784 ipywidgets .HBox ([self .w_vmin , self .w_vmax ]),
762785 ipywidgets .HBox ([self .w_wci_value , self .w_wci_render ]),
763- ipywidgets .HBox ([self .w_horizontal_pixels ]),
786+ ipywidgets .HBox ([self .w_horizontal_pixels , self . w_oversampling , self . w_oversampling_mode ]),
764787 ipywidgets .HBox ([self .w_time_sync , self .w_crosshair , self .w_time_warning ]),
765788 ])
766789
@@ -780,7 +803,7 @@ def _assemble_layout(self) -> None:
780803
781804 # Tab 5: Video export
782805 tab_video = ipywidgets .VBox ([
783- ipywidgets .HBox ([self .w_video_frames , self .w_video_fps , self .w_video_format ]),
806+ ipywidgets .HBox ([self .w_video_frames , self .w_video_fps , self .w_video_format , self . w_video_quality ]),
784807 ipywidgets .HBox ([self .w_video_filename , self .w_video_ping_time , self .w_video_live , self .w_export_video ]),
785808 self .w_video_status ,
786809 ])
@@ -821,6 +844,13 @@ def _assemble_layout(self) -> None:
821844 self .output ,
822845 ])
823846
847+ def _on_video_format_change (self , change : Dict [str , Any ]) -> None :
848+ """Show/hide the quality slider based on selected format."""
849+ if change ['new' ] == 'avif' :
850+ self .w_video_quality .layout .display = None
851+ else :
852+ self .w_video_quality .layout .display = 'none'
853+
824854 def _on_layout_change (self , change : Dict [str , Any ]) -> None :
825855 """Handle grid layout change."""
826856 new_rows , new_cols = change ['new' ]
@@ -1099,6 +1129,8 @@ def _sync_builder_args(self) -> None:
10991129 self .args_imagebuilder ["wci_render" ] = self .w_wci_render .value
11001130 self .args_imagebuilder ["horizontal_pixels" ] = self .w_horizontal_pixels .value
11011131 self .args_imagebuilder ["mp_cores" ] = self .w_mp_cores .value
1132+ self .args_imagebuilder ["oversampling" ] = self .w_oversampling .value
1133+ self .args_imagebuilder ["oversampling_mode" ] = self .w_oversampling_mode .value
11021134
11031135 # Update all slot imagebuilders
11041136 for slot in self .slots :
@@ -1413,6 +1445,36 @@ def _export_video(self, _event: Any = None) -> None:
14131445 imageio .mimsave (filename , frames , duration = durations )
14141446 else :
14151447 imageio .mimsave (filename , frames , duration = 1.0 / video_fps )
1448+ elif fmt == "avif" :
1449+ # Use pillow-avif-plugin for animated AVIF export
1450+ try :
1451+ import pillow_avif # noqa: F401 – registers .avif with Pillow
1452+ except ImportError :
1453+ self .w_video_status .value = "Error: pip install pillow-avif-plugin"
1454+ return
1455+ try :
1456+ from PIL import Image
1457+ pil_frames = [Image .fromarray (f ) for f in frames ]
1458+ quality = self .w_video_quality .value
1459+
1460+ if use_ping_time and durations :
1461+ duration_ms = [int (d * 1000 ) for d in durations ]
1462+ while len (duration_ms ) < len (pil_frames ):
1463+ duration_ms .append (int (1000 / video_fps ))
1464+ else :
1465+ duration_ms = int (1000 / video_fps )
1466+
1467+ pil_frames [0 ].save (
1468+ filename ,
1469+ save_all = True ,
1470+ append_images = pil_frames [1 :],
1471+ duration = duration_ms ,
1472+ loop = 0 ,
1473+ quality = quality ,
1474+ )
1475+ except Exception as e :
1476+ self .w_video_status .value = f"AVIF error: { e } "
1477+ return
14161478 else :
14171479 # For video formats (mp4, avi, webm), use imageio with ffmpeg plugin
14181480 try :
0 commit comments