Skip to content
jpstroop edited this page Oct 8, 2012 · 7 revisions

Design

Image Requests

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

  1. The JP2 library (Kakadu) I'm using lacks a Python API
  2. Only the Kakadu binaries are free an freely disputable
  3. 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 }

Info Requests

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).

Clone this wiki locally