iiif-processor
Version:
IIIF 2.1 & 3.0 Image API modules for NodeJS
393 lines (386 loc) • 12.1 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/calculator.ts
var calculator_exports = {};
__export(calculator_exports, {
default: () => calculator_default
});
module.exports = __toCommonJS(calculator_exports);
// src/calculator/base.ts
var import_debug = __toESM(require("debug"));
// src/error.ts
var IIIFError = class extends Error {
constructor(message, opts = {}) {
super(message);
this.statusCode = opts.statusCode;
}
};
// src/calculator/base.ts
var debug = (0, import_debug.default)("iiif-processor:calculator");
var IR = "\\d+";
var FR = "\\d+(?:\\.\\d+)?";
var PCTR = /^pct:(?<val>[\d.,]+)/;
var Formats = ["gif", "jpg", "tif", "png", "webp"];
var Qualities = ["color", "gray", "bitonal", "default"];
var extraFormats = ["jpeg", "tiff"];
var Validators = {
quality: Qualities,
format: [...Formats, ...extraFormats],
region: ["full", "square", `pct:${FR},${FR},${FR},${FR}`, `${IR},${IR},${IR},${IR}`],
size: ["full", "max", `pct:${FR}`, `${IR},`, `,${IR}`, `\\!?${IR},${IR}`],
rotation: [`\\!?${FR}`],
density: []
};
function validateDensity(v) {
debug("validating density %s", v);
if (v === null) return true;
if (v === void 0) return true;
if (typeof v !== "number" || v < 0) {
throw new IIIFError(`Invalid density value: ${v}`);
}
return true;
}
var Base = class _Base {
static _matchers() {
return Validators;
}
static _validator(type) {
const result = this._matchers()[type].join("|");
return `(?<${type}>${result})`;
}
static parsePath(path) {
path = decodeURIComponent(path);
debug("parsing IIIF path: %s", path);
const idOnlyRe = new RegExp("^/?(?<id>.+)/?$");
const infoJsonRe = new RegExp("^/?(?<id>.+)/(?<info>info.json)$");
const transformRe = new RegExp(
"^/?(?<id>.+)/(?<region>.+)/(?<size>.+)/(?<rotation>.+)/(?<quality>.+)\\.(?<format>.+)$"
);
let result = infoJsonRe.exec(path)?.groups;
debug("info.json match result: %j", result);
if (result) return result;
result = transformRe.exec(path)?.groups;
debug("transform match result: %j", result);
if (result) {
for (const component of [
"region",
"size",
"rotation",
"quality",
"format"
]) {
const validator = new RegExp(this._validator(component));
if (!validator.test(result[component])) {
throw new IIIFError(`Invalid ${component} in IIIF path: ${path}`, {
statusCode: 400
});
}
}
return result;
}
result = idOnlyRe.exec(path)?.groups;
debug("ID only match result: %j", result);
if (result) return result;
throw new IIIFError(`Not a valid IIIF path: ${path}`, {
statusCode: 400
});
}
constructor(dims, opts = {}) {
this.dims = { ...dims };
this.opts = { ...opts };
this._sourceDims = { ...dims };
this._canonicalInfo = {
region: "full",
size: "full",
rotation: "0",
quality: "default",
format: "jpg"
};
this._parsedInfo = {
region: { left: 0, top: 0, ...dims },
size: { width: dims.width, height: dims.height, fit: "fill" },
rotation: { flop: false, degree: 0 },
quality: "default",
format: { type: "jpg" },
upscale: true
};
}
_validate(type, v) {
if (type === "density") return validateDensity(v);
const re = new RegExp(`^${_Base._validator(type)}$`);
debug("validating %s %s against %s", type, v, re);
if (!re.test(String(v))) {
throw new IIIFError(`Invalid ${type}: ${v}`, { statusCode: 400 });
}
return true;
}
region(v) {
this._validate("region", v);
const pct = PCTR.exec(v);
let isFull = false;
if (v === "full") {
this._parsedInfo.region = { left: 0, top: 0, ...this.dims };
isFull = true;
} else if (v === "square") {
this._parsedInfo.region = regionSquare(this.dims);
} else if (pct) {
this._parsedInfo.region = regionPct(pct.groups?.val, this.dims);
} else {
this._parsedInfo.region = regionXYWH(v);
}
this._canonicalInfo.region = isFull ? "full" : this._parsedInfo.region;
this._constrainRegion();
return this;
}
size(v) {
this._validate("size", v);
const pct = PCTR.exec(v);
let isMax = false;
if (["full", "max"].includes(v)) {
this._setSize(this._parsedInfo.region);
isMax = true;
} else if (pct) {
this._setSize(
sizePct(pct.groups?.val, this._parsedInfo.region)
);
} else {
this._setSize(sizeWH(v));
}
this._canonicalInfo.size = isMax ? v : this._canonicalSize();
return this;
}
rotation(v) {
this._validate("rotation", v);
this._canonicalInfo.rotation = v;
this._parsedInfo.rotation = {
flop: v[0] === "!",
degree: Number(v.replace(/^!/, ""))
};
return this;
}
quality(v) {
this._validate("quality", v);
this._canonicalInfo.quality = v;
this._parsedInfo.quality = v;
return this;
}
format(v, density) {
this._validate("format", v);
this._validate("density", density);
this._canonicalInfo.format = v;
this._parsedInfo.format = { type: v, density };
return this;
}
info() {
return {
...this._parsedInfo,
fullSize: fullSize(this._sourceDims, this._parsedInfo)
};
}
canonicalPath() {
const { region, size, rotation, quality, format } = this._canonicalInfo;
return `${region}/${size}/${rotation}/${quality}.${format}`;
}
_setSize(v) {
const max = { ...this.opts?.max || {} };
max.height = max.height || max.width;
this._parsedInfo.size = "left" in v ? { width: v.width, height: v.height, fit: "fill" } : { ...v };
this._constrainSize(max);
return this;
}
_constrainSize(constraints) {
const full = fullSize(this._sourceDims, this._parsedInfo);
const constraint = minNum(
constraints.width / full.width,
constraints.height / full.height,
constraints.area / (full.width * full.height)
);
if (constraint < 1) {
if (this._parsedInfo.size.width) {
this._parsedInfo.size.width = Math.floor(
this._parsedInfo.size.width * constraint
);
}
if (this._parsedInfo.size.height) {
this._parsedInfo.size.height = Math.floor(
this._parsedInfo.size.height * constraint
);
}
}
}
_canonicalSize() {
const { width, height } = this._parsedInfo.size;
const result = `${width},${height}`;
return this._parsedInfo.size.fit === "inside" ? `!${result}` : result;
}
_constrainRegion() {
let { left, top, width, height } = this._parsedInfo.region;
left = Math.max(left, 0);
top = Math.max(top, 0);
if (left > this.dims.width || top > this.dims.height) {
throw new IIIFError("Region is out of bounds", { statusCode: 400 });
}
width = Math.min(width, this.dims.width - left);
height = Math.min(height, this.dims.height - top);
this._parsedInfo.region = { left, top, width, height };
}
};
function minNum(...args) {
const nums = args.filter((arg) => typeof arg === "number" && !isNaN(arg));
return Math.min(...nums);
}
function fillMissingDimension(size, aspect) {
if (!size.width && size.height != null)
size.width = Math.floor(size.height * aspect);
if (!size.height && size.width != null)
size.height = Math.floor(size.width / aspect);
}
function fullSize(dims, { region, size }) {
const regionAspect = region.width / region.height;
fillMissingDimension(size, regionAspect);
const scaleFactor = size.width / region.width;
const result = {
width: Math.floor(dims.width * scaleFactor),
height: Math.floor(dims.height * scaleFactor)
};
debug(
"Region %j at size %j yields full size %j, a scale factor of %f",
region,
size,
result,
scaleFactor
);
return result;
}
function regionSquare(dims) {
let result = {
left: 0,
top: 0,
width: dims.width,
height: dims.height
};
if (dims.width !== dims.height) {
const side = Math.min(dims.width, dims.height);
result = { ...result, width: side, height: side };
const offset = Math.abs(Math.floor((dims.width - dims.height) / 2));
if (dims.width > dims.height) {
result.left = offset;
result.top = 0;
} else {
result.left = 0;
result.top = offset;
}
}
return result;
}
function regionPct(v, dims) {
let x, y, w, h;
[x, y, w, h] = v.split(/\s*,\s*/).map((pct) => Number(pct) / 100);
[x, w] = [x, w].map((val) => Math.floor(dims.width * val));
[y, h] = [y, h].map((val) => Math.floor(dims.height * val));
return regionXYWH([x, y, w, h]);
}
function regionXYWH(v) {
const parts = typeof v === "string" ? v.split(/\s*,\s*/).map((val) => Number(val)) : v;
const result = {
left: parts[0],
top: parts[1],
width: parts[2],
height: parts[3]
};
if (result.width === 0 || result.height === 0) {
throw new IIIFError("Region width and height must both be > 0", {
statusCode: 400
});
}
return result;
}
function sizePct(v, dims) {
const pct = Number(v);
if (isNaN(pct) || pct <= 0) {
throw new IIIFError(`Invalid resize %: ${v}`, { statusCode: 400 });
}
const width = Math.floor(dims.width * (pct / 100));
return sizeWH(`${width},`);
}
function sizeWH(v) {
const result = { fit: "fill" };
if (v[0] === "!") {
result.fit = "inside";
v = v.slice(1);
}
const parts = v.split(/\s*,\s*/).map((val) => val === "" ? null : Number(val));
[result.width, result.height] = parts;
if (result.width === 0 || result.height === 0) {
throw new IIIFError("Resize width and height must both be > 0", {
statusCode: 400
});
}
return result;
}
// src/calculator/v2.ts
var Calculator = class extends Base {
};
// src/calculator/v3.ts
var Calculator2 = class extends Base {
static _matchers() {
const result = { ...super._matchers() };
result.size = [...result.size].reduce(
(sizes, pattern) => {
if (pattern !== "full") sizes.push(`\\^?${pattern}`);
return sizes;
},
[]
);
return result;
}
constructor(dims, opts = {}) {
super(dims, opts);
this._canonicalInfo.size = "max";
this._parsedInfo.upscale = false;
}
size(v) {
if (v[0] === "^") {
this._parsedInfo.upscale = true;
v = v.slice(1, v.length);
}
super.size(v);
const { region, size, upscale } = this._parsedInfo;
if (!upscale) {
if (size.width > region.width || size.height > region.height) {
throw new IIIFError("Requested size requires upscaling", {
statusCode: 400
});
}
}
return this;
}
};
// src/calculator.ts
var calculator_default = { 2: Calculator, 3: Calculator2 };
//# sourceMappingURL=calculator.js.map