Skip to content

Commit

Permalink
Add debugBorder and pageThreshhold options to Processor constru…
Browse files Browse the repository at this point in the history
…ctor

Add 1-pixel default fudge factor to pyramid page selection
  • Loading branch information
mbklein committed Jan 14, 2025
1 parent cf7128a commit 7c59f75
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 8 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ const processor = new IIIF.Processor(url, streamResolver, opts);
* `height` (integer) - the maximum pixel height of the returned image
* `area` (integer) - the maximum total number of pixels in the returned image
* `includeMetadata` (boolean) – if `true`, all metadata from the source image will be copied to the result
* `debugBorder` (boolean) – if `true`, add a 1px red border to every generated image (for tile debugging)
* `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
* `pageThreshold` (integer) – the fudge factor (in number of pixels) to mitigate rounding errors in pyramid page selection (default: `1`)
* `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}/`)

Expand Down
5 changes: 3 additions & 2 deletions examples/tiny-iiif/iiif.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { App } from '@tinyhttp/app';
import { Processor } from 'iiif-processor';
import iiif from 'iiif-processor';
const { Processor } = iiif;
import fs from 'fs';
import path from 'path';
import { iiifImagePath, iiifpathPrefix, fileTemplate } from './config.js';
Expand All @@ -21,7 +22,7 @@ function createRouter(version) {

try {
const iiifUrl = `${req.protocol}://${req.get("host")}${req.path}`;
const iiifProcessor = new Processor(iiifUrl, streamImageFromFile, { pathPrefix: iiifpathPrefix });
const iiifProcessor = new Processor(iiifUrl, streamImageFromFile, { pathPrefix: iiifpathPrefix, debugBorder: !!process.env.DEBUG_IIIF_BORDER });
const result = await iiifProcessor.execute();
return res
.set("Content-Type", result.contentType)
Expand Down
35 changes: 32 additions & 3 deletions src/processor.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const debug = require('debug')('iiif-processor:main');
const debugv = require('debug')('verbose:iiif-processor');
const mime = require('mime-types');
const path = require('path');
const sharp = require('sharp');
Expand Down Expand Up @@ -51,6 +52,8 @@ class Processor {
this.includeMetadata = !!opts.includeMetadata;
this.density = opts.density;
this.baseUrl = opts.prefix;
this.debugBorder = !!opts.debugBorder;
this.pageThreshold = opts.pageThreshold;
this.sharpOptions = { ...opts.sharpOptions };
this.version = opts.iiifVersion;
this.request = opts.request;
Expand Down Expand Up @@ -147,8 +150,9 @@ class Processor {
}

operations (dim) {
const { sharpOptions: sharp, max } = this;
return new Operations(this.version, dim, { sharp, max })
const { sharpOptions: sharp, max, pageThreshold } = this;
debug('pageThreshold: %d', pageThreshold);
return new Operations(this.version, dim, { sharp, max, pageThreshold })
.region(this.region)
.size(this.size)
.rotation(this.rotation)
Expand All @@ -157,14 +161,39 @@ class Processor {
.withMetadata(this.includeMetadata);
}

async applyBorder (transformed) {
const buf = await transformed.toBuffer();
const borderPipe = sharp(buf, { limitInputPixels: false })
const { width, height } = await borderPipe.metadata();
const background = { r: 255, g: 0, b: 0, alpha: 1 };

// Create small images for each border “strip”
const topBorder = { create: { width, height: 1, channels: 4, background } };
const bottomBorder = { create: { width, height: 1, channels: 4, background } };
const leftBorder = { create: { width: 1, height, channels: 4, background } };
const rightBorder = { create: { width: 1, height, channels: 4, background } };

return borderPipe.composite([
{ input: topBorder, left: 0, top: 0 },
{ input: bottomBorder, left: 0, top: height - 1 },
{ input: leftBorder, left: 0, top: 0 },
{ input: rightBorder, left: width - 1, top: 0 }
]);
}

async iiifImage () {
debugv('Request %s', this.request);
const dim = await this.dimensions();
const operations = this.operations(dim);
debugv('Operations: %j', operations);
const pipeline = await operations.pipeline();

const result = await this.withStream({ id: this.id, baseUrl: this.baseUrl }, async (stream) => {
debug('piping stream to pipeline');
const transformed = await stream.pipe(pipeline);
let transformed = await stream.pipe(pipeline);
if (this.debugBorder) {
transformed = await this.applyBorder(transformed);
}
debug('converting to buffer');
return await transformed.toBuffer();
});
Expand Down
8 changes: 5 additions & 3 deletions src/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ const ExtractAttributes = [
'heightPre'
];

const DEFAULT_PAGE_THRESHOLD = 1;
const SCALE_PRECISION = 10000000;

class Operations {
#pages;
#pipeline;

constructor (version, dims, opts) {
const { sharp, ...rest } = opts;
const { sharp, pageThreshold, ...rest } = opts;
const Implementation = IIIFVersions[version];
this.calculator = new Implementation.Calculator(dims[0], rest);
this.pageThreshold = typeof pageThreshold === 'number' ? pageThreshold : DEFAULT_PAGE_THRESHOLD;

this.#pages = dims
.map((dim, page) => {
Expand Down Expand Up @@ -119,8 +121,8 @@ class Operations {
const { fullSize } = this.info();
const { page } = this.#pages.find((_candidate, index) => {
const next = this.#pages[index + 1];
debug('comparing candidate %j to target %j', next, fullSize);
return !next || (next.width < fullSize.width && next.height < fullSize.height);
debug('comparing candidate %j to target %j with a %d-pixel buffer', next, fullSize, this.pageThreshold);
return !next || (next.width + this.pageThreshold < fullSize.width && next.height + this.pageThreshold < fullSize.height);
});

const resolution = this.#pages[page];
Expand Down
29 changes: 29 additions & 0 deletions tests/v2/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ describe('size', () => {
pipeline = await subject.operations(await subject.dimensions()).pipeline();
assert.strictEqual(pipeline.options.input.page, 1);
});

it('should respect the pixel page buffer', async () => {
let pipeline;
subject = new Processor(`${base}/full/312,165/0/default.png`, streamResolver);
pipeline = await subject.operations(await subject.dimensions()).pipeline();
assert.strictEqual(pipeline.options.input.page, 1);

subject = new Processor(`${base}/full/312,165/0/default.png`, streamResolver, { pageThreshold: 0 });
pipeline = await subject.operations(await subject.dimensions()).pipeline();
assert.strictEqual(pipeline.options.input.page, 0);
});
});

describe('rotation', () => {
Expand Down Expand Up @@ -146,3 +157,21 @@ describe('Two-argument streamResolver', () => {
assert.strictEqual(size.format, 'png');
});
});

describe('Debug border', () => {
it('should produce an image without a border by default', async () => {
subject = new Processor(`${base}/full/full/0/default.png`, streamResolver);
const result = await subject.execute();
const image = await Sharp(result.body).removeAlpha().raw().toBuffer();
const pixel = image.readUInt32LE(0);
assert.strictEqual(pixel, 0xffffffff);
});

it('should add a border when `debugBorder` is specified', async () => {
subject = new Processor(`${base}/full/full/0/default.png`, streamResolver, { debugBorder: true });
const result = await subject.execute();
const image = await Sharp(result.body).removeAlpha().raw().toBuffer();
const pixel = image.readUInt32LE(0);
assert.strictEqual(pixel, 0xff0000ff);
});
});
29 changes: 29 additions & 0 deletions tests/v3/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ describe('size', () => {
pipeline = await subject.operations(await subject.dimensions()).pipeline();
assert.strictEqual(pipeline.options.input.page, 1);
});

it('should respect the pixel page buffer', async () => {
let pipeline;
subject = new Processor(`${base}/full/312,165/0/default.png`, streamResolver);
pipeline = await subject.operations(await subject.dimensions()).pipeline();
assert.strictEqual(pipeline.options.input.page, 1);

subject = new Processor(`${base}/full/312,165/0/default.png`, streamResolver, { pageThreshold: 0 });
pipeline = await subject.operations(await subject.dimensions()).pipeline();
assert.strictEqual(pipeline.options.input.page, 0);
});
});

describe('rotation', () => {
Expand Down Expand Up @@ -144,3 +155,21 @@ describe('Two-argument streamResolver', () => {
assert.strictEqual(size.format, 'png');
});
});

describe('Debug border', () => {
it('should produce an image without a border by default', async () => {
subject = new Processor(`${base}/full/max/0/default.png`, streamResolver);
const result = await subject.execute();
const image = await Sharp(result.body).removeAlpha().raw().toBuffer();
const pixel = image.readUInt32LE(0);
assert.strictEqual(pixel, 0xffffffff);
});

it('should add a border when `debugBorder` is specified', async () => {
subject = new Processor(`${base}/full/max/0/default.png`, streamResolver, { debugBorder: true });
const result = await subject.execute();
const image = await Sharp(result.body).removeAlpha().raw().toBuffer();
const pixel = image.readUInt32LE(0);
assert.strictEqual(pixel, 0xff0000ff);
});
});

0 comments on commit 7c59f75

Please sign in to comment.