Skip to content

Commit

Permalink
iiif: moved resource to separate file
Browse files Browse the repository at this point in the history
  • Loading branch information
alejandromumo authored and slint committed Apr 22, 2024
1 parent df29f7c commit e47d0b7
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 223 deletions.
2 changes: 1 addition & 1 deletion invenio_rdm_records/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
RDMRecordRequestsResourceConfig,
RDMRecordResourceConfig,
)
from .iiif import IIIFResource
from .resources import (
IIIFResource,
RDMCommunityRecordsResource,
RDMGrantsAccessResource,
RDMParentGrantsResource,
Expand Down
229 changes: 226 additions & 3 deletions invenio_rdm_records/resources/iiif.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,238 @@
# Invenio-RDM is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.
"""IIIF Resource."""
# TODO move this somewhere else if needed

from abc import ABC, abstractmethod
from functools import wraps

import requests
from flask import Response, current_app, request
from flask_resources import resource_requestctx
from flask import Response, current_app, g, request, send_file
from flask_cors import cross_origin
from flask_resources import (
HTTPJSONException,
Resource,
ResponseHandler,
from_conf,
request_parser,
resource_requestctx,
response_handler,
route,
with_content_negotiation,
)
from importlib_metadata import version
from invenio_drafts_resources.resources.records.errors import RedirectException
from invenio_records_resources.resources.errors import ErrorHandlersMixin
from invenio_records_resources.resources.records.headers import etag_headers
from invenio_records_resources.resources.records.resource import (
request_headers,
request_read_args,
)
from werkzeug.utils import secure_filename

from .serializers import (
IIIFCanvasV2JSONSerializer,
IIIFInfoV2JSONSerializer,
IIIFManifestV2JSONSerializer,
IIIFSequenceV2JSONSerializer,
)

# IIIF decorators

iiif_request_view_args = request_parser(
from_conf("request_view_args"), location="view_args"
)


def with_iiif_content_negotiation(serializer):
"""Response as JSON LD regardless of the request type."""
return with_content_negotiation(
response_handlers={
"application/ld+json": ResponseHandler(serializer(), headers=etag_headers),
},
default_accept_mimetype="application/ld+json",
)


class IIIFResource(ErrorHandlersMixin, Resource):
"""IIIF resource."""

def __init__(self, config, service):
"""Instantiate resource."""
super().__init__(config)
self.service = service

def proxy_if_enabled(f):
"""Decorate a function to proxy the request to an Image Server if a proxy is enabled."""

@wraps(f)
def _wrapper(self, *args, **kwargs):
if self.proxy_enabled:
res = self.proxy_server()
if res:
return res, 200
return f(self, *args, **kwargs)

return _wrapper

@property
def proxy_enabled(self):
"""Check if proxy is enabled."""
return self.config.proxy_cls is not None

@property
def proxy_server(self):
"""Get the proxy configuration."""
return self.config.proxy_cls() if self.proxy_enabled else None

def create_url_rules(self):
"""Create the URL rules for the IIIF resource."""
routes = self.config.routes
return [
route("GET", routes["manifest"], self.manifest),
route("GET", routes["sequence"], self.sequence),
route("GET", routes["canvas"], self.canvas),
route("GET", routes["image_base"], self.base),
route("GET", routes["image_info"], self.info),
route("GET", routes["image_api"], self.image_api),
]

def _get_record_with_files(self):
uuid = resource_requestctx.view_args["uuid"]
return self.service.read_record(uuid=uuid, identity=g.identity)

#
# IIIF Manifest - not all clients support content-negotiation so we need a
# full endpoint.
#
# See https://iiif.io/api/presentation/2.1/#responses on
# "Access-Control-Allow-Origin: *"
#
@cross_origin(origin="*", methods=["GET"])
@with_iiif_content_negotiation(IIIFManifestV2JSONSerializer)
@iiif_request_view_args
@response_handler()
def manifest(self):
"""Manifest."""
return self._get_record_with_files().to_dict(), 200

@cross_origin(origin="*", methods=["GET"])
@with_iiif_content_negotiation(IIIFSequenceV2JSONSerializer)
@iiif_request_view_args
@response_handler()
def sequence(self):
"""Sequence."""
return self._get_record_with_files().to_dict(), 200

@cross_origin(origin="*", methods=["GET"])
@with_iiif_content_negotiation(IIIFCanvasV2JSONSerializer)
@iiif_request_view_args
@response_handler()
def canvas(self):
"""Canvas."""
uuid = resource_requestctx.view_args["uuid"]
key = resource_requestctx.view_args["file_name"]
file_ = self.service.get_file(uuid=uuid, identity=g.identity, key=key)
return file_.to_dict(), 200

@cross_origin(origin="*", methods=["GET"])
@with_iiif_content_negotiation(IIIFInfoV2JSONSerializer)
@iiif_request_view_args
@response_handler()
def base(self):
"""IIIF base endpoint, redirects to IIIF Info endpoint."""
item = self.service.get_file(
identity=g.identity,
uuid=resource_requestctx.view_args["uuid"],
)
raise RedirectException(item["links"]["iiif_info"])

@cross_origin(origin="*", methods=["GET"])
@with_iiif_content_negotiation(IIIFInfoV2JSONSerializer)
@iiif_request_view_args
@response_handler()
@proxy_if_enabled
def info(self):
"""Get IIIF image info."""
item = self.service.get_file(
identity=g.identity,
uuid=resource_requestctx.view_args["uuid"],
)
return item.to_dict(), 200

@cross_origin(origin="*", methods=["GET"])
@request_headers
@request_read_args
@iiif_request_view_args
@proxy_if_enabled
def image_api(self):
"""IIIF API Implementation.
.. note::
* IIF IMAGE API v1.0
* For more infos please visit <http://iiif.io/api/image/>.
* IIIF Image API v2.0
* For more infos please visit <http://iiif.io/api/image/2.0/>.
* The API works only for GET requests
* The image process must follow strictly the following workflow:
* Region
* Size
* Rotation
* Quality
* Format
"""
image_format = resource_requestctx.view_args["image_format"]
uuid = resource_requestctx.view_args["uuid"]
region = resource_requestctx.view_args["region"]
size = resource_requestctx.view_args["size"]
rotation = resource_requestctx.view_args["rotation"]
quality = resource_requestctx.view_args["quality"]
to_serve = self.service.image_api(
identity=g.identity,
uuid=uuid,
region=region,
size=size,
rotation=rotation,
quality=quality,
image_format=image_format,
)
# decide the mime_type from the requested image_format
mimetype = self.config.supported_formats.get(image_format, "image/jpeg")
# TODO: get from cache on the service image.last_modified
last_modified = None
send_file_kwargs = {"mimetype": mimetype}
# last_modified is not supported before flask 0.12
if last_modified:
send_file_kwargs.update(last_modified=last_modified)

dl = resource_requestctx.args.get("dl")
if dl is not None:
filename = secure_filename(dl)
if filename.lower() in {"", "1", "true"}:
filename = "{0}-{1}-{2}-{3}-{4}.{5}".format(
uuid, region, size, quality, rotation, image_format
)

send_file_kwargs.update(
as_attachment=True,
)
if version("Flask") < "2.2.0":
send_file_kwargs.update(
attachment_filename=secure_filename(filename),
)
else:
# Flask 2.2 renamed `attachment_filename` to `download_name`
send_file_kwargs.update(
download_name=secure_filename(filename),
)
if_modified_since = resource_requestctx.headers.get("If-Modified-Since")
if if_modified_since and last_modified and if_modified_since >= last_modified:
raise HTTPJSONException(code=304)

response = send_file(to_serve, **send_file_kwargs)
return response


# IIIF Proxies
class IIIFProxy(ABC):
"""IIIF Proxy interface.
Expand Down
Loading

0 comments on commit e47d0b7

Please sign in to comment.