Home

Awesome

go-iiif

spanking cat

What is this?

This began as a fork of @greut's iiif package that moves all of the processing logic for the IIIF Image API in to discrete Go packages and defines source, derivative and graphics details in a JSON config file. There is an additional caching layer for both source images and derivatives.

I did this to better understand the architecture behind (and to address my own concerns about) the IIIF Image API.

For the time being this package will probably not support the other IIIF Metadata or Publication APIs.

And by "forked" I mean that @greut and I decided that it was best for this code and his code to wave at each other across the divide but not necessarily to hold hands.

Releases

The current release is github.com/go-iiif/go-iiif/v6.

v7

Version 7.0.0 is in the early stages of development and it is expected that it will introduce a number of backwards incompatible changes to how the tools package is structured and the interface and method signatures it exposes.

Additionally, many properties in config.Config blocks will be removed in favour of a single URI-style declarative syntax.

The goal is to leave the command-line tools unchanged with the exception of removing any flags that have previously been flagged as deprecated.

There is no expected release date for the v7 branch yet.

v6

Version 6.0.0 was updated to use the aaronland/go-flickr-api package to retrieve Flickr photos which introduced backwards incompatible changes in the config.FlickrConfig block.

v5

Version 5.0.0 and higher of the go-iiif package introduces three backwards incompatible changes from previous versions. They are:

v2

Version 2.0.0 and higher of the go-iiif package introduces three backwards incompatible changes from previous versions. They are:

All three changes are discussed in detail below.

Drivers

go-iiif was first written with the libvips library and bimg Go wrapper for image processing. libvips is pretty great but it introduces non-trivial build and setup requirements. As of version 2.0 go-iiif no longer uses libvips by default but instead does all its image processing using native (Go) code. This allows go-iiif to run on any platform supported by Go without the need for external dependencies.

A longer discussion about drivers and how they work follows but if you want or need to use libvips for image processing you should use the go-iiif-vips package.

Support for alternative image processing libraries, like libvips is supported through the use of "drivers" (similar to the way the Go database/sql package works). A driver needs to support the driver.Driver interface which looks like this:

import (
	iiifcache "github.com/go-iiif/go-iiif/v6/cache"
	iiifconfig "github.com/go-iiif/go-iiif/v6/config"
	iiifsource "github.com/go-iiif/go-iiif/v6/source"
)

type Driver interface {
	NewImageFromConfigWithSource(*iiifconfig.Config, iiifsource.Source, string) (iiifimage.Image, error)
	NewImageFromConfigWithCache(*iiifconfig.Config, iiifcache.Cache, string) (iiifimage.Image, error)
	NewImageFromConfig(*iiifconfig.Config, string) (iiifimage.Image, error)
}

The idea here is that the bulk of the go-iiif code isn't aware of who or how images are actually being processed only that it can reliably pass around things that implement the image.Image interface (the go-iiif image interface, not the Go language interface).

Drivers are expected to "register" themselves through the driver.RegisterDriver method at runtime. For example:

package native

import (
	iiifdriver "github.com/go-iiif/go-iiif/v6/driver"
)

func init() {

	dr, err := NewNativeDriver()

	if err != nil {
		panic(err)
	}

	iiifdriver.RegisterDriver("native", dr)
}

And then in your code you might do something like this:

import (
	"context"
	"github.com/aaronland/gocloud-blob-bucket"	
	_ "github.com/go-iiif/go-iiif/v6/native"
	iiifconfig "github.com/go-iiif/go-iiif/v6/config"
	iiifdriver "github.com/go-iiif/go-iiif/v6/driver"	
)

ctx := context.Background()
	
config_bucket, _ := bucket.OpenBucket(ctx, "file:///etc/go-iiif")

cfg, _ := config.NewConfigFromBucket(ctx, config_bucket, "config.json")

driver, _ := iiifdriver.NewDriverFromConfig(cfg)

That's really the only change to existing code. Careful readers may note the calls to bucket.OpenBucket and config.NewConfigFromBucket to load go-iiif configuration files. This is discussed below. In the meantime the only other change is to update the previously default graphics.source property in the configuration file from VIPS (or vips) to native. For example:

    "graphics": {
	"source": { "name": "VIPS" }
    }

Becomes:

    "graphics": {
	"source": { "name": "native" }
    }

The value of the graphics.source property should match the name that driver uses to register itself with go-iiif.

The rest of the code in go-iiif has been updated to expect a driver.Driver object and to invoke the relevant NewImageFrom... method as needed. It is assumed that the driver package in question will also implement it's own implementation of the go-iiif image.Image interface. For working examples you should consult either of the following packages:

Buckets

Starting with version 2 the go-iiif package uses the Go Cloud Bucket and Blob interfaces for reading and writing all files. For example, instead of doing this:

cfg, _ := config.NewConfigFromFile("/etc/go-iiif/config.json")

It is now necessary to do this:

config_bucket, _ := bucket.OpenBucket(ctx, "file:///etc/go-iiif")
cfg, _ := config.NewConfigFromBucket(ctx, config_bucket, "config.json")

This allows for configuration files, and others, to be stored and retrieved from any "bucket" source that is supported by the Go Cloud package, notably remote storage services like AWS S3.

