From ff43deae368e98a0a9037d7262c0b05127497e1e Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 25 Jun 2025 14:34:24 +0100 Subject: [PATCH] Download stitched image of scan (if present) * Docstring for download stitch * use global STITCH_SUFFIX --- .../scan_directories.py | 66 +++++++++++++++---- .../things/smart_scan.py | 24 +++++++ .../tabContentComponents/scanListContent.vue | 26 ++++---- 3 files changed, 91 insertions(+), 25 deletions(-) diff --git a/src/openflexure_microscope_server/scan_directories.py b/src/openflexure_microscope_server/scan_directories.py index e2a33fbc..76fe0185 100644 --- a/src/openflexure_microscope_server/scan_directories.py +++ b/src/openflexure_microscope_server/scan_directories.py @@ -5,7 +5,7 @@ Currently it handles scan getting information from a information from a scan dir eventually is should handle all the file system operation for smart_scan. """ -from typing import Optional +from typing import Optional, Literal import os import re import shutil @@ -13,6 +13,7 @@ import zipfile from pydantic import BaseModel +STITCH_SUFFIX = "_stitched.jpg" IMG_DIR_NAME = "images" IMAGE_REGEX = re.compile(r"-?[0-9]+_-?[0-9]+\.jpe?g$") SCAN_ZERO_PAD_DIGITS = 4 @@ -88,7 +89,7 @@ class ScanDirectoryManager: """Return the file path for the file within a scan directory If check_exists is True, None is returned if the file does not exist. If False - then the path is returned anway + then the path is returned anyway """ file_path = os.path.join(self.img_dir_for(scan_name), filename) if check_exists: @@ -96,6 +97,13 @@ class ScanDirectoryManager: return None return file_path + def get_final_stitch(self, scan_name: str) -> Optional[str]: + """Return the file path for the final stitch. + + If no final stitch is found, return None + """ + return ScanDirectory(scan_name, self.base_dir).get_final_stitch() + @property def all_scans(self) -> list[str]: """Return a list of the scan names in the base directory""" @@ -242,25 +250,55 @@ class ScanDirectory: return [] return os.listdir(self.images_dir) + def _find_files( + self, file_types=Literal["all", "scan_images", "stitches", "dzi"] + ) -> tuple[list[str], list[str], list[str]] | list[str]: + folder_contents = self.get_scan_files() + + # Return empty lists if no folder contents. + if not folder_contents: + if file_types == "all": + return [], [], [] + return [] + + if file_types in ["all", "scan_images"]: + scan_images = [i for i in folder_contents if IMAGE_REGEX.search(i)] + if file_types == "scan_images": + return scan_images + + if file_types in ["all", "stitches"]: + stitches = [i for i in folder_contents if i.endswith("_stitched.jpg")] + if file_types == "stitches": + return stitches + + if file_types in ["all", "dzi"]: + dzi_files = [i for i in folder_contents if i.endswith("dzi")] + if file_types == "dzi": + return dzi_files + + return scan_images, stitches, dzi_files + + def get_final_stitch(self) -> Optional[str]: + """Return the file path for the final stitch. + + If no final stitch is found, return None + """ + stitches = self._find_files("stitches") + if not stitches: + return None + return stitches[0] + def get_modified_time(self) -> float: """Return the modified time of the directory""" return max(os.stat(root).st_mtime for root, _, _ in os.walk(self.dir_path)) def scan_info(self): """Return the information for the scan directory as a ScanInfo object""" - folder_contents = self.get_scan_files() - if folder_contents: - scan_images = [i for i in folder_contents if IMAGE_REGEX.search(i)] - stitches = [i for i in folder_contents if i.endswith("_stitched.jpg")] - dzi_files = [i for i in folder_contents if i.endswith("dzi")] - number_of_images = len(scan_images) - stitch_available = len(stitches) > 0 - dzi = None if not dzi_files else str(dzi_files[0]) - else: - number_of_images = 0 - stitch_available = False - dzi = None + scan_images, stitches, dzi_files = self._find_files("all") + number_of_images = len(scan_images) + stitch_available = len(stitches) > 0 + dzi = None if not dzi_files else str(dzi_files[0]) return ScanInfo( name=self.name, diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 832f6170..36e73273 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -666,6 +666,30 @@ class SmartScanThing(Thing): """ return self._scan_dir_manager.all_scans_info() + @fastapi_endpoint( + "get", + "get_stitch/{scan_name}", + responses={ + 200: { + "description": "Successfully downloading file", + "content": {"*/*": {}}, + }, + 403: {"description": "Filename not permitted"}, + 404: {"description": "File not found"}, + }, + ) + def get_stitch_file(self, scan_name: str) -> FileResponse: + """Return the stitched image corresponding to a given scan name, if it exists. + + Will only return a file ending in suffix STITCH_SUFFIX + Note: when downloading this, the default filename will be `scan_name`.jpeg""" + + stitch_path = self._scan_dir_manager.get_final_stitch(scan_name) + + if stitch_path is None: + raise HTTPException(404, "File not found") + return FileResponse(stitch_path) + @fastapi_endpoint( "delete", "scans/{scan_name}", diff --git a/webapp/src/components/tabContentComponents/scanListContent.vue b/webapp/src/components/tabContentComponents/scanListContent.vue index db60cc20..66311720 100644 --- a/webapp/src/components/tabContentComponents/scanListContent.vue +++ b/webapp/src/components/tabContentComponents/scanListContent.vue @@ -97,17 +97,13 @@ @error="modalError" class="uk-button uk-width-1-2" /> - + Download Stitch