diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9a7d4cf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: + - develop + - main +env: + DJANGO_ENV: test + +jobs: + pytest: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install Python dependencies + run: | + cd test_app + pip install --upgrade pip + pip install -r requirements.txt + + - name: Run migrations + run: | + cd test_app + python manage.py makemigrations readux_ingest_ecds + python manage.py migrate + + - name: Run Tests + run: | + cd test_app + pytest tests/ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..5686a71 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE +include README.md +prune test* diff --git a/README.md b/README.md index 6e87f3c..b7fd3fc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,23 @@ # readux-ingest-ecds + Django app for Readux ingest specific to ECDS' infrastructure + +## Install + +bash~~~ +pip ... +~~~ + +### Make and run migrations + +bash~~~ +python manage.py makemigrations readux_ingest_ecds +python manage.py migrate +~~~ + +## Settings + +- IIIF_APPS +- INGEST_TMP_DIR +- INGEST_PROCESSING_DIR +- INGEST_OCR_DIR diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c515c82 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ['setuptools>=40.8.0'] +build-backend = 'setuptools.build_meta' \ No newline at end of file diff --git a/readux_ingest_ecds/__init__.py b/readux_ingest_ecds/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/readux_ingest_ecds/admin.py b/readux_ingest_ecds/admin.py new file mode 100644 index 0000000..3d6529f --- /dev/null +++ b/readux_ingest_ecds/admin.py @@ -0,0 +1,35 @@ +import os +import logging +from django.contrib import admin +from django.urls import reverse +from django.utils.html import format_html +from django.shortcuts import redirect +from django_celery_results.models import TaskResult +from .models import Local +from .tasks import local_ingest_task + +LOGGER = logging.getLogger(__name__) + +class LocalAdmin(admin.ModelAdmin): + """Django admin ingest.models.local resource.""" + fields = ('bundle', 'image_server', 'collections') + show_save_and_add_another = False + + def save_model(self, request, obj, form, change): + LOGGER.info(f'INGEST: Local ingest - {obj.id} - started by {request.user.username}') + obj.creator = request.user + super().save_model(request, obj, form, change) + if os.environ["DJANGO_ENV"] != 'test': # pragma: no cover + local_ingest_task.apply_async(args=[obj.id]) + else: + local_ingest_task(obj.id) + + def response_add(self, request, obj, post_url_continue=None): + obj.refresh_from_db() + manifest_id = obj.manifest.id + return redirect('/admin/manifests/manifest/{m}/change/'.format(m=manifest_id)) + + class Meta: # pylint: disable=too-few-public-methods, missing-class-docstring + model = Local + +admin.site.register(Local, LocalAdmin) diff --git a/readux_ingest_ecds/apps.py b/readux_ingest_ecds/apps.py new file mode 100644 index 0000000..57c2748 --- /dev/null +++ b/readux_ingest_ecds/apps.py @@ -0,0 +1,9 @@ +"""Configuration for :class:`apps.ingest`""" +from django.apps import AppConfig + + +class ReaduxIngestEcdsConfig(AppConfig): + """Ingest config""" + name = 'readux_ingest_ecds' + verbose_name = 'Readux Ingest ECDS' + label = 'readux_ingest_ecds' diff --git a/readux_ingest_ecds/celery.py b/readux_ingest_ecds/celery.py new file mode 100644 index 0000000..3ed235c --- /dev/null +++ b/readux_ingest_ecds/celery.py @@ -0,0 +1,16 @@ +""" +Celery config for ingest tasks. +""" +import os +from celery import Celery +from django.conf import settings +# import config.settings.local as settings + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local') +app = Celery('readux_ingest_ecds', result_extended=True) + +# Using a string here means the worker will not have to +# pickle the object when using Windows. +app.config_from_object('django.conf:settings') +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) diff --git a/readux_ingest_ecds/forms.py b/readux_ingest_ecds/forms.py new file mode 100644 index 0000000..ef43df6 --- /dev/null +++ b/readux_ingest_ecds/forms.py @@ -0,0 +1,11 @@ +from django import forms +from django.forms import ClearableFileInput +from .models import Bulk + +class BulkVolumeUploadForm(forms.ModelForm): + class Meta: + model = Bulk + fields = ['image_server', 'volume_files', 'collections'] + widgets = { + 'volume_files': ClearableFileInput(attrs={'allow_multiple_selected': True}), + } diff --git a/readux_ingest_ecds/helpers.py b/readux_ingest_ecds/helpers.py new file mode 100644 index 0000000..d45f0e3 --- /dev/null +++ b/readux_ingest_ecds/helpers.py @@ -0,0 +1,21 @@ +from django.conf import settings +from django.apps import apps +from django.core.exceptions import AppRegistryNotReady + +def get_iiif_models(): + try: + return { + 'Manifest': apps.get_model(settings.IIIF_MANIFEST_MODEL), + 'ImageServer': apps.get_model(settings.IIIF_IMAGE_SERVER_MODEL), + 'RelatedLink': apps.get_model(settings.IIIF_RELATED_LINK_MODEL), + 'Canvas': apps.get_model(settings.IIIF_CANVAS_MODEL), + 'Collection': apps.get_model(settings.IIIF_COLLECTION_MODEL), + } + except AppRegistryNotReady: + return { + 'Manifest': settings.IIIF_MANIFEST_MODEL, + 'ImageServer': settings.IIIF_IMAGE_SERVER_MODEL, + 'RelatedLink': settings.IIIF_RELATED_LINK_MODEL, + 'Canvas': settings.IIIF_CANVAS_MODEL, + 'Collection': settings.IIIF_COLLECTION_MODEL, + } diff --git a/readux_ingest_ecds/mail.py b/readux_ingest_ecds/mail.py new file mode 100644 index 0000000..89c3310 --- /dev/null +++ b/readux_ingest_ecds/mail.py @@ -0,0 +1,68 @@ +from traceback import format_tb +from django.urls.base import reverse +from django.template.loader import get_template +from django.conf import settings +from django.core.mail import send_mail + +def send_email_on_failure(task_watcher=None, exception=None, traceback=None): + """Function to send an email on task failure signal from Celery. + + :param task_watcher: The task watcher object + :type task_watcher: app.ingest.models.TaskWatcher + :param exception: Exception instance raised + :type exception: Exception + :param traceback: Stack trace object + :type traceback: traceback + """ + context = {} + if task_watcher is not None: + context['filename'] = task_watcher.filename + if exception is not None: + context['exception'] = exception.__repr__() + if traceback is not None: + context['traceback'] = '\n'.join(format_tb(traceback)) + context['result_url'] = settings.HOSTNAME + reverse( + "admin:%s_%s_change" + % ( + task_watcher.task_result._meta.app_label, + task_watcher.task_result._meta.model_name, + ), + args=[task_watcher.task_result.id], + ) + html_email = get_template('ingest_failure_email.html').render(context) + text_email = get_template('ingest_failure_email.txt').render(context) + if task_watcher is not None and task_watcher.task_creator is not None: + send_mail( + '[Readux] Failed: Ingest ' + task_watcher.filename, + text_email, + settings.READUX_EMAIL_SENDER, + [task_watcher.task_creator.email], + fail_silently=False, + html_message=html_email + ) + +def send_email_on_success(task_watcher=None): + context = {} + if task_watcher is not None: + context['filename'] = task_watcher.filename + if task_watcher is not None and task_watcher.associated_manifest is not None: + context['manifest_url'] = settings.HOSTNAME + reverse( + 'admin:manifests_manifest_change', args=(task_watcher.associated_manifest.id,) + ) + context['manifest_pid'] = task_watcher.associated_manifest.pid + context['volume_url'] = task_watcher.associated_manifest.get_volume_url() + else: + context['manifests_list_url'] = settings.HOSTNAME + reverse( + 'admin:manifests_manifest_changelist' + ) + html_email = get_template('ingest_success_email.html').render(context) + text_email = get_template('ingest_success_email.txt').render(context) + if task_watcher is not None and task_watcher.task_creator is not None: + send_mail( + '[Readux] Ingest complete: ' + task_watcher.filename, + text_email, + settings.READUX_EMAIL_SENDER, + [task_watcher.task_creator.email], + fail_silently=False, + html_message=html_email + ) \ No newline at end of file diff --git a/readux_ingest_ecds/models.py b/readux_ingest_ecds/models.py new file mode 100644 index 0000000..a9d5bb5 --- /dev/null +++ b/readux_ingest_ecds/models.py @@ -0,0 +1,189 @@ +import os +import logging +from zipfile import ZipFile +from shutil import move +from django.core.files.storage import FileSystemStorage +from django.db import models +from django.conf import settings +from .services import is_image, is_ocr, is_junk, metadata_from_file, create_manifest, move_image_file, move_ocr_file, canvas_dimensions +from .helpers import get_iiif_models + +Manifest = get_iiif_models()['Manifest'] +ImageServer = get_iiif_models()['ImageServer'] +Collection = get_iiif_models()['Collection'] + +LOGGER = logging.getLogger(__name__) + +tmp_storage = FileSystemStorage( + location=settings.INGEST_TMP_DIR +) + +class IngestAbstractModel(models.Model): + metadata = models.JSONField(default=dict, blank=True) + manifest = models.ForeignKey( + Manifest, + on_delete=models.DO_NOTHING, + null=True, + related_name='ecds_ingest_manifest' + ) + image_server = models.ForeignKey( + ImageServer, + on_delete=models.DO_NOTHING, + null=True, + related_name='ecds_ingest_image_server' + ) + creator = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + related_name='ecds_ingest_created_locals' + ) + collections = models.ManyToManyField( + Collection, + blank=True, + help_text="Optional: Collections to attach to the volume ingested in this form.", + related_name='ecds_ingest_collections' + ) + + class Meta: # pylint: disable=too-few-public-methods, missing-class-docstring + abstract = True + +class Local(IngestAbstractModel): + bundle = models.FileField( + null=True, + blank=True, + storage=tmp_storage + ) + + class Meta: + verbose_name_plural = 'Local' + + @property + def tmp_directory(self): + return os.path.join( + settings.INGEST_TMP_DIR, + self.manifest.pid + ) + + @property + def ocr_directory(self): + target_directory = os.path.join(settings.INGEST_OCR_DIR, self.manifest.pid) + os.makedirs(target_directory, exist_ok=True) + return target_directory + + @property + def trigger_file(self): + return os.path.join(settings.INGEST_TMP_DIR, f'{self.manifest.pid}.csv') + + def process(self): + """ + Open metadata + Create manifest + Unzip bundle + """ + os.makedirs(settings.INGEST_TMP_DIR, exist_ok=True) + os.makedirs(settings.INGEST_PROCESSING_DIR, exist_ok=True) + os.makedirs(settings.INGEST_OCR_DIR, exist_ok=True) + self.open_metadata() + self.manifest = create_manifest(self) + self.save() + + + def unzip_bundle(self): + open(self.trigger_file, 'a').close() + + with ZipFile(self.bundle, 'r') as zip_ref: + for member in zip_ref.infolist(): + file_name = member.filename + + if is_junk(os.path.basename(file_name)): + continue + + file_path = os.path.join( + settings.INGEST_TMP_DIR, + file_name + ) + + if is_image(file_name): + zip_ref.extract( + member=member, + path=settings.INGEST_TMP_DIR + ) + + file_to_process = move_image_file(self, file_path) + with open(self.trigger_file, 'a') as t_file: + t_file.write(f'{file_to_process}\n') + + elif is_ocr(file_name): + zip_ref.extract( + member=member, + path=settings.INGEST_TMP_DIR + ) + + move_ocr_file(self, file_path) + + def open_metadata(self): + if bool(self.metadata): + return + + metadata_file = None + + with ZipFile(self.bundle, 'r') as zip_ref: + for member in zip_ref.infolist(): + file_name = member.filename + + if is_junk(os.path.basename(file_name)): + continue + + if is_image(file_name): + continue + + if os.path.splitext(os.path.basename(file_name))[0] == 'metadata': + metadata_file = os.path.join( + settings.INGEST_TMP_DIR, + file_name + ) + zip_ref.extract( + member=member, + path=settings.INGEST_TMP_DIR + ) + + if metadata_file is None or os.path.exists(metadata_file) is False: + return + + self.metadata = metadata_from_file(metadata_file) + + def create_canvases(self): + Canvas = get_iiif_models()['Canvas'] + images = None + with open(self.trigger_file, 'r') as t_file: + images =t_file.read().splitlines() + images.sort() + + for index, image in enumerate(images): + position = index + 1 + image_name = os.path.splitext(image)[0] + canvas_pid = f'{image_name}.tiff' + width, height = canvas_dimensions(image_name) + try: + ocr_file = [ocr for ocr in os.listdir(settings.INGEST_OCR_DIR) if image_name in ocr][0] + ocr_file_path = os.path.abspath(os.path.join(settings.INGEST_OCR_DIR, ocr_file)) + except IndexError: + ocr_file_path = None + + Canvas.objects.get_or_create( + manifest=self.manifest, + pid=canvas_pid, + ocr_file_path=ocr_file_path, + position=position, + width=width, + height=height + ) + + def ingest(self): + self.process() + LOGGER.info(f'INGEST: Local ingest - {self.id} - saved for {self.manifest.pid}') + self.unzip_bundle() + self.create_canvases() + self.delete() + LOGGER.info(f'INGEST: Local ingest - {self.id} - finished for {self.manifest.pid}') diff --git a/readux_ingest_ecds/services.py b/readux_ingest_ecds/services.py new file mode 100644 index 0000000..98ad3ad --- /dev/null +++ b/readux_ingest_ecds/services.py @@ -0,0 +1,353 @@ +""" Module of service classes and methods for ingest. """ +import itertools +import os +from shutil import move +from PIL import Image +from boto3 import resource +from tablib.core import Dataset +from mimetypes import guess_type +from urllib.parse import unquote, urlparse + +from django.conf import settings + +from .helpers import get_iiif_models + +Manifest = get_iiif_models()['Manifest'] +RelatedLink = get_iiif_models()['RelatedLink'] + +def clean_metadata(metadata): + """Remove keys that do not align with Manifest fields. + + :param metadata: + :type metadata: tablib.Dataset + :return: Dictionary with keys matching Manifest fields + :rtype: dict + """ + metadata = {key.casefold().replace(' ', '_'): value for key, value in metadata.items()} + fields = [f.name for f in get_iiif_models()['Manifest']._meta.get_fields()] + invalid_keys = [] + + for key in metadata.keys(): + if key != 'metadata' and isinstance(metadata[key], list): + if isinstance(metadata[key][0], dict): + for meta_key in metadata[key][0].keys(): + if 'value' in meta_key: + metadata[key] = metadata[key][0][meta_key] + else: + metadata[key] = ', '.join(metadata[key]) + if key not in fields: + invalid_keys.append(key) + + for invalid_key in invalid_keys: + metadata.pop(invalid_key) + + return metadata + +def create_manifest(ingest): + """ + Create or update a Manifest from supplied metadata and images. + :return: New or updated Manifest with supplied `pid` + :rtype: iiif.manifest.models.Manifest + """ + Manifest = get_iiif_models()['Manifest'] + manifest = None + # Make a copy of the metadata so we don't extract it over and over. + try: + if not bool(ingest.manifest) or ingest.manifest is None: + ingest.open_metadata() + + metadata = dict(ingest.metadata) + except TypeError: + metadata = None + if metadata: + if 'pid' in metadata: + manifest, created = Manifest.objects.get_or_create(pid=metadata['pid'].replace('_', '-')) + else: + manifest = Manifest.objects.create() + for (key, value) in metadata.items(): + setattr(manifest, key, value) + else: + manifest = Manifest() + + manifest.image_server = ingest.image_server + + # This was giving me a 'django.core.exceptions.AppRegistryNotReady: Models aren't loaded yet' error. + # Remote = apps.get_model('ingest.remote') + + # Ensure that manifest has an ID before updating the M2M relationship + manifest.save() + # if not isinstance(ingest, Remote): + manifest.refresh_from_db() + manifest.collections.set(ingest.collections.all()) + # Save again once relationship is set + manifest.save() + + # if type(ingest, .models.Remote): + # if isinstance(ingest, Remote): + # RelatedLink( + # manifest=manifest, + # link=ingest.remote_url, + # format='application/ld+json' + # ).save() + + return manifest + +def extract_image_server(canvas): + """Determines the IIIF image server URL for a given IIIF Canvas + + :param canvas: IIIF Canvas + :type canvas: dict + :return: IIIF image server URL + :rtype: str + """ + url = urlparse(canvas['images'][0]['resource']['service']['@id']) + parts = url.path.split('/') + parts.pop() + base_path = '/'.join(parts) + host = url.hostname + if url.port is not None: + host = '{h}:{p}'.format(h=url.hostname, p=url.port) + return '{s}://{h}{p}'.format(s=url.scheme, h=host, p=base_path) + +def parse_iiif_v2_manifest(data): + """Parse IIIF Manifest based on v2.1.1 or the presentation API. + https://iiif.io/api/presentation/2.1 + + :param data: IIIF Presentation v2.1.1 manifest + :type data: dict + :return: Extracted metadata + :rtype: dict + """ + properties = {} + manifest_data = [] + + if 'metadata' in data: + manifest_data.append({ 'metadata': data['metadata'] }) + + for iiif_metadata in [{prop['label']: prop['value']} for prop in data['metadata']]: + properties.update(iiif_metadata) + + # Sometimes, the label appears as a list. + if 'label' in data.keys() and isinstance(data['label'], list): + data['label'] = ' '.join(data['label']) + + manifest_data.extend([{prop: data[prop]} for prop in data if isinstance(data[prop], str)]) + + for datum in manifest_data: + properties.update(datum) + + uri = urlparse(data['@id']) + + if not uri.query: + properties['pid'] = uri.path.split('/')[-2] + else: + properties['pid'] = uri.query + + if 'description' in data.keys(): + if isinstance(data['description'], list): + if isinstance(data['description'][0], dict): + en = [lang['@value'] for lang in data['description'] if lang['@language'] == 'en'] + properties['summary'] = data['description'][0]['@value'] if not en else en[0] + else: + properties['summary'] = data['description'][0] + else: + properties['summary'] = data['description'] + + if 'logo' in properties: + properties['logo_url'] = properties['logo'] + properties.pop('logo') + + manifest_metadata = clean_metadata(properties) + + return manifest_metadata + +def parse_iiif_v2_canvas(canvas): + """ """ + canvas_id = canvas['@id'].split('/') + pid = canvas_id[-1] if canvas_id[-1] != 'canvas' else canvas_id[-2] + + service = urlparse(canvas['images'][0]['resource']['service']['@id']) + resource = unquote(service.path.split('/').pop()) + + summary = canvas['description'] if 'description' in canvas.keys() else '' + label = canvas['label'] if 'label' in canvas.keys() else '' + return { + 'pid': pid, + 'height': canvas['height'], + 'width': canvas['width'], + 'summary': summary, + 'label': label, + 'resource': resource + } + +def get_metadata_from(files): + """ + Find metadata file in uploaded files. + :return: If metadata file exists, returns the values. If no file, returns None. + :rtype: list or None + """ + metadata = None + for file in files: + if metadata is not None: + continue + if 'zip' in guess_type(file.name)[0]: + continue + if 'metadata' in file.name.casefold(): + stream = file.read() + if 'csv' in guess_type(file.name)[0] or 'tab-separated' in guess_type(file.name)[0]: + metadata = Dataset().load(stream.decode('utf-8-sig'), format='csv').dict + else: + metadata = Dataset().load(stream).dict + return metadata + +def metadata_from_file(metadata_file): + format = metadata_file_format(metadata_file) + if format is None: + return + + metadata = None + + if format == 'excel': + with open(metadata_file, 'rb') as fh: + metadata = Dataset().load(fh.read(), format=metadata_file.split('.')[-1]) + else: + with open(metadata_file, 'r', encoding="utf-8-sig") as fh: + metadata = Dataset().load(fh.read(), format=format) + + if metadata is not None: + metadata = clean_metadata(metadata.dict[0]) + + return metadata + +def get_associated_meta(all_metadata, file): + """ + Associate metadata with filename. + :return: If a matching filename is found, returns the row as dict, + with generated pid. Otherwise, returns {}. + :rtype: dict + """ + file_meta = {} + extless_filename = file.name[0:file.name.rindex('.')] + for meta_dict in all_metadata: + for key, val in meta_dict.items(): + if key.casefold() == 'filename': + metadata_found_filename = val + # Match filename column, case-sensitive, against filename + if metadata_found_filename and metadata_found_filename in (extless_filename, file.name): + file_meta = meta_dict + return file_meta + +def lowercase_first_line(iterator): + """Lowercase the first line of a text file (such as the header row of a CSV)""" + return itertools.chain( + # ignore unicode characters, set lowercase, and strip whitespace + [next(iterator).encode('ascii', 'ignore').decode().casefold().strip()], iterator + ) + +def is_image(file_path): + """Check if file is expected type for image files + + :param file_path: Name of file to check + :type file_path: str + :return: Bool if file type is an image. + :rtype: bool + """ + return file_path is not None and 'images' in file_path and 'image' in guess_type(file_path)[0] + +def is_ocr(file_path): + """Check if file is expected type for OCR files + + :param file_path: Name of file to check + :type file_path: str + :return: Bool if file type matches OCR file types. + :rtype: bool + """ + ocr_file_types = ['text', 'xml','json','html', 'hocr', 'tsv'] + return file_path is not None and 'ocr' in file_path and any(file_path.endswith(ocr_type) for ocr_type in ocr_file_types) + +def metadata_file_format(file_path): + """Get format used to read the metadata file + + :param file_path: Name of metadata file + :type file_path: str + :return: Format of metadata file, csv, tsv, excel, or None + :rtype: str, None + """ + if file_path is None: + return None + + file_type = guess_type(file_path)[0] + + if 'csv' in file_type: + return 'csv' + elif 'tab-separated' in file_type: + return 'tsv' + elif 'officedocument' in file_type: + return 'excel' + + return None + +def is_junk(file_path): + """Check if a file should be considered junk + + :param file_path: File name to check + :type file_path: str + :return: True if file name starts with special char + :rtype: bol + """ + return file_path.startswith('.') or file_path.startswith('~') or file_path.startswith('__') or file_path.endswith('/') or file_path == '' + +def move_image_file(ingest, file_path): + """ Move files to directory where they processed. + Add the Manifest pid to the file name if not already there. + + :param ingest: Ingest object + :type ingest: _type_ + :param file_path: Absolute path of tmp file + :type file_path: str + :return: File name file to be processed + :rtype: str + """ + base_name = os.path.basename(file_path) + if ingest.manifest.pid not in base_name: + base_name = f'{ingest.manifest.pid}_{base_name}' + move(file_path, os.path.join(settings.INGEST_PROCESSING_DIR, base_name)) + return base_name + +def move_ocr_file(ingest, file_path): + """ Move OCR file to where it belongs. + + :param ingest: Ingest object + :type ingest: _type_ + :param file_path: Absolute path of tmp file + :type file_path: str + """ + base_name = os.path.basename(file_path) + if ingest.manifest.pid not in base_name: + base_name = f'{ingest.manifest.pid}_{base_name}' + move(file_path, os.path.join(ingest.ocr_directory, base_name)) + +def upload_trigger_file(trigger_file): + """ + Upload trigger file to S3. The file contains a list of images being ingested. + The file will be picked up by an AWS lambda function and the images will be + converted to ptiffs. + + :param trigger_file: Absolute path to trigger file. + :type trigger_file: str + """ + s3 = resource('s3') + s3.Bucket(settings.INGEST_TRIGGER_BUCKET).upload_file(trigger_file, os.path.basename(trigger_file)) + +def canvas_dimensions(image_name): + """Get canvas dimensions + + :param image_name: File name without extension of image file. + :type image_name: str + :return: 2-tuple containing width and height (in pixels) + :rtype: tuple + """ + original_image = [img for img in os.listdir(settings.INGEST_PROCESSING_DIR) if img.startswith(image_name)] + if len(original_image) > 0: + return Image.open(os.path.join(settings.INGEST_PROCESSING_DIR, original_image[0])).size + return (0,0) diff --git a/readux_ingest_ecds/storages.py b/readux_ingest_ecds/storages.py new file mode 100644 index 0000000..b10f47b --- /dev/null +++ b/readux_ingest_ecds/storages.py @@ -0,0 +1,8 @@ +from storages.backends.s3boto3 import S3Boto3Storage + +class TmpStorage(S3Boto3Storage): + bucket_name = 'readux' + location = 'tmp' + +class IngestStorage(S3Boto3Storage): + bucket_name = 'readux-ingest' \ No newline at end of file diff --git a/readux_ingest_ecds/tasks.py b/readux_ingest_ecds/tasks.py new file mode 100644 index 0000000..04916fc --- /dev/null +++ b/readux_ingest_ecds/tasks.py @@ -0,0 +1,31 @@ +# pylint: disable = unused-argument + +""" Common tasks for ingest. """ +from celery import Celery +from django.apps import apps +from django.conf import settings +from .helpers import get_iiif_models + +# Use `apps.get_model` to avoid circular import error. Because the parameters used to +# create a background task have to be serializable, we can't just pass in the model object. +Local = apps.get_model('readux_ingest_ecds.local') # pylint: disable = invalid-name +# Remote = apps.get_model('ingest.remote') +# S3Ingest = apps.get_model('ingest.S3Ingest') +Manifest = get_iiif_models()['Manifest'] + +Canvas = get_iiif_models()['Canvas'] + +app = Celery('readux_ingest_ecds', result_extended=True) +app.config_from_object('django.conf:settings') +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) + +@app.task(name='local_ingest_task', autoretry_for=(Exception,), retry_backoff=True, max_retries=20) +def local_ingest_task(ingest_id): + """Background task to start ingest process. + + :param ingest_id: Primary key for .models.Local object + :type ingest_id: UUID + + """ + local_ingest = Local.objects.get(pk=ingest_id) + local_ingest.ingest() diff --git a/readux_ingest_ecds/templates/.gitkeep b/readux_ingest_ecds/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/readux_ingest_ecds/templates/admin/ingest/local/change_form.html b/readux_ingest_ecds/templates/admin/ingest/local/change_form.html new file mode 100644 index 0000000..1d87df6 --- /dev/null +++ b/readux_ingest_ecds/templates/admin/ingest/local/change_form.html @@ -0,0 +1,41 @@ +{% extends "admin/change_form.html" %} +{% load i18n admin_urls %} +{% load static %} + +{% block extrastyle %} + {{ block.super }} + +{% endblock %} +{% block submit_buttons_bottom %} + +{% endblock %} +{% block admin_change_form_document_ready %} + {{ block.super }} + +{% endblock %} +{% block content %} + {{block.super}} +
loading
+{% endblock %} \ No newline at end of file diff --git a/readux_ingest_ecds/tmp/.gitkeep b/readux_ingest_ecds/tmp/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/readux_ingest_ecds/views.py b/readux_ingest_ecds/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/readux_ingest_ecds/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..7faa55e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,38 @@ +[metadata] +name = readux-ingest-ecds +version = 0.1 +description = A Django app for Readux ingest specific to ECDS infrastructure. +long_description = file: README.md +url = https://github.com/ecds/readux-ingest-ecds +author = Emory Center for Digital Scholarship +author_email = jay.varner@emory.edu +license = Apache-2.0 +classifiers = + Environment :: Web Environment + Framework :: Django + Framework :: Django :: 4.2 # Replace "X.Y" as appropriate + Intended Audience :: Developers + License :: OSI Approved :: Apache-2.0 + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 + Topic :: Internet :: WWW/HTTP + Topic :: Internet :: WWW/HTTP :: Dynamic Content + +[options] +include_package_data = true +packages = find: +python_requires = >=3.8 +install_requires = + Django >= 3.2.0,<4.0 + tablib + tablib[xlsx] + django-celery-results~=2.4.0 + boto3 + Pillow==9.4.0 # wagtail 4.2.4 depends on Pillow<10.0.0 and >=4.0.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5c31d41 --- /dev/null +++ b/setup.py @@ -0,0 +1,5 @@ +from setuptools import setup + +setup( + include_package_data=True +) \ No newline at end of file diff --git a/test_app/fixtures/bundle.zip b/test_app/fixtures/bundle.zip new file mode 100644 index 0000000..6522789 Binary files /dev/null and b/test_app/fixtures/bundle.zip differ diff --git a/test_app/fixtures/bundle_with_junk.zip b/test_app/fixtures/bundle_with_junk.zip new file mode 100644 index 0000000..22f61f6 Binary files /dev/null and b/test_app/fixtures/bundle_with_junk.zip differ diff --git a/test_app/fixtures/bundle_with_underscores.zip b/test_app/fixtures/bundle_with_underscores.zip new file mode 100644 index 0000000..6e26603 Binary files /dev/null and b/test_app/fixtures/bundle_with_underscores.zip differ diff --git a/test_app/fixtures/csv_meta.zip b/test_app/fixtures/csv_meta.zip new file mode 100644 index 0000000..44025ef Binary files /dev/null and b/test_app/fixtures/csv_meta.zip differ diff --git a/test_app/fixtures/heidelberg.json b/test_app/fixtures/heidelberg.json new file mode 100644 index 0000000..1430de1 --- /dev/null +++ b/test_app/fixtures/heidelberg.json @@ -0,0 +1,1841 @@ +{ + "@context" : "http://iiif.io/api/presentation/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/manifest", + "@type" : "sc:Manifest", + "attribution" : [ + { + "@language" : "de", + "@value" : "Universit\u00e4tsbibliothek Heidelberg" + }, + { + "@language" : "en", + "@value" : "Heidelberg University Library" + } + ], + "description" : [ + { + "@language" : "de", + "@value" : "Nvovi Disegni Dell' Architettvre, E Piante De Palazzi Di Roma De Piv Celebri Architetti; Universit\u00e4tsbibliothek Heidelberg, C 6426-2-10 GROSS RES" + }, + { + "@language" : "en", + "@value" : "Nvovi Disegni Dell' Architettvre, E Piante De Palazzi Di Roma De Piv Celebri Architetti; Universit\u00e4tsbibliothek Heidelberg, C 6426-2-10 GROSS RES" + } + ], + "label" : "Nvovi Disegni Dell' Architettvre, E Piante De Palazzi Di Roma De Piv Celebri Architetti", + "license" : "http://creativecommons.org/publicdomain/mark/1.0/deed.de", + "logo" : "https://www.ub.uni-heidelberg.de/nav4/grafik/layout/ub_logo2.gif", + "metadata" : [ + { + "label" : "Author", + "value" : [ + "Ferrerio, Pietro; Rossi, Giovanni Giacomo de [Editor]" + ] + }, + { + "label" : "Location", + "value" : [ + "[Rom]" + ] + }, + { + "label" : "Date", + "value" : [ + "[1655]" + ] + }, + { + "label" : "Published", + "value" : [ + "[Rom], [1655]" + ] + }, + { + "label" : "Identifier (URN)", + "value" : [ + "http://nbn-resolving.de/urn:nbn:de:bsz:16-diglit-25939" + ] + }, + { + "label" : "Identifier (DOI)", + "value" : [ + "https://doi.org/10.11588/diglit.2593" + ] + }, + { + "label" : "Local Identifier", + "value" : [ + "Cicognara, 3719-2" + ] + }, + { + "label" : "Local Identifier", + "value" : [ + "dcl:d2v" + ] + }, + { + "label" : "Bibliographic Information", + "value" : [ + "http://katalog.ub.uni-heidelberg.de/cgi-bin/search.cgi?query=si:1355847117+-(teil:ezblf)&pos=1" + ] + }, + { + "label" : "Shelfmark", + "value" : [ + "Universit\u00e4tsbibliothek Heidelberg, C 6426-2-10 GROSS RES" + ] + } + ], + "seeAlso" : "https://digi.ub.uni-heidelberg.de/diglit/ferrerio1655bd2/mets", + "sequences" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/sequence/normal", + "@type" : "sc:Sequence", + "canvases" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0001", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0001-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0001", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_001.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_001.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_001", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0002", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0002-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0002", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_002.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_002.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_002", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0003", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0003-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0003", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_003.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_003.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_003", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0004", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0004-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0004", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_004.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_004.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_004", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0005", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0005-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0005", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_005.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_005.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_005", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0006", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0006-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0006", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_006.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_006.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_006", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0007", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0007-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0007", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_007.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_007.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_007", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0008", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0008-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0008", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_008.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_008.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_008", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0009", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0009-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0009", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_009.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_009.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_009", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0010", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0010-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0010", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_010.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_010.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_010", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0011", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0011-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0011", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_011.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_011.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_011", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0012", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0012-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0012", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_012.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_012.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_012", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0013", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0013-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0013", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_013.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_013.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_013", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0014", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0014-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0014", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_014.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_014.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_014", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0015", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0015-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0015", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_015.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_015.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_015", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0016", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0016-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0016", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_016.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_016.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_016", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0017", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0017-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0017", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_017.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_017.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_017", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0018", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0018-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0018", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_018.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_018.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_018", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0019", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0019-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0019", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_019.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_019.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_019", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0020", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0020-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0020", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_020.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_020.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_020", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0021", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0021-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0021", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_021.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_021.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_021", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0022", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0022-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0022", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_022.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_022.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_022", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0023", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0023-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0023", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_023.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_023.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_023", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0024", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0024-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0024", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_024.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_024.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_024", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0025", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0025-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0025", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_025.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_025.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_025", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0026", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0026-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0026", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_026.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_026.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_026", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0027", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0027-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0027", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_027.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_027.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_027", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0028", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0028-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0028", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_028.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_028.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_028", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0029", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0029-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0029", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_029.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_029.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_029", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0030", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0030-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0030", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_030.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_030.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_030", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0031", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0031-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0031", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_031.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_031.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_031", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0032", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0032-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0032", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_032.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_032.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_032", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0033", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0033-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0033", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_033.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_033.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_033", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0034", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0034-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0034", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_034.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_034.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_034", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0035", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0035-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0035", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_035.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_035.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_035", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0036", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0036-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0036", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_036.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_036.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_036", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0037", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0037-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0037", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_037.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_037.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_037", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0038", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0038-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0038", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_038.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_038.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_038", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0039", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0039-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0039", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_039.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_039.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_039", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0040", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0040-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0040", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_040.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_040.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_040", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0041", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0041-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0041", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_041.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_041.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_041", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0042", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0042-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0042", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_042.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_042.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_042", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0043", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0043-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0043", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_043.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_043.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_043", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0044", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0044-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0044", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_044.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_044.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_044", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0045", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0045-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0045", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_045.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_045.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_045", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0046", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0046-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0046", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_046.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_046.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_046", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0047", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0047-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0047", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_047.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_047.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_047", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0048", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0048-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0048", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_048.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_048.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_048", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0049", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0049-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0049", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_049.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_049.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_049", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0050", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0050-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0050", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_050.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_050.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_050", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0051", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0051-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0051", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_051.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_051.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_051", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0052", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0052-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0052", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_052.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_052.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_052", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0053", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0053-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0053", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_053.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_053.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_053", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0054", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0054-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0054", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_054.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_054.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_054", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0055", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0055-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0055", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_055.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_055.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_055", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0056", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0056-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0056", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_056.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_056.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_056", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0057", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0057-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0057", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_057.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_057.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_057", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0058", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0058-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0058", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_058.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_058.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_058", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0059", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0059-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0059", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_059.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_059.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_059", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0060", + "@type" : "sc:Canvas", + "height" : 3729, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0060-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0060", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_060.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3729, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_060.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 5053 + } + } + ], + "label" : "Tafel_060", + "width" : 5053 + }, + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0061", + "@type" : "sc:Canvas", + "height" : 3556, + "images" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/annotation/0061-image", + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "on" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0061", + "resource" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglitData/image/ferrerio1655bd2/4/Tafel_061.jpg", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "height" : 3556, + "service" : { + "@context" : "http://iiif.io/api/image/2/context.json", + "@id" : "https://digi.ub.uni-heidelberg.de/iiif/2/ferrerio1655bd2%3ATafel_061.jpg", + "profile" : "http://iiif.io/api/image/2/profiles/level2.json" + }, + "width" : 4455 + } + } + ], + "label" : "Tafel_061", + "width" : 4455 + } + ], + "label" : [ + { + "@language" : "de", + "@value" : "Standardabfolge" + }, + { + "@language" : "en", + "@value" : "Normal Sequence" + } + ], + "startCanvas" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0001", + "viewingDirection" : "left-to-right", + "viewingHint" : "paged" + } + ], + "service" : "https://digi.ub.uni-heidelberg.de/diglit/ferrerio1655bd2", + "structures" : [ + { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/range/r0", + "@type" : "sc:Range", + "canvases" : [ + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0001", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0002", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0003", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0004", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0005", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0006", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0007", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0008", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0009", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0010", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0011", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0012", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0013", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0014", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0015", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0016", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0017", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0018", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0019", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0020", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0021", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0022", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0023", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0024", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0025", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0026", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0027", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0028", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0029", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0030", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0031", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0032", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0033", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0034", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0035", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0036", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0037", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0038", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0039", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0040", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0041", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0042", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0043", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0044", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0045", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0046", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0047", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0048", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0049", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0050", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0051", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0052", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0053", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0054", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0055", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0056", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0057", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0058", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0059", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0060", + "https://digi.ub.uni-heidelberg.de/diglit/iiif/ferrerio1655bd2/canvas/0061" + ], + "label" : "Tafel 1 - 61" + } + ], + "thumbnail" : { + "@id" : "https://digi.ub.uni-heidelberg.de/diglit/ferrerio1655bd2/introimage", + "format" : "image/jpeg", + "height" : 283, + "width" : 399 + }, + "viewingDirection" : "left-to-right", + "viewingHint" : "paged", + "within" : [ + "https://digi.ub.uni-heidelberg.de/diglit/ferrerio1655ga" + ] +} diff --git a/test_app/fixtures/manifest-label-as-array.json b/test_app/fixtures/manifest-label-as-array.json new file mode 100644 index 0000000..01152bf --- /dev/null +++ b/test_app/fixtures/manifest-label-as-array.json @@ -0,0 +1,232 @@ +{ + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu/manifest.json", + "@type": "sc:Manifest", + "attribution": "The Internet Archive", + "description": "Respect Frederick Douglass.", + "label": ["Address", "by", "American", "Hero", "Frederick", "Douglass"], + "logo": "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcReMN4l9cgu_qb1OwflFeyfHcjp8aUfVNSJ9ynk2IfuHwW1I4mDSw", + "metadata": [ + { + "label": "title", + "value": "Address by Hon. Frederick Douglass, delivered in the Metropolitan A.M.E. Church, Washington, D.C., Tuesday, January 9th, 1894, on the lessons of the hour : in which he discusses the various aspects of the so-called, but mis-called, Negro problem" + }, + { + "label": "publisher", + "value": "Baltimore : Press of Thomas & Evans" + }, + { + "label": "subject", + "value": "African Americans" + }, + { + "label": "date", + "value": "1894" + }, + { + "label": "contributor", + "value": "Emory University, Robert W. Woodruff Library" + }, + { + "label": "creator", + "value": "Douglass, Frederick, 1818-1895. Metropolitan A.M.E. Church (Washington, D.C.)" + } + ], + "related": "http://archive.org/details/09359080.4757.emory.edu", + "seeAlso": "http://archive.org/metadata/09359080.4757.emory.edu", + "sequences": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu/canvas/default", + "@type": "sc:Sequence", + "canvases": [ + { + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$0/canvas", + "@type": "sc:Canvas", + "description": "", + "height": 2908, + "images": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$0/annotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$0/annotation", + "resource": { + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$0/full/full/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2908, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$0", + "profile": "https://iiif.io/api/image/2/profiles/level2.json" + }, + "width": 2039 + } + } + ], + "label": "p. ", + "width": 2039 + }, + { + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$1/canvas", + "@type": "sc:Canvas", + "description": "", + "height": 2908, + "images": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$1/annotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$1/annotation", + "resource": { + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$1/full/full/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2908, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$1", + "profile": "https://iiif.io/api/image/2/profiles/level2.json" + }, + "width": 2039 + } + } + ], + "label": "p. ", + "width": 2039 + }, + { + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$2/canvas", + "@type": "sc:Canvas", + "description": "", + "height": 2908, + "images": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$2/annotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$2/annotation", + "resource": { + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$2/full/full/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2908, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$2", + "profile": "https://iiif.io/api/image/2/profiles/level2.json" + }, + "width": 2039 + } + } + ], + "label": "p. ", + "width": 2039 + }, + { + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$3/canvas", + "@type": "sc:Canvas", + "description": "", + "height": 2909, + "images": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$3/annotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$3/annotation", + "resource": { + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$3/full/full/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2909, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$3", + "profile": "https://iiif.io/api/image/2/profiles/level2.json" + }, + "width": 2040 + } + } + ], + "label": "p. ", + "width": 2040 + }, + { + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$4/canvas", + "@type": "sc:Canvas", + "description": "", + "height": 2908, + "images": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$4/annotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$4/annotation", + "resource": { + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$4/full/full/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2908, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$4", + "profile": "https://iiif.io/api/image/2/profiles/level2.json" + }, + "width": 2039 + } + } + ], + "label": "p. ", + "width": 2039 + }, + { + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$5/canvas", + "@type": "sc:Canvas", + "description": "", + "height": 2909, + "images": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$5/annotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$5/annotation", + "resource": { + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$5/full/full/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2909, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$5", + "profile": "https://iiif.io/api/image/2/profiles/level2.json" + }, + "width": 2040 + } + } + ], + "label": "p. ", + "width": 2040 + } + ], + "label": "default" + } + ], + "thumbnail": { + "@id": "https://ia801601.us.archive.org/BookReader/BookReaderPreview.php?id=09359080.4757.emory.edu&subPrefix=09359080_4757&itemPath=/0/items/09359080.4757.emory.edu&server=ia801601.us.archive.org&page=preview&" + }, + "viewingHint": "paged", + "viewingDirection": "left-to-right" +} \ No newline at end of file diff --git a/test_app/fixtures/manifest.json b/test_app/fixtures/manifest.json new file mode 100644 index 0000000..78e6905 --- /dev/null +++ b/test_app/fixtures/manifest.json @@ -0,0 +1,232 @@ +{ + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu/manifest.json", + "@type": "sc:Manifest", + "attribution": "The Internet Archive", + "description": "Respect Frederick Douglass.", + "label": "Address by Hon. Frederick Douglass", + "logo": "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcReMN4l9cgu_qb1OwflFeyfHcjp8aUfVNSJ9ynk2IfuHwW1I4mDSw", + "metadata": [ + { + "label": "title", + "value": "Address by Hon. Frederick Douglass, delivered in the Metropolitan A.M.E. Church, Washington, D.C., Tuesday, January 9th, 1894, on the lessons of the hour : in which he discusses the various aspects of the so-called, but mis-called, Negro problem" + }, + { + "label": "publisher", + "value": "Baltimore : Press of Thomas & Evans" + }, + { + "label": "subject", + "value": "African Americans" + }, + { + "label": "date", + "value": "1894" + }, + { + "label": "contributor", + "value": "Emory University, Robert W. Woodruff Library" + }, + { + "label": "creator", + "value": "Douglass, Frederick, 1818-1895. Metropolitan A.M.E. Church (Washington, D.C.)" + } + ], + "related": "http://archive.org/details/09359080.4757.emory.edu", + "seeAlso": "http://archive.org/metadata/09359080.4757.emory.edu", + "sequences": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu/canvas/default", + "@type": "sc:Sequence", + "canvases": [ + { + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$0/canvas", + "@type": "sc:Canvas", + "description": "", + "height": 2908, + "images": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$0/annotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$0/annotation", + "resource": { + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$0/full/full/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2908, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$0", + "profile": "https://iiif.io/api/image/2/profiles/level2.json" + }, + "width": 2039 + } + } + ], + "label": "p. ", + "width": 2039 + }, + { + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$1/canvas", + "@type": "sc:Canvas", + "description": "", + "height": 2908, + "images": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$1/annotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$1/annotation", + "resource": { + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$1/full/full/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2908, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$1", + "profile": "https://iiif.io/api/image/2/profiles/level2.json" + }, + "width": 2039 + } + } + ], + "label": "p. ", + "width": 2039 + }, + { + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$2/canvas", + "@type": "sc:Canvas", + "description": "", + "height": 2908, + "images": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$2/annotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$2/annotation", + "resource": { + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$2/full/full/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2908, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$2", + "profile": "https://iiif.io/api/image/2/profiles/level2.json" + }, + "width": 2039 + } + } + ], + "label": "p. ", + "width": 2039 + }, + { + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$3/canvas", + "@type": "sc:Canvas", + "description": "", + "height": 2909, + "images": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$3/annotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$3/annotation", + "resource": { + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$3/full/full/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2909, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$3", + "profile": "https://iiif.io/api/image/2/profiles/level2.json" + }, + "width": 2040 + } + } + ], + "label": "p. ", + "width": 2040 + }, + { + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$4/canvas", + "@type": "sc:Canvas", + "description": "", + "height": 2908, + "images": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$4/annotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$4/annotation", + "resource": { + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$4/full/full/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2908, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$4", + "profile": "https://iiif.io/api/image/2/profiles/level2.json" + }, + "width": 2039 + } + } + ], + "label": "p. ", + "width": 2039 + }, + { + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$5/canvas", + "@type": "sc:Canvas", + "description": "", + "height": 2909, + "images": [ + { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$5/annotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$5/annotation", + "resource": { + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$5/full/full/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2909, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://fake.archivelab.io/iiif/09359080.4757.emory.edu$5", + "profile": "https://iiif.io/api/image/2/profiles/level2.json" + }, + "width": 2040 + } + } + ], + "label": "p. ", + "width": 2040 + } + ], + "label": "default" + } + ], + "thumbnail": { + "@id": "https://ia801601.us.archive.org/BookReader/BookReaderPreview.php?id=09359080.4757.emory.edu&subPrefix=09359080_4757&itemPath=/0/items/09359080.4757.emory.edu&server=ia801601.us.archive.org&page=preview&" + }, + "viewingHint": "paged", + "viewingDirection": "left-to-right" +} \ No newline at end of file diff --git a/test_app/fixtures/metadata.csv b/test_app/fixtures/metadata.csv new file mode 100644 index 0000000..35814bd --- /dev/null +++ b/test_app/fixtures/metadata.csv @@ -0,0 +1,2 @@ +Filename,Label,Summary,Author,Published city,Published date,Publisher +no_meta_file,Test Bundle,Test file,Test author,Test City,2021,Pubilsher test \ No newline at end of file diff --git a/test_app/fixtures/metadata.zip b/test_app/fixtures/metadata.zip new file mode 100644 index 0000000..bee3de7 Binary files /dev/null and b/test_app/fixtures/metadata.zip differ diff --git a/test_app/fixtures/metadata/images/0001.jpg b/test_app/fixtures/metadata/images/0001.jpg new file mode 100644 index 0000000..2cde7f5 Binary files /dev/null and b/test_app/fixtures/metadata/images/0001.jpg differ diff --git a/test_app/fixtures/metadata/metadata.csv b/test_app/fixtures/metadata/metadata.csv new file mode 100644 index 0000000..a29d365 --- /dev/null +++ b/test_app/fixtures/metadata/metadata.csv @@ -0,0 +1,2 @@ +PID,Label,Summary,Author,Published city,Published date,Publisher,PDF +t9wtf-sample,t9wtf-sample,x,x,x,x,x,www.google.com \ No newline at end of file diff --git a/test_app/fixtures/metadata/ocr/0001.tsv b/test_app/fixtures/metadata/ocr/0001.tsv new file mode 100644 index 0000000..24ec7b5 --- /dev/null +++ b/test_app/fixtures/metadata/ocr/0001.tsv @@ -0,0 +1,47 @@ +content x y w h +RACCOLTA 2689 1166 576 71 +M. 2449 1279 287 84 +Valute 2721 1277 199 84 +intiche 2995 1273 203 86 +Moderne 3390 1270 245 84 +3 1878 1476 257 58 +) 1882 1400 256 36 +DELLA 2239 1433 434 80 +CITTA 2752 1430 380 79 +DI 3193 1426 130 83 +ROMA 3392 1425 358 80 +ll 4103 1374 226 130 +E 2629 1600 41 52 +SUE 2723 1599 143 50 +VICINANZE 2898 1598 428 47 +Incise 2231 1698 141 107 +da 2382 1696 61 107 +Mord 2497 1694 128 106 +, 2636 1692 34 108 +Neguaroni 2690 1691 246 104 +, 2929 1686 34 109 +Sarboni 2970 1685 230 105 +, 3193 1682 34 108 +ed 3243 1681 43 108 +altri 3315 1680 124 106 +oletni 3455 1677 175 107 +bulini 3640 1674 142 107 +1867 2917 1812 117 53 +. 3033 1815 9 50 +IN 2790 2629 83 50 +ROMA 2944 2628 258 47 +Presso 2272 2844 139 47 +F. 2431 2843 28 48 +D'ATRI 2469 2843 153 47 +Via 2650 2842 56 47 +Condoffi 2732 2841 161 47 +N. 2910 2840 47 47 +82 2966 2839 41 48 +e 3020 2839 19 48 +Via 3053 2837 60 49 +del 3129 2837 59 49 +Corso 3207 2836 111 49 +NO 3336 2835 41 49 +142.e 3392 2835 95 47 +395.395 3501 2834 161 47 +A. 3672 2833 40 48 diff --git a/test_app/fixtures/metadata/~$metadata.xlsx b/test_app/fixtures/metadata/~$metadata.xlsx new file mode 100644 index 0000000..5a93205 Binary files /dev/null and b/test_app/fixtures/metadata/~$metadata.xlsx differ diff --git a/test_app/fixtures/nested_volume.zip b/test_app/fixtures/nested_volume.zip new file mode 100644 index 0000000..002236f Binary files /dev/null and b/test_app/fixtures/nested_volume.zip differ diff --git a/test_app/fixtures/nga.json b/test_app/fixtures/nga.json new file mode 100644 index 0000000..9f5d453 --- /dev/null +++ b/test_app/fixtures/nga.json @@ -0,0 +1,64 @@ +{ + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://www.nga.gov/content/ngaweb/api/v1/iiif/presentation/manifest.json?cultObj:id=164652", + "@type": "sc:Manifest", + "label": "Autre veue du Campo Vacine", + "description": "Autre veue du Campo Vacine", + "logo": "https://www.nga.gov/etc/designs/ngaweb/images/nga-seal.png", + "attribution": "Provided by National Gallery of Art", + "metadata": [ + { + "label": "Artist", + "value": "Silvestre, Israël " + }, + { + "label": "Accession Number", + "value": "1981.69.12.242" + }, + { + "label": "Creation Year", + "value": "1660" + }, + { + "label": "Title", + "value": "Autre veue du Campo Vacine" + } + ], + "viewingDirection": "left-to-right", + "viewingHint": "individuals", + "sequences": [{ + "@type": "sc:Sequence", + "label": "Autre veue du Campo Vacine", + "canvases": [{ + "@id": "https://cq.nga.gov/content/ngaweb/api/v1/iiif/presentation/manifest/sequence/canvas.json?cultObj:id=164652", + "@type": "sc:Canvas", + "width": 4000, + "height": 2074, + "label": "Autre veue du Campo Vacine", + "images": [{ + "@type": "oa:Annotation", + "motivation": "sc:painting", + "resource": { + "@id": "https://www.nga.gov/content/ngaweb/api/v1/iiif/presentation/manifest/sequence/canvas/resource.json?cultObj:id=164652", + "@type": "dctypes:Image", + "format": "image/jpeg", + "height": 2074, + "width": 4000, + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://media.nga.gov/iiif/cba52a13-d390-4ee4-8965-250593c600ba", + "profile": "http://iiif.io/api/image/2/level2.json" + } + }, + "on": "https://cq.nga.gov/content/ngaweb/api/v1/iiif/presentation/manifest/sequence/canvas.json?cultObj:id=164652" + }], + "thumbnail": { + "@id": "https://media.nga.gov/iiif/cba52a13-d390-4ee4-8965-250593c600ba/full/!100,100/0/default.jpg", + "@type": "dctypes:Image", + "format": "image/jpeg", + "width": 100, + "height": 100 + } + }] + }] + } \ No newline at end of file diff --git a/test_app/fixtures/no_meta_file.zip b/test_app/fixtures/no_meta_file.zip new file mode 100644 index 0000000..8896492 Binary files /dev/null and b/test_app/fixtures/no_meta_file.zip differ diff --git a/test_app/fixtures/princeton.json b/test_app/fixtures/princeton.json new file mode 100644 index 0000000..55d43c9 --- /dev/null +++ b/test_app/fixtures/princeton.json @@ -0,0 +1,147 @@ +{ + "@context": "http://iiif.io/api/presentation/2/context.json", + "@type": "sc:Manifest", + "@id": "https://figgy.princeton.edu/concern/scanned_resources/e38d7ebb-3ae6-4eb8-a3ef-d70b5184b005/manifest", + "label": ["Nvova pianta et alzata della citt"], + "description": ["Con l'aggivunta delle nove"], + "viewingHint": "paged", + "viewingDirection": "left-to-right", + "metadata": [{ + "label": "Author", + "value": ["Falda, Giovanni Battista, approximately 1640-1678"] + }, { + "label": "Alternative", + "value": ["Nuova pianta et alzata della città di Roma con tutte le strade, piazze et edificii de tempi, palazzi, giardini et altre fabbriche antiche e moderne come si trovano al presente nel pontificato di N.S. Papa Innocentio XI con le loro dichiarationi nomi et indice copiosissimo"] + }, { + "label": "Extent", + "value": ["1 map (12 sheets) mounted on cloth ; 138 x 151 cm. in case 54 cm.", "Graphic scales given in Italian miles."] + }, { + "label": "Identifier", + "value": ["\u003ca href='http://arks.princeton.edu/ark:/88435/5d86p0240' alt='Identifier'\u003ehttp://arks.princeton.edu/ark:/88435/5d86p0240\u003c/a\u003e"] + }, { + "label": "Replaces", + "value": ["pudl0023/4166894"] + }, { + "label": "Title", + "value": [{ + "@value": "Nvova pianta et alzata della città di Roma con tvtte le strade, piazze et edificii de tempi, palazzi, giardini et altre fabbriche antiche e moderne come si trovano al presente nel pontificato di N.S. Papa Innocentio XI con le loro dichiarationi nomi et indice copiosissimo / disegniata et intagliata da Gio", + "@language": "ita" + }] + }, { + "label": "Type", + "value": ["Maps", "Maps, Pictorial"] + }, { + "label": "Contributor", + "value": ["Rossi, Giovanni Giacomo de, 1627-1691", "Widman, Georgio"] + }, { + "label": "Creator", + "value": ["Falda, Giovanni Battista, approximately 1640-1678"] + }, { + "label": "Date", + "value": ["1730"] + }, { + "label": "Language", + "value": ["Italian"] + }, { + "label": "Local Identifier", + "value": ["p0p097960k"] + }, { + "label": "Publisher", + "value": ["[Roma] : G. G. de Rossi, 1730."] + }, { + "label": "Subject", + "value": ["Rome (Italy)—Maps—Early works to 1800"] + }, { + "label": "Call Number", + "value": ["Oversize G6714.R7 F342 1730e", "Electronic Resource"] + }, { + "label": "Location", + "value": ["SAX Oversize G6714.R7 F342 1730e", "SAX Electronic Resource", "ELF1 Oversize G6714.R7 F342 1730e", "ELF1 Electronic Resource"] + }, { + "label": "Electronic Locations", + "value": [{ + "id": null, + "internal_resource": "LabeledURI", + "created_at": null, + "updated_at": null, + "new_record": true, + "uri": { + "@id": "https://catalog.princeton.edu/catalog/4166894#view" + }, + "label": "Digital content" + }] + }, { + "label": "Member Of Collections", + "value": ["Marquand Library Selections"] + }], + "sequences": [{ + "@type": "sc:Sequence", + "@id": "https://figgy.princeton.edu/concern/scanned_resources/e38d7ebb-3ae6-4eb8-a3ef-d70b5184b005/manifest/sequence/normal", + "rendering": [{ + "@id": "https://figgy.princeton.edu/concern/scanned_resources/e38d7ebb-3ae6-4eb8-a3ef-d70b5184b005/pdf", + "label": "Download as PDF", + "format": "application/pdf" + }], + "canvases": [{ + "@type": "sc:Canvas", + "@id": "https://figgy.princeton.edu/concern/scanned_resources/e38d7ebb-3ae6-4eb8-a3ef-d70b5184b005/manifest/canvas/1ba0d507-a39d-4720-bb7d-2abe2cfa4b1e", + "label": "Composite view", + "local_identifier": "p1257bv430", + "rendering": [{ + "@id": "https://figgy.princeton.edu/downloads/1ba0d507-a39d-4720-bb7d-2abe2cfa4b1e/file/64074eff-c630-44a8-9733-485e5e0e0f34", + "label": "Download the original file", + "format": "image/tiff" + }], + "width": 6945, + "height": 7200, + "images": [{ + "@type": "oa:Annotation", + "motivation": "sc:painting", + "resource": { + "@type": "dctypes:Image", + "@id": "https://iiif-cloud.princeton.edu/iiif/2/0b%2F21%2F99%2F0b21998720154beaacc8218b5063373d%2Fintermediate_file/full/1000,/0/default.jpg", + "height": 7200, + "width": 6945, + "format": "image/jpeg", + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://iiif-cloud.princeton.edu/iiif/2/0b%2F21%2F99%2F0b21998720154beaacc8218b5063373d%2Fintermediate_file", + "profile": "http://iiif.io/api/image/2/level2.json" + } + }, + "@id": "https://figgy.princeton.edu/concern/scanned_resources/e38d7ebb-3ae6-4eb8-a3ef-d70b5184b005/manifest/image/1ba0d507-a39d-4720-bb7d-2abe2cfa4b1e", + "on": "https://figgy.princeton.edu/concern/scanned_resources/e38d7ebb-3ae6-4eb8-a3ef-d70b5184b005/manifest/canvas/1ba0d507-a39d-4720-bb7d-2abe2cfa4b1e" + }] + }], + "viewingHint": "paged" + }], + "structures": [{ + "@type": "sc:Range", + "@id": "https://figgy.princeton.edu/concern/scanned_resources/e38d7ebb-3ae6-4eb8-a3ef-d70b5184b005/manifest/range/rb5280354-6427-4407-b83e-364733b1a988", + "label": "", + "viewingHint": "top", + "ranges": [], + "canvases": ["https://figgy.princeton.edu/concern/scanned_resources/e38d7ebb-3ae6-4eb8-a3ef-d70b5184b005/manifest/canvas/1ba0d507-a39d-4720-bb7d-2abe2cfa4b1e"] + }], + "seeAlso": [{ + "@id": "https://figgy.princeton.edu/catalog/e38d7ebb-3ae6-4eb8-a3ef-d70b5184b005.jsonld", + "format": "application/ld+json" + }, { + "@id": "https://bibdata.princeton.edu/bibliographic/4166894", + "format": "text/xml" + }], + "license": "http://rightsstatements.org/vocab/NKC/1.0/", + "thumbnail": { + "@id": "https://iiif-cloud.princeton.edu/iiif/2/0b%2F21%2F99%2F0b21998720154beaacc8218b5063373d%2Fintermediate_file/full/!200,150/0/default.jpg", + "service": { + "@context": "http://iiif.io/api/image/2/context.json", + "@id": "https://iiif-cloud.princeton.edu/iiif/2/0b%2F21%2F99%2F0b21998720154beaacc8218b5063373d%2Fintermediate_file", + "profile": "http://iiif.io/api/image/2/level2.json" + } + }, + "rendering": { + "@id": "http://arks.princeton.edu/ark:/88435/5d86p0240", + "format": "text/html" + }, + "logo": "https://figgy.princeton.edu/assets/pul_logo_icon-7b5f9384dfa5ca04f4851c6ee9e44e2d6953e55f893472a3e205e1591d3b2ca6.png" +} \ No newline at end of file diff --git a/test_app/fixtures/single-image.zip b/test_app/fixtures/single-image.zip new file mode 100644 index 0000000..f7ab31f Binary files /dev/null and b/test_app/fixtures/single-image.zip differ diff --git a/test_app/fixtures/tsv.zip b/test_app/fixtures/tsv.zip new file mode 100644 index 0000000..d0a4c02 Binary files /dev/null and b/test_app/fixtures/tsv.zip differ diff --git a/test_app/iiif/__init__.py b/test_app/iiif/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_app/iiif/admin.py b/test_app/iiif/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/test_app/iiif/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/test_app/iiif/apps.py b/test_app/iiif/apps.py new file mode 100644 index 0000000..67a2721 --- /dev/null +++ b/test_app/iiif/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class IiifConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'iiif' diff --git a/test_app/iiif/migrations/0001_initial.py b/test_app/iiif/migrations/0001_initial.py new file mode 100644 index 0000000..811aa77 --- /dev/null +++ b/test_app/iiif/migrations/0001_initial.py @@ -0,0 +1,85 @@ +# Generated by Django 3.2.23 on 2024-01-09 15:17 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Collection', + fields=[ + ('pid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ], + ), + migrations.CreateModel( + name='ImageServer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('server_base', models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name='Manifest', + fields=[ + ('pid', models.CharField(default=uuid.uuid4, max_length=255, primary_key=True, serialize=False)), + ('collections', models.ManyToManyField(blank=True, related_name='manifests', to='iiif.Collection')), + ('image_server', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='iiif.imageserver')), + ], + ), + migrations.CreateModel( + name='RelatedLink', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('manifest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='iiif.manifest')), + ], + ), + migrations.CreateModel( + name='Canvas', + fields=[ + ('pid', models.CharField(default=uuid.uuid4, max_length=255, primary_key=True, serialize=False)), + ('position', models.IntegerField()), + ('ocr_file_path', models.CharField(blank=True, max_length=500, null=True)), + ('image_server', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='iiif.imageserver')), + ('manifest', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='iiif.manifest')), + ], + ), + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('name', models.CharField(blank=True, max_length=255)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/test_app/iiif/migrations/0002_auto_20240110_1917.py b/test_app/iiif/migrations/0002_auto_20240110_1917.py new file mode 100644 index 0000000..d8b3b5d --- /dev/null +++ b/test_app/iiif/migrations/0002_auto_20240110_1917.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.23 on 2024-01-10 19:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('iiif', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='canvas', + name='height', + field=models.IntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='canvas', + name='width', + field=models.IntegerField(default=0), + preserve_default=False, + ), + ] diff --git a/test_app/iiif/migrations/0003_auto_20240110_1921.py b/test_app/iiif/migrations/0003_auto_20240110_1921.py new file mode 100644 index 0000000..a065c2d --- /dev/null +++ b/test_app/iiif/migrations/0003_auto_20240110_1921.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.23 on 2024-01-10 19:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('iiif', '0002_auto_20240110_1917'), + ] + + operations = [ + migrations.AlterField( + model_name='canvas', + name='height', + field=models.IntegerField(default=0), + ), + migrations.AlterField( + model_name='canvas', + name='width', + field=models.IntegerField(default=0), + ), + ] diff --git a/test_app/iiif/migrations/__init__.py b/test_app/iiif/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_app/iiif/models.py b/test_app/iiif/models.py new file mode 100644 index 0000000..9f620fd --- /dev/null +++ b/test_app/iiif/models.py @@ -0,0 +1,30 @@ +from django.db import models +from uuid import uuid4 +from django.contrib.auth.models import AbstractUser + +class Collection(models.Model): + pid = models.UUIDField(primary_key=True, default=uuid4, editable=True) + +class ImageServer(models.Model): + server_base = models.CharField(max_length=255) + +class Manifest(models.Model): + pid = models.CharField(max_length=255, primary_key=True, default=uuid4, editable=True) + image_server = models.ForeignKey(ImageServer, on_delete=models.DO_NOTHING, null=True) + collections = models.ManyToManyField(Collection, blank=True, related_name='manifests') + +class Canvas(models.Model): + pid = models.CharField(max_length=255, primary_key=True, default=uuid4, editable=True) + image_server = models.ForeignKey(ImageServer, on_delete=models.DO_NOTHING, null=True) + position = models.IntegerField() + width = models.IntegerField(default=0) + height = models.IntegerField(default=0) + ocr_file_path = models.CharField(max_length=500, null=True, blank=True) + manifest = models.ForeignKey(Manifest, on_delete=models.DO_NOTHING) + +class RelatedLink(models.Model): + """ Links to related resources """ + manifest = models.ForeignKey(Manifest, on_delete=models.CASCADE) + +class User(AbstractUser): + name = models.CharField(blank=True, max_length=255) diff --git a/test_app/iiif/tests.py b/test_app/iiif/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/test_app/iiif/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/test_app/iiif/views.py b/test_app/iiif/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/test_app/iiif/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/test_app/manage.py b/test_app/manage.py new file mode 100755 index 0000000..0084e92 --- /dev/null +++ b/test_app/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_app.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/test_app/pytest.ini b/test_app/pytest.ini new file mode 100644 index 0000000..a599e3a --- /dev/null +++ b/test_app/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +DJANGO_SETTINGS_MODULE=test_app.settings +python_files = tests.py test_*.py *_tests.py diff --git a/test_app/requirements.txt b/test_app/requirements.txt new file mode 100644 index 0000000..8a27504 --- /dev/null +++ b/test_app/requirements.txt @@ -0,0 +1,10 @@ +Django>=3.2.0,<4.0 # https://www.djangoproject.com/ +../ # readux_ingest_ecds +django-storages==1.14 # https://github.com/jschneier/django-storages +django-environ==0.11.2 # https://github.com/joke2k/django-environ +pytest>=7.4.3 # https://github.com/pytest-dev/pytest +pytest-sugar==0.9.7 # https://github.com/Frozenball/pytest-sugar +pytest-django==4.5.2 # https://github.com/pytest-dev/pytest-django +factory-boy~=3.2.1 # https://github.com/FactoryBoy/factory_boy +faker~=20.1.0 +moto==4.2.0 # https://github.com/spulec/moto diff --git a/test_app/test_app/__init__.py b/test_app/test_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_app/test_app/asgi.py b/test_app/test_app/asgi.py new file mode 100644 index 0000000..8ef2db3 --- /dev/null +++ b/test_app/test_app/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for test_app project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_app.settings') + +application = get_asgi_application() diff --git a/test_app/test_app/settings.py b/test_app/test_app/settings.py new file mode 100644 index 0000000..efc81e3 --- /dev/null +++ b/test_app/test_app/settings.py @@ -0,0 +1,169 @@ +""" +Django settings for test_app project. + +Generated by 'django-admin startproject' using Django 3.2.21. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +from pathlib import Path +import os +import environ + +os.environ['DJANGO_ENV'] = 'test' + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent +ROOT_DIR = environ.Path(__file__) - 2 +FIXTURE_DIR = ROOT_DIR.path('fixtures') + +IIIF_MANIFEST_MODEL = 'iiif.Manifest' +IIIF_IMAGE_SERVER_MODEL = 'iiif.ImageServer' +IIIF_RELATED_LINK_MODEL = 'iiif.RelatedLink' +IIIF_CANVAS_MODEL = 'iiif.Canvas' +IIIF_COLLECTION_MODEL = 'iiif.Collection' +INGEST_TMP_DIR = os.path.join('tmp') +INGEST_PROCESSING_DIR = os.path.join('tmp', 'processing') +INGEST_OCR_DIR = os.path.join('tmp', 'ocr') +INGEST_TRIGGER_BUCKET = 'readux-ingest' + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-^xwy*n+ucn4vzl7a5rnzl)2st_&fhktx5pmgcgqgx3u-2r66@*' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +AUTH_USER_MODEL = 'iiif.User' + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.messages', + 'django_celery_results', + 'iiif.apps.IiifConfig', + 'readux_ingest_ecds', + 'test_app' +] + +LOCAL_APPS = [] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +] + +ROOT_URLCONF = 'test_app.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'test_app.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", + "style": "{", + }, + "simple": { + "format": "{levelname} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + }, + }, + "loggers": { + "readux_ingest_ecds": { + "handlers": ["console"], + "level": "DEBUG", + } + }, +} diff --git a/test_app/test_app/urls.py b/test_app/test_app/urls.py new file mode 100644 index 0000000..ea90ebd --- /dev/null +++ b/test_app/test_app/urls.py @@ -0,0 +1,21 @@ +"""test_app URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/test_app/test_app/wsgi.py b/test_app/test_app/wsgi.py new file mode 100644 index 0000000..e1b62bf --- /dev/null +++ b/test_app/test_app/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for test_app project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_app.settings') + +application = get_wsgi_application() diff --git a/test_app/tests/__init__.py b/test_app/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_app/tests/factories.py b/test_app/tests/factories.py new file mode 100644 index 0000000..e0ee950 --- /dev/null +++ b/test_app/tests/factories.py @@ -0,0 +1,43 @@ +from os.path import join +from django_celery_results.models import TaskResult +from factory.django import DjangoModelFactory, FileField +from factory import Faker, SubFactory +from django.conf import settings +from readux_ingest_ecds.models import Local +from iiif.models import ImageServer, Manifest, User + +class ImageServerFactory(DjangoModelFactory): + server_base = 'http://images.ecds.emory.edu' + + class Meta: # pylint: disable=too-few-public-methods, missing-class-docstring + model = ImageServer + +class ManifestFactory(DjangoModelFactory): + """Creates a Manifest object for testing.""" + class Meta: # pylint: disable=too-few-public-methods, missing-class-docstring + model = Manifest + +class LocalFactory(DjangoModelFactory): + class Meta: + model = Local + + bundle = FileField(filename='bundle.zip', filepath=join(settings.FIXTURE_DIR, 'bundle.zip')) + image_server = SubFactory(ImageServerFactory) + manifest = None + +class UserFactory(DjangoModelFactory): + """ Factory for fake user """ + username = Faker("user_name") + email = Faker("email") + name = Faker("name") + + class Meta: + model = User + +class LocalFactory(DjangoModelFactory): + class Meta: + model = Local + + bundle = FileField(filename='bundle.zip', filepath=join(settings.FIXTURE_DIR, 'bundle.zip')) + image_server = SubFactory(ImageServerFactory) + manifest = None diff --git a/test_app/tests/test_admin.py b/test_app/tests/test_admin.py new file mode 100644 index 0000000..9a7a16b --- /dev/null +++ b/test_app/tests/test_admin.py @@ -0,0 +1,81 @@ +from os.path import join +from django.conf import settings +from django.contrib.admin.sites import AdminSite +from django.core import files +# from django.http import HttpResponseRedirect +from django.test import TestCase +from django.test.client import RequestFactory +# from django.urls.base import reverse +# from django_celery_results.models import TaskResult +from moto import mock_s3 +from iiif.models import Manifest, Canvas +from .factories import ImageServerFactory, UserFactory, LocalFactory +from readux_ingest_ecds.models import Local +from readux_ingest_ecds.admin import LocalAdmin + +@mock_s3 +class IngestAdminTest(TestCase): + # @classmethod + # def setUpClass(cls): + # cls.sftp_server = MockSFTP() + + # @classmethod + # def tearDownClass(cls): + # cls.sftp_server.stop_server() + + def setUp(self): + """ Set instance variables. """ + self.fixture_path = settings.FIXTURE_DIR + + self.image_server = ImageServerFactory.create() + + self.user = UserFactory.create(is_superuser=True) + + # Create fake bucket for moto's mock S3 service. + # conn = boto3.resource('s3', region_name='us-east-1') + # conn.create_bucket(Bucket='readux') + # conn.create_bucket(Bucket='readux-ingest') + + def test_local_admin_save(self): + """It should add a create a manifest and canvases and delete the Local object""" + local = LocalFactory.build(image_server=self.image_server) + + original_manifest_count = Manifest.objects.count() + original_canvas_count = Canvas.objects.count() + + request_factory = RequestFactory() + + with open(join(self.fixture_path, 'no_meta_file.zip'), 'rb') as f: + content = files.base.ContentFile(f.read()) + + local.bundle = files.File(content.file, 'no_meta_file.zip') + + req = request_factory.post('/admin/readux_ingest_ecds/local/add/', data={}) + req.user = self.user + + local_model_admin = LocalAdmin(model=Local, admin_site=AdminSite()) + local_model_admin.save_model(obj=local, request=req, form=None, change=None) + + # Saving should kick off the task to create the canvases and then delete + # the `Local` ingest object when done. + try: + local.refresh_from_db() + assert False + except Local.DoesNotExist: + assert True + + # A new `Manifest` should have been created along with the canvases + # in the ingest + assert Manifest.objects.count() == original_manifest_count + 1 + assert Canvas.objects.count() == original_canvas_count + 10 + + # def test_local_admin_response_add(self): + # """It should redirect to new manifest""" + + # local = LocalFactory.create(manifest=ManifestFactory.create()) + + # local_model_admin = LocalAdmin(model=Local, admin_site=AdminSite()) + # response = local_model_admin.response_add(obj=local, request=None) + + # assert isinstance(response, HttpResponseRedirect) + # assert response.url == f'/admin/manifests/manifest/{local.manifest.id}/change/' diff --git a/test_app/tests/test_local.py b/test_app/tests/test_local.py new file mode 100644 index 0000000..13c2ef2 --- /dev/null +++ b/test_app/tests/test_local.py @@ -0,0 +1,212 @@ +""" Tests for local ingest """ +import os +from shutil import rmtree +import pytest +import boto3 +from uuid import UUID +from zipfile import ZipFile +from moto import mock_s3 +from django.test import TestCase +from django.core.files.uploadedfile import SimpleUploadedFile +from django.conf import settings +from .factories import ImageServerFactory +from readux_ingest_ecds.models import Local +from readux_ingest_ecds.services import create_manifest +from iiif.models import Canvas + +pytestmark = pytest.mark.django_db(transaction=True) # pylint: disable = invalid-name + +@mock_s3 +class LocalTest(TestCase): + """ Tests for ingest.models.Local """ + + def setUp(self): + """ Set instance variables. """ + super().setUp() + rmtree(settings.INGEST_TMP_DIR, ignore_errors=True) + self.fixture_path = settings.FIXTURE_DIR + self.image_server = ImageServerFactory() + + conn = boto3.resource('s3', region_name='us-east-1') + conn.create_bucket(Bucket=settings.INGEST_TRIGGER_BUCKET) + + # def teardown_class(): + # rmtree(settings.INGEST_TMP_DIR, ignore_errors=True) + + def mock_local(self, bundle, with_manifest=False, metadata={}, from_bulk=False): + # Note, I tried to use the factory here, but could not get it to override the file for bundle. + local = Local( + image_server = self.image_server, + metadata = metadata + ) + local.save() + file = SimpleUploadedFile( + name=bundle, + content=open(os.path.join(self.fixture_path, bundle), 'rb').read() + ) + if from_bulk: + local.bundle_from_bulk.save(bundle, file) + else: + local.bundle = file + + local.save() + + if with_manifest: + local.manifest = create_manifest(local) + local.save() + + local.refresh_from_db() + return local + + + def test_bundle_upload(self): + """ It should upload the images using a fake S3 service from moto. """ + for bundle in ['bundle.zip', 'nested_volume.zip', 'csv_meta.zip']: + self.mock_local(bundle) + assert bundle in os.listdir(settings.INGEST_TMP_DIR) + + def test_open_excel_metadata(self): + """ It should open the metadata Excel file. """ + local = self.mock_local('bundle.zip') + local.open_metadata() + assert local.metadata['pid'] == 'sqn75' + + def test_open_csv_metadata(self): + """ It should open the metadata CSV file. """ + local = self.mock_local('csv_meta.zip') + local.open_metadata() + assert local.metadata['pid'] == 'sqn75' + + def test_open_tsv_metadata(self): + """ It should open the metadata TSV file. """ + local = self.mock_local('tsv.zip') + local.open_metadata() + assert local.metadata['pid'] == 'cdc-ledger-1' + + def test_creating_manifest(self): + """ It should open the metadata CSV file. """ + local = self.mock_local('csv_meta.zip') + local.manifest = create_manifest(local) + assert local.manifest.pid == 'sqn75' + + def test_metadata_from_excel(self): + """ It should create a manifest with metadata supplied in an Excel file. """ + local = self.mock_local('bundle.zip') + local.process() + + assert 'pid' in local.metadata.keys() + + for key in local.metadata.keys(): + assert str(local.metadata[key]) == str(getattr(local.manifest, key)) + + def test_metadata_from_csv(self): + """ It should create a manifest with metadata supplied in a CSV file. """ + local = self.mock_local('csv_meta.zip', with_manifest=True) + local.process() + + assert 'pid' in local.metadata.keys() + + for key in local.metadata.keys(): + assert local.metadata[key] == getattr(local.manifest, key) + + def test_metadata_from_tsv(self): + """ It should create a manifest with metadata supplied in a TSV file. """ + local = self.mock_local('tsv.zip', with_manifest=True) + local.open_metadata() + + assert 'pid' in local.metadata.keys() + + for key in local.metadata.keys(): + assert local.metadata[key] == getattr(local.manifest, key) + + def test_no_metadata_file(self): + """ It should create a Manifest even when no metadata file is supplied. """ + local = self.mock_local('no_meta_file.zip', with_manifest=True) + local.process() + + # New manifest should have a default pid - UUID in test app. + assert UUID(local.manifest.pid, version=4) + + def test_unzip_bundle(self): + local = self.mock_local('csv_meta.zip') + local.process() + local.refresh_from_db() + local.unzip_bundle() + + assert os.path.isfile(os.path.join(settings.INGEST_PROCESSING_DIR, f'{local.manifest.pid}_00000005.jpg')) + assert os.path.isfile(os.path.join(settings.INGEST_OCR_DIR, local.manifest.pid, f'{local.manifest.pid}_00000007.tsv')) + + def test_create_canvases(self): + local = self.mock_local('csv_meta.zip') + local.process() + local.refresh_from_db() + local.unzip_bundle() + local.create_canvases() + + assert local.manifest.canvas_set.count() == 10 + + def test_ignoring_junk(self): + """ + Any hidden files should not be uploaded. + """ + local = self.mock_local('bundle_with_junk.zip') + local.process() + local.unzip_bundle() + + with ZipFile(os.path.join(self.fixture_path, 'bundle_with_junk.zip'), 'r') as zip_ref: + files_in_bundle = zip_ref.namelist() + + assert os.path.isfile(os.path.join(settings.INGEST_PROCESSING_DIR, f'{local.manifest.pid}_00000009.jpg')) + assert os.path.isfile(os.path.join(settings.INGEST_OCR_DIR, local.manifest.pid, f'{local.manifest.pid}_00000003.tsv')) + assert 'images/.00000010.jpg' in files_in_bundle + assert '__MACOSX/images/._00000010.jpg' in files_in_bundle + assert 'ocr/.junk.tsv' in files_in_bundle + assert os.path.isfile(os.path.join(settings.INGEST_PROCESSING_DIR, f'{local.manifest.pid}.000000010.jpg')) is False + assert os.path.isfile(os.path.join(settings.INGEST_OCR_DIR, local.manifest.pid, f'{local.manifest.pid}.junk.tsv')) is False + + def test_when_metadata_in_filename(self): + """ + Make sure it doesn't get get confused when the word "metadata" is in + every path. + """ + local = self.mock_local('metadata.zip', with_manifest=True) + local.open_metadata() + + assert local.metadata['pid'] == 't9wtf-sample' + + def test_creating_canvases(self): + """ + Make sure it doesn't get get confused when the word "metadata" is in + every path. + """ + local = self.mock_local('bundle.zip', with_manifest=True) + local.process() + local.unzip_bundle() + local.create_canvases() + + pid = local.manifest.pid + + assert local.manifest.canvas_set.all().count() == 10 + assert Canvas.objects.get(pid=f'{pid}_00000001.tiff').position == 1 + assert Canvas.objects.get(pid=f'{pid}_00000002.tiff').position == 2 + assert Canvas.objects.get(pid=f'{pid}_00000003.tiff').position == 3 + assert Canvas.objects.get(pid=f'{pid}_00000004.tiff').position == 4 + assert Canvas.objects.get(pid=f'{pid}_00000005.tiff').position == 5 + assert Canvas.objects.get(pid=f'{pid}_00000006.tiff').position == 6 + assert Canvas.objects.get(pid=f'{pid}_00000007.tiff').position == 7 + assert Canvas.objects.get(pid=f'{pid}_00000008.tiff').position == 8 + assert Canvas.objects.get(pid=f'{pid}_00000009.tiff').position == 9 + assert Canvas.objects.get(pid=f'{pid}_00000010.tiff').position == 10 + assert Canvas.objects.get(pid=f'{pid}_00000010.tiff').width == 32 + assert Canvas.objects.get(pid=f'{pid}_00000010.tiff').height == 43 + + def test_it_creates_manifest_with_metadata_property(self): + metadata = { + 'pid': '808', + 'title': 'Goodie Mob' + } + local = self.mock_local('no_meta_file.zip', metadata=metadata) + local.manifest = create_manifest(local) + local.process() + assert local.manifest.pid == '808' + assert local.manifest.title == 'Goodie Mob' diff --git a/test_app/tmp/metadata.zip b/test_app/tmp/metadata.zip new file mode 100644 index 0000000..bee3de7 Binary files /dev/null and b/test_app/tmp/metadata.zip differ diff --git a/test_app/tmp/metadata/metadata.csv b/test_app/tmp/metadata/metadata.csv new file mode 100644 index 0000000..a29d365 --- /dev/null +++ b/test_app/tmp/metadata/metadata.csv @@ -0,0 +1,2 @@ +PID,Label,Summary,Author,Published city,Published date,Publisher,PDF +t9wtf-sample,t9wtf-sample,x,x,x,x,x,www.google.com \ No newline at end of file