Skip to content

Commit

Permalink
Merge pull request #22 from NaturalHistoryMuseum/dev
Browse files Browse the repository at this point in the history
v0.12.0 release
  • Loading branch information
jrdh authored May 16, 2022
2 parents 1ba9de4 + a097269 commit ffcb50c
Show file tree
Hide file tree
Showing 25 changed files with 1,755 additions and 1,738 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v2

- name: Update OS package list
run: sudo apt-get update

- name: Install OS dependencies
run: sudo apt-get -y install libffi-dev libturbojpeg0-dev libjpeg-dev libpcre3 libpcre3-dev libcurl4-openssl-dev libssl-dev build-essential libmagickwand-dev

Expand Down
8 changes: 3 additions & 5 deletions iiif/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
#!/usr/bin/env python3
# encoding: utf-8

from copy import deepcopy
from typing import Callable

import os
import yaml
from pathlib import Path
Expand All @@ -26,8 +23,9 @@ def __init__(self, **options):
self.min_sizes_size = options.get('min_sizes_size', 200)

# image processing settings
self.image_pool_size = options.get('image_pool_size', 2)
self.image_cache_size_per_process = options.get('image_cache_size_per_process', 5)
self.processing_pool_size = options.get('processing_pool_size', 2)
self.processed_cache_size = options.get('processed_cache_size', 1024 * 1024 * 256)
self.processed_cache_ttl = options.get('processed_cache_ttl', 12 * 60 * 60)

# size definitions for the quick access endpoints
self.thumbnail_width = options.get('thumbnail_width', 512)
Expand Down
83 changes: 74 additions & 9 deletions iiif/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,85 @@
#!/usr/bin/env python3
# encoding: utf-8

from fastapi import HTTPException
import logging
from fastapi import Request
from starlette.responses import JSONResponse
from typing import Optional

# this is all assuming we're using uvicorn...
uvicorn_logger = logging.getLogger('uvicorn.error')
# use this logger to dump stuff into the same channels as the uvicorn logs
logger = uvicorn_logger.getChild('iiif')

def profile_not_found() -> HTTPException:
return HTTPException(status_code=404, detail='Profile not recognised')

class IIIFServerException(Exception):

def image_not_found() -> HTTPException:
return HTTPException(status_code=404, detail='Image not found')
def __init__(self, public: str, status_code: int = 500, log: Optional[str] = None,
level: int = logging.WARNING, cause: Optional[Exception] = None,
use_public_as_log: bool = True):
super().__init__(public)
self.status_code = status_code
self.public = public
self.log = log
self.level = level
self.cause = cause
if log is not None:
self.log = log
else:
if self.cause is not None:
self.log = f'An error occurred: {self.cause}'
elif use_public_as_log:
self.log = self.public


def too_many_images(max_files) -> HTTPException:
return HTTPException(status_code=400, detail=f'Too many images requested (max: {max_files})')
class Timeout(IIIFServerException):

def __init__(self, *args, **kwargs):
super().__init__(f'A timeout occurred, please try again', 500, *args, **kwargs)

def invalid_iiif_parameter(name: str, value: str) -> HTTPException:
return HTTPException(status_code=400, detail=f'{name} value "{value}" is invalid')

class ProfileNotFound(IIIFServerException):

def __init__(self, name: str, *args, **kwargs):
super().__init__(f'Profile {name} not recognised', 404, *args, **kwargs)
self.name = name


class ImageNotFound(IIIFServerException):

def __init__(self, profile: str, name: str, *args, **kwargs):
super().__init__(f'Image {name} not found in profile {profile}', 404, *args,
**kwargs)
self.profile = profile
self.name = name


class TooManyImages(IIIFServerException):

def __init__(self, max_files: int, *args, **kwargs):
super().__init__(f'Too many images requested (max: {max_files})', 400, *args, **kwargs)
self.max_files = max_files


class InvalidIIIFParameter(IIIFServerException):

def __init__(self, name: str, value: str, *args, **kwargs):
super().__init__(f'Invalid IIIF option: {name} value "{value}" is invalid', 400,
use_public_as_log=False, *args, **kwargs)
self.name = name
self.value = value


async def handler(request: Request, exception: IIIFServerException) -> JSONResponse:
log_error(exception)
return JSONResponse(
status_code=exception.status_code,
content={
'error': exception.public
},
)


def log_error(exception: IIIFServerException):
if exception.log:
logger.log(exception.level, exception.log)
59 changes: 40 additions & 19 deletions iiif/ops.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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 contextlib import suppress

from iiif.exceptions import invalid_iiif_parameter
from iiif.exceptions import InvalidIIIFParameter
from iiif.profiles.base import ImageInfo

# this server currently supports IIIF level1
# this server currently supports IIIF level2
IIIF_LEVEL = 2


Expand Down Expand Up @@ -81,7 +82,7 @@ def parse_region(region: str, info: ImageInfo) -> Region:
return Region(x, y, w, h, full=(w == info.width and h == info.height))

# if we get here, the region is no good :(
raise invalid_iiif_parameter('Region', region)
raise InvalidIIIFParameter('Region', region)


@dataclass
Expand Down Expand Up @@ -147,14 +148,13 @@ def parse_size(size: str, region: Region) -> Size:
if 0 < w <= region.w and 0 < h <= region.h:
return Size(w, h, max=(w == region.w and h == region.h))

raise invalid_iiif_parameter('Size', size)
raise InvalidIIIFParameter('Size', size)


@dataclass
class Rotation:
# jpegtran only supports rotating in 90 degree increments and IIIF suggests only supporting
# rotating by 90 unless the png format is supported so that the background can be made
# transparent
# IIIF suggests only supporting rotating by 90 unless the png format is supported so that the
# background can be made transparent
angle: int
mirror: bool = False

Expand Down Expand Up @@ -189,14 +189,32 @@ def parse_rotation(rotation: str) -> Rotation:
if angle in allowed_angles:
return Rotation(angle, mirror)

raise invalid_iiif_parameter('Rotation', rotation)
raise InvalidIIIFParameter('Rotation', rotation)


class Quality(Enum):
default = 'default'
color = 'color'
gray = 'gray'
bitonal = 'bitonal'
default = ('default',)
color = ('color', 'colour')
gray = ('gray', 'grey')
bitonal = ('bitonal',)

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]


def parse_quality(quality: str) -> Quality:
Expand All @@ -210,16 +228,19 @@ def parse_quality(quality: str) -> Quality:
:return: a Quality
"""
for option in Quality:
if option.value == quality:
if option.matches(quality):
return option

raise invalid_iiif_parameter('Quality', quality)
raise InvalidIIIFParameter('Quality', quality)


class Format(Enum):
jpg = 'jpg'
png = 'png'

def __str__(self) -> str:
return self.value


def parse_format(fmt: str) -> Format:
"""
Expand All @@ -232,10 +253,10 @@ def parse_format(fmt: str) -> Format:
:return: a Format
"""
for option in Format:
if option.value == fmt:
if fmt == option.value:
return option

raise invalid_iiif_parameter('Format', fmt)
raise InvalidIIIFParameter('Format', fmt)


def parse_params(info: ImageInfo, region: str = 'full', size: str = 'max', rotation: str = '0',
Expand Down Expand Up @@ -276,4 +297,4 @@ def location(self) -> Path:
:return: the path where the image produced by this set of ops should be stored.
"""
return Path(str(self.region), str(self.size), str(self.rotation),
f'{self.quality.value}.{self.format.value}')
f'{self.quality}.{self.format}')
Loading

0 comments on commit ffcb50c

Please sign in to comment.