Skip to content

Commit e483c51

Browse files
authored
Merge pull request #36 from samvera/30-flexible-path-template
Add more flexibility to `pathPrefix` constructor option
2 parents dbb0b30 + c2eefa8 commit e483c51

10 files changed

+50
-47
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

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

5+
##### v5.1.0
6+
- Update `pathPrefix` constructor option to accept a `{{version}}` placeholder and RegExp elements (default: `/iiif/{{version}}/`)
7+
58
##### v5.0.0
69
- Export `Calculator`
710
- Make `sharp` an optional dependency for those who just want to use `Calculator`

README.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const processor = new IIIF.Processor(url, streamResolver, opts);
3333
* `density` (integer) – the pixel density to be included in the result image in pixels per inch
3434
* This has no effect whatsoever on the size of the image that gets returned; it's simply for convenience when using
3535
the resulting image in software that calculates a default print size based on the height, width, and density
36-
* `pathPrefix` (string) – the default prefix that precedes the `id` part of the URL path (default: `/iiif/2/`)
36+
* `pathPrefix` (string) – the template used to extract the IIIF version and API parameters from the URL path (default: `/iiif/{{version}}/`) ([see below](#path-prefix))
3737
* `version` (number) – the major version (`2` or `3`) of the IIIF Image API to use (default: inferred from `/iiif/{version}/`)
3838

3939
## Examples
@@ -109,6 +109,14 @@ async function dimensionFunction({ id, baseUrl }) {
109109
}
110110
```
111111

112+
### Path Prefix
113+
114+
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.
115+
116+
* The `pathPrefix` _must_ start and end with `/`.
117+
* The `pathPrefix` _must_ include the `{{version}}` placeholder _unless_ the `version` constructor option is specified. If both are present, the constructor option will take precedence.
118+
* 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.
119+
112120
### Processing
113121

114122
#### Promise

examples/tiny-iiif/config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const iiifImagePath = process.env.IIIF_IMAGE_PATH;
2+
const iiifpathPrefix = process.env.IIIF_PATH_TEMPLATE;
23
const fileTemplate = process.env.IMAGE_FILE_TEMPLATE || '{{id}}.tif';
34
const port = process.env.PORT || 3000;
45

5-
export { iiifImagePath, fileTemplate, port };
6+
export { iiifImagePath, iiifpathPrefix, fileTemplate, port };

examples/tiny-iiif/iiif.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { App } from '@tinyhttp/app';
22
import { Processor } from 'iiif-processor';
33
import fs from 'fs';
44
import path from 'path';
5-
import { iiifImagePath, fileTemplate } from './config.js';
5+
import { iiifImagePath, iiifpathPrefix, fileTemplate } from './config.js';
66

77
function createRouter(version) {
88
const streamImageFromFile = ({ id }) => {
@@ -21,7 +21,7 @@ function createRouter(version) {
2121

2222
try {
2323
const iiifUrl = `${req.protocol}://${req.get("host")}${req.path}`;
24-
const iiifProcessor = new Processor(iiifUrl, streamImageFromFile);
24+
const iiifProcessor = new Processor(iiifUrl, streamImageFromFile, { pathPrefix: iiifpathPrefix });
2525
const result = await iiifProcessor.execute();
2626
return res
2727
.set("Content-Type", result.contentType)

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "iiif-processor",
3-
"version": "5.0.0",
3+
"version": "5.1.0",
44
"description": "IIIF 2.1 & 3.0 Image API modules for NodeJS",
55
"main": "./src",
66
"repository": "https://github.com/samvera/node-iiif",

src/processor.js

+23-33
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,25 @@ const { Operations } = require('./transform');
66
const IIIFError = require('./error');
77
const IIIFVersions = require('./versions');
88

9-
const fixupSlashes = (path, leaveOne) => {
10-
const replacement = leaveOne ? '/' : '';
11-
return path?.replace(/^\/*/, replacement).replace(/\/*$/, replacement);
12-
};
13-
14-
const getIIIFVersion = (url, opts = {}) => {
15-
const uri = new URL(url);
16-
try {
17-
let { iiifVersion, pathPrefix } = opts;
18-
if (!iiifVersion) {
19-
const match = /^\/iiif\/(?<v>\d)\//.exec(uri.pathname);
20-
iiifVersion = match.groups.v;
21-
}
22-
if (!pathPrefix) pathPrefix = `iiif/${iiifVersion}/`;
23-
return { iiifVersion, pathPrefix };
24-
} catch {
25-
throw new IIIFError(`Cannot determine IIIF version from path ${uri.path}`);
9+
const defaultpathPrefix = '/iiif/{{version}}/';
10+
11+
function getIiifVersion (url, template) {
12+
const { origin, pathname } = new URL(url);
13+
const templateMatcher = template.replace(/\{\{version\}\}/, '(?<iiifVersion>2|3)');
14+
const pathMatcher = `^(?<prefix>${templateMatcher})(?<request>.+)$`;
15+
const re = new RegExp(pathMatcher);
16+
const parsed = re.exec(pathname);
17+
if (parsed) {
18+
parsed.groups.prefix = origin + parsed.groups.prefix;
19+
return { ...parsed.groups };
20+
} else {
21+
throw new IIIFError('Invalid IIIF path');
2622
}
2723
};
2824

2925
class Processor {
3026
constructor (url, streamResolver, opts = {}) {
31-
const { iiifVersion, pathPrefix } = getIIIFVersion(url, opts);
27+
const { prefix, iiifVersion, request } = getIiifVersion(url, opts.pathPrefix || defaultpathPrefix);
3228

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

4642
this
47-
.setOpts({ ...defaults, ...opts, pathPrefix, iiifVersion })
48-
.initialize(url, streamResolver);
43+
.setOpts({ ...defaults, iiifVersion, ...opts, prefix, request })
44+
.initialize(streamResolver);
4945
}
5046

5147
setOpts (opts) {
@@ -54,29 +50,21 @@ class Processor {
5450
this.max = { ...opts.max };
5551
this.includeMetadata = !!opts.includeMetadata;
5652
this.density = opts.density;
57-
this.pathPrefix = fixupSlashes(opts.pathPrefix, true);
53+
this.baseUrl = opts.prefix;
5854
this.sharpOptions = { ...opts.sharpOptions };
5955
this.version = opts.iiifVersion;
56+
this.request = opts.request;
6057

6158
return this;
6259
}
6360

64-
parseUrl (url) {
65-
const parser = new RegExp(`(?<baseUrl>https?://[^/]+${this.pathPrefix})(?<path>.+)$`);
66-
const { baseUrl, path } = parser.exec(url).groups;
67-
const result = this.Implementation.Calculator.parsePath(path);
68-
result.baseUrl = baseUrl;
69-
70-
return result;
71-
}
72-
73-
initialize (url, streamResolver) {
61+
initialize (streamResolver) {
7462
this.Implementation = IIIFVersions[this.version];
7563
if (!this.Implementation) {
7664
throw new IIIFError(`No implementation found for IIIF Image API v${this.version}`);
7765
}
7866

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

146-
const id = [fixupSlashes(this.baseUrl), fixupSlashes(this.id)].join('/');
134+
const uri = new URL(this.baseUrl);
135+
uri.pathname = path.join(uri.pathname, this.id);
136+
const id = uri.toString();
147137
const doc = this.Implementation.infoDoc({ id, ...dim, sizes, max: this.max });
148138
for (const prop in doc) {
149139
if (doc[prop] === null || doc[prop] === undefined) delete doc[prop];

tests/v2/integration.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ let consoleWarnMock;
1414

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

2626
it('respects the maxWidth option', async () => {
27-
subject = new Processor(`${base}/info.json`, streamResolver, { pathPrefix: 'iiif/2/ab/cd/ef/gh', max: { width: 600 }});
27+
subject = new Processor(`${base}/info.json`, streamResolver, { pathPrefix: '/iiif/{{version}}/ab/cd/ef/gh/', max: { width: 600 }});
2828
const result = await subject.execute();
2929
const info = JSON.parse(result.body);
3030
assert.strictEqual(info.profile[1].maxWidth, 600);

tests/v2/processor.test.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@ describe('constructor', () => {
178178
{ iiifVersion: 3, pathPrefix: '/iiif/III/' }
179179
);
180180
assert.strictEqual(subject.version, 3);
181-
assert.strictEqual(subject.pathPrefix, '/iiif/III/');
181+
assert.strictEqual(subject.id, 'ab/cd/ef/gh/i');
182+
assert.strictEqual(subject.baseUrl, 'https://example.org/iiif/III/');
182183
});
183184
});
184185

@@ -221,7 +222,7 @@ describe('stream processor', () => {
221222
});
222223
}
223224

224-
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'});
225+
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/'});
225226
subject.execute();
226227
})
227228
})
@@ -245,7 +246,7 @@ describe('dimension function', () => {
245246
const subject = new Processor(
246247
`https://example.org/iiif/2/ab/cd/ef/gh/i/10,20,30,40/pct:50/45/default.png`,
247248
streamResolver,
248-
{ dimensionFunction, pathPrefix: 'iiif/2/ab/cd/ef/gh' }
249+
{ dimensionFunction, pathPrefix: '/iiif/{{version}}/ab/cd/ef/gh/' }
249250
);
250251
subject.execute();
251252
})

tests/v3/integration.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ let consoleWarnMock;
1414

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

2626
it('respects max size options', async () => {
27-
subject = new Processor(`${base}/info.json`, streamResolver, { pathPrefix: 'iiif/3/ab/cd/ef/gh', max: { width: 600 } });
27+
subject = new Processor(`${base}/info.json`, streamResolver, { pathPrefix: '/iiif/{{version}}/ab/cd/ef/gh/', max: { width: 600 } });
2828
const result = await subject.execute();
2929
const info = JSON.parse(result.body);
3030
assert.strictEqual(info.maxWidth, 600);

tests/v3/processor.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ describe('stream processor', () => {
220220
});
221221
}
222222

223-
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'});
223+
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/'});
224224
subject.execute();
225225
})
226226
})
@@ -244,7 +244,7 @@ describe('dimension function', () => {
244244
const subject = new Processor(
245245
`https://example.org/iiif/3/ab/cd/ef/gh/i/10,20,30,40/pct:50/45/default.png`,
246246
streamResolver,
247-
{ dimensionFunction, pathPrefix: 'iiif/3/ab/cd/ef/gh' }
247+
{ dimensionFunction, pathPrefix: '/iiif/{{version}}/ab/cd/ef/gh/' }
248248
);
249249
subject.execute();
250250
})

0 commit comments

Comments
 (0)