Skip to content

Commit

Permalink
Replace URL parser with RegEx with configurable path prefix
Browse files Browse the repository at this point in the history
  • Loading branch information
mbklein committed Jul 13, 2022
1 parent a8af5d6 commit 5f515d1
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 61 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
72 changes: 31 additions & 41 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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(`(?<baseUrl>https?://[^/]+${this.pathPrefix})(?<path>.+)$`);
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;
}
Expand Down Expand Up @@ -156,28 +150,24 @@ class Processor {
}

async iiifImage () {
try {
//try {
const dim = await this.dimensions();
const pipeline = this.pipeline(dim);

const result = await this.withStream({ id: this.id, baseUrl: this.baseUrl }, async (stream) => {
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();
}
}
}
Expand Down
19 changes: 13 additions & 6 deletions lib/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(`^/?(?<id>.+?)/(?:(?<info>info.json)|${transformation})$`);
}

class Operations {
constructor (dims) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -207,6 +213,7 @@ class Operations {
module.exports = {
Qualities: Validators.quality,
Formats: Validators.format,
IIIFRegExp: iiifRegExp(),
Operations,
IIIFError
};
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": "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",
Expand Down
46 changes: 37 additions & 9 deletions tests/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -91,19 +98,40 @@ 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);

assert.strictEqual(size.width, 25);
assert.strictEqual(size.height, 25);
assert.strictEqual(size.mime, 'image/png');
});
});
})
10 changes: 6 additions & 4 deletions tests/processor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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();
})
})
Expand All @@ -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();
})
Expand Down

0 comments on commit 5f515d1

Please sign in to comment.