diff --git a/CHANGELOG.md b/CHANGELOG.md index 902377a..e33a022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Only features and major fixes are listed. Everything else can be considered a minor bugfix or maintenance release. +##### v3.0.0 + +- Add `pathPrefix` option (default: `/iiif/2/`) to constructor instead of popping a specific number of path segments off of the end of the URL + ##### v2.0.0 - Pass `baseUrl` to `streamResolver` and `dimension` functions diff --git a/README.md b/README.md index fbebd82..4b0fdd4 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,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/`) ## Examples diff --git a/index.js b/index.js index 84f185a..80d6173 100644 --- a/index.js +++ b/index.js @@ -3,39 +3,19 @@ const mime = require('mime-types'); const transform = require('./lib/transform'); const IIIFError = require('./lib/error'); -const filenameRe = /(color|gray|bitonal|default)\.(jpe?g|tiff?|gif|png|webp)/; - -function parseUrl (url) { - const result = {}; - const segments = url.split('/'); - result.filename = segments.pop(); - if (result.filename.match(filenameRe)) { - result.rotation = segments.pop(); - result.size = segments.pop(); - result.region = segments.pop(); - result.quality = RegExp.$1; - result.format = RegExp.$2; - } - result.id = decodeURIComponent(segments.pop()); - result.baseUrl = segments.join('/'); - return result; -} +const DefaultPathPrefix = '/iiif/2/'; class Processor { constructor (url, streamResolver, ...args) { const opts = this.parseOpts(args); - this - .initialize(url, streamResolver) - .setOpts(opts); - - if (!filenameRe.test(this.filename) && this.filename !== 'info.json') { - throw new IIIFError(`Invalid IIIF URL: ${url}`); - } - if (typeof streamResolver !== 'function') { throw new IIIFError('streamResolver option must be specified'); } + + this + .setOpts(opts) + .initialize(url, streamResolver); } parseOpts (args) { @@ -59,19 +39,39 @@ class Processor { this.maxWidth = opts.maxWidth; this.includeMetadata = !!opts.includeMetadata; this.density = opts.density || null; + this.pathPrefix = opts.pathPrefix?.replace(/^\/*/, '/').replace(/\/*$/, '/') || DefaultPathPrefix; + return this; } - initialize (url, streamResolver) { - let params = url; + parseUrl (url) { + let result; + if (typeof url === 'string') { - params = parseUrl(params); + const parser = new RegExp(`(?https?://[^/]+${this.pathPrefix})(?.+)$`); + const { baseUrl, path } = parser.exec(url).groups; + result = transform.IIIFRegExp.exec(path)?.groups; + if (result === undefined) { + throw new IIIFError(`Invalid IIIF URL: ${url}`); + } + result.baseUrl = baseUrl; + } else { + result = url; } + + return result; + } + + initialize (url, streamResolver) { + const params = this.parseUrl(url); + Object.assign(this, params); this.streamResolver = streamResolver; if (this.quality && this.format) { this.filename = [this.quality, this.format].join('.'); + } else if (this.info) { + this.filename = 'info.json'; } return this; } diff --git a/lib/transform.js b/lib/transform.js index 7ca4b2e..6806890 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -19,11 +19,12 @@ function validator (type) { if (result instanceof Array) { result = result.join('|'); } - return new RegExp('^(' + result + ')$'); + return `(?<${type}>${result})`; } function validate (type, v) { - if (!validator(type).test(v)) { + const re = new RegExp(`^${validator(type)}$`); + if (!re.test(v)) { throw new IIIFError(`Invalid ${type}: ${v}`); } return true; @@ -36,7 +37,15 @@ function validateDensity (v) { throw new IIIFError(`Invalid density value: ${v}`); } return true; -}; +} + +function iiifRegExp () { + const transformation = + ['region', 'size', 'rotation'].map(type => validator(type)).join('/') + + '/' + validator('quality') + '.' + validator('format'); + + return new RegExp(`^/?(?.+?)/(?:(?info.json)|${transformation})$`); +} class Operations { constructor (dims) { @@ -207,6 +216,7 @@ class Operations { module.exports = { Qualities: Validators.quality, Formats: Validators.format, + IIIFRegExp: iiifRegExp(), Operations, IIIFError }; diff --git a/package.json b/package.json index 246ba9f..0c65ce7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iiif-processor", - "version": "2.0.1", + "version": "3.0.0", "description": "IIIF 2.1 Image API modules for NodeJS", "main": "index.js", "repository": "https://github.com/samvera-labs/node-iiif", diff --git a/tests/integration.test.js b/tests/integration.test.js index 21486a2..92b193e 100644 --- a/tests/integration.test.js +++ b/tests/integration.test.js @@ -13,7 +13,7 @@ let consoleWarnMock; describe('info.json', () => { beforeEach(() => { - subject = new iiif.Processor(`${base}/info.json`, streamResolver); + subject = new iiif.Processor(`${base}/info.json`, streamResolver, { pathPrefix: 'iiif/2/ab/cd/ef/gh' }); }); it('produces a valid info.json', async () => { diff --git a/tests/processor.test.js b/tests/processor.test.js index 75b4abc..ad5b6c2 100644 --- a/tests/processor.test.js +++ b/tests/processor.test.js @@ -14,6 +14,8 @@ describe('IIIF Processor', () => { }); it('Parse URL', () => { + assert.strictEqual(subject.id, 'ab/cd/ef/gh/i'); + assert.strictEqual(subject.baseUrl, 'https://example.org/iiif/2/'); assert.strictEqual(subject.rotation, '45'); assert.strictEqual(subject.size, 'pct:50'); assert.strictEqual(subject.region, '10,20,30,40'); @@ -166,14 +168,14 @@ describe('stream processor', () => { const streamResolver = ({ id, baseUrl }) => { expect(id).toEqual('i'); - expect(baseUrl).toEqual('https://example.org/iiif/2/ab/cd/ef/gh'); + expect(baseUrl).toEqual('https://example.org/iiif/2/ab/cd/ef/gh/'); return new Stream.Readable({ read() {} }); } - const subject = new iiif.Processor(`https://example.org/iiif/2/ab/cd/ef/gh/i/10,20,30,40/pct:50/45/default.png`, streamResolver); + const subject = new iiif.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'}); subject.execute(); }) }) @@ -190,14 +192,14 @@ describe('dimension function', () => { const dimensionFunction = ({ id, baseUrl }) => { expect(id).toEqual('i'); - expect(baseUrl).toEqual('https://example.org/iiif/2/ab/cd/ef/gh'); + expect(baseUrl).toEqual('https://example.org/iiif/2/ab/cd/ef/gh/'); return { w: 100, h: 100 } } const subject = new iiif.Processor( `https://example.org/iiif/2/ab/cd/ef/gh/i/10,20,30,40/pct:50/45/default.png`, streamResolver, - { dimensionFunction } + { dimensionFunction, pathPrefix: 'iiif/2/ab/cd/ef/gh' } ); subject.execute(); })