The source and caching layers have also been updated accordingly but support for the older Disk, S3 and Memory sources has been updated to use the Go Cloud packages so there is no need to update any existing go-iiif configuration files.

URIs

go-iiif-uri URI strings are still a work in progress. While they may still change a bit around the edges efforts will be made to ensure backwards compatibility going forward.

go-iiif-uri URI strings are defined by a named scheme which indicates how an URI should be processed, a path which is a reference to an image and zero or more query parameters which are the specific instructions for processing the URI.

file

file:///path/to/source/image.jpg
file:///path/to/source/image.jpg?target=/path/to/target/image.jpg

The file:// URI scheme is basically just a path or filename. It has an option target property which allows the name of the source image to be changed. These filenames are not the final name of the image as processed by go-iiif but the name of the directory structure that files will be written to, as in the weird IIIF instructions-based URIs.

Valid parameters for the file:// URI scheme are:

NameTypeRequired
targetstringno

idsecret

idsecret:///path/to/source/image.jpg?id=1234&secret=s33kret&secret_o=seekr3t&label

The idsecret:// URI scheme is designed to rewrite a source image URI to {UNIQUE_ID} + {SECRET} + {LABEL} style filenames. For example cat.jpg becomes 1234_s33kret_b.jpg and specifically 123/4/1234_s33kret_b.jpg where the unique ID is used to generate a nested directory tree in which the final image lives.

The idsecret:// URI scheme was developed for use with go-iiif "instructions" files where a single image produced multiple derivatives that need to share commonalities in their final URIs.

Valid parameters for the idsecret:// URI scheme are:

NameTypeRequired
idint64yes
labelstringyes
formatstringyes
originalstringno
secretstringno
secret_ostringno

If either the secret or secret_o parameters are absent they will be auto-generated.

rewrite

rewrite:///path/to/source/image.jpg?target=/path/to/target/picture.jpg

The rewrite:// URI scheme is a variant of the file:// URI scheme except that the target query parameter is required and it will be used to redefine the final URI, rather than just its directory tree, of the processed image.

NameTypeRequired
targetstringyes

Example

Here's a excerpted example taken from the process/parallel.go package that processes a single source image, defined as an idsecret:// URI, in to multiple derivatives defined in an "instructions" file.

The idsecret:// URI is output as a string using the instructions set to define the label and other query parameters. That string is then used to create a new rewrite:// URI where source is derived from the original idsecret:// URI and the target is newly generate URI string.

go func(u iiifuri.URI, label Label, i IIIFInstructions) {

	var process_uri iiifuri.URI

	switch u.Driver() {
	case "idsecret":

		str_label := fmt.Sprintf("%s", label)

		opts := &url.Values{}
		opts.Set("label", str_label)
		opts.Set("format", i.Format)

		if str_label == "o" {
			opts.Set("original", "1")
		}

		target_str, _ := u.Target(opts)

		origin := u.Origin()

		rw_str := fmt.Sprintf("%s?target=%s", origin, target_str)
		rw_str = iiifuri.NewRewriteURIString(rw_str)

		rw_uri, err := iiifuri.NewURI(rw_str)

		process_uri = rw_uri

	default:
		process_uri = u
	}

	new_uri, im, _ := pr.ProcessURIWithInstructions(process_uri, label, i)
	// do something with new_uri and im here...
	
}(u, label, i)

Command line tools

go-iiif was designed to expose all of its functionality outside of the included tools although that hasn't been documented yet. The source code for the iiif-tile-seed, iiif-transform and iiif-process tools is a good place to start poking around if you're curious.

For the sake of shortening this file documentation for the command line tools has been moved in to cmd/README.md.

Config files

There is a sample config file included with this repo. The easiest way to understand config files is that they consist of at least five top-level groupings, with nested section-specific details, followed by zero or more implementation specific configuration blocks. The five core blocks are:

level

	"level": {
		"compliance": "2"
	}

Indicates which level of IIIF Image API compliance the server (or associated tools) should support. Basically, there is no reason to ever change this right now.

profile

    "profile": {
    	"services": {
		    ...
	} 
    }

Additional configurations for a IIIF profile (aka info.json). Currently this is limited to defining one or more addtional services to append to a profile.

services

    "profile": {
    	"services": {
		    "enable": [ "palette" ]
	} 
    }

Services configurations are currently limited to enabling a fixed set of named services, where that fixed set numbers exactly three:

As of this writing adding custom services is a nuisance. There is an open issue to address this problem, but no ETA yet for its completion.

blurhash
    "blurhash": {
    	"x": 4,
	"y": 3,
	"size": 32
    }

go-iiif uses the go-blurhash to generate a compact base-83 encoded representation of an image using the BlurHash algorithm.

The blurhash service configuration has no specific properties as of this writing.

Sample out for the blurhash service is included below.

imagehash
    "imagehash": {}

go-iiif uses the goimagehash to extract average and difference perceptual hashes.

The imagehash service configuration has no specific properties as of this writing.

Sample out for the imagehash service is included below.

palette
    "palette": {
    	"extruder": { "name": "vibrant", "count": 5 },
    	"grid": { "name": "euclidian" },
	"palettes": [
		    { "name": "crayola" },
		    { "name": "css4" }
        ]
    }

go-iiif uses the go-colours package to extract colours. go-colours itself is a work in progress so you should approach colours extraction as a service accordingly.

