Skip to content

Commit

Permalink
Merge pull request #36 from samvera/30-flexible-path-template
Browse files Browse the repository at this point in the history
Add more flexibility to `pathPrefix` constructor option
  • Loading branch information
mbklein authored Nov 22, 2024
2 parents dbb0b30 + c2eefa8 commit e483c51
Show file tree
Hide file tree
Showing 10 changed files with 50 additions and 47 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Only features and major fixes are listed. Everything else can be considered a minor bugfix or maintenance release.

##### v5.1.0
- Update `pathPrefix` constructor option to accept a `{{version}}` placeholder and RegExp elements (default: `/iiif/{{version}}/`)

##### v5.0.0
- Export `Calculator`
- Make `sharp` an optional dependency for those who just want to use `Calculator`
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const processor = new IIIF.Processor(url, streamResolver, opts);
* `density` (integer) – the pixel density to be included in the result image in pixels per inch
* This has no effect whatsoever on the size of the image that gets returned; it's simply for convenience when using
the resulting image in software that calculates a default print size based on the height, width, and density
* `pathPrefix` (string) – the default prefix that precedes the `id` part of the URL path (default: `/iiif/2/`)
* `pathPrefix` (string) – the template used to extract the IIIF version and API parameters from the URL path (default: `/iiif/{{version}}/`) ([see below](#path-prefix))
* `version` (number) – the major version (`2` or `3`) of the IIIF Image API to use (default: inferred from `/iiif/{version}/`)

## Examples
Expand Down Expand Up @@ -109,6 +109,14 @@ async function dimensionFunction({ id, baseUrl }) {
}
```

### Path Prefix

The `pathPrefix` constructor option provides a tremendous amount of flexibility in how IIIF URLs are structured. The prefix includes one placeholder `{{version}}`, indicating the major version of the IIIF Image API to use when interpreting the rest of the path.

* The `pathPrefix` _must_ start and end with `/`.
* The `pathPrefix` _must_ include the `{{version}}` placeholder _unless_ the `version` constructor option is specified. If both are present, the constructor option will take precedence.
* To allow for maximum flexibility, the `pathPrefix` is interpreted as a [JavaScript regular expression](https://www.w3schools.com/jsref/jsref_obj_regexp.asp). For example, `/.+?/iiif/{{version}}/` would allow your path to have arbitrary path elements before `/iiif/`. Be careful when including greedy quantifiers (e.g., `+` as opposed to `+?`), as they may produce unexpected results. `/` characters are treated as literal path separators, not regular expression delimiters as they would be in JavaScript code.

### Processing

#### Promise
Expand Down
3 changes: 2 additions & 1 deletion examples/tiny-iiif/config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const iiifImagePath = process.env.IIIF_IMAGE_PATH;
const iiifpathPrefix = process.env.IIIF_PATH_TEMPLATE;
const fileTemplate = process.env.IMAGE_FILE_TEMPLATE || '{{id}}.tif';
const port = process.env.PORT || 3000;

export { iiifImagePath, fileTemplate, port };
export { iiifImagePath, iiifpathPrefix, fileTemplate, port };
4 changes: 2 additions & 2 deletions examples/tiny-iiif/iiif.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { App } from '@tinyhttp/app';
import { Processor } from 'iiif-processor';
import fs from 'fs';
import path from 'path';
import { iiifImagePath, fileTemplate } from './config.js';
import { iiifImagePath, iiifpathPrefix, fileTemplate } from './config.js';

function createRouter(version) {
const streamImageFromFile = ({ id }) => {
Expand All @@ -21,7 +21,7 @@ function createRouter(version) {

try {
const iiifUrl = `${req.protocol}://${req.get("host")}${req.path}`;
const iiifProcessor = new Processor(iiifUrl, streamImageFromFile);
const iiifProcessor = new Processor(iiifUrl, streamImageFromFile, { pathPrefix: iiifpathPrefix });
const result = await iiifProcessor.execute();
return res
.set("Content-Type", result.contentType)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "iiif-processor",
"version": "5.0.0",
"version": "5.1.0",
"description": "IIIF 2.1 & 3.0 Image API modules for NodeJS",
"main": "./src",
"repository": "https://github.com/samvera/node-iiif",
Expand Down
56 changes: 23 additions & 33 deletions src/processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,25 @@ const { Operations } = require('./transform');
const IIIFError = require('./error');
const IIIFVersions = require('./versions');

const fixupSlashes = (path, leaveOne) => {
const replacement = leaveOne ? '/' : '';
return path?.replace(/^\/*/, replacement).replace(/\/*$/, replacement);
};

const getIIIFVersion = (url, opts = {}) => {
const uri = new URL(url);
try {
let { iiifVersion, pathPrefix } = opts;
if (!iiifVersion) {
const match = /^\/iiif\/(?<v>\d)\//.exec(uri.pathname);
iiifVersion = match.groups.v;
}
if (!pathPrefix) pathPrefix = `iiif/${iiifVersion}/`;
return { iiifVersion, pathPrefix };
} catch {
throw new IIIFError(`Cannot determine IIIF version from path ${uri.path}`);
const defaultpathPrefix = '/iiif/{{version}}/';

function getIiifVersion (url, template) {
const { origin, pathname } = new URL(url);
const templateMatcher = template.replace(/\{\{version\}\}/, '(?<iiifVersion>2|3)');
const pathMatcher = `^(?<prefix>${templateMatcher})(?<request>.+)$`;
const re = new RegExp(pathMatcher);
const parsed = re.exec(pathname);
if (parsed) {
parsed.groups.prefix = origin + parsed.groups.prefix;
return { ...parsed.groups };
} else {
throw new IIIFError('Invalid IIIF path');
}
};

class Processor {
constructor (url, streamResolver, opts = {}) {
const { iiifVersion, pathPrefix } = getIIIFVersion(url, opts);
const { prefix, iiifVersion, request } = getIiifVersion(url, opts.pathPrefix || defaultpathPrefix);

if (typeof streamResolver !== 'function') {
throw new IIIFError('streamResolver option must be specified');
Expand All @@ -44,8 +40,8 @@ class Processor {
};

this
.setOpts({ ...defaults, ...opts, pathPrefix, iiifVersion })
.initialize(url, streamResolver);
.setOpts({ ...defaults, iiifVersion, ...opts, prefix, request })
.initialize(streamResolver);
}

setOpts (opts) {
Expand All @@ -54,29 +50,21 @@ class Processor {
this.max = { ...opts.max };
this.includeMetadata = !!opts.includeMetadata;
this.density = opts.density;
this.pathPrefix = fixupSlashes(opts.pathPrefix, true);
this.baseUrl = opts.prefix;
this.sharpOptions = { ...opts.sharpOptions };
this.version = opts.iiifVersion;
this.request = opts.request;

return this;
}

parseUrl (url) {
const parser = new RegExp(`(?<baseUrl>https?://[^/]+${this.pathPrefix})(?<path>.+)$`);
const { baseUrl, path } = parser.exec(url).groups;
const result = this.Implementation.Calculator.parsePath(path);
result.baseUrl = baseUrl;

return result;
}

initialize (url, streamResolver) {
initialize (streamResolver) {
this.Implementation = IIIFVersions[this.version];
if (!this.Implementation) {
throw new IIIFError(`No implementation found for IIIF Image API v${this.version}`);
}

const params = this.parseUrl(url);
const params = this.Implementation.Calculator.parsePath(this.request);
debug('Parsed URL: %j', params);
Object.assign(this, params);
this.streamResolver = streamResolver;
Expand Down Expand Up @@ -143,7 +131,9 @@ class Processor {
sizes.push({ width: size[0], height: size[1] });
}

const id = [fixupSlashes(this.baseUrl), fixupSlashes(this.id)].join('/');
const uri = new URL(this.baseUrl);
uri.pathname = path.join(uri.pathname, this.id);
const id = uri.toString();
const doc = this.Implementation.infoDoc({ id, ...dim, sizes, max: this.max });
for (const prop in doc) {
if (doc[prop] === null || doc[prop] === undefined) delete doc[prop];
Expand Down
4 changes: 2 additions & 2 deletions tests/v2/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ let consoleWarnMock;

describe('info.json', () => {
it('produces a valid info.json', async () => {
subject = new Processor(`${base}/info.json`, streamResolver, { pathPrefix: 'iiif/2/ab/cd/ef/gh' });
subject = new Processor(`${base}/info.json`, streamResolver, { pathPrefix: '/iiif/{{version}}/ab/cd/ef/gh/' });
const result = await subject.execute();
const info = JSON.parse(result.body);
assert.strictEqual(info['@id'], 'https://example.org/iiif/2/ab/cd/ef/gh/i');
Expand All @@ -24,7 +24,7 @@ describe('info.json', () => {
});

it('respects the maxWidth option', async () => {
subject = new Processor(`${base}/info.json`, streamResolver, { pathPrefix: 'iiif/2/ab/cd/ef/gh', max: { width: 600 }});
subject = new Processor(`${base}/info.json`, streamResolver, { pathPrefix: '/iiif/{{version}}/ab/cd/ef/gh/', max: { width: 600 }});
const result = await subject.execute();
const info = JSON.parse(result.body);
assert.strictEqual(info.profile[1].maxWidth, 600);
Expand Down
7 changes: 4 additions & 3 deletions tests/v2/processor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ describe('constructor', () => {
{ iiifVersion: 3, pathPrefix: '/iiif/III/' }
);
assert.strictEqual(subject.version, 3);
assert.strictEqual(subject.pathPrefix, '/iiif/III/');
assert.strictEqual(subject.id, 'ab/cd/ef/gh/i');
assert.strictEqual(subject.baseUrl, 'https://example.org/iiif/III/');
});
});

Expand Down Expand Up @@ -221,7 +222,7 @@ describe('stream processor', () => {
});
}

const subject = new Processor(`https://example.org/iiif/2/ab/cd/ef/gh/i/10,20,30,40/pct:50/45/default.png`, streamResolver, {pathPrefix: 'iiif/2/ab/cd/ef/gh'});
const subject = new Processor(`https://example.org/iiif/2/ab/cd/ef/gh/i/10,20,30,40/pct:50/45/default.png`, streamResolver, {pathPrefix: '/iiif/{{version}}/ab/cd/ef/gh/'});
subject.execute();
})
})
Expand All @@ -245,7 +246,7 @@ describe('dimension function', () => {
const subject = new Processor(
`https://example.org/iiif/2/ab/cd/ef/gh/i/10,20,30,40/pct:50/45/default.png`,
streamResolver,
{ dimensionFunction, pathPrefix: 'iiif/2/ab/cd/ef/gh' }
{ dimensionFunction, pathPrefix: '/iiif/{{version}}/ab/cd/ef/gh/' }
);
subject.execute();
})
Expand Down
4 changes: 2 additions & 2 deletions tests/v3/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ let consoleWarnMock;

describe('info.json', () => {
it('produces a valid info.json', async () => {
subject = new Processor(`${base}/info.json`, streamResolver, { pathPrefix: 'iiif/3/ab/cd/ef/gh' });
subject = new Processor(`${base}/info.json`, streamResolver, { pathPrefix: '/iiif/{{version}}/ab/cd/ef/gh/' });
const result = await subject.execute();
const info = JSON.parse(result.body);
assert.strictEqual(info.id, 'https://example.org/iiif/3/ab/cd/ef/gh/i');
Expand All @@ -24,7 +24,7 @@ describe('info.json', () => {
});

it('respects max size options', async () => {
subject = new Processor(`${base}/info.json`, streamResolver, { pathPrefix: 'iiif/3/ab/cd/ef/gh', max: { width: 600 } });
subject = new Processor(`${base}/info.json`, streamResolver, { pathPrefix: '/iiif/{{version}}/ab/cd/ef/gh/', max: { width: 600 } });
const result = await subject.execute();
const info = JSON.parse(result.body);
assert.strictEqual(info.maxWidth, 600);
Expand Down
4 changes: 2 additions & 2 deletions tests/v3/processor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ describe('stream processor', () => {
});
}

const subject = new Processor(`https://example.org/iiif/3/ab/cd/ef/gh/i/10,20,30,40/pct:50/45/default.png`, streamResolver, {pathPrefix: 'iiif/3/ab/cd/ef/gh'});
const subject = new Processor(`https://example.org/iiif/3/ab/cd/ef/gh/i/10,20,30,40/pct:50/45/default.png`, streamResolver, {pathPrefix: '/iiif/{{version}}/ab/cd/ef/gh/'});
subject.execute();
})
})
Expand All @@ -244,7 +244,7 @@ describe('dimension function', () => {
const subject = new Processor(
`https://example.org/iiif/3/ab/cd/ef/gh/i/10,20,30,40/pct:50/45/default.png`,
streamResolver,
{ dimensionFunction, pathPrefix: 'iiif/3/ab/cd/ef/gh' }
{ dimensionFunction, pathPrefix: '/iiif/{{version}}/ab/cd/ef/gh/' }
);
subject.execute();
})
Expand Down

0 comments on commit e483c51

Please sign in to comment.