-
Notifications
You must be signed in to change notification settings - Fork 0
Design
Given an IIIF URI:
http://example.edu/loris/<id>/<region>/<size>/<rotation>/<quality>.<format>
The region
, size
, and rotation
parameters are handled by custom subclasses of Werkzeug's BaseConverter
class. All these converters require is a regex to determine what how to match a part of the URI (for routing) and two methods: one for turning it into a [slice of] a URI, and one for turning the URI segment into a Python object. The latter is our only concern. These methods instantiate RegionParameter
, SizeParameter
, and RotationParameter
objects that do most of the work wrt parsing, validating, and fulfilling the request.
http://example.edu/loris/<id>
/<region> --> RegionConverter.to_python() --> new RegionParameter
/<size> --> SizeConverter.to_python() --> new SizeParameter
/<rotation> --> RotationConverter.to_python() --> new RotationParameter
/<quality>.<format>
The __init__
methods of the *Parameter
objects take care of parsing the URI slice into attributes of the object that can then be tested and used to build commands that are shelled out via the subprocess
module to actually derive the image, optionally cache it, and return it. The other properties of the request, id
, quality
, and format
are kept as strings.
The first step is to check the filesystem cache, in which case we return the cached derivative, or a 304
if the image hasn't changed and an If-Modified-Since
header is included with the request.
Otherwise, as stated, the images are derived via shell outs. I felt that this was like going to be the most performant approach, and
- The JP2 library (Kakadu) I'm using lacks a Python API
- Only the Kakadu binaries are free an freely disputable
- I'm lazy
The *Parameter
classes, plus the id
, quality
, and format
are sent to a method
on_get_img(self, request, id, region, size, rotation, quality, format=None)
That begins to build commands. There are four shell commands per request. (Below, values in { } are the methods that are called to get the value that is inserted in the command)
First, we make a named pipe :
/usr/bin/mkfifo { conf[tmp] + app._random_str(10) }
Next, we build and make the kakadu call:
kdu -i {app._resolve_identifier} -region { region.to_kdu_arg() } -o { named pipe from above }
This is necessary because kdu only knows what format to output based on the file extension of -o
. Kakadu can only output bmp
, pbm
or ppm
bitmaps.
This subprocess stays running while we make the Third call, which to ImageMagick's convert
utility:
/usr/bin/convert -i { named pipe from above } \
-rotate { rotation.to_convert_arg() } \
-resize { size.to_convert_arg() }
-o { conf[cache_root]/<id>/<region>/<size>/<rotation>/<quality>.<format> }
(Some additional args are added if the quality is grey or bitonal, etc., but you get the gist).
Only after the stream has been read from the named pipe can the kdu_expand
call exit, so we check that, handle errors, and finally
, since we're already in shell land:
/bin/rm { named pipe from above }
This is easier in many ways though trickier in others. The identifier is simply resolved to an image on the filesystem, and from there we start reading in bytes of the JP2, pulling out the info we need. See the code for details (ImgInfo.fromJP2()
). The information is held in an instance of an ImgInfo
object which can be marshalled to XML or json. ImgInfo object can be constructed from JP2s or unmarshalled from a json or XML file on the filesystem, so there is no __init__
method for the class but instead a couple of static methods that will return an ImgInfo
instance (.fromJP2()
and .unmarshal()
at the moment but this is left open should we want to work with other image formats at some point).