Skip to content

Commit

Permalink
Merge pull request #24 from NaturalHistoryMuseum/josh/add_extras_to_i…
Browse files Browse the repository at this point in the history
…nfo_json

Add extras to info.json
  • Loading branch information
jrdh authored May 16, 2022
2 parents dfaa73a + 16f1988 commit a097269
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 48 deletions.
14 changes: 14 additions & 0 deletions iiif/ops.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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]

Expand Down
45 changes: 1 addition & 44 deletions iiif/profiles/base.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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)
"""
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
30 changes: 27 additions & 3 deletions iiif/routers/iiif.py
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -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}',
Expand Down
11 changes: 10 additions & 1 deletion tests/test_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:

Expand Down

0 comments on commit a097269

Please sign in to comment.