zerobucket 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -10,15 +10,16 @@ This is the Node.js/TypeScript sibling of the [Python package](https://pypi.org/
10
10
  **It uses the exact same table**, so a Node service and a Python service can share one database
11
11
  and read each other's images.
12
12
 
13
- > **Status: early (0.5.x).** PostgreSQL **and SQLite**, each in classic and dedup modes, with S3-compatible
14
- > object-storage tiering and a `zerobucket` command line tool. MySQL and HEIC are on the roadmap
15
- > (see [What's not here yet](#whats-not-here-yet)).
13
+ > **Status: early (0.6.x).** PostgreSQL **and SQLite**, each in classic and dedup modes, with S3-compatible
14
+ > object-storage tiering, HEIC (iPhone photo) support and a `zerobucket` command line tool. MySQL is on the
15
+ > roadmap (see [What's not here yet](#whats-not-here-yet)).
16
16
 
17
17
  ## Install
18
18
 
19
19
  ```bash
20
20
  npm install zerobucket pg sharp # PostgreSQL
21
21
  npm install zerobucket better-sqlite3 sharp # or SQLite (see below): no database server at all
22
+ npm install libheif-js # optional: accept HEIC (iPhone) photos, see below
22
23
  ```
23
24
 
24
25
  `pg` (the Postgres driver), `better-sqlite3` (SQLite) and `sharp` (image decoding/optimizing) are peer dependencies, so you
@@ -177,6 +178,38 @@ await images.put(pdfBuf, { validator: new PDFValidator() }); // any ContentValid
177
178
  Optimize strips metadata, applies EXIF orientation first, never upscales, and re-validates its own
178
179
  output. Implement `ContentValidator` (`validate(data, { maxBytes })`) to store other content types.
179
180
 
181
+ ## HEIC (iPhone photos)
182
+
183
+ iPhones upload HEIC by default, and most validators reject it. With the optional `libheif-js` installed, HEIC/HEIF
184
+ is accepted like any other format: validated by really decoding it, stored **as uploaded** (`image/heic`), and
185
+ convertible with `optimize`.
186
+
187
+ ```bash
188
+ npm install libheif-js
189
+ ```
190
+
191
+ ```ts
192
+ const id = await images.put(iphonePhoto); // stored unchanged, mimeType "image/heic"
193
+ const id2 = await images.put(iphonePhoto, {
194
+ optimize: { format: "webp", maxWidth: 1600 },
195
+ }); // convert on the way in
196
+ ```
197
+
198
+ - **Convert for the web.** HEIC is only displayable in Safari. Serve a web format by uploading with
199
+ `optimize: { format: "webp" | "jpeg" | "png" }`, as above. HEIC orientation is applied during conversion.
200
+ - **Decode only, by design.** There is no HEVC _encoder_ for Node, so `optimize` can convert **from** HEIC but not **to** it:
201
+ `optimize: { format: "heic" }` throws a clear error (Python's `pillow-heif` can encode), and `optimize: true` on a HEIC
202
+ needs an explicit target format instead of silently choosing one.
203
+ - **Never blocks your server.** Decoding is ~2 s of CPU for a 12-megapixel photo. It runs in a dedicated worker thread, so
204
+ other requests keep being served (tested), and the worker never keeps your process from exiting.
205
+ - **Safe on hostile input.** Pixel bombs are refused from the file header _before_ any decoding (`maxPixels`), truncated or
206
+ corrupt files are rejected with the decoder's own diagnosis, and a missing `libheif-js` is reported as "install it", never as "corrupt image".
207
+ - Recognised by content (`ftyp` brands `heic heix hevc heim heis mif1 msf1`), never by file extension: same rules as the Python package, and both
208
+ accept and reject the same files (checked by the conformance harness against `pillow-heif`).
209
+ - **License:** `libheif-js` is LGPL-3.0. It is an optional peer dependency that **you** install; it is not bundled with or redistributed by
210
+ this MIT-licensed package. If you use it, review its license for your distribution model.
211
+ - If a file contains several images, the first top-level image is used.
212
+
180
213
  ## Command line
181
214
 
182
215
  The package installs a `zerobucket` command (`npx zerobucket ...`, or install globally with `npm i -g zerobucket`).
@@ -333,28 +366,29 @@ same corpus of valid and corrupt files identically.
333
366
 
334
367
  ## Differences from the Python package
335
368
 
336
- | | Python | npm |
337
- | ---------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
338
- | API | sync + async clients | one async client |
339
- | Malformed id (`"abc"`) | `StorageError` from Postgres | treated as not found (`ImageNotFoundError`, no DB round trip), friendlier for `/images/:id` routes |
340
- | `optimize` | Pillow; EXIF stripped, not applied | sharp; EXIF **applied then stripped**; transparency → **white** for JPEG |
341
- | `putMany` inserts | pipelined | one transaction, statements run sequentially |
342
- | Formats | JPEG, PNG, WebP, HEIC (extra) | JPEG, PNG, WebP (HEIC not yet) |
343
- | Event duration | `duration_seconds` | `durationMs` |
344
- | `migrateClassicToDedup` | loads the whole table into memory; fails if re-run or if any row is tiered | batched, safe to re-run, refuses tiered rows with a clear message |
345
- | Dedup `putMany` / `deleteMany` locking | arbitrary order (overlapping batches can deadlock) | sorted checksum order (cannot deadlock) |
346
- | SQLite API | synchronous `SQLiteBackend` + separate async backend | one async client; the sync driver runs on the event loop, lock waits are async (never block it) |
347
- | SQLite `onOperation` / busy retry | none | `onOperation` events and async `SQLITE_BUSY` retry (`busyTimeoutMs`) |
348
- | SQLite stream deleted mid-read | sync adapter keeps reading a WAL snapshot (async adapter raises) | raises `StorageError`, like Postgres; use `connection` in a transaction for a snapshot |
349
- | CLI | PostgreSQL, classic mode only; `verify` crashes on tiered rows | PostgreSQL **and** SQLite, classic **and** dedup; `info`/`verify` understand tiered rows; `migrate --to-dedup` |
350
- | `connection=` without an open transaction | multi-statement ops rely on the driver's implicit transaction | always atomic (own transaction or savepoint) |
351
- | `delete(id, connection=...)` on a tiered image | deletes the S3 object immediately | leaves the object until you delete it after commit (a rolled-back transaction can never lose data) |
352
- | S3 delete failure after `delete()` | raises after the row is gone | never fails the delete; reported via `onOperation` |
353
- | Options | snake_case kwargs | camelCase options object |
369
+ | | Python | npm |
370
+ | ---------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
371
+ | API | sync + async clients | one async client |
372
+ | Malformed id (`"abc"`) | `StorageError` from Postgres | treated as not found (`ImageNotFoundError`, no DB round trip), friendlier for `/images/:id` routes |
373
+ | `optimize` | Pillow; EXIF stripped, not applied | sharp; EXIF **applied then stripped**; transparency → **white** for JPEG |
374
+ | `putMany` inserts | pipelined | one transaction, statements run sequentially |
375
+ | Formats | JPEG, PNG, WebP, HEIC (`[heic]` extra) | JPEG, PNG, WebP, HEIC (optional `libheif-js`) |
376
+ | Convert TO HEIC with `optimize` | yes (pillow-heif) | no (no HEVC encoder for Node); converting FROM HEIC works; `optimize: true` on a HEIC needs an explicit `format` |
377
+ | Event duration | `duration_seconds` | `durationMs` |
378
+ | `migrateClassicToDedup` | loads the whole table into memory; fails if re-run or if any row is tiered | batched, safe to re-run, refuses tiered rows with a clear message |
379
+ | Dedup `putMany` / `deleteMany` locking | arbitrary order (overlapping batches can deadlock) | sorted checksum order (cannot deadlock) |
380
+ | SQLite API | synchronous `SQLiteBackend` + separate async backend | one async client; the sync driver runs on the event loop, lock waits are async (never block it) |
381
+ | SQLite `onOperation` / busy retry | none | `onOperation` events and async `SQLITE_BUSY` retry (`busyTimeoutMs`) |
382
+ | SQLite stream deleted mid-read | sync adapter keeps reading a WAL snapshot (async adapter raises) | raises `StorageError`, like Postgres; use `connection` in a transaction for a snapshot |
383
+ | CLI | PostgreSQL, classic mode only; `verify` crashes on tiered rows | PostgreSQL **and** SQLite, classic **and** dedup; `info`/`verify` understand tiered rows; `migrate --to-dedup` |
384
+ | `connection=` without an open transaction | multi-statement ops rely on the driver's implicit transaction | always atomic (own transaction or savepoint) |
385
+ | `delete(id, connection=...)` on a tiered image | deletes the S3 object immediately | leaves the object until you delete it after commit (a rolled-back transaction can never lose data) |
386
+ | S3 delete failure after `delete()` | raises after the row is gone | never fails the delete; reported via `onOperation` |
387
+ | Options | snake_case kwargs | camelCase options object |
354
388
 
355
389
  ## What's not here yet
356
390
 
357
- Planned, in order: **MySQL/MariaDB** and **HEIC**.
391
+ Planned: **MySQL/MariaDB**.
358
392
  Tiering and dedup cannot be combined (same rule as the Python package).
359
393
 
360
394
  ## Runtime notes
@@ -65,4 +65,4 @@ export {
65
65
  StorageError,
66
66
  AccessDeniedError
67
67
  };
68
- //# sourceMappingURL=chunk-W2C37XYV.js.map
68
+ //# sourceMappingURL=chunk-SCPC3QDR.js.map
package/dist/http.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  AccessDeniedError,
3
3
  ImageNotFoundError
4
- } from "./chunk-W2C37XYV.js";
4
+ } from "./chunk-SCPC3QDR.js";
5
5
 
6
6
  // src/http.ts
7
7
  import { Readable } from "stream";
package/dist/index.cjs CHANGED
@@ -54,10 +54,14 @@ __export(src_exports, {
54
54
  });
55
55
  module.exports = __toCommonJS(src_exports);
56
56
 
57
+ // node_modules/tsup/assets/cjs_shims.js
58
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
59
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
60
+
57
61
  // src/client.ts
58
62
  var import_node_crypto2 = require("crypto");
59
63
  var import_promises = require("fs/promises");
60
- var import_node_path = require("path");
64
+ var import_node_path2 = require("path");
61
65
 
62
66
  // src/errors.ts
63
67
  var ZeroBucketError = class extends Error {
@@ -1980,11 +1984,191 @@ var SQLiteBackend = class {
1980
1984
  }
1981
1985
  };
1982
1986
 
1987
+ // src/heic.ts
1988
+ var import_node_module = require("module");
1989
+ var import_node_path = require("path");
1990
+ var import_node_worker_threads = require("worker_threads");
1991
+ var HEIC_BRANDS = /* @__PURE__ */ new Set([
1992
+ "heic",
1993
+ "heix",
1994
+ "hevc",
1995
+ "heim",
1996
+ "heis",
1997
+ "mif1",
1998
+ "msf1"
1999
+ ]);
2000
+ function looksLikeHeic(data) {
2001
+ if (data.length < 12) return false;
2002
+ if (String.fromCharCode(data[4], data[5], data[6], data[7]) !== "ftyp")
2003
+ return false;
2004
+ return HEIC_BRANDS.has(
2005
+ String.fromCharCode(data[8], data[9], data[10], data[11])
2006
+ );
2007
+ }
2008
+ var HEIC_INSTALL_HINT = "npm install libheif-js";
2009
+ var WORKER_SOURCE = `
2010
+ const { parentPort, workerData } = require("node:worker_threads");
2011
+ const libheif = require(workerData.libPath);
2012
+ // libheif-js reports decode problems by dumping a raw error object to the console. Capture that text instead:
2013
+ // it becomes part of the error we return (the useful diagnosis), and nothing is printed to the host's stderr.
2014
+ let diag = "";
2015
+ for (const level of ["log", "info", "warn", "error"]) {
2016
+ console[level] = (...args) => {
2017
+ diag = args.map((a) => (a && a.message) || (typeof a === "string" ? a : "")).filter(Boolean).join(" ").trim();
2018
+ };
2019
+ }
2020
+ const describe = (e) => {
2021
+ const base = String((e && e.message) || e);
2022
+ return diag && !base.includes(diag) ? base + ": " + diag : base;
2023
+ };
2024
+ function open(bytes) {
2025
+ const images = new libheif.HeifDecoder().decode(bytes);
2026
+ if (!images || images.length === 0) throw new Error("no images found in HEIF container");
2027
+ return images[0];
2028
+ }
2029
+ parentPort.on("message", async ({ id, op, data }) => {
2030
+ diag = "";
2031
+ try {
2032
+ const img = open(new Uint8Array(data));
2033
+ const width = img.get_width();
2034
+ const height = img.get_height();
2035
+ if (op === "info") return parentPort.postMessage({ id, ok: true, width, height });
2036
+ const out = await new Promise((resolve, reject) =>
2037
+ img.display({ data: new Uint8ClampedArray(width * height * 4), width, height }, (d) =>
2038
+ d ? resolve(d) : reject(new Error("HEIF processing error")),
2039
+ ),
2040
+ );
2041
+ if (op === "validate") return parentPort.postMessage({ id, ok: true, width, height });
2042
+ const rgba = out.data.buffer;
2043
+ parentPort.postMessage({ id, ok: true, width, height, rgba }, [rgba]);
2044
+ } catch (e) {
2045
+ parentPort.postMessage({ id, ok: false, error: describe(e) });
2046
+ }
2047
+ });
2048
+ `;
2049
+ var HeicWorkerError = class extends StorageError {
2050
+ };
2051
+ var worker;
2052
+ var nextId = 1;
2053
+ var pending = /* @__PURE__ */ new Map();
2054
+ var stats = { info: 0, validate: 0, decode: 0, workersStarted: 0 };
2055
+ var resolveLib = () => {
2056
+ const attempts = [
2057
+ () => (0, import_node_module.createRequire)(importMetaUrl).resolve("libheif-js/wasm-bundle"),
2058
+ // next to / above this package
2059
+ () => (0, import_node_module.createRequire)((0, import_node_path.join)(process.cwd(), "noop.js")).resolve(
2060
+ "libheif-js/wasm-bundle"
2061
+ )
2062
+ // the app's own node_modules
2063
+ ];
2064
+ let last;
2065
+ for (const attempt of attempts) {
2066
+ try {
2067
+ return attempt();
2068
+ } catch (e) {
2069
+ last = e;
2070
+ }
2071
+ }
2072
+ throw last;
2073
+ };
2074
+ function heicAvailable() {
2075
+ try {
2076
+ resolveLib();
2077
+ return true;
2078
+ } catch {
2079
+ return false;
2080
+ }
2081
+ }
2082
+ function releaseIfIdle(w) {
2083
+ for (const p of pending.values()) if (p.owner === w) return;
2084
+ w.unref();
2085
+ }
2086
+ function startWorker() {
2087
+ let libPath;
2088
+ try {
2089
+ libPath = resolveLib();
2090
+ } catch (cause) {
2091
+ throw new StorageError(
2092
+ `HEIC support requires the optional dependency 'libheif-js'. Install it with: ${HEIC_INSTALL_HINT}`,
2093
+ { cause }
2094
+ );
2095
+ }
2096
+ const w = new import_node_worker_threads.Worker(WORKER_SOURCE, { eval: true, workerData: { libPath } });
2097
+ stats.workersStarted += 1;
2098
+ w.on(
2099
+ "message",
2100
+ (m) => {
2101
+ const p = pending.get(m.id);
2102
+ if (!p) return;
2103
+ pending.delete(m.id);
2104
+ releaseIfIdle(w);
2105
+ if (m.ok) p.resolve(m);
2106
+ else p.reject(new Error(m.error));
2107
+ }
2108
+ );
2109
+ const fail = (err) => {
2110
+ if (worker === w) worker = void 0;
2111
+ for (const [id, p] of pending) {
2112
+ if (p.owner !== w) continue;
2113
+ pending.delete(id);
2114
+ p.reject(err);
2115
+ }
2116
+ };
2117
+ w.on(
2118
+ "error",
2119
+ (e) => fail(
2120
+ new HeicWorkerError(
2121
+ `HEIC decoder worker crashed: ${e instanceof Error ? e.message : String(e)}`,
2122
+ { cause: e }
2123
+ )
2124
+ )
2125
+ );
2126
+ w.on(
2127
+ "exit",
2128
+ (code) => fail(
2129
+ new HeicWorkerError(
2130
+ `HEIC decoder worker exited unexpectedly (code ${code})`
2131
+ )
2132
+ )
2133
+ );
2134
+ w.unref();
2135
+ return w;
2136
+ }
2137
+ function call(op, data) {
2138
+ stats[op] += 1;
2139
+ const w = worker ??= startWorker();
2140
+ const id = nextId++;
2141
+ const copy = new Uint8Array(data).buffer;
2142
+ return new Promise((resolve, reject) => {
2143
+ pending.set(id, { owner: w, resolve, reject });
2144
+ w.ref();
2145
+ w.postMessage({ id, op, data: copy }, [copy]);
2146
+ });
2147
+ }
2148
+ async function heicInfo(data) {
2149
+ const r = await call("info", data);
2150
+ return { width: r.width, height: r.height };
2151
+ }
2152
+ async function heicValidate(data) {
2153
+ const r = await call("validate", data);
2154
+ return { width: r.width, height: r.height };
2155
+ }
2156
+ async function decodeHeic(data) {
2157
+ const r = await call("decode", data);
2158
+ return {
2159
+ width: r.width,
2160
+ height: r.height,
2161
+ rgba: Buffer.from(r.rgba)
2162
+ };
2163
+ }
2164
+
1983
2165
  // src/validation.ts
1984
2166
  var FORMAT_TO_MIME = {
1985
2167
  jpeg: "image/jpeg",
1986
2168
  png: "image/png",
1987
- webp: "image/webp"
2169
+ webp: "image/webp",
2170
+ heic: "image/heic"
2171
+ // optional: needs `npm install libheif-js` (see heic.ts)
1988
2172
  };
1989
2173
  var SUPPORTED_FORMATS = Object.keys(FORMAT_TO_MIME);
1990
2174
  var DEFAULT_MAX_PIXELS = 89e6;
@@ -2012,6 +2196,8 @@ async function validateImage(data, opts) {
2012
2196
  const sizeBytes = data.length;
2013
2197
  if (sizeBytes > maxBytes) throw new ImageTooLargeError(sizeBytes, maxBytes);
2014
2198
  if (sizeBytes === 0) throw new CorruptedImageError("Image data is empty");
2199
+ if (looksLikeHeic(data))
2200
+ return validateHeic(data, { maxBytes, maxPixels, allowedFormats });
2015
2201
  const sharp = await loadSharp();
2016
2202
  const make = () => sharp(data, { limitInputPixels: maxPixels, failOn: "error" });
2017
2203
  let format;
@@ -2047,6 +2233,45 @@ async function validateImage(data, opts) {
2047
2233
  }
2048
2234
  return { mimeType: FORMAT_TO_MIME[format], width, height, sizeBytes };
2049
2235
  }
2236
+ async function validateHeic(data, {
2237
+ maxBytes,
2238
+ maxPixels,
2239
+ allowedFormats
2240
+ }) {
2241
+ const sizeBytes = data.length;
2242
+ if (!heicAvailable()) {
2243
+ throw new ImageValidationError(
2244
+ `This looks like a HEIC/HEIF image, which requires an optional dependency. Install it with: ${HEIC_INSTALL_HINT}`
2245
+ );
2246
+ }
2247
+ if (!allowedFormats.includes("heic"))
2248
+ throw new UnsupportedFormatError("heic", allowedFormats);
2249
+ let width;
2250
+ let height;
2251
+ try {
2252
+ ({ width, height } = await heicInfo(data));
2253
+ } catch (exc) {
2254
+ if (exc instanceof StorageError) throw exc;
2255
+ throw new CorruptedImageError(
2256
+ `Could not decode image: ${errMessage(exc)}`,
2257
+ { cause: exc }
2258
+ );
2259
+ }
2260
+ if (!width || !height)
2261
+ throw new CorruptedImageError("Image has no readable dimensions");
2262
+ if (width * height > maxPixels)
2263
+ throw new ImageTooLargeError(sizeBytes, maxBytes);
2264
+ try {
2265
+ await heicValidate(data);
2266
+ } catch (exc) {
2267
+ if (exc instanceof StorageError) throw exc;
2268
+ throw new CorruptedImageError(
2269
+ `Image data is truncated or corrupted: ${errMessage(exc)}`,
2270
+ { cause: exc }
2271
+ );
2272
+ }
2273
+ return { mimeType: FORMAT_TO_MIME.heic, width, height, sizeBytes };
2274
+ }
2050
2275
  function isPixelLimit(exc) {
2051
2276
  return /pixel limit/i.test(errMessage(exc));
2052
2277
  }
@@ -2055,28 +2280,53 @@ function errMessage(exc) {
2055
2280
  }
2056
2281
 
2057
2282
  // src/optimize.ts
2283
+ var ENCODABLE_FORMATS = ["jpeg", "png", "webp"];
2058
2284
  var DEFAULT_JPEG_QUALITY = 90;
2059
2285
  var DEFAULT_WEBP_QUALITY = 88;
2060
- var FORMAT_ALIASES = { jpg: "jpeg" };
2061
2286
  async function optimizeImage(data, opts) {
2062
2287
  const { maxWidth, quality, maxBytes, maxPixels = DEFAULT_MAX_PIXELS } = opts;
2063
2288
  const sharp = await loadSharp();
2064
2289
  const originalSizeBytes = data.length;
2065
- const base = sharp(data, { limitInputPixels: maxPixels, failOn: "error" });
2066
- const meta = await base.metadata();
2067
- const requested = (opts.format ?? meta.format ?? "jpeg").toLowerCase();
2068
- const target = FORMAT_ALIASES[requested] ?? requested;
2069
- if (!SUPPORTED_FORMATS.includes(target)) {
2290
+ const requestedRaw = opts.format?.toLowerCase();
2291
+ const requested = requestedRaw === "jpg" ? "jpeg" : requestedRaw;
2292
+ if (requested === "heic" || requested === "heif") {
2070
2293
  throw new ImageValidationError(
2071
- `Cannot re-encode to unsupported format ${JSON.stringify(target)}. Supported: ${JSON.stringify(
2072
- [...SUPPORTED_FORMATS].sort()
2073
- )}`
2294
+ "Encoding to HEIC is not available in the Node package (there is no HEVC encoder for Node). Optimize to jpeg, webp or png instead, or store the original HEIC without `optimize`."
2074
2295
  );
2075
2296
  }
2076
- let pipeline = sharp(data, {
2297
+ let input = data;
2298
+ let inputOptions = {
2077
2299
  limitInputPixels: maxPixels,
2078
2300
  failOn: "error"
2079
- }).rotate();
2301
+ };
2302
+ let sourceFormat;
2303
+ if (looksLikeHeic(data)) {
2304
+ if (!heicAvailable()) {
2305
+ throw new ImageValidationError(
2306
+ `This looks like a HEIC/HEIF image, which requires an optional dependency. Install it with: ${HEIC_INSTALL_HINT}`
2307
+ );
2308
+ }
2309
+ if (!requested) {
2310
+ throw new ImageValidationError(
2311
+ "Optimizing a HEIC image needs an explicit output format, because re-encoding to HEIC is not available in the Node package. Pass optimize: { format: 'jpeg' | 'webp' | 'png' } (for example { format: 'webp', maxWidth: 1600 })."
2312
+ );
2313
+ }
2314
+ const decoded = await decodeHeic(data);
2315
+ input = decoded.rgba;
2316
+ inputOptions = {
2317
+ raw: { width: decoded.width, height: decoded.height, channels: 4 }
2318
+ };
2319
+ sourceFormat = "heic";
2320
+ } else {
2321
+ sourceFormat = (await sharp(data, inputOptions).metadata()).format;
2322
+ }
2323
+ const target = requested ?? sourceFormat ?? "jpeg";
2324
+ if (!ENCODABLE_FORMATS.includes(target)) {
2325
+ throw new ImageValidationError(
2326
+ `Cannot re-encode to unsupported format ${JSON.stringify(target)}. Supported: ${JSON.stringify([...ENCODABLE_FORMATS].sort())}`
2327
+ );
2328
+ }
2329
+ let pipeline = sharp(input, inputOptions).rotate();
2080
2330
  if (maxWidth !== void 0) {
2081
2331
  pipeline = pipeline.resize({
2082
2332
  width: maxWidth,
@@ -2519,7 +2769,7 @@ async function readInput(image, filename, maxBytes) {
2519
2769
  if (size > maxBytes) throw new ImageTooLargeError(size, maxBytes);
2520
2770
  return {
2521
2771
  data: await (0, import_promises.readFile)(image),
2522
- filename: filename ?? (0, import_node_path.basename)(image)
2772
+ filename: filename ?? (0, import_node_path2.basename)(image)
2523
2773
  };
2524
2774
  }
2525
2775
  if (Buffer.isBuffer(image)) return { data: image, filename };
@@ -2554,7 +2804,7 @@ async function readInput(image, filename, maxBytes) {
2554
2804
  const p = image.path;
2555
2805
  return {
2556
2806
  data: Buffer.concat(chunks, total),
2557
- filename: filename ?? (typeof p === "string" ? (0, import_node_path.basename)(p) : void 0)
2807
+ filename: filename ?? (typeof p === "string" ? (0, import_node_path2.basename)(p) : void 0)
2558
2808
  };
2559
2809
  }
2560
2810
  throw new TypeError(