6767 AnimatedGifOutput ,
6868 MovieOutput ,
6969 ProjectionOutput ,
70+ SyncGifOptions ,
71+ build_sync_gif_options ,
7072 load_z_stack ,
7173 save_all_projections ,
7274 save_slice_movies_for_well ,
8385 save_mosaic ,
8486)
8587from .labeling import FigureLabeler , get_labeler
88+ from .exporters import DeckExportOptions , collect_figure_assets , export_slide_deck
8689from openhcs .processing .backends .processors .numpy_processor import (
8790 create_orthogonal_projections ,
8891)
@@ -110,6 +113,9 @@ class ProcessingConfig:
110113 create_composites : bool = True
111114 create_movies : bool = False
112115 create_sync_gifs : bool = False
116+ sync_gif_options : SyncGifOptions = SyncGifOptions ()
117+ export_pptx : bool = False
118+ pptx_options : Optional [DeckExportOptions ] = None
113119 movie_types : Tuple [str , ...] = ("xy" , "xz" , "yz" )
114120 movie_fps : int = 10
115121 movie_bit_depth : int = 8
@@ -147,6 +153,7 @@ class ProcessingResult:
147153 movie_outputs : List [MovieOutput ] = field (default_factory = list )
148154 sync_gif_outputs : List [AnimatedGifOutput ] = field (default_factory = list )
149155 mosaic_outputs : List [Path ] = field (default_factory = list )
156+ deck_outputs : List [Path ] = field (default_factory = list )
150157
151158
152159def _get_default_dtype_config ():
@@ -286,7 +293,7 @@ def process_well_all_channels(
286293 layout = CompositeLayout (z_gap = config .z_gap , z_aspect = config .z_aspect ),
287294 channel_colors = config .channel_colors ,
288295 z_gap = config .z_gap ,
289- fps = config .movie_fps ,
296+ options = config .sync_gif_options ,
290297 )
291298 for sg in sync_gif_outputs :
292299 logger .info (f" Saved sync GIF: { sg .output_path } " )
@@ -361,10 +368,20 @@ def _process_well_worker(
361368 create_sync_gifs = config_dict .get ("create_sync_gifs" , False )
362369 movie_types = tuple (config_dict .get ("movie_types" , ["xy" , "xz" , "yz" ]))
363370 movie_fps = config_dict .get ("movie_fps" , 10 )
371+ sync_gif_options_dict = config_dict .get ("sync_gif_options" , {})
364372 movie_bit_depth = config_dict .get ("movie_bit_depth" , 8 )
365373 z_gap = config_dict .get ("z_gap" , 1.0 )
366374 z_aspect = config_dict .get ("z_aspect" , 0.1 )
367375 dpi = config_dict .get ("dpi" , 150 )
376+ sync_gif_options = build_sync_gif_options (
377+ fps = sync_gif_options_dict .get ("fps" , movie_fps ),
378+ profile = sync_gif_options_dict .get ("profile" , "quality" ),
379+ scale = sync_gif_options_dict .get ("scale" ),
380+ frame_step = sync_gif_options_dict .get ("frame_step" ),
381+ max_colors = sync_gif_options_dict .get ("max_colors" ),
382+ dither = sync_gif_options_dict .get ("dither" ),
383+ diff_mode = sync_gif_options_dict .get ("diff_mode" ),
384+ )
368385
369386 z_paths_by_channel = {
370387 WellChannelKey (
@@ -454,7 +471,7 @@ def _process_well_worker(
454471 layout = CompositeLayout (z_gap = z_gap , z_aspect = z_aspect ),
455472 channel_colors = channel_colors or DEFAULT_CHANNEL_COLORS ,
456473 z_gap = z_gap ,
457- fps = movie_fps ,
474+ options = sync_gif_options ,
458475 )
459476
460477 return WellResult (
@@ -528,6 +545,15 @@ def process_plate(config: ProcessingConfig) -> ProcessingResult:
528545 "create_sync_gifs" : config .create_sync_gifs ,
529546 "movie_types" : config .movie_types ,
530547 "movie_fps" : config .movie_fps ,
548+ "sync_gif_options" : {
549+ "fps" : config .sync_gif_options .fps ,
550+ "profile" : "quality" ,
551+ "scale" : config .sync_gif_options .compression .scale ,
552+ "frame_step" : config .sync_gif_options .compression .frame_step ,
553+ "max_colors" : config .sync_gif_options .compression .max_colors ,
554+ "dither" : config .sync_gif_options .compression .dither ,
555+ "diff_mode" : config .sync_gif_options .compression .diff_mode ,
556+ },
531557 "movie_bit_depth" : config .movie_bit_depth ,
532558 "z_gap" : config .z_gap ,
533559 "z_aspect" : config .z_aspect ,
@@ -671,6 +697,25 @@ def parse_mosaic_group(group_str: str) -> ArbitraryMosaicSpec:
671697 return ArbitraryMosaicSpec (name = name , well_ids = wells )
672698
673699
700+ def export_powerpoint_deck (
701+ config : ProcessingConfig , result : ProcessingResult
702+ ) -> Optional [Path ]:
703+ """Export generated figures to a PowerPoint deck."""
704+ if not config .export_pptx or config .pptx_options is None :
705+ return None
706+
707+ assets = collect_figure_assets (
708+ composite_outputs = result .composite_outputs ,
709+ sync_gif_outputs = result .sync_gif_outputs ,
710+ movie_outputs = result .movie_outputs ,
711+ )
712+ deck_result = export_slide_deck (assets , config .pptx_options )
713+ result .deck_outputs .append (deck_result .output_path )
714+ logger .info (f"Saved PowerPoint deck: { deck_result .output_path } " )
715+ logger .info (f" Slide count: { deck_result .slide_count } " )
716+ return deck_result .output_path
717+
718+
674719def main ():
675720 """Main entry point."""
676721 parser = argparse .ArgumentParser (
@@ -754,6 +799,66 @@ def main():
754799 help = "Create synchronized composite GIFs (XY left, XZ/YZ right)" ,
755800 )
756801
802+ parser .add_argument (
803+ "--sync-gif-profile" ,
804+ choices = ["quality" , "balanced" , "powerpoint" , "compact" ],
805+ default = "quality" ,
806+ help = "Compression profile for synchronized GIFs" ,
807+ )
808+
809+ parser .add_argument (
810+ "--sync-gif-scale" ,
811+ type = float ,
812+ help = "Optional scale override for synchronized GIF output" ,
813+ )
814+
815+ parser .add_argument (
816+ "--sync-gif-frame-step" ,
817+ type = int ,
818+ help = "Optional frame-step override for synchronized GIF output" ,
819+ )
820+
821+ parser .add_argument (
822+ "--sync-gif-max-colors" ,
823+ type = int ,
824+ help = "Optional palette-size override for synchronized GIF output" ,
825+ )
826+
827+ parser .add_argument (
828+ "--sync-gif-dither" ,
829+ choices = ["sierra2_4a" , "bayer" , "none" ],
830+ help = "Optional dithering mode for synchronized GIF output" ,
831+ )
832+
833+ parser .add_argument (
834+ "--export-pptx" ,
835+ action = "store_true" ,
836+ help = "Export generated figures to a PowerPoint deck" ,
837+ )
838+
839+ parser .add_argument (
840+ "--pptx-output" ,
841+ help = "Output path for PowerPoint deck (defaults to output-dir/orthogonal_projections.pptx)" ,
842+ )
843+
844+ parser .add_argument (
845+ "--pptx-include-composites" ,
846+ action = "store_true" ,
847+ help = "Include composite PNGs in PowerPoint export" ,
848+ )
849+
850+ parser .add_argument (
851+ "--pptx-include-sync-gifs" ,
852+ action = "store_true" ,
853+ help = "Include synchronized GIFs in PowerPoint export" ,
854+ )
855+
856+ parser .add_argument (
857+ "--pptx-include-movies" ,
858+ action = "store_true" ,
859+ help = "Include composite movies in PowerPoint export" ,
860+ )
861+
757862 parser .add_argument (
758863 "--movie-types" ,
759864 nargs = "+" ,
@@ -821,6 +926,38 @@ def main():
821926 if args .mosaic_groups :
822927 arbitrary_mosaics = tuple (parse_mosaic_group (g ) for g in args .mosaic_groups )
823928
929+ sync_gif_options = build_sync_gif_options (
930+ fps = args .movie_fps ,
931+ profile = args .sync_gif_profile ,
932+ scale = args .sync_gif_scale ,
933+ frame_step = args .sync_gif_frame_step ,
934+ max_colors = args .sync_gif_max_colors ,
935+ dither = args .sync_gif_dither ,
936+ )
937+
938+ pptx_include_any = (
939+ args .pptx_include_composites
940+ or args .pptx_include_sync_gifs
941+ or args .pptx_include_movies
942+ )
943+ pptx_include_composites = args .pptx_include_composites or not pptx_include_any
944+ pptx_include_sync_gifs = args .pptx_include_sync_gifs or not pptx_include_any
945+ pptx_output = (
946+ Path (args .pptx_output )
947+ if args .pptx_output
948+ else Path (args .output_dir ) / "orthogonal_projections.pptx"
949+ )
950+ pptx_options = None
951+ if args .export_pptx :
952+ pptx_options = DeckExportOptions (
953+ output_path = pptx_output ,
954+ include_composites = pptx_include_composites ,
955+ include_sync_gifs = pptx_include_sync_gifs ,
956+ include_movies = args .pptx_include_movies ,
957+ deck_title = "OpenHCS Orthogonal Projections" ,
958+ plate_name = Path (args .plate_dir ).name ,
959+ )
960+
824961 config = ProcessingConfig (
825962 plate_path = Path (args .plate_dir ),
826963 output_dir = Path (args .output_dir ),
@@ -835,6 +972,9 @@ def main():
835972 create_composites = not args .no_composites ,
836973 create_movies = args .create_movies ,
837974 create_sync_gifs = args .create_sync_gifs ,
975+ sync_gif_options = sync_gif_options ,
976+ export_pptx = args .export_pptx ,
977+ pptx_options = pptx_options ,
838978 movie_types = tuple (args .movie_types ),
839979 movie_fps = args .movie_fps ,
840980 movie_bit_depth = args .movie_bit_depth ,
@@ -852,6 +992,8 @@ def main():
852992 logger .info (f"Workers: { config .num_workers } " )
853993
854994 result = process_plate (config )
995+ if config .export_pptx :
996+ export_powerpoint_deck (config , result )
855997
856998 logger .info ("=" * 50 )
857999 logger .info ("Processing complete!" )
@@ -864,6 +1006,7 @@ def main():
8641006 logger .info (f" Movies: { len (result .movie_outputs )} " )
8651007 logger .info (f" Sync GIFs: { len (result .sync_gif_outputs )} " )
8661008 logger .info (f" Mosaics: { len (result .mosaic_outputs )} " )
1009+ logger .info (f" Decks: { len (result .deck_outputs )} " )
8671010
8681011
8691012if __name__ == "__main__" :
0 commit comments