A palette service configuration has the following properties:

Sample out for the palette service is included below.

graphics

	"graphics": {
		"source": { "name": "native" }
	}

features

	"features": {
		"enable": {},
		"disable": { "rotation": [ "rotationArbitrary"] },
		"append": {}
	}

The features block allows you to enable or disable specific IIIF features. Currently only image related features may be manipulated.

For example the level 2 spec does not say GIF outputs is required so the level 2 compliance definition in go-iiif disables it by default. If you are using a graphics engine (not libvips though) that can produce GIF files you would enable it here.

Likewise you may need to disable a feature that is supported by not required or features that are required but can't be used for one reason or another. For example libvips does not allow support for the following features: sizeByDistortedWh (size), rotationArbitrary (rotation), bitonal (quality).

Finally, maybe you've got an IIIF implementation that knows how to do things not defined in the spec. This is also where you would add them.

compliance

Here's how that dynamic plays out in reality. The table below lists all the IIIF parameters and their associate features. Each feature lists its syntax and whether or not it is required and supported according to the official spec but then also according to the example go-iiif config file, included with this repo.

This table was generated using the iiif-dump-config tool and if anyone can tell me how to make Markdown tables (in GitHub) render colours I would be grateful.

region
featuresyntaxrequired (spec)supported (spec)required (config)supported (config)
fullfull<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
regionByPctpct:x,y,w,h<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
regionByPxx,y,w,h<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
regionSquaresquare<span style="color:red;">false</span><span style="color:red;">false</span><span style="color:green;">true</span><span style="color:red;">false</span>
size
featuresyntaxrequired (spec)supported (spec)required (config)supported (config)
fullfull<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
maxmax<span style="color:red;">false</span><span style="color:green;">true</span><span style="color:red;">false</span><span style="color:green;">true</span>
sizeByConfinedWh!w,h<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
sizeByDistortedWhw,h<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:red;">false</span>
sizeByH,h<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
sizeByPctpct:n<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
sizeByWw,<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
sizeByWhw,h<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
rotation
featuresyntaxrequired (spec)supported (spec)required (config)supported (config)
mirroring!n<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
none0<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
rotationArbitrary<span style="color:red;">false</span><span style="color:green;">true</span><span style="color:red;">false</span><span style="color:red;">false</span>
rotationBy90s90,180,270<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
noAutoRotate-1<span style="color:red;">false</span><span style="color:red;">false</span><span style="color:red;">false</span><span style="color:green;">true</span>
quality
featuresyntaxrequired (spec)supported (spec)required (config)supported (config)
bitonalbitonal<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:red;">false</span>
colorcolor<span style="color:red;">false</span><span style="color:green;">true</span><span style="color:red;">false</span><span style="color:green;">true</span>
defaultdefault<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
ditherdither<span style="color:red;">false</span><span style="color:green;">false</span><span style="color:red;">false</span><span style="color:green;">true</span>
graygray<span style="color:red;">false</span><span style="color:red;">false</span><span style="color:red;">false</span><span style="color:red;">false</span>

Careful readers may notice the presence of an undefined (by the IIIF spec) feature named dither. This is a go-iiif -ism and discussed in detail below in the features.append and non-standard features sections.

format
featuresyntaxrequired (spec)supported (spec)required (config)supported (config)
gifgif<span style="color:red;">false</span><span style="color:red;">false</span><span style="color:red;">false</span><span style="color:red;">false</span>
jp2jp2<span style="color:red;">false</span><span style="color:red;">false</span><span style="color:red;">false</span><span style="color:red;">false</span>
jpgjpg<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
pdfpdf<span style="color:red;">false</span><span style="color:red;">false</span><span style="color:red;">false</span><span style="color:red;">false</span>
pngpng<span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span><span style="color:green;">true</span>
tiftif<span style="color:red;">false</span><span style="color:red;">false</span><span style="color:red;">false</span><span style="color:green;">true</span>
webpwebp<span style="color:red;">false</span><span style="color:red;">false</span><span style="color:red;">false</span><span style="color:green;">true</span>

Support for GIF output is not enabled by default because it is not currently supported by bimg (the Go library on top of lipvips). There is however native support for converting final images to be GIFs but you will need to enable that by hand, below.

features.enable

	"features": {
		"enable": {
			"size": [ "max" ],
			"format": [ "webp", "tif" ]
		}
	}

Individual features for a given parameter are enabled by including the parameter name as a key to the features.enabled dictionary whose value is a list of specific feature names to enable.

features.disable

	"features": {
		"disable": {
			"size": [ "sizeByDistortedWh" ] ,
			"rotation": [ "rotationArbitrary" ],
			"quality": [ "bitonal" ]
		}
	}

Individual features for a given parameter are disabled by including the parameter name as a key to the features.disabled dictionary whose value is a list of specific feature names to disabled.

features.append

	"features": {
		"append": { "quality": {
			"dither": { "syntax": "dither", "required": false, "supported": true, "match": "^dither$" }
		}}
	}

New features are added by including their corresponding parameter name as a key to the features.append dictionary whose value is a model for that feature. The data model for new features to append looks like this:

	NAME (STRING): {
		"syntax": SYNTAX (STRING),
		"required": BOOLEAN,
		"supported": BOOLEAN,
		"match": REGULAR_EXPRESSION (STRING)
	}

