Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more flexibility to pathPrefix constructor option #36

Merged
merged 1 commit into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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