Skip to content

Commit

Permalink
First pass implementation - book/page content from paged IIIF
Browse files Browse the repository at this point in the history
  • Loading branch information
rlskoeser committed Jan 31, 2017
1 parent 615f7ac commit 9cce0ec
Show file tree
Hide file tree
Showing 17 changed files with 1,456 additions and 0 deletions.
8 changes: 8 additions & 0 deletions djiffy/__init__.py
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],))
13 changes: 13 additions & 0 deletions djiffy/admin.py
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)

6 changes: 6 additions & 0 deletions djiffy/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class DjiffyConfig(AppConfig):
name = 'djiffy'

899 changes: 899 additions & 0 deletions djiffy/fixtures/chto-manifest.json

Large diffs are not rendered by default.

106 changes: 106 additions & 0 deletions djiffy/management/commands/import_manifest.py
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]


48 changes: 48 additions & 0 deletions djiffy/migrations/0001_initial.py
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 added djiffy/migrations/__init__.py
Empty file.
141 changes: 141 additions & 0 deletions djiffy/models.py
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)]

17 changes: 17 additions & 0 deletions djiffy/templates/djiffy/book_detail.html
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>
14 changes: 14 additions & 0 deletions djiffy/templates/djiffy/book_list.html
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>
17 changes: 17 additions & 0 deletions djiffy/templates/djiffy/page_detail.html
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>
Loading

0 comments on commit 9cce0ec

Please sign in to comment.