From 6584cea4e156fae41ac00f1dc6496edb3a645a2c Mon Sep 17 00:00:00 2001 From: Josh Humphries Date: Mon, 16 May 2022 10:02:51 +0100 Subject: [PATCH 1/2] Fix test name --- tests/test_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ops.py b/tests/test_ops.py index a23d199..9704933 100644 --- a/tests/test_ops.py +++ b/tests/test_ops.py @@ -186,7 +186,7 @@ def test_level0(self): def test_level2_color(self): assert parse_quality('color') == Quality.color - def test_level2_color(self): + def test_level2_colour(self): assert parse_quality('colour') == Quality.color def test_level2_gray(self): From 16f19885310b356dbd8a970cb69f00405b83de24 Mon Sep 17 00:00:00 2001 From: Josh Humphries Date: Mon, 16 May 2022 10:04:48 +0100 Subject: [PATCH 2/2] Move info.json generation out of profile and add extras This solves a circular import issue which arose from generating the extra qualities in the ops module but then needing to import it into the profile base module. Additionally, it simplifies the info.json generation by removing the profile override ability (currently unused) and caching (was not well thought out as the part of the process that might take time, getting the info object, was not cached). --- iiif/ops.py | 14 ++++++++++++++ iiif/profiles/base.py | 45 +------------------------------------------ iiif/routers/iiif.py | 30 ++++++++++++++++++++++++++--- tests/test_ops.py | 9 +++++++++ 4 files changed, 51 insertions(+), 47 deletions(-) diff --git a/iiif/ops.py b/iiif/ops.py index db07094..122c206 100644 --- a/iiif/ops.py +++ b/iiif/ops.py @@ -1,7 +1,9 @@ from contextlib import suppress from dataclasses import dataclass from enum import Enum +from itertools import chain from pathlib import Path +from typing import List from iiif.exceptions import InvalidIIIFParameter from iiif.profiles.base import ImageInfo @@ -199,6 +201,18 @@ class Quality(Enum): def matches(self, value: str) -> bool: return value in self.value + @staticmethod + def extras() -> List[str]: + """ + Returns the values that should be use in the info.json response. This should include + eveything except the default value. + + :return: a list of extra qualities available on this IIIF server + """ + return list(chain.from_iterable( + quality.value for quality in Quality if quality != Quality.default) + ) + def __str__(self) -> str: return self.value[0] diff --git a/iiif/profiles/base.py b/iiif/profiles/base.py index 4c1bbd2..aa1b412 100644 --- a/iiif/profiles/base.py +++ b/iiif/profiles/base.py @@ -1,11 +1,9 @@ import abc -from cachetools import TTLCache from contextlib import asynccontextmanager from pathlib import Path from typing import Tuple, Optional, Any, AsyncIterable from iiif.config import Config -from iiif.utils import generate_sizes class ImageInfo: @@ -51,13 +49,11 @@ class AbstractProfile(abc.ABC): jpeg file, it can be processed in a common way (see the processing module). """ - def __init__(self, name: str, config: Config, rights: str, info_json_cache_size: int = 1000, - cache_for: float = 60): + def __init__(self, name: str, config: Config, rights: str, cache_for: float = 60): """ :param name: the name of the profile, should be unique across profiles :param config: the config object :param rights: the rights definition for all images handled by this profile - :param info_json_cache_size: the size of the info.json cache :param cache_for: how long in seconds a client should cache the results from this profile (both info.json and image data) """ @@ -70,7 +66,6 @@ def __init__(self, name: str, config: Config, rights: str, info_json_cache_size: self.rights = rights self.source_path.mkdir(exist_ok=True) self.cache_path.mkdir(exist_ok=True) - self.info_json_cache = TTLCache(maxsize=info_json_cache_size, ttl=cache_for) self.cache_for = cache_for @abc.abstractmethod @@ -134,43 +129,6 @@ async def stream_original(self, name: str, chunk_size: int = 4096) -> AsyncItera """ ... - async def generate_info_json(self, info: ImageInfo, iiif_level: int) -> dict: - """ - Generates an info.json dict for the given image. The info.json is cached locally in this - profile's attributes. - - :param info: the ImageInfo object to create the info.json dict for - :param iiif_level: the IIIF image server compliance level to include in the info.json - :return: the generated or cached info.json dict for the image - """ - # if the image's info.json isn't cached, create and add the complete info.json to the cache - if info not in self.info_json_cache: - id_url = f'{self.config.base_url}/{info.identifier}' - self.info_json_cache[info] = { - '@context': 'http://iiif.io/api/image/3/context.json', - 'id': id_url, - # mirador/openseadragon seems to need this to work even though I don't think it's - # correct under the IIIF image API v3 - '@id': id_url, - 'type': 'ImageService3', - 'protocol': 'http://iiif.io/api/image', - 'width': info.width, - 'height': info.height, - 'rights': self.rights, - 'profile': f'level{iiif_level}', - 'tiles': [ - {'width': 512, 'scaleFactors': [1, 2, 4, 8, 16]}, - {'width': 256, 'scaleFactors': [1, 2, 4, 8, 16]}, - {'width': 1024, 'scaleFactors': [1, 2, 4, 8, 16]}, - ], - 'sizes': generate_sizes(info.width, info.height, self.config.min_sizes_size), - # suggest to clients that upscaling isn't supported - 'maxWidth': info.width, - 'maxHeight': info.height, - } - - return self.info_json_cache[info] - async def close(self): """ Close down the profile ensuring any resources are released. This will be called before @@ -186,6 +144,5 @@ async def get_status(self) -> dict: """ status = { 'name': self.name, - 'info_json_cache_size': len(self.info_json_cache), } return status diff --git a/iiif/routers/iiif.py b/iiif/routers/iiif.py index 0b0c095..724bb94 100644 --- a/iiif/routers/iiif.py +++ b/iiif/routers/iiif.py @@ -1,9 +1,9 @@ from fastapi import APIRouter from starlette.responses import FileResponse, JSONResponse -from iiif.ops import IIIF_LEVEL, parse_params +from iiif.ops import IIIF_LEVEL, parse_params, Quality from iiif.state import state -from iiif.utils import get_mimetype +from iiif.utils import get_mimetype, generate_sizes router = APIRouter() @@ -22,7 +22,31 @@ async def get_image_info(identifier: str) -> JSONResponse: :return: the info.json as a dict """ profile, info = await state.get_profile_and_info(identifier) - info_json = await profile.generate_info_json(info, IIIF_LEVEL) + id_url = f'{state.config.base_url}/{info.identifier}' + info_json = { + '@context': 'http://iiif.io/api/image/3/context.json', + 'id': id_url, + # mirador/openseadragon seems to need this to work even though I don't think it's correct + # under the IIIF image API v3 + '@id': id_url, + 'type': 'ImageService3', + 'protocol': 'http://iiif.io/api/image', + 'width': info.width, + 'height': info.height, + 'rights': profile.rights, + 'profile': f'level{IIIF_LEVEL}', + 'tiles': [ + {'width': 512, 'scaleFactors': [1, 2, 4, 8, 16]}, + {'width': 256, 'scaleFactors': [1, 2, 4, 8, 16]}, + {'width': 1024, 'scaleFactors': [1, 2, 4, 8, 16]}, + ], + 'sizes': generate_sizes(info.width, info.height, state.config.min_sizes_size), + # suggest to clients that upscaling isn't supported + 'maxWidth': info.width, + 'maxHeight': info.height, + 'extraQualities': Quality.extras(), + 'extraFeatures': ['mirroring'], + } # add a cache-control header and iiif header headers = { 'cache-control': f'max-age={profile.cache_for}', diff --git a/tests/test_ops.py b/tests/test_ops.py index 9704933..76e7c2f 100644 --- a/tests/test_ops.py +++ b/tests/test_ops.py @@ -203,6 +203,15 @@ def test_invalid(self): parse_quality('banana') assert exc_info.value.status_code == 400 + def test_extras(self): + extras = Quality.extras() + for quality in Quality: + for value in quality.value: + if value == 'default': + assert value not in extras + else: + assert value in extras + class TestParseFormat: