image-renditions 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,142 @@
1
+ //#region src/assert.ts
2
+ /** Throws a `RangeError` unless a value is an integer from `min` to `max`. */
3
+ function assertInteger(name, value, min, max) {
4
+ if (!Number.isInteger(value) || value < min || value > max) throw new RangeError(`Invalid ${name} ${value}; expected an integer from ${min} to ${max}.`);
5
+ }
6
+ //#endregion
7
+ //#region src/recipe.ts
8
+ /** File extension of each output format, used in rendition URLs and storage keys. */
9
+ const formatExtensions = {
10
+ avif: "avif",
11
+ webp: "webp",
12
+ jpeg: "jpg",
13
+ png: "png"
14
+ };
15
+ /** MIME type of each output format. */
16
+ const formatContentTypes = {
17
+ avif: "image/avif",
18
+ webp: "image/webp",
19
+ jpeg: "image/jpeg",
20
+ png: "image/png"
21
+ };
22
+ /** Library-owned recipe version; bumping it regenerates every rendition of every application. */
23
+ const RECIPE_ENGINE_VERSION = 1;
24
+ /** Upper bound of a rendition side in physical pixels; WebP cannot encode larger sides. */
25
+ const MAX_DIMENSION = 16383;
26
+ const defaultSourcePolicy = {
27
+ svg: "rasterize",
28
+ orientation: "auto"
29
+ };
30
+ const animations = ["first-frame", "preserve"];
31
+ const formats = [
32
+ "avif",
33
+ "webp",
34
+ "jpeg",
35
+ "png"
36
+ ];
37
+ const fits = [
38
+ "cover",
39
+ "contain",
40
+ "fill",
41
+ "inside",
42
+ "outside"
43
+ ];
44
+ const positions = [
45
+ "center",
46
+ "top",
47
+ "bottom",
48
+ "left",
49
+ "right",
50
+ "entropy",
51
+ "attention"
52
+ ];
53
+ /** Encoder quality defaults; `png` is lossless and has no quality. */
54
+ const defaultQuality = {
55
+ avif: 50,
56
+ webp: 80,
57
+ jpeg: 80
58
+ };
59
+ /** Accepted effort range and default per format; formats without an entry ignore effort. */
60
+ const effortRange = {
61
+ avif: {
62
+ min: 0,
63
+ max: 9,
64
+ default: 4
65
+ },
66
+ webp: {
67
+ min: 0,
68
+ max: 6,
69
+ default: 4
70
+ }
71
+ };
72
+ /**
73
+ * Builds the canonical recipe for transformation options at a density.
74
+ *
75
+ * @throws {RangeError} When an option is out of range or inconsistent.
76
+ */
77
+ function normalizeRecipe({ options, density = 1, pipeline, policy, animation = "first-frame" }) {
78
+ assertInteger("pipeline", pipeline, 0, Number.MAX_SAFE_INTEGER);
79
+ if (!(Number.isFinite(density) && density > 0)) throw new RangeError(`Invalid density ${density}.`);
80
+ const format = options.format ?? "avif";
81
+ assertOneOf("format", format, formats);
82
+ assertOneOf("animation", animation, animations);
83
+ if (options.fit !== void 0) assertOneOf("fit", options.fit, fits);
84
+ if (options.position !== void 0) assertOneOf("position", options.position, positions);
85
+ const color = options.background === void 0 ? void 0 : normalizeColor(options.background);
86
+ if (options.quality !== void 0) assertInteger("quality", options.quality, 1, 100);
87
+ const range = effortRange[format];
88
+ if (options.effort !== void 0) assertInteger("effort", options.effort, range?.min ?? 0, range?.max ?? 9);
89
+ const width = scaleDimension("width", options.width, density);
90
+ const height = scaleDimension("height", options.height, density);
91
+ const resized = width !== void 0 || height !== void 0;
92
+ const fit = width !== void 0 && height !== void 0 ? options.fit ?? "cover" : void 0;
93
+ const position = fit === "cover" || fit === "contain" ? options.position ?? "center" : void 0;
94
+ if (fit === "contain" && (position === "entropy" || position === "attention")) throw new RangeError(`Position "${position}" applies to the "cover" fit only.`);
95
+ const flattened = format === "jpeg";
96
+ const background = fit === "contain" || flattened ? color ?? (flattened ? "#ffffffff" : "#00000000") : void 0;
97
+ return Object.freeze({
98
+ engine: 1,
99
+ pipeline,
100
+ width,
101
+ height,
102
+ fit,
103
+ position,
104
+ enlarge: resized ? options.enlarge ?? false : void 0,
105
+ background,
106
+ format,
107
+ quality: format === "png" ? void 0 : options.quality ?? defaultQuality[format],
108
+ effort: range ? options.effort ?? range.default : void 0,
109
+ svg: resolvePolicyValue("svg", policy, ["rasterize"]),
110
+ animation,
111
+ orientation: resolvePolicyValue("orientation", policy, ["auto"])
112
+ });
113
+ }
114
+ /** Serializes a recipe deterministically: sorted keys, no undefined fields, no whitespace. */
115
+ function serializeRecipe(recipe) {
116
+ return JSON.stringify(Object.fromEntries(Object.entries(recipe).filter(([, value]) => value !== void 0).sort(([a], [b]) => a < b ? -1 : 1)));
117
+ }
118
+ function resolvePolicyValue(name, policy, allowed) {
119
+ const value = policy?.[name] ?? defaultSourcePolicy[name];
120
+ assertOneOf(name, value, allowed);
121
+ return value;
122
+ }
123
+ function scaleDimension(name, value, density) {
124
+ if (value === void 0) return;
125
+ assertInteger(name, value, 1, MAX_DIMENSION);
126
+ const scaled = Math.round(value * density);
127
+ assertInteger(`${name} at density ${density}`, scaled, 1, MAX_DIMENSION);
128
+ return scaled;
129
+ }
130
+ /** Expands a hex color to lowercase `#rrggbbaa`, so equivalent spellings hash equally. */
131
+ function normalizeColor(color) {
132
+ const match = /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.exec(color);
133
+ if (!match) throw new RangeError(`Invalid background color "${color}"; use a hex color.`);
134
+ const digits = match[1].toLowerCase();
135
+ const full = digits.length <= 4 ? [...digits].map((digit) => digit + digit).join("") : digits;
136
+ return `#${full.length === 6 ? `${full}ff` : full}`;
137
+ }
138
+ function assertOneOf(name, value, allowed) {
139
+ if (!allowed.includes(value)) throw new RangeError(`Invalid ${name} "${value}"; expected one of ${allowed.join(", ")}.`);
140
+ }
141
+ //#endregion
142
+ export { normalizeRecipe as a, formatExtensions as i, RECIPE_ENGINE_VERSION as n, serializeRecipe as o, formatContentTypes as r, assertInteger as s, MAX_DIMENSION as t };
package/dist/s3.d.mts ADDED
@@ -0,0 +1,26 @@
1
+ import { n as ResultStore, r as SourceStore } from "./stores-P-TUmax4.mjs";
2
+ import { S3Client } from "@aws-sdk/client-s3";
3
+ //#region src/stores/s3.d.ts
4
+ /** Options of {@link createS3Source}. */
5
+ export interface S3SourceOptions {
6
+ /** Client of AWS SDK v3, such as `S3Client` or the aggregated `S3`. */
7
+ client: Pick<S3Client, "send">;
8
+ bucket: string;
9
+ /** Largest source object, in bytes; larger sources fail as too large. */
10
+ maxBytes?: number;
11
+ }
12
+ /** Options of {@link createS3Results}. */
13
+ export interface S3ResultsOptions {
14
+ /** Client of AWS SDK v3, such as `S3Client` or the aggregated `S3`. */
15
+ client: Pick<S3Client, "send">;
16
+ bucket: string;
17
+ /** Public URL of the bucket root, such as `https://storage.example.com/bucket`; objects must be publicly readable. */
18
+ publicUrl: string;
19
+ /** `Cache-Control` stored with each rendition; renditions never change, so they are immutable for a year by default. */
20
+ cacheControl?: string;
21
+ }
22
+ /** Creates a source store reading objects from an S3 bucket. */
23
+ export declare function createS3Source({ client, bucket, maxBytes }: S3SourceOptions): SourceStore;
24
+ /** Creates a result store writing objects to an S3 bucket that serves them publicly. */
25
+ export declare function createS3Results({ client, bucket, publicUrl, cacheControl }: S3ResultsOptions): ResultStore;
26
+ //#endregion
package/dist/s3.mjs ADDED
@@ -0,0 +1,81 @@
1
+ import { t as encodePath } from "./path-hNFUjCbA.mjs";
2
+ import { t as RenditionError } from "./error-DViVpVmH.mjs";
3
+ import { GetObjectCommand, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
4
+ //#region src/stores/s3.ts
5
+ /** Longest S3 object key, in UTF-8 bytes. */
6
+ const MAX_KEY_BYTES = 1024;
7
+ const encoder = new TextEncoder();
8
+ /** Creates a source store reading objects from an S3 bucket. */
9
+ function createS3Source({ client, bucket, maxBytes = 52428800 }) {
10
+ return { async read(key, { signal }) {
11
+ if (!fitsKeyLimit(key)) return;
12
+ const controller = new AbortController();
13
+ const output = await client.send(new GetObjectCommand({
14
+ Bucket: bucket,
15
+ Key: key
16
+ }), { abortSignal: AbortSignal.any([signal, controller.signal]) }).catch(ignoreNotFound);
17
+ if (!output?.Body) return;
18
+ const tooLarge = new RenditionError("source-too-large", `The source ${key} exceeds ${maxBytes} bytes.`);
19
+ if ((output.ContentLength ?? 0) > maxBytes) {
20
+ controller.abort();
21
+ throw tooLarge;
22
+ }
23
+ const reader = output.Body.transformToWebStream().getReader();
24
+ const chunks = [];
25
+ let size = 0;
26
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
27
+ size += chunk.value.byteLength;
28
+ if (size > maxBytes) {
29
+ controller.abort();
30
+ reader.cancel().catch(() => {});
31
+ throw tooLarge;
32
+ }
33
+ chunks.push(chunk.value);
34
+ }
35
+ const data = new Uint8Array(size);
36
+ let offset = 0;
37
+ for (const chunk of chunks) {
38
+ data.set(chunk, offset);
39
+ offset += chunk.byteLength;
40
+ }
41
+ return data;
42
+ } };
43
+ }
44
+ /** Creates a result store writing objects to an S3 bucket that serves them publicly. */
45
+ function createS3Results({ client, bucket, publicUrl, cacheControl = "public, max-age=31536000, immutable" }) {
46
+ return {
47
+ async exists(key, { signal }) {
48
+ assertKeyLimit(key);
49
+ return await client.send(new HeadObjectCommand({
50
+ Bucket: bucket,
51
+ Key: key
52
+ }), { abortSignal: signal }).catch(ignoreNotFound) !== void 0;
53
+ },
54
+ async write(key, { data, contentType }, { signal }) {
55
+ assertKeyLimit(key);
56
+ await client.send(new PutObjectCommand({
57
+ Bucket: bucket,
58
+ Key: key,
59
+ Body: data,
60
+ ContentType: contentType,
61
+ CacheControl: cacheControl
62
+ }), { abortSignal: signal });
63
+ },
64
+ url: (key) => `${publicUrl.replace(/\/$/, "")}/${encodePath(key)}`
65
+ };
66
+ }
67
+ /** Checks a key against the S3 limit, which counts UTF-8 bytes rather than characters. */
68
+ function fitsKeyLimit(key) {
69
+ return encoder.encode(key).byteLength <= MAX_KEY_BYTES;
70
+ }
71
+ function assertKeyLimit(key) {
72
+ if (!fitsKeyLimit(key)) throw new RenditionError("invalid-request", `The key ${key} exceeds the S3 limit of ${MAX_KEY_BYTES} bytes.`);
73
+ }
74
+ function ignoreNotFound(error) {
75
+ const { name, $metadata } = error;
76
+ if (name === "NoSuchBucket") throw error;
77
+ if ($metadata?.httpStatusCode === 404 || name === "NoSuchKey" || name === "NotFound") return;
78
+ throw error;
79
+ }
80
+ //#endregion
81
+ export { createS3Results, createS3Source };
@@ -0,0 +1,22 @@
1
+ //#region src/scopes.ts
2
+ /** Checks that a target has a scope the catalog declares; a catalog without scopes accepts any. */
3
+ function isDeclaredScope(scopes, scope) {
4
+ return !scopes || scope !== void 0 && scopes.includes(scope);
5
+ }
6
+ /**
7
+ * Throws a `RangeError` unless a target has a scope the catalog declares.
8
+ *
9
+ * @throws {RangeError} When the scope is missing or not declared.
10
+ */
11
+ function assertDeclaredScope(scopes, scope) {
12
+ if (!isDeclaredScope(scopes, scope)) throw new RangeError(scope === void 0 ? "Missing rendition scope." : `Undeclared rendition scope "${scope}".`);
13
+ }
14
+ /** Target of a scope and a path, typed by the scopes of the catalog, or `undefined` when the catalog does not declare the scope. */
15
+ function toDeclaredTarget(scopes, scope, path) {
16
+ return isDeclaredScope(scopes, scope) ? {
17
+ scope,
18
+ path
19
+ } : void 0;
20
+ }
21
+ //#endregion
22
+ export { isDeclaredScope as n, toDeclaredTarget as r, assertDeclaredScope as t };
@@ -0,0 +1,4 @@
1
+ import { _ as RenditionFailureReason, a as createRenditionHandler, c as RenditionServerOptions, d as MemoryReadinessIndexOptions, f as ReadinessIndex, g as RenditionError, h as createLocalCoordinator, i as RenditionRequest, l as SourceResolution, m as Coordinator, n as RenditionHandler, o as Rendition, p as createMemoryReadinessIndex, r as RenditionHandlerOptions, s as RenditionServer, t as RenditionFailure, u as createRenditionServer } from "./handler-9XP5SZhg.mjs";
2
+ import { i as StoreCallOptions, n as ResultStore, r as SourceStore, t as EncodedImage } from "./stores-P-TUmax4.mjs";
3
+ import { t as Processor } from "./processor-DvyPs-OZ.mjs";
4
+ export { Coordinator, EncodedImage, MemoryReadinessIndexOptions, Processor, ReadinessIndex, Rendition, RenditionError, RenditionFailure, RenditionFailureReason, RenditionHandler, RenditionHandlerOptions, RenditionRequest, RenditionServer, RenditionServerOptions, ResultStore, SourceResolution, SourceStore, StoreCallOptions, createLocalCoordinator, createMemoryReadinessIndex, createRenditionHandler, createRenditionServer };
@@ -0,0 +1,218 @@
1
+ import { i as formatExtensions, s as assertInteger } from "./recipe-BavgOq0b.mjs";
2
+ import { s as hashRecipe } from "./url-BAU5GYXC.mjs";
3
+ import { n as isSafePath } from "./path-hNFUjCbA.mjs";
4
+ import { n as isDeclaredScope } from "./scopes-C1kzqQHz.mjs";
5
+ import { t as RenditionError } from "./error-DViVpVmH.mjs";
6
+ import { t as createRenditionHandler } from "./handler-CPJllrYi.mjs";
7
+ //#region src/server/coordinator.ts
8
+ /** Creates a coordinator that merges concurrent equal requests within the process. */
9
+ function createLocalCoordinator() {
10
+ const running = /* @__PURE__ */ new Map();
11
+ return { run(key, task) {
12
+ const current = running.get(key);
13
+ if (current) return current;
14
+ const promise = task().finally(() => running.delete(key));
15
+ running.set(key, promise);
16
+ return promise;
17
+ } };
18
+ }
19
+ //#endregion
20
+ //#region src/server/readiness.ts
21
+ /**
22
+ * Creates a process-local readiness index with least-recently-used eviction.
23
+ *
24
+ * @throws {RangeError} When `maxEntries` is not a non-negative integer.
25
+ */
26
+ function createMemoryReadinessIndex({ maxEntries = 1e4 } = {}) {
27
+ assertInteger("maxEntries", maxEntries, 0, Number.MAX_SAFE_INTEGER);
28
+ const entries = /* @__PURE__ */ new Set();
29
+ return {
30
+ has(url) {
31
+ if (!entries.has(url)) return false;
32
+ entries.delete(url);
33
+ entries.add(url);
34
+ return true;
35
+ },
36
+ add(url) {
37
+ entries.delete(url);
38
+ entries.add(url);
39
+ if (entries.size > maxEntries) entries.delete(entries.values().next().value);
40
+ }
41
+ };
42
+ }
43
+ //#endregion
44
+ //#region src/server/limiter.ts
45
+ /** Creates a limiter with the given bounds. */
46
+ function createLimiter({ concurrency, queueSize, queueTimeout }) {
47
+ let active = 0;
48
+ const waiting = [];
49
+ function release() {
50
+ const next = waiting.shift();
51
+ if (next) next();
52
+ else active--;
53
+ }
54
+ function acquire() {
55
+ if (active < concurrency) {
56
+ active++;
57
+ return Promise.resolve();
58
+ }
59
+ if (waiting.length >= queueSize) return Promise.reject(new RenditionError("overloaded", "The processing queue is full."));
60
+ return new Promise((resolve, reject) => {
61
+ const grant = () => {
62
+ clearTimeout(timer);
63
+ resolve();
64
+ };
65
+ const timer = setTimeout(() => {
66
+ waiting.splice(waiting.indexOf(grant), 1);
67
+ reject(new RenditionError("overloaded", "Timed out waiting for a processing slot."));
68
+ }, queueTimeout);
69
+ waiting.push(grant);
70
+ });
71
+ }
72
+ return { async run(task) {
73
+ await acquire();
74
+ try {
75
+ return await task();
76
+ } finally {
77
+ release();
78
+ }
79
+ } };
80
+ }
81
+ //#endregion
82
+ //#region src/server/server.ts
83
+ /** Longest delay of Node timers, in milliseconds. */
84
+ const MAX_TIMEOUT = 2 ** 31 - 1;
85
+ /**
86
+ * Creates a rendition server over source and result stores.
87
+ *
88
+ * @throws {RangeError} When a limit is not an integer in its range, or the processor cannot render a variant of the catalog.
89
+ */
90
+ function createRenditionServer(options) {
91
+ const { catalog, source, results, processor, resolveSource = resolveUnscoped, onError } = options;
92
+ const memory = createMemoryReadinessIndex(options.readiness?.memory);
93
+ const shared = options.readiness?.shared;
94
+ const coordinator = options.coordinator ?? createLocalCoordinator();
95
+ const { concurrency = 2, queueSize = 100, queueTimeout = 3e4, storageTimeout = 15e3, indexTimeout = 1e3 } = options.limits ?? {};
96
+ assertInteger("limits.concurrency", concurrency, 1, Number.MAX_SAFE_INTEGER);
97
+ assertInteger("limits.queueSize", queueSize, 0, Number.MAX_SAFE_INTEGER);
98
+ assertInteger("limits.queueTimeout", queueTimeout, 1, MAX_TIMEOUT);
99
+ assertInteger("limits.storageTimeout", storageTimeout, 1, MAX_TIMEOUT);
100
+ assertInteger("limits.indexTimeout", indexTimeout, 1, MAX_TIMEOUT);
101
+ const limiter = createLimiter({
102
+ concurrency,
103
+ queueSize,
104
+ queueTimeout
105
+ });
106
+ for (const variant of catalog.allVariants) try {
107
+ processor.assertRecipe?.(variant.recipe);
108
+ } catch (error) {
109
+ throw error instanceof RangeError ? new RangeError(`The processor cannot render "${variant.label}": ${error.message}`, { cause: error }) : error;
110
+ }
111
+ async function resolve(target, context) {
112
+ if (!isDeclaredScope(catalog.scopes, target.scope)) throw new RenditionError("invalid-request", `The scope of the target ${describe(target)} is not declared.`);
113
+ const resolution = await resolveSource(target, context);
114
+ if (!resolution) throw new RenditionError("invalid-request", `The target ${describe(target)} is not served.`);
115
+ const { sourceKey } = resolution;
116
+ const resultPrefix = resolution.resultPrefix ?? sourceKey;
117
+ if (!isSafePath(sourceKey) || !isSafePath(resultPrefix)) throw createUnsafeKeyError(target);
118
+ return {
119
+ sourceKey,
120
+ resultPrefix
121
+ };
122
+ }
123
+ async function locate(hash, format, target, context) {
124
+ const { sourceKey, resultPrefix } = await resolve(target, context);
125
+ const key = `${resultPrefix}/${hash}.${formatExtensions[format]}`;
126
+ if (!isSafePath(key)) throw createUnsafeKeyError(target);
127
+ return {
128
+ sourceKey,
129
+ rendition: {
130
+ key,
131
+ url: results.url(key)
132
+ }
133
+ };
134
+ }
135
+ async function isIndexed(url) {
136
+ if (memory.has(url)) return true;
137
+ const found = await callShared((signal) => shared?.has(url, { signal }));
138
+ if (found) memory.add(url);
139
+ return found === true;
140
+ }
141
+ async function markPublished(url) {
142
+ memory.add(url);
143
+ await callShared((signal) => shared?.add(url, { signal }));
144
+ }
145
+ /** Calls the shared index, reporting its failures, synchronous ones and timeouts included, instead of failing the operation. */
146
+ async function callShared(call) {
147
+ if (!shared) return;
148
+ try {
149
+ return await withTimeout(indexTimeout, call);
150
+ } catch (error) {
151
+ report(error);
152
+ return;
153
+ }
154
+ }
155
+ /** Passes an error to `onError` without waiting for it, so a failing or hanging callback cannot affect the operation. */
156
+ function report(error) {
157
+ try {
158
+ Promise.resolve(onError?.(error)).catch(() => {});
159
+ } catch {}
160
+ }
161
+ function callStorage(reason, call) {
162
+ return withTimeout(storageTimeout, call).catch((error) => {
163
+ throw error instanceof RenditionError ? error : new RenditionError(reason, `Storage call failed: ${error}`, { cause: error });
164
+ });
165
+ }
166
+ async function render(recipe, sourceKey, key) {
167
+ const data = await callStorage("source-error", (signal) => source.read(sourceKey, { signal }));
168
+ if (!data) throw new RenditionError("source-not-found", `The source ${sourceKey} does not exist.`);
169
+ const image = await processor.process(data, recipe).catch((error) => {
170
+ throw error instanceof RenditionError ? error : new RenditionError("processing-failed", `Processing failed: ${error}`, { cause: error });
171
+ });
172
+ await callStorage("storage-error", (signal) => results.write(key, image, { signal }));
173
+ }
174
+ return {
175
+ catalog,
176
+ async ensure(input, target, context) {
177
+ const recipe = Object.freeze({ ...input });
178
+ processor.assertRecipe?.(recipe);
179
+ const { sourceKey, rendition } = await locate(hashRecipe(recipe), recipe.format, target, context);
180
+ if (await isIndexed(rendition.url)) return rendition;
181
+ return coordinator.run(rendition.url, async () => {
182
+ if (!await isIndexed(rendition.url)) {
183
+ if (!await callStorage("storage-error", (signal) => results.exists(rendition.key, { signal }))) await limiter.run(() => render(recipe, sourceKey, rendition.key));
184
+ await markPublished(rendition.url);
185
+ }
186
+ return rendition;
187
+ });
188
+ },
189
+ async findPublished(hash, format, target, context) {
190
+ const { rendition } = await locate(hash, format, target, context);
191
+ return await isIndexed(rendition.url) ? rendition : void 0;
192
+ },
193
+ resolve
194
+ };
195
+ }
196
+ /**
197
+ * Runs a call with an abort signal that fires after the timeout.
198
+ *
199
+ * The call may ignore the signal, so the timeout also settles the result, keeping a hung call from holding the request or a processing slot.
200
+ */
201
+ function withTimeout(timeout, call) {
202
+ const signal = AbortSignal.timeout(timeout);
203
+ const expired = new Promise((_, reject) => {
204
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
205
+ });
206
+ return Promise.race([new Promise((resolve) => resolve(call(signal))), expired]);
207
+ }
208
+ function resolveUnscoped({ scope, path }) {
209
+ return scope === void 0 ? { sourceKey: path } : void 0;
210
+ }
211
+ function describe({ scope, path }) {
212
+ return scope === void 0 ? path : `${scope}:${path}`;
213
+ }
214
+ function createUnsafeKeyError(target) {
215
+ return new RenditionError("invalid-request", `The target ${describe(target)} resolves to an unsafe key.`);
216
+ }
217
+ //#endregion
218
+ export { RenditionError, createLocalCoordinator, createMemoryReadinessIndex, createRenditionHandler, createRenditionServer };
@@ -0,0 +1,22 @@
1
+ import { t as Processor } from "./processor-DvyPs-OZ.mjs";
2
+ //#region src/processors/sharp/processor.d.ts
3
+ /** Options of {@link createSharpProcessor}. */
4
+ export interface SharpProcessorOptions {
5
+ /** Largest decoded source, in pixels; larger sources fail as too large, and `0` removes the limit. */
6
+ maxInputPixels?: number;
7
+ /**
8
+ * Largest animation a recipe keeps, in pixels of all its frames at the larger of the source and the rendition size; `0` removes the limit.
9
+ *
10
+ * A larger animation renders its first frame.
11
+ */
12
+ maxAnimationPixels?: number;
13
+ /** Most frames of an animation a recipe keeps; an animation with more renders its first frame, and `0` removes the limit. */
14
+ maxFrames?: number;
15
+ }
16
+ /**
17
+ * Creates a processor backed by sharp and libvips.
18
+ *
19
+ * @throws {RangeError} When a limit is not a non-negative integer.
20
+ */
21
+ export declare function createSharpProcessor({ maxInputPixels, maxAnimationPixels, maxFrames }?: SharpProcessorOptions): Processor;
22
+ //#endregion