From 0c2bd3e906c7262125ca978f468c1a8a597bf970 Mon Sep 17 00:00:00 2001 From: Jojojoppe Date: Sun, 3 Aug 2025 18:28:01 +0200 Subject: [PATCH] Exporting fixed --- negstation/negstation.py | 2 +- negstation/widgets/export_widget.py | 126 ++++++++++++++++++++++--- negstation/widgets/histogram_widget.py | 3 + negstation_layout.ini | 8 +- 4 files changed, 121 insertions(+), 18 deletions(-) diff --git a/negstation/negstation.py b/negstation/negstation.py index d9d5548..9e170f7 100644 --- a/negstation/negstation.py +++ b/negstation/negstation.py @@ -13,7 +13,7 @@ from .layout_manager import LayoutManager from .widgets.base_widget import BaseWidget -logging.basicConfig(level=logging.DEBUG, +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") logger = logging.getLogger(__name__) diff --git a/negstation/widgets/export_widget.py b/negstation/widgets/export_widget.py index 409c89b..d9171d8 100644 --- a/negstation/widgets/export_widget.py +++ b/negstation/widgets/export_widget.py @@ -1,5 +1,42 @@ +# import dearpygui.dearpygui as dpg +# import numpy as np + +# from .pipeline_stage_widget import PipelineStageWidget + + +# class ExportStage(PipelineStageWidget): +# name = "Export Image" +# register = True +# has_pipeline_in = True +# has_pipeline_out = False + +# def __init__(self, manager, logger): +# super().__init__(manager, logger, default_stage_out="opened_image") +# self.manager.bus.subscribe( +# "process_full_res", self._on_process_full_res, True) + +# def create_pipeline_stage_content(self): +# dpg.add_text("Some export fields") + +# def _on_process_full_res(self, data): +# self.logger.info("Starting full res pipeline export") + +# def on_pipeline_data(self, img): +# if img is None: +# return +# self.logger.info("low res image received, ignore") + +# def on_full_res_pipeline_data(self, img): +# if img is None: +# return +# h, w, _ = img.shape +# self.logger.info(f"Full res image received: {w}x{h}") + + +import os import dearpygui.dearpygui as dpg import numpy as np +from PIL import Image from .pipeline_stage_widget import PipelineStageWidget @@ -11,23 +48,86 @@ class ExportStage(PipelineStageWidget): has_pipeline_out = False def __init__(self, manager, logger): - super().__init__(manager, logger, default_stage_out="opened_image") - self.manager.bus.subscribe( - "process_full_res", self._on_process_full_res, True) + # we don’t register an output stage — this widget only consumes + super().__init__(manager, logger, default_stage_out="unused") + # tags for our “Save As” dialog + self._save_dialog_tag = dpg.generate_uuid() + self._save_path = None def create_pipeline_stage_content(self): - dpg.add_text("Some export fields") + # Button to pop up the file-save dialog + dpg.add_button(label="Save As…", + callback=lambda s,a,u: dpg.configure_item(self._save_dialog_tag, show=True)) - def _on_process_full_res(self, data): - self.logger.info("Starting full res pipeline export") + # File dialog for choosing export path & extension + with dpg.file_dialog( + directory_selector=False, + show=False, + callback=self._on_save_selected, + tag=self._save_dialog_tag, + width=400, + height=300 + ): + dpg.add_file_extension("PNG {.png}") + dpg.add_file_extension("JPEG {.jpg,.jpeg}") + dpg.add_file_extension("TIFF {.tif,.tiff}") + dpg.add_file_extension("All files {.*}") - def on_pipeline_data(self, img): + def _on_save_selected(self, sender, app_data): + """ + Called when the user picks a filename in the Save As… dialog. + Stores the path for the next full-res pass. + """ + # app_data is a dict with 'current_path' and 'selections' + path = os.path.join( + app_data["current_path"], + app_data["file_name"] + ) + self._save_path = path + self.logger.info(f"Export path set to: {path}") + + def on_pipeline_data(self, img: np.ndarray): + # ignore all previews + return + + def on_full_res_pipeline_data(self, img: np.ndarray): + """ + Receives the full-resolution NumPy image when the user fires + the “Run full-res pipeline” action. Saves via Pillow. + """ if img is None: + self.logger.error("on_full_res_pipeline_data called with None image") + return + if not self._save_path: + self.logger.warning("No export path set — click Save As… first") return - self.logger.info("low res image received, ignore") - def on_full_res_pipeline_data(self, img): - if img is None: - return - h, w, _ = img.shape - self.logger.info(f"Full res image received: {w}x{h}") + # Decide bit depth by extension + ext = os.path.splitext(self._save_path)[1].lower() + # Convert floats → uint; or leave ints alone + if np.issubdtype(img.dtype, np.floating): + if ext in (".tif", ".tiff"): + arr = np.clip(img * 65535.0, 0, 65535).astype(np.uint16) + else: + arr = np.clip(img * 255.0, 0, 255).astype(np.uint8) + else: + arr = img + + # Determine PIL mode + mode = None + if arr.ndim == 2: + mode = "L" + elif arr.ndim == 3: + c = arr.shape[2] + if c == 3: + mode = "RGB" + elif c == 4: + mode = "RGBA" + + try: + im = Image.fromarray(arr, mode) if mode else Image.fromarray(arr) + im.save(self._save_path) + except Exception as e: + self.logger.error(f"Failed to save image to {self._save_path}: {e}") + else: + self.logger.info(f"Saved full-resolution image to {self._save_path}") diff --git a/negstation/widgets/histogram_widget.py b/negstation/widgets/histogram_widget.py index 38308c7..b4f675f 100644 --- a/negstation/widgets/histogram_widget.py +++ b/negstation/widgets/histogram_widget.py @@ -41,6 +41,9 @@ class HistogramWidget(PipelineStageWidget): self.img = img self.needs_redraw = True + def on_full_res_pipeline_data(self, img): + pass + def update(self): # TODO move calculations to on_pipeline_data if not self.needs_redraw or self.img is None: diff --git a/negstation_layout.ini b/negstation_layout.ini index 21a609b..90e53b7 100644 --- a/negstation_layout.ini +++ b/negstation_layout.ini @@ -142,7 +142,7 @@ DockId=0x00000022,0 [Window][###103] Pos=984,19 -Size=216,679 +Size=216,634 Collapsed=0 DockId=0x0000003F,0 @@ -225,14 +225,14 @@ Collapsed=0 DockId=0x00000039,0 [Window][###123] -Pos=984,634 -Size=216,166 +Pos=984,655 +Size=216,145 Collapsed=0 DockId=0x0000003E,0 [Window][###102] Pos=984,19 -Size=216,781 +Size=216,613 Collapsed=0 DockId=0x0000003D,0