Skip to content
Snippets Groups Projects
Commit 7292331c authored by Bastien Abadie's avatar Bastien Abadie
Browse files

Merge branch 'generic-iiif-manifests' into 'master'

ElementNewType support on IIIF APIs

See merge request !480
parents affec640 947848b5
No related branches found
No related tags found
1 merge request!480ElementNewType support on IIIF APIs
......@@ -3,28 +3,35 @@ from django.views.decorators.cache import cache_page
from rest_framework.generics import RetrieveAPIView
from rest_framework.exceptions import PermissionDenied
from arkindex_common.enums import TranscriptionType
from arkindex.documents.models import Element, ElementType, Corpus
from arkindex.documents.models import Element, Corpus
from arkindex.documents.serializers.search import IIIFSearchQuerySerializer
from arkindex.documents.serializers.iiif import \
VolumeManifestSerializer, ActManifestSerializer, \
PageAnnotationListSerializer, PageActAnnotationListSerializer, \
SurfaceAnnotationListSerializer, TranscriptionSearchAnnotationListSerializer
from arkindex.documents.serializers.iiif import (
FolderManifestSerializer,
ElementAnnotationListSerializer,
TranscriptionSearchAnnotationListSerializer,
)
from arkindex.documents.search import search_transcriptions_filter_post
from arkindex.project.elastic import ESTranscription
from arkindex.project.mixins import SearchAPIMixin
class VolumeManifest(RetrieveAPIView):
class FolderManifest(RetrieveAPIView):
"""
Get a IIIF manifest for a specific volume
"""
serializer_class = VolumeManifestSerializer
serializer_class = FolderManifestSerializer
openapi_overrides = {
'operationId': 'RetrieveFolderManifest',
'description': 'Retrieve an IIIF manifest for a folder element.',
'security': [],
'tags': ['iiif'],
}
def get_queryset(self):
return Element.objects.filter(
type=ElementType.Volume,
corpus__in=Corpus.objects.readable(self.request.user),
new_type__folder=True,
)
@method_decorator(cache_page(3600))
......@@ -32,70 +39,25 @@ class VolumeManifest(RetrieveAPIView):
return super().get(*args, **kwargs)
class ActManifest(RetrieveAPIView):
"""
Get a IIIF manfiest for a specific act
"""
serializer_class = ActManifestSerializer
def get_queryset(self):
return Element.objects.filter(
corpus__in=Corpus.objects.readable(self.request.user),
type=ElementType.Act,
)
@method_decorator(cache_page(3600))
def get(self, *args, **kwargs):
return super().get(*args, **kwargs)
class PageAnnotationList(RetrieveAPIView):
class ElementAnnotationList(RetrieveAPIView):
"""
Get a IIIF annotation list for transcriptions of a specific page
"""
serializer_class = PageAnnotationListSerializer
def get_queryset(self):
return Element.objects.filter(
corpus__in=Corpus.objects.readable(self.request.user),
type=ElementType.Page,
)
@method_decorator(cache_page(3600))
def get(self, *args, **kwargs):
return super().get(*args, **kwargs)
class PageActAnnotationList(RetrieveAPIView):
"""
Get a IIIF annotation list for acts of a specific page
"""
serializer_class = PageActAnnotationListSerializer
def get_queryset(self):
return Element.objects.filter(
corpus__in=Corpus.objects.readable(self.request.user),
type=ElementType.Page,
)
@method_decorator(cache_page(3600))
def get(self, *args, **kwargs):
return super().get(*args, **kwargs)
class SurfaceAnnotationList(RetrieveAPIView):
"""
Get a IIIF annotation list for a single surface
"""
serializer_class = SurfaceAnnotationListSerializer
serializer_class = ElementAnnotationListSerializer
openapi_overrides = {
'operationId': 'RetrieveElementAnnotationList',
'description': 'Retrive an IIIF annotation list for transcriptions on elements with zones',
'security': [],
'tags': ['iiif'],
}
def get_queryset(self):
return Element.objects.filter(
type=ElementType.Surface,
corpus__in=Corpus.objects.readable(self.request.user),
new_type__folder=False,
new_type__hidden=False,
zone__isnull=False,
)
@method_decorator(cache_page(3600))
......@@ -110,6 +72,12 @@ class TranscriptionSearchAnnotationList(SearchAPIMixin, RetrieveAPIView):
serializer_class = TranscriptionSearchAnnotationListSerializer
query_serializer_class = IIIFSearchQuerySerializer
openapi_overrides = {
'operationId': 'SearchTranscriptionsAnnotationList',
'description': 'Retrieve an IIIF Search API annotation list for transcriptions on a folder element',
'security': [],
'tags': ['iiif'],
}
elt = None
def get_element(self):
......@@ -130,9 +98,12 @@ class TranscriptionSearchAnnotationList(SearchAPIMixin, RetrieveAPIView):
'terms',
element=list(
Element.objects
.get_descending(self.get_element().id)
.filter(type=ElementType.Page)
.values_list('id', flat=True)
.filter(
paths__path__last=self.get_element().id,
new_type__folder=False,
new_type__hidden=False,
zone__isnull=False,
).values_list('id', flat=True)
),
) \
.filter('range', score={'gte': min_score}) \
......
# flake8: noqa
from arkindex.documents.serializers.iiif.manifests import VolumeManifestSerializer, ActManifestSerializer
from arkindex.documents.serializers.iiif.annotations import \
PageAnnotationListSerializer, PageActAnnotationListSerializer, \
SurfaceAnnotationListSerializer, TranscriptionSearchAnnotationListSerializer
from arkindex.documents.serializers.iiif.manifests import FolderManifestSerializer # noqa: F401
from arkindex.documents.serializers.iiif.annotations import ( # noqa: F401
ElementAnnotationListSerializer,
TranscriptionSearchAnnotationListSerializer,
)
......@@ -51,7 +51,7 @@ class TranscriptionAnnotationSerializer(AnnotationSerializer):
return build_absolute_url(
ts,
self.context['request'],
'api:transcription-manifest',
'api:transcription-annotation',
id_argument='page_pk',
transcription_pk=ts.id,
)
......@@ -69,7 +69,7 @@ class TranscriptionSearchAnnotationSerializer(TranscriptionAnnotationSerializer)
def get_target(self, ts):
assert isinstance(ts, Transcription)
url = build_absolute_url(ts.element, self.context['request'], 'api:canvas-manifest')
url = build_absolute_url(ts.element, self.context['request'], 'api:iiif-canvas')
return "{0}#xywh={1.x},{1.y},{1.width},{1.height}".format(url, ts.zone.polygon)
......@@ -114,7 +114,7 @@ class AnnotationListSerializer(serializers.BaseSerializer):
"Get a list of elements to serialize as annotations."
class PageAnnotationListSerializer(AnnotationListSerializer):
class ElementAnnotationListSerializer(AnnotationListSerializer):
"""
Serialize a page's transcriptions into a IIIF annotation list
"""
......
......@@ -6,7 +6,6 @@ from arkindex_common.enums import MetaType
from arkindex.documents.models import Element, ElementType, MetaData, Classification
from arkindex.images.models import Image, Zone
from arkindex.project.tools import sslify_url, build_absolute_url
import urllib.parse
class ImageResourceManifestSerializer(serializers.BaseSerializer):
......@@ -78,7 +77,7 @@ class ElementCanvasManifestSerializer(serializers.BaseSerializer):
label = '{} ; {}'.format(label, element.best_classification)
return {
"@id": build_absolute_url(element, self.context['request'], 'api:canvas-manifest'),
"@id": build_absolute_url(element, self.context['request'], 'api:iiif-canvas'),
"@type": "sc:Canvas",
"label": label,
"height": zone.polygon.height,
......@@ -87,7 +86,7 @@ class ElementCanvasManifestSerializer(serializers.BaseSerializer):
{
"@type": "oa:Annotation",
"resource": ImageResourceManifestSerializer(zone.image, context=self.context).data,
"on": build_absolute_url(element, self.context['request'], 'api:canvas-manifest'),
"on": build_absolute_url(element, self.context['request'], 'api:iiif-canvas'),
"motivation": "sc:painting"
}
],
......@@ -101,48 +100,22 @@ class PageCanvasManifestSerializer(ElementCanvasManifestSerializer):
"""
def get_other_content(self, page):
annotation_list_endpoint, annotation_list_name = \
("api:page-transcription-manifest", "Transcriptions") if settings.IIIF_TRANSCRIPTION_LIST \
else ("api:page-act-manifest", "Actes")
return [
{
"@id": build_absolute_url(page, self.context['request'], annotation_list_endpoint),
"@id": build_absolute_url(page, self.context['request'], 'api:element-annotation-list'),
"@type": "sc:AnnotationList",
"label": annotation_list_name
"label": "Transcriptions",
}
]
class ActPageCanvasManifestSerializer(PageCanvasManifestSerializer):
class FolderManifestSerializer(serializers.BaseSerializer):
"""
Serialize a page into a IIIF canvas with annotation lists for surfaces
Serialize a folder into a IIIF manifest
"""
def get_other_content(self, page):
assert hasattr(page, 'act')
query = self.context['request'].query_params.get('q')
suffix = '?' + urllib.parse.urlencode({'q': query}) if query else ''
return [
{
"@id": build_absolute_url(surface, self.context['request'], 'api:surface-manifest') + suffix,
"@type": "sc:AnnotationList",
"label": surface.name
}
for surface in Element.objects.get_descending(
page.act.id,
type=ElementType.Surface,
zone__image_id=page.zone.image_id
)
]
class ManifestSerializer(serializers.BaseSerializer):
"""
Serialize an element into a IIIF manifest
"""
canvas_serializer = ElementCanvasManifestSerializer
id_url_name = 'api:volume-manifest'
canvas_serializer = PageCanvasManifestSerializer
id_url_name = 'api:folder-manifest'
def to_representation(self, element):
assert isinstance(element, Element)
......@@ -170,13 +143,21 @@ class ManifestSerializer(serializers.BaseSerializer):
{
"canvases": canvases,
"label": "",
"@id": build_absolute_url(element, self.context['request'], 'api:sequence-manifest'),
"@id": build_absolute_url(element, self.context['request'], 'api:iiif-sequence'),
"@type": "sc:Sequence"
}
],
"viewingHint": "individuals",
"label": element.name,
"viewingDirection": "left-to-right",
"service": [
{
"@context": settings.IIIF_SEARCH_CONTEXT,
"@id": build_absolute_url(element, self.context['request'], 'api:iiif-search'),
"profile": settings.IIIF_SEARCH_SERVICE_PROFILE,
"label": "Search transcriptions",
},
],
"metadata": ManifestMetadataSerializer(
element.metadatas.exclude(type=MetaType.HTML),
context=self.context,
......@@ -185,38 +166,9 @@ class ManifestSerializer(serializers.BaseSerializer):
}
def get_canvases(self, element):
return Element.objects.get_descending(element.id).prefetch_related('zone__image')
def get_structures(self, element, canvases):
return [{
"viewingHint": "top",
"label": element.name,
"@id": build_absolute_url(element, self.context['request'], self.id_url_name),
"ranges": [c['@id'] for c in canvases],
"@type": "sc:Range"
}] + [
{
"canvases": [c['@id']],
"label": c['label'],
"@id": c['@id'],
"within": c['@id'],
"@type": "sc:Range"
}
for c in canvases]
assert isinstance(element, Element) and element.type == ElementType.Volume
class VolumeManifestSerializer(ManifestSerializer):
"""
Serialize a volume into a IIIF manifest
"""
canvas_serializer = PageCanvasManifestSerializer
id_url_name = 'api:volume-manifest'
def get_canvases(self, volume):
assert isinstance(volume, Element) and volume.type == ElementType.Volume
# To get a single classification for each page in a volume, we force Django to use a subquery;
# To get a single classification for each page in a element, we force Django to use a subquery;
# however this only allows fetching a single column. We circumvent this limit using some
# database functions to get both the MLClass name and the confidence at once.
# See https://docs.djangoproject.com/en/2.2/ref/models/database-functions/
......@@ -240,43 +192,28 @@ class VolumeManifestSerializer(ManifestSerializer):
)
return Element.objects \
.get_descending(volume.id) \
.filter(type=ElementType.Page) \
.annotate(best_classification=Subquery(best_classification_subquery)) \
.filter(
paths__path__last=element.id, # Direct children
new_type__folder=False,
new_type__hidden=False,
zone__isnull=False,
).annotate(best_classification=Subquery(best_classification_subquery)) \
.order_by('paths__ordering') \
.prefetch_related('zone__image__server', folio_prefetch)
def to_representation(self, volume):
serialized = super().to_representation(volume)
if 'service' not in serialized:
serialized['service'] = []
serialized['service'].append({
"@context": settings.IIIF_SEARCH_CONTEXT,
"@id": build_absolute_url(volume, self.context['request'], 'api:ts-search-manifest'),
"profile": settings.IIIF_SEARCH_SERVICE_PROFILE,
"label": "Search transcriptions"
})
return serialized
class ActManifestSerializer(ManifestSerializer):
"""
Serialize an act into a IIIF manifest
"""
canvas_serializer = ActPageCanvasManifestSerializer
id_url_name = 'api:act-manifest'
def get_canvases(self, element):
assert element.type == ElementType.Act
image_ids = list(Element.objects
.get_descending(element.id, type=ElementType.Surface)
.values_list('zone__image_id', flat=True))
pages = Element.objects \
.filter(zone__image_id__in=image_ids, type=ElementType.Page) \
.select_related('zone__image__server')
# This query gives unordered pages so we reorder them manually
ordered_pages = sorted(pages, key=lambda p: image_ids.index(p.zone.image_id))
# Add act info for canvas serializer
for p in ordered_pages:
p.act = element
return ordered_pages
def get_structures(self, element, canvases):
return [{
"viewingHint": "top",
"label": element.name,
"@id": build_absolute_url(element, self.context['request'], self.id_url_name),
"ranges": [c['@id'] for c in canvases],
"@type": "sc:Range"
}] + [
{
"canvases": [c['@id']],
"label": c['label'],
"@id": c['@id'],
"within": c['@id'],
"@type": "sc:Range"
}
for c in canvases]
......@@ -70,7 +70,7 @@ class IIIFSearchQuerySerializer(serializers.Serializer):
Search parameters for IIIF transcription search
See https://iiif.io/api/search/1.0/#request-1
"""
q = serializers.CharField(source='query', default=None)
q = serializers.CharField(source='query')
class ElementSearchResultSerializer(serializers.ModelSerializer):
......
......@@ -3,11 +3,11 @@ from rest_framework import status
from arkindex.project.tests import FixtureAPITestCase
class TestPageAnnotationListSerializer(FixtureAPITestCase):
class TestElementAnnotationListSerializer(FixtureAPITestCase):
def test_normal_list(self):
page = self.corpus.elements.get(name='Volume 1, page 1r')
response = self.client.get(reverse('api:page-transcription-manifest', kwargs={'pk': page.id}))
response = self.client.get(reverse('api:element-annotation-list', kwargs={'pk': page.id}))
self.assertEqual(response.status_code, status.HTTP_200_OK)
annotation_list = response.json()
......@@ -42,55 +42,7 @@ class TestPageAnnotationListSerializer(FixtureAPITestCase):
def test_empty_list(self):
# An annotation list with nothing in it
response = self.client.get(reverse('api:page-transcription-manifest', kwargs={
'pk': self.corpus.elements.get(name='Volume 2, page 2r').id
}))
self.assertEqual(response.status_code, status.HTTP_200_OK)
annotation_list = response.json()
self.assertEqual(len(annotation_list['resources']), 0)
class TestPageActAnnotationListSerializer(FixtureAPITestCase):
def test_normal_list(self):
page = self.corpus.elements.get(name='Volume 1, page 1r')
response = self.client.get(reverse('api:page-act-manifest', kwargs={'pk': page.id}))
self.assertEqual(response.status_code, status.HTTP_200_OK)
annotation_list = response.json()
self.assertIn('@context', annotation_list)
self.assertIn('@id', annotation_list)
self.assertIn('@type', annotation_list)
self.assertIn('resources', annotation_list)
self.assertEqual(annotation_list['@type'], 'sc:AnnotationList')
self.assertEqual(annotation_list['@context'], 'http://iiif.io/api/presentation/2/context.json')
self.assertEqual(len(annotation_list['resources']), 2)
for annotation in annotation_list['resources']:
self.assertIn('@type', annotation)
self.assertIn('@id', annotation)
self.assertIn('on', annotation)
self.assertIn('motivation', annotation)
self.assertIn('resource', annotation)
self.assertEqual(annotation['@type'], 'oa:Annotation')
self.assertEqual(annotation['motivation'], 'sc:painting')
resource = annotation['resource']
self.assertIn('@id', resource)
self.assertIn('@type', resource)
self.assertIn('format', resource)
self.assertIn('chars', resource)
self.assertEqual(resource['format'], 'text/plain')
self.assertEqual(resource['@type'], 'cnt:ContentAsText')
self.assertIn(resource['chars'], ['Surface A', 'Surface B'])
def test_empty_list(self):
# An annotation list with nothing in it
response = self.client.get(reverse('api:page-act-manifest', kwargs={
response = self.client.get(reverse('api:element-annotation-list', kwargs={
'pk': self.corpus.elements.get(name='Volume 2, page 2r').id
}))
self.assertEqual(response.status_code, status.HTTP_200_OK)
......
......@@ -6,8 +6,8 @@ from arkindex.project.tests import FixtureAPITestCase
from arkindex.documents.models import Element, ElementType, DataSource
class TestVolumeManifestSerializer(FixtureAPITestCase):
"""Tests for VolumeManifestSerializer class"""
class TestFolderManifestSerializer(FixtureAPITestCase):
"""Tests for FolderManifestSerializer class"""
@classmethod
def setUpTestData(cls):
......@@ -20,7 +20,7 @@ class TestVolumeManifestSerializer(FixtureAPITestCase):
cls.page = Element.objects.get(type=ElementType.Page, name='Volume 1, page 1r')
def test_normal_manifest(self):
response = self.client.get(reverse('api:volume-manifest', kwargs={'pk': self.vol.id}))
response = self.client.get(reverse('api:folder-manifest', kwargs={'pk': self.vol.id}))
self.assertEqual(response.status_code, status.HTTP_200_OK)
manifest = response.json()
......@@ -92,7 +92,7 @@ class TestVolumeManifestSerializer(FixtureAPITestCase):
def test_no_page(self):
# A manifest for an empty volume
response = self.client.get(reverse('api:volume-manifest', kwargs={
response = self.client.get(reverse('api:folder-manifest', kwargs={
'pk': self.corpus.elements.create(
type=ElementType.Volume,
new_type=self.corpus.himanis_volume,
......@@ -111,7 +111,7 @@ class TestVolumeManifestSerializer(FixtureAPITestCase):
def test_normal_manifest_iiif_validation(self):
vol = Element.objects.get(type=ElementType.Volume, name="Volume 1")
response = self.client.get(reverse('api:volume-manifest', kwargs={'pk': vol.id}))
response = self.client.get(reverse('api:folder-manifest', kwargs={'pk': vol.id}))
self.assertEqual(response.status_code, status.HTTP_200_OK)
manifest = response.json()
iv = IIIFValidator()
......@@ -126,7 +126,7 @@ class TestVolumeManifestSerializer(FixtureAPITestCase):
self.page.classifications.create(ml_class=text_class, confidence=0.42, source=source)
self.page.classifications.create(ml_class=cover_class, confidence=0.12, source=source)
response = self.client.get(reverse('api:volume-manifest', kwargs={'pk': self.vol.id}))
response = self.client.get(reverse('api:folder-manifest', kwargs={'pk': self.vol.id}))
self.assertEqual(response.status_code, status.HTTP_200_OK)
manifest = response.json()
......
......@@ -175,7 +175,7 @@ class TestSearchAPI(FixtureAPITestCase):
list(map(self.make_transcription_hit, unfiltered))
)
response = self.client.get(reverse('api:ts-search-manifest', kwargs={'pk': str(vol.id)}), {'q': 'paris'})
response = self.client.get(reverse('api:iiif-search', kwargs={'pk': str(vol.id)}), {'q': 'paris'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()
......
......@@ -16,10 +16,7 @@ from arkindex.documents.api.entities import (
CorpusMLClassList, TranscriptionEntityCreate, TranscriptionEntities, ElementEntities,
ElementLinks
)
from arkindex.documents.api.iiif import (
VolumeManifest, ActManifest, PageAnnotationList, PageActAnnotationList, SurfaceAnnotationList,
TranscriptionSearchAnnotationList,
)
from arkindex.documents.api.iiif import FolderManifest, ElementAnnotationList, TranscriptionSearchAnnotationList
from arkindex.dataimport.api import (
DataImportsList, DataImportDetails, DataImportRetry, DataImportDemo,
DataFileList, DataFileRetrieve, DataFileUpload, DataImportFromFiles,
......@@ -68,33 +65,25 @@ api = [
path('classifications/<uuid:pk>/reject/', ClassificationReject.as_view(), name='classification-reject'),
# Manifests
path('manifest/<uuid:pk>/pages/', VolumeManifest.as_view(), name='volume-manifest'),
path('manifest/<uuid:pk>/act/', ActManifest.as_view(), name='act-manifest'),
path('manifest/<uuid:pk>/transcriptions/', PageAnnotationList.as_view(), name='page-transcription-manifest'),
path('manifest/<uuid:pk>/acts/', PageActAnnotationList.as_view(), name='page-act-manifest'),
path('manifest/<uuid:pk>/surface/', SurfaceAnnotationList.as_view(), name='surface-manifest'),
path('manifest/<uuid:pk>/search/', TranscriptionSearchAnnotationList.as_view(), name='ts-search-manifest'),
path('iiif/<uuid:pk>/manifest/', FolderManifest.as_view(), name='folder-manifest'),
path('iiif/<uuid:pk>/list/transcriptions/', ElementAnnotationList.as_view(), name='element-annotation-list'),
path('iiif/<uuid:pk>/search/', TranscriptionSearchAnnotationList.as_view(), name='iiif-search'),
# Placeholder URLs for IIIF IDs
path(
'manifest/<uuid:pk>/sequence/',
RedirectView.as_view(pattern_name='api:volume-manifest', permanent=False),
name='sequence-manifest',
),
path(
'manifest/<uuid:pk>/canvas/',
RedirectView.as_view(pattern_name='api:page-act-manifest', permanent=False),
name='canvas-manifest',
'iiif/<uuid:pk>/sequence/',
RedirectView.as_view(pattern_name='api:folder-manifest', permanent=False),
name='iiif-sequence',
),
path(
'manifest/<uuid:page_pk>/transcriptions/<uuid:transcription_pk>/',
RedirectView.as_view(pattern_name='api:page-transcription-manifest', permanent=False),
name='transcription-manifest',
'iiif/<uuid:pk>/canvas/',
RedirectView.as_view(pattern_name='api:element-annotation-list', permanent=False),
name='iiif-canvas',
),
path(
'manifest/<uuid:page_pk>/surfaces/<uuid:surface_pk>/',
RedirectView.as_view(pattern_name='api:page-act-manifest', permanent=False),
name='surface-manifest',
'iiif/<uuid:page_pk>/annotation/<uuid:transcription_pk>/',
RedirectView.as_view(pattern_name='api:element-annotation-list', permanent=False),
name='transcription-annotation',
),
# Search engines
......
......@@ -262,9 +262,6 @@ IIIF_SEARCH_CONTEXT = "http://iiif.io/api/search/0/context.json"
IIIF_IMAGE_SERVICE_PROFILE = "http://iiif.io/api/image/2/level2.json"
IIIF_SEARCH_SERVICE_PROFILE = "http://iiif.io/api/search/0/search"
# Set to True to show transcription annotation lists instead of act annotation lists
IIIF_TRANSCRIPTION_LIST = False
# IIIF manifest download timeout
# See http://docs.python-requests.org/en/master/user/advanced/#timeouts
IIIF_DOWNLOAD_TIMEOUT = (30, 60)
......
......@@ -663,48 +663,6 @@ paths:
description: Retry a data import. Can only be used on imports with Error or Failed states.
tags:
- imports
/api/v1/manifest/{id}/act/:
get:
operationId: RetrieveActManifest
description: Retrieve a IIIF manifest for an act
security: []
tags:
- iiif
/api/v1/manifest/{id}/acts/:
get:
operationId: RetrieveActAnnotationList
description: Retrieve an IIIF annotation list for a volume's surfaces and acts
security: []
tags:
- iiif
/api/v1/manifest/{id}/pages/:
get:
description: Retrieve an IIIF manifest for a volume.
security: []
tags:
- iiif
/api/v1/manifest/{id}/search/:
get:
operationId: SearchTranscriptionsAnnotationList
description: >-
Search for transcriptions on a volume manifest.
This endpoint is intended as a IIIF Search API 2.0 service.
security: []
tags:
- iiif
/api/v1/manifest/{id}/surface/:
get:
description: Retrieve an IIIF annotation list for a single surface
security: []
tags:
- iiif
/api/v1/manifest/{id}/transcriptions/:
get:
operationId: RetrieveTranscriptionAnnotationList
description: Retrieve an IIIF annotation list for a volume's transcriptions
security: []
tags:
- iiif
/api/v1/ml-classes/:
get:
operationId: ListMLClasses
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment