-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First pass implementation - book/page content from paged IIIF
- Loading branch information
Showing
17 changed files
with
1,456 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
default_app_config = 'djiffy.apps.DjiffyConfig' | ||
|
||
__version_info__ = (0, 1, 0, 'dev') | ||
|
||
# Dot-connect all but the last. Last is dash-connected if not None. | ||
__version__ = '.'.join([str(i) for i in __version_info__[:-1]]) | ||
if __version_info__[-1] is not None: | ||
__version__ += ('-%s' % (__version_info__[-1],)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
from django.contrib import admin | ||
|
||
from .models import IfBook, IfPage | ||
|
||
class PageInline(admin.StackedInline): | ||
model = IfPage | ||
|
||
class BookAdmin(admin.ModelAdmin): | ||
inlines = [PageInline] | ||
|
||
admin.site.register(IfBook, BookAdmin) | ||
admin.site.register(IfPage) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class DjiffyConfig(AppConfig): | ||
name = 'djiffy' | ||
|
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import os.path | ||
from django.core.management.base import BaseCommand, CommandError | ||
import requests | ||
|
||
from djiffy.models import IfBook, IfPage, IIIFPresentation | ||
|
||
|
||
class Command(BaseCommand): | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument('path', | ||
help='IIIF Collection or Manifest as file or URL') | ||
|
||
def handle(self, *args, **kwargs): | ||
|
||
manifest = IIIFPresentation.from_file_or_url(kwargs['path']) | ||
if manifest.type == 'sc:Collection': | ||
# import all manifests in the collection | ||
for brief_manifest in manifest.manifests: | ||
# check if content is supported | ||
if not self.is_supported(brief_manifest): | ||
continue | ||
print('Importing %s %s' % (brief_manifest.label, brief_manifest.id)) | ||
|
||
manifest = IIIFPresentation.from_file_or_url(brief_manifest.id) | ||
if manifest: | ||
self.import_book(manifest, brief_manifest.id) | ||
|
||
if manifest.type == 'sc:Manifest': | ||
if self.is_supported(manifest): | ||
print('Importing %s %s' % (manifest.label, manifest.id)) | ||
self.import_book(manifest, kwargs['path']) | ||
|
||
def is_supported(self, manifest): | ||
print('viewing hint, direction = %s %s' % (manifest.viewingHint, manifest.viewingDirection)) | ||
# FIXME: individuals vs paged? | ||
if manifest.viewingHint == 'paged' and \ | ||
manifest.viewingDirection == 'left-to-right': | ||
return True | ||
|
||
self.stdout.write(self.style.ERROR('Currently import only supports paged, left-to-right manifests; skipping %s (%s, %s)' \ | ||
% (manifest.id, manifest.viewingHint, manifest.viewingDirection))) | ||
return False | ||
|
||
def import_book(self, manifest, path): | ||
# check if book with uri identifier already exists | ||
if IfBook.objects.filter(uri=manifest.id).count(): | ||
# NOTE: not updating for now; may want to add later | ||
self.stderr.write('%s has already been imported' % path) | ||
return | ||
# check if the type of manifest is supported | ||
if not self.is_supported(manifest): | ||
return | ||
|
||
# create a new book | ||
ifbk = IfBook() | ||
# TODO: how do we want to handle lists of labels? | ||
if len(manifest.label) == 1: | ||
ifbk.label = manifest.label[0] | ||
else: | ||
ifbk.label = '; '.join(manifest.label) | ||
ifbk.uri = manifest.id | ||
ifbk.short_id = self.short_id(manifest.id) | ||
ifbk.save() | ||
|
||
thumbnail_id = None | ||
if hasattr(manifest, 'thumbnail'): | ||
thumbnail_id = manifest.thumbnail.service.id | ||
print('thumbnail id = %s ' % thumbnail_id) | ||
|
||
# for now, only worry about the first sequence | ||
order = 0 | ||
# create a page for each canvas | ||
for canvas in manifest.sequences[0].canvases: | ||
ifpage = IfPage(book=ifbk, order=order) | ||
ifpage.label = canvas.label | ||
ifpage.uri = canvas.id | ||
ifpage.short_id = self.short_id(canvas.id) | ||
# only support single image per canvas for now | ||
ifpage.iiif_image_id = canvas.images[0].resource.service.id | ||
# check if this page is the thumbnail image | ||
if thumbnail_id is not None and ifpage.uri == thumbnail_id: | ||
ifpage.thumbnail = True | ||
ifpage.save() | ||
|
||
order += 1 | ||
|
||
# NOTE: IIIF metadata is meant for display to end users | ||
# convert metadata into a more useful format ? | ||
# metadata = {item['label'].lower(): item['value'] for item in manifest.metadata} | ||
# return manifest.sequences[0].canvases[int(self.kwargs['id'])] | ||
|
||
def short_id(self, uri): | ||
# generate a short id from full manifest/canvas uri identifiers | ||
# for use in urls | ||
|
||
# NOTE: very PUL specific for now, will need to be generalized or | ||
# adapted for other sources | ||
# example: https://plum.princeton.edu/concern/scanned_resources/ph415q7581/manifest | ||
# remove trailing /manifest at the end of the url | ||
if uri.endswith('/manifest'): | ||
uri = uri[:-len('/manifest')] | ||
# split on slashes and return the last portion | ||
return uri.split('/')[-1] | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# -*- coding: utf-8 -*- | ||
# Generated by Django 1.10.5 on 2017-01-31 10:03 | ||
from __future__ import unicode_literals | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='IfBook', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('label', models.TextField()), | ||
('short_id', models.CharField(max_length=255)), | ||
('uri', models.URLField()), | ||
('created', models.DateField(auto_now_add=True)), | ||
('last_modified', models.DateField(auto_now=True)), | ||
], | ||
options={ | ||
'verbose_name': 'IIIF Book', | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name='IfPage', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('label', models.TextField()), | ||
('short_id', models.CharField(max_length=255)), | ||
('uri', models.URLField()), | ||
('iiif_image_id', models.URLField()), | ||
('thumbnail', models.BooleanField(default=False)), | ||
('order', models.PositiveIntegerField()), | ||
('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pages', to='djiffy.IfBook')), | ||
], | ||
options={ | ||
'verbose_name': 'IIIF Page', | ||
'ordering': ['book', 'order'], | ||
}, | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import json | ||
import os.path | ||
|
||
from attrdict import AttrMap | ||
from django.db import models | ||
from piffle import iiif | ||
import requests | ||
|
||
|
||
class IfBook(models.Model): | ||
'''Minimal db model representation of a Book from an IIIF manifest''' | ||
label = models.TextField() | ||
short_id = models.CharField(max_length=255) | ||
uri = models.URLField() | ||
created = models.DateField(auto_now_add=True) | ||
last_modified = models.DateField(auto_now=True) | ||
|
||
class Meta: | ||
verbose_name = 'IIIF Book' | ||
|
||
# todo: metadata? thumbnail references | ||
|
||
def __str__(self): | ||
return self.label or self.short_id | ||
|
||
|
||
class IIIFImage(iiif.IIIFImageClient): | ||
'''Subclass of :class:`piffle.iiif.IIIFImageClient`, for generating | ||
IIIF Image URIs for book page images.''' | ||
|
||
long_side = 'height' | ||
|
||
# NOTE: using long edge instead of specifying both with exact | ||
# results in cleaner urls/filenams (no !), and more reliable result | ||
# depending on IIIF implementation | ||
|
||
def thumbnail(self): | ||
'default thumbnail: 300px on the long edge' | ||
return self.size(**{self.long_side: 300}).format('png') | ||
|
||
def mini_thumbnail(self): | ||
'mini thumbnail: 100px on the long edge' | ||
return self.size(**{self.long_side: 100}).format('png') | ||
|
||
#: long edge size for single page display | ||
SINGLE_PAGE_SIZE = 1000 | ||
|
||
def page_size(self): | ||
'page size for display: :attr:`SINGLE_PAGE_SIZE` on the long edge' | ||
return self.size(**{self.long_side: self.SINGLE_PAGE_SIZE}) | ||
|
||
|
||
class IfPage(models.Model): | ||
'''Minimal db model representation of a Page from an IIIF manifest''' | ||
label = models.TextField() | ||
short_id = models.CharField(max_length=255) | ||
uri = models.URLField() | ||
iiif_image_id = models.URLField() | ||
book = models.ForeignKey(IfBook, related_name='pages') | ||
thumbnail = models.BooleanField(default=False) | ||
# for now only storing a single sequence, so just store order on the page | ||
order = models.PositiveIntegerField() | ||
# format? size? (ocr text eventually?) | ||
|
||
class Meta: | ||
ordering = ["book", "order"] | ||
verbose_name = 'IIIF Page' | ||
|
||
def __str__(self): | ||
return '%s %d %s' % (self.book, self.order, self.label) | ||
|
||
@property | ||
def image(self): | ||
# NOTE: piffle iiif image wants service & id split out. | ||
# Should update to handle iiif image ids as provided in manifests | ||
# for now, split into service and image id. (is this reliable?) | ||
return IIIFImage(*self.iiif_image_id.rsplit('/', 1)) | ||
|
||
|
||
class IIIFPresentation(AttrMap): | ||
|
||
at_fields = ['type', 'id', 'context'] | ||
|
||
@classmethod | ||
def from_file(cls, path): | ||
with open(path) as manifest: | ||
data = json.loads(manifest.read()) | ||
return cls(data) | ||
|
||
@classmethod | ||
def from_url(cls, uri): | ||
response = requests.get(uri) | ||
return cls(response.json()) | ||
|
||
@classmethod | ||
def from_file_or_url(cls, path): | ||
if os.path.isfile(path): | ||
return cls.from_file(path) | ||
else: | ||
return cls.from_url(path) | ||
|
||
def __getattr__(self, key): | ||
""" | ||
Access an item as an attribute. | ||
""" | ||
# override getattr to allow use of keys with leading @, | ||
# which are otherwise not detected as present and not valid | ||
at_key = self._handle_at_keys(key) | ||
if key not in self or \ | ||
(key not in self.at_fields and at_key not in self) or \ | ||
not self._valid_name(key): | ||
raise AttributeError( | ||
"'{cls}' instance has no attribute '{name}'".format( | ||
cls=self.__class__.__name__, name=key | ||
) | ||
) | ||
return self._build(self[key]) | ||
|
||
def _handle_at_keys(self, key): | ||
if key in self.at_fields: | ||
key = '@%s' % key | ||
return key | ||
|
||
def __getitem__(self, key): | ||
""" | ||
Access a value associated with a key. | ||
""" | ||
return self._mapping[self._handle_at_keys(key)] | ||
|
||
def __setitem__(self, key, value): | ||
""" | ||
Add a key-value pair to the instance. | ||
""" | ||
self._mapping[self._handle_at_keys(key)] = value | ||
|
||
def __delitem__(self, key): | ||
""" | ||
Delete a key-value pair | ||
""" | ||
del self._mapping[self._handle_at_keys(key)] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<html> | ||
<body> | ||
<h1>{{ book.label }}</h1> | ||
|
||
{{ book.short_id }} | ||
{{ book.uri }} | ||
{{ book.pages.all }} | ||
<p>{{ book.pages.all|length }} page{{ book.pages.all|pluralize }}</p> | ||
|
||
<ul> | ||
{% for page in book.pages.all %} | ||
<li><a href="{% url 'djiffy:page' book.short_id page.short_id %}">{{ page.label }}</a></li> | ||
{% endfor %} | ||
</ul> | ||
|
||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<html> | ||
<body> | ||
|
||
<h1>books</h1> | ||
{% for book in books %} | ||
<h2><a href="{% url 'djiffy:book' book.short_id %}">{{ book.label }}</a></h2> | ||
{{ book.short_id }} | ||
{{ book.uri }} | ||
<p>{{ book.pages.all|length }} page{{ book.pages.all|pluralize }}</p> | ||
|
||
{% endfor %} | ||
|
||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<html> | ||
<body> | ||
<h1>{{ page.label }}</h1> | ||
|
||
<div class="text-center"> | ||
<div id="zoom-page"></div> | ||
<div class="page"> | ||
<div class="content"> | ||
<section class="inner"> | ||
<img class="page-image" src="{{ page.image.page_size }}"/> | ||
</section> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
</body> | ||
</html> |
Oops, something went wrong.