UNPKG

iiif-processor

Version:

IIIF 2.1 & 3.0 Image API modules for NodeJS

369 lines (364 loc) 10.7 kB
var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/error.ts var IIIFError = class extends Error { constructor(message, opts = {}) { super(message); this.statusCode = opts.statusCode; } }; // src/calculator/base.ts import Debug from "debug"; var debug = Debug("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; } }; export { __export, IIIFError, Formats, Qualities, Base, Calculator, Calculator2 }; //# sourceMappingURL=chunk-F4YR7VB6.mjs.map