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..747756d 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,33 @@ 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; - if (typeof url === 'string') { - params = parseUrl(params); + parseUrl (url) { + const parser = new RegExp(`(?https?://[^/]+${this.pathPrefix})(?.+)$`); + const { baseUrl, path } = parser.exec(url).groups; + let result = transform.IIIFRegExp.exec(path)?.groups; + if (result === undefined) { + throw new IIIFError(`Invalid IIIF URL: ${url}`); } + result.baseUrl = baseUrl; + + 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; } @@ -156,7 +150,7 @@ class Processor { } async iiifImage () { - try { + //try { const dim = await this.dimensions(); const pipeline = this.pipeline(dim); @@ -164,20 +158,16 @@ class Processor { return await stream.pipe(pipeline).toBuffer(); }); return { contentType: mime.lookup(this.format), body: result }; - } catch (err) { - throw new IIIFError(`Unhandled transformation error: ${err.message}`); - } + //} catch (err) { + // throw new IIIFError(`Unhandled transformation error: ${err.message}`); + //} } async execute () { - try { - if (this.filename === 'info.json') { - return this.infoJson(); - } else { - return this.iiifImage(); - } - } catch (err) { - console.log('Caught while executing', err.message); + if (this.filename === 'info.json') { + return await this.infoJson(); + } else { + return await this.iiifImage(); } } } diff --git a/lib/transform.js b/lib/transform.js index 7ca4b2e..102ab66 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) { @@ -87,9 +96,6 @@ class Operations { this.pipeline = this.pipeline.flop(); } const value = Number(v.replace(/^!/, '')); - if (isNaN(value)) { - throw new IIIFError(`Invalid rotation value: ${v}`); - } this.pipeline = this.pipeline.rotate(value); return this; } @@ -207,6 +213,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..00ede47 100644 --- a/tests/integration.test.js +++ b/tests/integration.test.js @@ -12,13 +12,20 @@ let subject; let consoleWarnMock; describe('info.json', () => { - beforeEach(() => { - subject = new iiif.Processor(`${base}/info.json`, streamResolver); + it('produces a valid info.json', async () => { + subject = new iiif.Processor(`${base}/info.json`, streamResolver, { pathPrefix: 'iiif/2/ab/cd/ef/gh' }); + const result = await subject.execute(); + const info = JSON.parse(result.body); + assert.strictEqual(info.profile[1].maxWidth, undefined); + assert.strictEqual(info.width, 621); + assert.strictEqual(info.height, 327); }); - it('produces a valid info.json', async () => { + it('respects the maxWidth option', async () => { + subject = new iiif.Processor(`${base}/info.json`, streamResolver, { pathPrefix: 'iiif/2/ab/cd/ef/gh', maxWidth: 600 }); const result = await subject.execute(); const info = JSON.parse(result.body); + assert.strictEqual(info.profile[1].maxWidth, 600); assert.strictEqual(info.width, 621); assert.strictEqual(info.height, 327); }); @@ -91,14 +98,35 @@ describe('IIIF transformation', () => { `${base}/10,20,30,40/pct:50/45/default.png`, streamResolver, { dimensionFunction: () => null } - ); + ); + }); + + afterEach(() => { + consoleWarnMock.mockRestore(); + }); + + it('transforms the image', async () => { + const result = await subject.execute(); + const size = await probe.sync(result.body); + + assert.strictEqual(size.width, 25); + assert.strictEqual(size.height, 25); + assert.strictEqual(size.mime, 'image/png'); + }); }); - - afterEach(() => { - consoleWarnMock.mockRestore(); + + describe('Two-argument streamResolver', () => { + beforeEach(() => { + subject = new iiif.Processor( + `${base}/10,20,30,40/pct:50/45/default.png`, + ({id, baseUrl}, callback) => { + const stream = streamResolver({id, baseUrl}); + return callback(stream); + } + ); }); - it('transforms the image', async () => { + it('works with the two-argument streamResolver', async () => { const result = await subject.execute(); const size = await probe.sync(result.body); @@ -106,4 +134,4 @@ describe('IIIF transformation', () => { assert.strictEqual(size.height, 25); assert.strictEqual(size.mime, 'image/png'); }); -}); +}) 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(); })