Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add debugBorder and pageThreshhold options to Processor constructor #39

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});