From 4ffe4a7e89adb1a2cf6f714b2ab91c3ffdfe3f48 Mon Sep 17 00:00:00 2001 From: lshaw8317 Date: Fri, 28 Nov 2025 12:17:30 +0100 Subject: [PATCH 1/2] First attempt --- caterva2/services/plugins/image/__init__.py | 123 ++++++++++++++++++ .../plugins/image/templates/display.html | 70 ++++++++++ caterva2/services/server.py | 9 +- 3 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 caterva2/services/plugins/image/__init__.py create mode 100644 caterva2/services/plugins/image/templates/display.html diff --git a/caterva2/services/plugins/image/__init__.py b/caterva2/services/plugins/image/__init__.py new file mode 100644 index 00000000..d0853fb0 --- /dev/null +++ b/caterva2/services/plugins/image/__init__.py @@ -0,0 +1,123 @@ +import ast +import pathlib + +# Requirements +import jinja2 +import numpy as np +import PIL.Image +from fastapi import Depends, FastAPI, Request, responses +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +# Project +from caterva2.services import db +from caterva2.services.server import get_container, optional_user, resize_image +from caterva2.services.server import templates as sub_templates + +app = FastAPI() +BASE_DIR = pathlib.Path(__file__).resolve().parent +templates = Jinja2Templates(directory=BASE_DIR / "templates") +templates.env.loader = jinja2.ChoiceLoader( + [ + templates.env.loader, # Preserve the original loader + sub_templates.env.loader, # Add the sub-templates loader + ] +) + +name = "image" # Identifies the plugin +label = "Image" +contenttype = "image" + + +urlbase = None + + +def init(urlbase_): + global urlbase + urlbase = urlbase_ + + +def url(path: str) -> str: + return f"{urlbase}/{path}" + + +def guess(path: pathlib.Path, meta) -> bool: + """Does dataset (given path and metadata) seem of this content type?""" + if not hasattr(meta, "dtype"): + return False # not an array + + dtype = meta.dtype + if dtype is None: + return False + + # Structured dtype + if isinstance(dtype, str) and dtype.startswith("["): + dtype = eval(dtype) # TODO Make it safer + + # Sometimes dtype is a tuple (e.g. (' + + + {% with id="image-spinner" %} + {% include 'includes/loading.html' %} + {% endwith %} + + +{{ width }} x {{ height }} (original size) +
+ + + diff --git a/caterva2/services/server.py b/caterva2/services/server.py index 7570aace..93154f33 100644 --- a/caterva2/services/server.py +++ b/caterva2/services/server.py @@ -2609,13 +2609,18 @@ def main(): # Register display plugins (delay module load) try: - from .plugins import tomography # When used as module + from .plugins import image, tomography # When used as module except ImportError: - from caterva2.services.plugins import tomography # When used as script + from caterva2.services.plugins import image, tomography # When used as script + # tomography app.mount(f"/plugins/{tomography.name}", tomography.app) plugins[tomography.contenttype] = tomography tomography.init(settings.urlbase) + # image + app.mount(f"/plugins/{image.name}", image.app) + plugins[image.contenttype] = image + image.init(settings.urlbase) # Mount media media = settings.statedir / "media" From 840a35dd6fc80591a20eae6e9d03bcd5306625f2 Mon Sep 17 00:00:00 2001 From: Luke Shaw Date: Fri, 28 Nov 2025 14:16:06 +0100 Subject: [PATCH 2/2] Handle image rescaling --- caterva2/services/plugins/image/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/caterva2/services/plugins/image/__init__.py b/caterva2/services/plugins/image/__init__.py index d0853fb0..5bf25bfe 100644 --- a/caterva2/services/plugins/image/__init__.py +++ b/caterva2/services/plugins/image/__init__.py @@ -100,7 +100,12 @@ async def __get_image(path, user, ndim, i): index = [slice(None) for x in array.shape] index[ndim] = slice(i, i + 1, 1) content = array[tuple(index)].squeeze() - return PIL.Image.fromarray(content) + if content.dtype.kind != "u": + content = (content - content.min()) / (content.max() - content.min()) # normalise to 0-1 + content = (content * 255).astype(np.uint8) + return PIL.Image.fromarray( + content, mode="RGB" + ("A" if content.shape[-1] == 4 else "") if content.ndim == 3 else "L" + ) @app.get("/image/{path:path}")