All keys are required.

The supported key is used to determine whether a given feature is enabled or not. The match key is used to validate user input and should be a valid regular expression that will match that value. For example here is the compliance definition for images returned in the JPEG format:

		"format": {
	     	       "jpg": { "syntax": "jpg",  "required": true, "supported": true, "match": "^jpe?g$" }
		}

Important: It is left to you to actually implement support for new features in the code for whichever graphics engine you are using. If you don't then any new features will be ignored at best or cause fatal errors at worst.

images

	"images": {
		"source": { "name": "Disk", "path": "example/images" },
		"cache": { "name": "Memory", "ttl": 300, "limit": 100 }
	}

Details about source images.

images.source

Where to find source images.

Blob
	"images": {
		"source": { "name": "Blob", "path": "file:///example/images" }
	}

Fetch sources images from any supported Go Cloud storage service.

Some notes about the Blob source:

	"images": {
		"source": { "name": "Blob", "path": "s3:///bucket-name?region=us-east-1&credentials=iam:" }
	}
Disk
	"images": {
		"source": { "name": "Disk", "path": "example/images" }
	}

Fetch source images from a locally available filesystem.

The Disk source is still supported but has been replaced by the Blob source.

Flickr
	"images": {
		"source": { "name": "Flickr" },
		"cache": { "name": "Memory", "ttl": 60, "limit": 100 }
	},
	"flickr": {
		"client_uri": "oauth1://?consumer_key={KEY}&consumer_secret={SECRET}",
	}

Fetch source images from Flickr. You will need to provide a valid Flickr API key. A few caveats:

Here's an example with this photo:

S3
	"images": {
		"source": { "name": "S3", "path": "your.S3.bucket", "region": "us-east-1", "credentials": "default" }
	}

Fetch source images from Amazon's S3 service. S3 caches assume that that the path key is the name of the S3 bucket you are reading from. S3 caches have three addition properties:

For the sake of backwards compatibilty if the value of credentials is any other string then it will be assumed to be the name of the profile you wish to use for a valid credential files in the home directory of the current user. Likewise if the value of credentials is an empty string (or absent) it will be assumed that valid AWS access credentials have been defined as environment variables.

It is not possible to define your AWS credentials as properties in your go-iiif config file.

Important: If you are both reading source files and writing cached derivatives to S3 in the same bucket make sure they have different prefixes. If you don't then AWS will happily overwrite your original source files with the directory (which shares the same names as the original file) containing your derivatives. Good times.

The S3 source is still supported but has been replaced by the Blob source.

URI
	"images": {
		"source": { "name": "URI", "path": "https://images.collection.cooperhewitt.org/{id}" }
	}

Fetch source images from a remote URI. The path parameter must be a valid (Level 4) URI Template with an {id} placeholder.

images.cache

Caching options for source images.

Blob
	"images": {
		"cache": { "name": "Blob", "path": "file:///example/images" }
	}

Cache sources images to any supported Go Cloud storage service.

Some notes about the Blob cache:

	"images": {
		"cache": { "name": "Blob", "path": "s3:///bucket-name?region=us-east-1&credentials=iam:" }
	}
	"images": {
		"cache": { "name": "Blob", "path": "s3:///bucket-name?region=us-east-1&credentials=iam:&acl=public-read" }
	}
Disk
	"images": {
		"cache": { "name": "Disk", "path": "example/cache" }
	}

Cache images to a locally available filesystem.

The Disk cache is still supported but has been replaced by the Blob cache.

Memory
	"images": {
		"cache": { "name": "Memory", "ttl": 300, "limit": 100 }
	}

Cache images in memory. Memory caches have two addition properties:

Null
	"images": {
		"cache": { "name": "Null" }
	}

Because you must define a caching layer this is here to satify the requirements without actually caching anything, anywhere.

derivatives

	"derivatives": {
		"cache": { "name": "Disk", "path": "example/cache" }
	}

Details about derivative images.

derivatives.cache

Caching options for derivative images.

Blob
	"derivatives": {
		"cache": { "name": "Blob", "path": "file:///example/images" }
	}

Cache derivation images to any supported Go Cloud storage service.

Some notes about the Blob cache:

	"derivatives": {
		"cache": { "name": "Blob", "path": "s3:///bucket-name?region=us-east-1&credentials=iam:" }
	}
	"derivatives": {
		"cache": { "name": "Blob", "path": "s3:///bucket-name?region=us-east-1&credentials=iam:&acl=public-read" }
	}
Disk
	"derivatives": {
		"cache": { "name": "Disk", "path": "example/cache" }
	}

Cache images to a locally available filesystem.

The Disk cache is still supported but has been replaced by the Blob cache.

Memory
	"derivatives": {
		"cache": { "name": "Memory", "ttl": 300, "limit": 100 }
	}

Cache images in memory. Memory caches have two addition properties:

Null
	"derivatives": {
		"cache": { "name": "Null" }
	}

Because you must define a caching layer this is here to satify the requirements without actually caching anything, anywhere.

S3
	"derivatives": {
		"cache": { "name": "S3", "path": "your.S3.bucket", "region": "us-east-1", "credentials": "default" }
	}

Cache images using Amazon's S3 service. S3 caches assume that that the path key is the name of the S3 bucket you are reading from. S3 caches have three addition properties:

For the sake of backwards compatibilty if the value of credentials is any other string then it will be assumed to be the name of the profile you wish to use for a valid credential files in the home directory of the current user. Likewise if the value of credentials is an empty string (or absent) it will be assumed that valid AWS access credentials have been defined as environment variables.

It is not possible to define your AWS credentials as properties in your go-iiif config file.

Important: If you are both reading source files and writing cached derivatives to S3 in the same bucket make sure they have different prefixes. If you don't then AWS will happily overwrite your original source files with the directory (which shares the same names as the original file) containing your derivatives. Good times.

The S3 cache is still supported but has been replaced by the Blob cache.

Non-standard features

Non-standard region features

regionByPx (and "smart" cropping)

If you are using VIPS as a graphics engine and pass a regionByPx instruction whose X and Y values are -1 then the code will ask libvips to crop the image (to the dimensions defined in the W and H values) centered on whatever libvips thinks it the most interesting or relevant part of the image.

See also: https://github.com/jcupitt/libvips/issues/317

Non-standard rotation features

go-iiif supports the following non-standard IIIF rotation features:

noAutoRotate

	"enable": {
	    "rotation": [ "noAutoRotate" ]
	}

If the noAutoRotate feature is enabled this will act as a signal to the underlying image processing library to not auto-rotate images according to the EXIF Orientation property (assuming it is present).

This feature exists because both the libvips library and the bimg wrapper code enable auto-rotation by default but neither updates the EXIF Orientation property to reflect the change so every time the newly created image is read by a piece of software that supports auto-rotation (including this one) that image will be doubly-rotated (and then triply-rotated and so on...)

If the noAutoRotate feature is enabled is can be triggered by setting the rotation element of your request URI to be -1, for example:

https://example.com/example.jpg/{REGION}/{SIZE}/-1/{QUALITY}.{FORMAT}

As of this writing the noAutoRotate feature does not work in combination with other rotation commands (for example -1,180 or equivalent, meaning "do not auto-rotate but please still rotate 180 degrees") but it probably should.

Non-standard quality features

go-iiif supports the following non-standard IIIF quality features:

"Crisp"-ing

	"append": {
	    "quality": {
			"crisp": { "syntax": "crisp", "required": false, "supported": true, "match": "^crisp(?:\\:(\\d+\\.\\d+),(\\d+\\.\\d+),(\\d+\\.\\d+))?$"
	    }
	}

crisp will apply an "UnsharpMask" filter followed by a "Median" filter on an image using the bild/effect package.

The crisp filter takes three positional parameters:

PositionNameDefault
1Unsharp Mask Radius2.0
2Unsharp Mask Amount0.5
3Mediam Radius0.025

For example, this:

http://localhost:8080/spanking-cat.jpg/-1,-1,320,320/full/0/crisp:10.0,2.0,0.05.png

Would produce the following image:

spanking cat

Dithering

	"append": {
		"quality": {
			"dither": { "syntax": "dither", "required": false, "supported": true, "match": "^dither$" }
		}
	}

dither will create a black and white halftone derivative of an image using the Atkinson dithering algorithm. Dithering is enabled in the example config file and you can invoke it like this:

http://localhost:8082/184512_5f7f47e5b3c66207_x.jpg/pct:41,7,40,70/,5000/0/dither.png

And here's what you should see, keeping in mind that this screenshot shows only a section of the image at full size:

spanking cat

There are a few caveats about dithering images:

Primitive-ing

	"features": {
		"append": {
			"quality": {
				"primitive": { "syntax": "primitive:mode,iterations,alpha", "required": false, "supported": true, "match": "^primitive\\:[0-5]\\,\\d+\\,\\d+$" }
			}
		}
	},
	"primitive": { "max_iterations": 100 }

Note the way the primitive block is a top-level element in your config file.

primitive use @fogleman's primitive library to reproduce the final image using geometric primitives. Like this:

The syntax for invoking this feature is primitive:{MODE},{ITERATIONS},{ALPHA} where:

For example:

http://localhost:8082/184512_5f7f47e5b3c66207_x.jpg/full/500,/0/primitive:5,200,255.jpg

Be aware that it's not exactly "fast". It's getting faster but it still takes a while. Also, this code should probably have a flag to downsize the input image for processing (and then resizing it back up to the requested size) but that doesn't happen yet. Basically you should not enable this feature as a public-facing web service because it will take seconds (not microseconds) or sometimes even minutes to render a single 256x256 tile. For example:

./bin/iiif-server -host 0.0.0.0 -config config.json
2016/09/21 15:43:08 Serving [::]:8080 with pid 5877
2016/09/21 15:43:13 starting model at 2016-09-21 15:43:13.626117993 +0000 UTC
2016/09/21 15:43:13 finished step 1 in 8.229683ms
2016/09/21 15:43:16 finished step 2 in 3.019413861s
…
2016/09/21 15:45:38 finished step 100 in 2m24.626232387s
2016/09/21 15:45:39 finished model in 2m25.611790848s

But it is pretty darn cool!

If you specify a gif format parameter then go-iiif will return an animated GIF for the requested image consisting of each intermediate stage that the primitive library generated the final image. For example:

http://localhost:8082/184512_5f7f47e5b3c66207_x.jpg/full/500,/0/primitive:5,100,255.gif

Which would produce this:

Here are examples where each of the tiles in an slippy image are animated GIFs:

Note: You will need to manually enable support for GIF images in your config file for animated GIFs to work.

Non-standard services

palette

go-iiif supports the following additional services for profiles:

Details for configuring these service are discussed above but here is the output for a service with the default settings:

$> curl -s localhost:8080/spanking.jpg/info.json | jq '.service'

[
  {
    "@context": "x-urn:service:go-iiif#palette",
    "profile": "x-urn:service:go-iiif#palette",
    "label": "x-urn:service:go-iiif#palette",
    "palette": [
      {
        "name": "#4e3c24",
        "hex": "#4e3c24",
        "reference": "vibrant"
      },
      {
        "name": "#9d8959",
        "hex": "#9d8959",
        "reference": "vibrant"
      },
      {
        "name": "#c7bca6",
        "hex": "#c7bca6",
        "reference": "vibrant"
      },
      {
        "name": "#5a4b36",
        "hex": "#5a4b36",
        "reference": "vibrant"
      }
    ]
  },
  {
    "@context": "x-urn:service:go-iiif#blurhash",
    "profile": "x-urn:service:go-iiif#blurhash",
    "label": "x-urn:service:go-iiif#blurhash",
    "hash": "LOOWsZxu_4-;~pj[Rjof-;kBIAWB"
  },
  {
    "@context": "x-urn:service:go-iiif#imagehash",
    "profile": "x-urn:service:go-iiif#imagehash",
    "label": "x-urn:service:go-iiif#imagehash",
    "average": "a:ffffc7e7c3c3c3c3",
    "difference": "d:c48c0c0e8e8f0e0f"
  }
]

Please remember that go-colours itself is a work in progress so you should approach the palette service accordingly.

Writing your own non-standard services

Services are invoked by the go-iiif codebase using URI-style identifiers. For example, assuming an "example" service you would invoke it like this:

    	service_name := "example"	
	service_uri := fmt.Sprintf("%s://", service_name)
	service, _ := iiifservice.NewService(ctx, service_uri, cfg, im)

In addition to implementing the service.Service interface custom services need to also "register" themselves on initialization with a (golang) context, a (go-iiif), a unique scheme used to identify the service and a service.ServiceInitializationFunc callback function. The callback function implements the following interface:

type ServiceInitializationFunc func(ctx context.Context, config *iiifconfig.Config, im iiifimage.Image) (Service, error)

Here is an abbreviated example, with error handling removed for the sake of brevity. For real working examples, take a look at any of the built-in services in the services directory.

package example	// for example "github.com/example/go-iiif-example"

import (
	"context"
	iiifconfig "github.com/go-iiif/go-iiif/v6/config"
	iiifimage "github.com/go-iiif/go-iiif/v6/image"	
	iiifservice "github.com/go-iiif/go-iiif/v6/service"	
)

func init() {
	ctx := context.Background()
	iiifservice.RegisterService(ctx, "example", initExampleService)
}

func initExampleService(ctx context.Context, cfg *iiifconfig.Config, im iiifimage.Image) (iiifservice.Service, error) {
	return NewExampleService(cfg, im)
}

type ExampleService struct {
	iiifservice.Service        `json:",omitempty"`
	// your properties here...
}

// your implementation of the iiifservice.Service interface here...

func NewExampleService(cfg *iiifconfig.Config, im iiifimage.Image) (iiifservice.Service, error){

     // presumably you'd do something with im here...
     
     s := &ExampleService{
       // your properties here...
     }

     return s, nil
}

Finally, you will need to create custom versions of any go-iiif tools you want to you use your new service. For example, here's a modified version of the cmd/iiif-server/main.go server implementation.

package main

import (
       _ "github.com/example/go-iiif-example"
)

import (
	"context"
	_ "github.com/aaronland/go-cloud-s3blob"
	_ "github.com/go-iiif/go-iiif/v6/native"
	"github.com/go-iiif/go-iiif/v6/tools"
	_ "gocloud.dev/blob/fileblob"
	"log"
)

func main() {

	tool, err := tools.NewIIIFServerTool()

	if err != nil {
		log.Fatal(err)
	}

	err = tool.Run(context.Background())

	if err != nil {
		log.Fatal(err)
	}
}

The only change from the default server tool is the addition of the _ "github.com/example/go-iiif-example" import statement. That will allow the core go-iiif software to find and use your custom service.

It's unfortunate that using custom and bespoke services requires compiling your own version of the go-iiif tools but such is life when you are using a language like Go.

Example

There is a live demo of the Leaflet-IIIF slippymap provider used in conjunction with a series of tiles images generated using the iiif-tile-seed utility available for viewing over here:

https://go-iiif.github.io/go-iiif/

The iiif-server tool also comes with a canned example (consisting of exactly one image) so you can see things in the context of a slippy map. Here's what you need to do to get it set up:

First, make sure have a valid go-iiif config file. If you don't then you can copy the example config included in this repo:

$> cp docs/config.json.example config.json

Next, pre-seed some tiles for an image. You don't necessarily need to do this step but it's included to show you how it's done:

$> ./bin/iiif-tile-seed -config config.json -endpoint http://localhost:8082 -scale-factors 8,4,2,1 184512_5f7f47e5b3c66207_x.jpg

Note how we are specifying the endpoint where these tiles will be served from. That's necessary so that we can also pre-seed a profile description for each image as well as tiles.

Finally start up the iiff-server and be sure to pass the -example flag:

$> ./bin/iiif-server -config config.json -host localhost -port 8082 -example

Now if you visit http://localhost:8082/example/ in your browser you should see this:

spanking cat

Assuming you've pre-seed your tiles if you open up the network console in your browser then you should see something like this, namely that the individual tiles are returned speedy and fast:

spanking cat

Generating static images

spanking cat

The example included with go-iiif has an added super power which is the ability to create a static image of the current state of the map/image.

Just click the handy 📷 button to the bottom right of the image and you will be prompted for where you'd like to save your new image.

This is not a feature of go-iiif itself. It's entirely client-side magic in your browser but it's still pretty cool...

Performance and load testing

iiif-tile-seed

Processing individual or small batches of images go-iiif ranges from pretty fast to very fast. For example here is a picture of Spanking Cat width a maximum dimension of 4096 pixels:

$> ./bin/iiif-tile-seed -config config.json -refresh -scale-factors 8,4,2,1 184512_5f7f47e5b3c66207_x.jpg
[184512_5f7f47e5b3c66207_x.jpg] time to process 340 tiles: 27.537429902s

So any individual tile is pretty speedy but in the aggregate it starts to add up. I will need to do some continued digging to make sure that the source image isn't being processed unnecessarily for each tile. Here is the same image but with a maximum dimension of 2048 pixels:

$> ./bin/iiif-tile-seed -config config.json -refresh -scale-factors 4,2,1 184512_b812003c86c3525b_k.jpg
[184512_b812003c86c3525b_k.jpg] time to process 84 tiles: 1.894074539s

Note that we are only generating tiles for three scale factors instead of four. But that's not where things slow down as we can see seeding tiles for only three scale factors for the larger image:

$> ./bin/iiif-tile-seed -config config.json -refresh -scale-factors 4,2,1 184512_5f7f47e5b3c66207_x.jpg
[184512_5f7f47e5b3c66207_x.jpg] time to process 336 tiles: 26.925253066s

For processing large, or large volumes of, images the bottlenecks will be:

That said on a machine with 8 CPUs and 32GB RAM I was able to run the machine hot with all the CPUs pegged at 100% usage and seed 100, 000 (2048x pixel) images yielding a little over 3 million, or approximately 70GB of, tiles in 24 hours. Some meaningful but not overwhelming amount of time was spent fetching source images across the network so presumably things would be faster reading from a local filesystem.

Memory usage across all the iiif-tile-seed processes never went above 5GB and, in the end, I ran out of inodes.

The current strategy for seeding tiles may also be directly responsible for some of the bottlenecks. Specifically, when processing large volumes of images (defined in a CSV file) the ifff-tile-seed will spawn and queue as many concurrent Go routines as there are CPUs. For each of those processes then another (n) CPUs * 2 subprocesses will be spawned to generate tiles. Maybe this is just too image concurrent image processing routines to have? I mean it works but still... Or maybe it's just that every one is waiting for bytes to be written to disk. Or all of the above. I'm not sure yet.

iiif-server

All of the notes so far have assumed that you are using iiif-tile-seed. If you are running iiif-server the principle concern will be getting overwhelmed by too many requests for too many different images, especially if they are large, and running out of memory. That is why you can define an in-memory cache for source images but that will only be of limited use if your problem is handling concurrent requests. It is probably worth adding checks and throttles around current memory usage to the various handlers...

Docker

Yes. There is a Dockerfile included with this distribution. It will build a container with the following tools:

To build the container run:

$> docker build -f Dockerfile -t go-iiif .

To start the iiif-server tool run:

$> docker run -it -p 6161:8080 \
   -v /usr/local/go-iiif/docker/etc:/etc/iiif-server \
   -v /usr/local/go-iiif/docker/images:/usr/local/iiif-server \
   go-iiif \
   /bin/iiif-server -host 0.0.0.0 \
   -config-source file:///etc/iiif-server
   
2018/06/20 23:03:10 Listening for requests at 0.0.0.0:8080

See the way we are mapping /etc/iiif-server and /usr/local/iiif-server to local directories? By default the iiif-server Dockerfile does not bundle config files or images. Maybe some day, but that day is not today.

Then, in another terminal:

$> curl localhost:6161/test.jpg/info.json
{"@context":"http://iiif.io/api/image/2/context.json","@id":"http://localhost:6161/test.jpg","@type":"iiif:Image","protocol":"http://iiif.io/api/image","width":3897,"height":4096,"profile":["http://iiif.io/api/image/2/level2.json",{"formats":["gif","webp","jpg","png","tif"],"qualities":["default","color","dither"],"supports":["full","regionByPx","regionByPct","regionSquare","sizeByDistortedWh","sizeByWh","full","max","sizeByW","sizeByH","sizeByPct","sizeByConfinedWh","none","rotationBy90s","mirroring","noAutoRotate","baseUriRedirect","cors","jsonldMediaType"]}],"service":[{"@context":"x-urn:service:go-iiif#palette","profile":"x-urn:service:go-iiif#palette","label":"x-urn:service:go-iiif#palette","palette":[{"name":"#2f2013","hex":"#2f2013","reference":"vibrant"},{"name":"#9e8e65","hex":"#9e8e65","reference":"vibrant"},{"name":"#c6bca6","hex":"#c6bca6","reference":"vibrant"},{"name":"#5f4d32","hex":"#5f4d32","reference":"vibrant"}]}]}

Let's say you're using S3 as an image source and reading (S3) credentials from environment variables (something like {"source": { "name": "S3", "path": "{BUCKET}", "region": "us-east-1", "credentials": "env:" }) then you would start up iiif-server like this:

$> docker run -it -p 6161:8080 \
       -v /usr/local/go-iiif/docker/etc:/etc/iiif-server \
       -v /usr/local/go-iiif/docker/images:/usr/local/iiif-server \
       -e AWS_ACCESS_KEY_ID={AWS_KEY} -e AWS_SECRET_ACCESS_KEY={AWS_SECRET} \
       go-iiif \
       /bin/iiif-server -host 0.0.0.0 \
       -config-source file:///etc/iiif-server

The process an image using the iiif-process tool you would run something like:

$> docker run \
   -v /usr/local/go-iiif/docker/etc:/etc/go-iiif \
   go-iiif /bin/iiif-process \
   -config-source file:///etc/go-iiif \
   -instructions-source file:///etc/go-iiif \
   file:///test.jpg

To tile an image using the iiif-tile-seed tool you would run something like:

$> docker run -v /usr/local/go-iiif-vips/docker:/usr/local/go-iiif \
	go-iiif /bin/iiif-tile-seed \
	-config-source file:////usr/local/go-iiif/config \
	-scale-factors 1,2,4,8 \
	file:///zuber.jpg

Again, see the way we're mapping /etc/go-iiif to a local folder, like we do in the iiif-server Docker example? The same rules apply here.

Amazon ECS

I still find ECS to be a world of poorly-to-weirdly documented strangeness .Remy Dewolf's AWS Fargate: First hands-on experience and review is a pretty good introduction.

I have gotten IIIF-related things to work in ECS but it's always a bit nerve-wracking and I haven't completely internalized the steps in order to repeat them to someone else. What follows should be considered a "current best-attempt".

iiif-server

What follows are non-comprehensive notes for getting iiif-server to work under ECS. The bad news is that it's fiddly (and weird, did I mention that?) The good news is that I did get it to work.

These are not detailed instructions for setting up iiif-server in ECS from scratch. You should consult the Amazon Elastic Container Service Documentation for that.

What follows assumes that you're using an S3 "source" for source images and derivatives. I have not tried any of this with EBS volumes mounted as Docker volumes so if you have I'd love to hear about it.

Services

You will need to ensure that the service has Auto-assign public IP(s) enabled.This is necessary in order to fetch the actual Docker container.

The corollary to that is that unless you are wanting to expose your instances of iiif-server to the public internet you will need to add a security group (to your ECS service) with suitable restrictions.

Task definitions
Command

/bin/iiif-server

Port mappings

8080

Environment variables
VariableValue
IIIF_CONFIG_JSONYour IIIF config file encoded as a string
IIIF_SERVER_CONFIGenv:IIIF_CONFIG_JSON
AWS_ACCESS_KEY_IDA valid AWS access key ID for talking to the S3 bucket defined in your config file
AWS_SECRET_ACCESS_KEYA valid AWS access secret for talking to the S3 bucket defined in your config file

As of this writing the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY variables are necessary because if you specify credential="iam:" for an S3 source (in your IIIF config file) the server fails to start up with a weird error I've never seen before. Computers...

iiif-process

I have had an easier time setting up a Docker-ized iiif-process container in ECS and running it as a simple ECS task.

Services

You will need to ensure that the service has Auto-assign public IP(s) enabled.This is necessary in order to fetch the actual Docker container.

Task definitions

Your "task definition" will need a suitable AWS IAM role with the following properties:

And the following policies assigned to it:

The task should be run in awsvpc network mode and required the FARGATE capability.

Unlike the iiif-server container as of this writing it is not possible to pass in the IIIF config file (or the instructions file) as an environment variable. I've never really loved that approach and want to reconsider it for all the ifff- tools.

This means you have a container that can run /bin/iiif-process but where does it find any of it's configuration information? The short answer is you don't use the Dockerfile.process Dockerfile in this package. Or you create a local copy of it customizing it as necessary.

Instead you should use the Dockerfile.process.ecs Dockerfile defined in the go-iiif-aws package.

This package will create a custom iiif-process container copying a custom IIIF config and instructions file into /etc/go-iiif/config.json and /etc/go-iiif/instructions.json respectively. This is the container image that you would then upload as a task to your AWS ECS account.

It will also build a iiif-process-ecs tool that can be:

The Dockerfile in the go-iiif-aws package will build the iiif-process binary from this package but otherwise manages all of the ECS, Lambda and other AWS-specific code in its own codebase.

Notes

Bugs?

Probably. Please consult the currently known-known issues and if you don't see what ails you please feel free to add it.

See also

IIIF stuff

go-iiig stuff

Go stuff

Slippy map stuff

Blog posts

Other stuff