From c2eefa8712d4449d1bdf9c1c677905665b5197dc Mon Sep 17 00:00:00 2001 From: "Michael B. Klein" <mbklein@gmail.com> Date: Thu, 21 Nov 2024 18:12:57 +0000 Subject: [PATCH] Add more flexibility to `pathPrefix` constructor option --- CHANGELOG.md | 3 ++ README.md | 10 ++++++- examples/tiny-iiif/config.js | 3 +- examples/tiny-iiif/iiif.js | 4 +-- package.json | 2 +- src/processor.js | 56 +++++++++++++++--------------------- tests/v2/integration.test.js | 4 +-- tests/v2/processor.test.js | 7 +++-- tests/v3/integration.test.js | 4 +-- tests/v3/processor.test.js | 4 +-- 10 files changed, 50 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93571ed..8ca67bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/README.md b/README.md index 95c4f2f..dfb3cb6 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/examples/tiny-iiif/config.js b/examples/tiny-iiif/config.js index 70a6772..619e76f 100644 --- a/examples/tiny-iiif/config.js +++ b/examples/tiny-iiif/config.js @@ -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 }; diff --git a/examples/tiny-iiif/iiif.js b/examples/tiny-iiif/iiif.js index 6fe57bb..2382c5c 100644 --- a/examples/tiny-iiif/iiif.js +++ b/examples/tiny-iiif/iiif.js @@ -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 }) => { @@ -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) diff --git a/package.json b/package.json index f8c33a4..adc626b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/processor.js b/src/processor.js index 44d036e..1e13f42 100644 --- a/src/processor.js +++ b/src/processor.js @@ -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'); @@ -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) { @@ -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; @@ -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]; diff --git a/tests/v2/integration.test.js b/tests/v2/integration.test.js index c165fa9..b9b5970 100644 --- a/tests/v2/integration.test.js +++ b/tests/v2/integration.test.js @@ -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'); @@ -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); diff --git a/tests/v2/processor.test.js b/tests/v2/processor.test.js index bd831eb..a905cec 100644 --- a/tests/v2/processor.test.js +++ b/tests/v2/processor.test.js @@ -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/'); }); }); @@ -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(); }) }) @@ -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(); }) diff --git a/tests/v3/integration.test.js b/tests/v3/integration.test.js index 38a2795..dfca886 100644 --- a/tests/v3/integration.test.js +++ b/tests/v3/integration.test.js @@ -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'); @@ -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); diff --git a/tests/v3/processor.test.js b/tests/v3/processor.test.js index c2814d2..fdcfe5f 100644 --- a/tests/v3/processor.test.js +++ b/tests/v3/processor.test.js @@ -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(); }) }) @@ -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(); })