zerobucket 0.5.0 → 0.6.1
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 +59 -22
- package/dist/{chunk-W2C37XYV.js → chunk-SCPC3QDR.js} +1 -1
- package/dist/http.js +1 -1
- package/dist/index.cjs +265 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +259 -13
- package/dist/index.js.map +1 -1
- package/package.json +14 -6
- /package/dist/{chunk-W2C37XYV.js.map → chunk-SCPC3QDR.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
StorageError,
|
|
9
9
|
UnsupportedFormatError,
|
|
10
10
|
ZeroBucketError
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-SCPC3QDR.js";
|
|
12
12
|
|
|
13
13
|
// src/client.ts
|
|
14
14
|
import { createHash } from "crypto";
|
|
@@ -1880,11 +1880,191 @@ var SQLiteBackend = class {
|
|
|
1880
1880
|
}
|
|
1881
1881
|
};
|
|
1882
1882
|
|
|
1883
|
+
// src/heic.ts
|
|
1884
|
+
import { createRequire } from "module";
|
|
1885
|
+
import { join } from "path";
|
|
1886
|
+
import { Worker } from "worker_threads";
|
|
1887
|
+
var HEIC_BRANDS = /* @__PURE__ */ new Set([
|
|
1888
|
+
"heic",
|
|
1889
|
+
"heix",
|
|
1890
|
+
"hevc",
|
|
1891
|
+
"heim",
|
|
1892
|
+
"heis",
|
|
1893
|
+
"mif1",
|
|
1894
|
+
"msf1"
|
|
1895
|
+
]);
|
|
1896
|
+
function looksLikeHeic(data) {
|
|
1897
|
+
if (data.length < 12) return false;
|
|
1898
|
+
if (String.fromCharCode(data[4], data[5], data[6], data[7]) !== "ftyp")
|
|
1899
|
+
return false;
|
|
1900
|
+
return HEIC_BRANDS.has(
|
|
1901
|
+
String.fromCharCode(data[8], data[9], data[10], data[11])
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
var HEIC_INSTALL_HINT = "npm install libheif-js";
|
|
1905
|
+
var WORKER_SOURCE = `
|
|
1906
|
+
const { parentPort, workerData } = require("node:worker_threads");
|
|
1907
|
+
const libheif = require(workerData.libPath);
|
|
1908
|
+
// libheif-js reports decode problems by dumping a raw error object to the console. Capture that text instead:
|
|
1909
|
+
// it becomes part of the error we return (the useful diagnosis), and nothing is printed to the host's stderr.
|
|
1910
|
+
let diag = "";
|
|
1911
|
+
for (const level of ["log", "info", "warn", "error"]) {
|
|
1912
|
+
console[level] = (...args) => {
|
|
1913
|
+
diag = args.map((a) => (a && a.message) || (typeof a === "string" ? a : "")).filter(Boolean).join(" ").trim();
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
const describe = (e) => {
|
|
1917
|
+
const base = String((e && e.message) || e);
|
|
1918
|
+
return diag && !base.includes(diag) ? base + ": " + diag : base;
|
|
1919
|
+
};
|
|
1920
|
+
function open(bytes) {
|
|
1921
|
+
const images = new libheif.HeifDecoder().decode(bytes);
|
|
1922
|
+
if (!images || images.length === 0) throw new Error("no images found in HEIF container");
|
|
1923
|
+
return images[0];
|
|
1924
|
+
}
|
|
1925
|
+
parentPort.on("message", async ({ id, op, data }) => {
|
|
1926
|
+
diag = "";
|
|
1927
|
+
try {
|
|
1928
|
+
const img = open(new Uint8Array(data));
|
|
1929
|
+
const width = img.get_width();
|
|
1930
|
+
const height = img.get_height();
|
|
1931
|
+
if (op === "info") return parentPort.postMessage({ id, ok: true, width, height });
|
|
1932
|
+
const out = await new Promise((resolve, reject) =>
|
|
1933
|
+
img.display({ data: new Uint8ClampedArray(width * height * 4), width, height }, (d) =>
|
|
1934
|
+
d ? resolve(d) : reject(new Error("HEIF processing error")),
|
|
1935
|
+
),
|
|
1936
|
+
);
|
|
1937
|
+
if (op === "validate") return parentPort.postMessage({ id, ok: true, width, height });
|
|
1938
|
+
const rgba = out.data.buffer;
|
|
1939
|
+
parentPort.postMessage({ id, ok: true, width, height, rgba }, [rgba]);
|
|
1940
|
+
} catch (e) {
|
|
1941
|
+
parentPort.postMessage({ id, ok: false, error: describe(e) });
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
`;
|
|
1945
|
+
var HeicWorkerError = class extends StorageError {
|
|
1946
|
+
};
|
|
1947
|
+
var worker;
|
|
1948
|
+
var nextId = 1;
|
|
1949
|
+
var pending = /* @__PURE__ */ new Map();
|
|
1950
|
+
var stats = { info: 0, validate: 0, decode: 0, workersStarted: 0 };
|
|
1951
|
+
var resolveLib = () => {
|
|
1952
|
+
const attempts = [
|
|
1953
|
+
() => createRequire(import.meta.url).resolve("libheif-js/wasm-bundle"),
|
|
1954
|
+
// next to / above this package
|
|
1955
|
+
() => createRequire(join(process.cwd(), "noop.js")).resolve(
|
|
1956
|
+
"libheif-js/wasm-bundle"
|
|
1957
|
+
)
|
|
1958
|
+
// the app's own node_modules
|
|
1959
|
+
];
|
|
1960
|
+
let last;
|
|
1961
|
+
for (const attempt of attempts) {
|
|
1962
|
+
try {
|
|
1963
|
+
return attempt();
|
|
1964
|
+
} catch (e) {
|
|
1965
|
+
last = e;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
throw last;
|
|
1969
|
+
};
|
|
1970
|
+
function heicAvailable() {
|
|
1971
|
+
try {
|
|
1972
|
+
resolveLib();
|
|
1973
|
+
return true;
|
|
1974
|
+
} catch {
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
function releaseIfIdle(w) {
|
|
1979
|
+
for (const p of pending.values()) if (p.owner === w) return;
|
|
1980
|
+
w.unref();
|
|
1981
|
+
}
|
|
1982
|
+
function startWorker() {
|
|
1983
|
+
let libPath;
|
|
1984
|
+
try {
|
|
1985
|
+
libPath = resolveLib();
|
|
1986
|
+
} catch (cause) {
|
|
1987
|
+
throw new StorageError(
|
|
1988
|
+
`HEIC support requires the optional dependency 'libheif-js'. Install it with: ${HEIC_INSTALL_HINT}`,
|
|
1989
|
+
{ cause }
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
const w = new Worker(WORKER_SOURCE, { eval: true, workerData: { libPath } });
|
|
1993
|
+
stats.workersStarted += 1;
|
|
1994
|
+
w.on(
|
|
1995
|
+
"message",
|
|
1996
|
+
(m) => {
|
|
1997
|
+
const p = pending.get(m.id);
|
|
1998
|
+
if (!p) return;
|
|
1999
|
+
pending.delete(m.id);
|
|
2000
|
+
releaseIfIdle(w);
|
|
2001
|
+
if (m.ok) p.resolve(m);
|
|
2002
|
+
else p.reject(new Error(m.error));
|
|
2003
|
+
}
|
|
2004
|
+
);
|
|
2005
|
+
const fail = (err) => {
|
|
2006
|
+
if (worker === w) worker = void 0;
|
|
2007
|
+
for (const [id, p] of pending) {
|
|
2008
|
+
if (p.owner !== w) continue;
|
|
2009
|
+
pending.delete(id);
|
|
2010
|
+
p.reject(err);
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
w.on(
|
|
2014
|
+
"error",
|
|
2015
|
+
(e) => fail(
|
|
2016
|
+
new HeicWorkerError(
|
|
2017
|
+
`HEIC decoder worker crashed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2018
|
+
{ cause: e }
|
|
2019
|
+
)
|
|
2020
|
+
)
|
|
2021
|
+
);
|
|
2022
|
+
w.on(
|
|
2023
|
+
"exit",
|
|
2024
|
+
(code) => fail(
|
|
2025
|
+
new HeicWorkerError(
|
|
2026
|
+
`HEIC decoder worker exited unexpectedly (code ${code})`
|
|
2027
|
+
)
|
|
2028
|
+
)
|
|
2029
|
+
);
|
|
2030
|
+
w.unref();
|
|
2031
|
+
return w;
|
|
2032
|
+
}
|
|
2033
|
+
function call(op, data) {
|
|
2034
|
+
stats[op] += 1;
|
|
2035
|
+
const w = worker ??= startWorker();
|
|
2036
|
+
const id = nextId++;
|
|
2037
|
+
const copy = new Uint8Array(data).buffer;
|
|
2038
|
+
return new Promise((resolve, reject) => {
|
|
2039
|
+
pending.set(id, { owner: w, resolve, reject });
|
|
2040
|
+
w.ref();
|
|
2041
|
+
w.postMessage({ id, op, data: copy }, [copy]);
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
async function heicInfo(data) {
|
|
2045
|
+
const r = await call("info", data);
|
|
2046
|
+
return { width: r.width, height: r.height };
|
|
2047
|
+
}
|
|
2048
|
+
async function heicValidate(data) {
|
|
2049
|
+
const r = await call("validate", data);
|
|
2050
|
+
return { width: r.width, height: r.height };
|
|
2051
|
+
}
|
|
2052
|
+
async function decodeHeic(data) {
|
|
2053
|
+
const r = await call("decode", data);
|
|
2054
|
+
return {
|
|
2055
|
+
width: r.width,
|
|
2056
|
+
height: r.height,
|
|
2057
|
+
rgba: Buffer.from(r.rgba)
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1883
2061
|
// src/validation.ts
|
|
1884
2062
|
var FORMAT_TO_MIME = {
|
|
1885
2063
|
jpeg: "image/jpeg",
|
|
1886
2064
|
png: "image/png",
|
|
1887
|
-
webp: "image/webp"
|
|
2065
|
+
webp: "image/webp",
|
|
2066
|
+
heic: "image/heic"
|
|
2067
|
+
// optional: needs `npm install libheif-js` (see heic.ts)
|
|
1888
2068
|
};
|
|
1889
2069
|
var SUPPORTED_FORMATS = Object.keys(FORMAT_TO_MIME);
|
|
1890
2070
|
var DEFAULT_MAX_PIXELS = 89e6;
|
|
@@ -1912,6 +2092,8 @@ async function validateImage(data, opts) {
|
|
|
1912
2092
|
const sizeBytes = data.length;
|
|
1913
2093
|
if (sizeBytes > maxBytes) throw new ImageTooLargeError(sizeBytes, maxBytes);
|
|
1914
2094
|
if (sizeBytes === 0) throw new CorruptedImageError("Image data is empty");
|
|
2095
|
+
if (looksLikeHeic(data))
|
|
2096
|
+
return validateHeic(data, { maxBytes, maxPixels, allowedFormats });
|
|
1915
2097
|
const sharp = await loadSharp();
|
|
1916
2098
|
const make = () => sharp(data, { limitInputPixels: maxPixels, failOn: "error" });
|
|
1917
2099
|
let format;
|
|
@@ -1947,6 +2129,45 @@ async function validateImage(data, opts) {
|
|
|
1947
2129
|
}
|
|
1948
2130
|
return { mimeType: FORMAT_TO_MIME[format], width, height, sizeBytes };
|
|
1949
2131
|
}
|
|
2132
|
+
async function validateHeic(data, {
|
|
2133
|
+
maxBytes,
|
|
2134
|
+
maxPixels,
|
|
2135
|
+
allowedFormats
|
|
2136
|
+
}) {
|
|
2137
|
+
const sizeBytes = data.length;
|
|
2138
|
+
if (!heicAvailable()) {
|
|
2139
|
+
throw new ImageValidationError(
|
|
2140
|
+
`This looks like a HEIC/HEIF image, which requires an optional dependency. Install it with: ${HEIC_INSTALL_HINT}`
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
if (!allowedFormats.includes("heic"))
|
|
2144
|
+
throw new UnsupportedFormatError("heic", allowedFormats);
|
|
2145
|
+
let width;
|
|
2146
|
+
let height;
|
|
2147
|
+
try {
|
|
2148
|
+
({ width, height } = await heicInfo(data));
|
|
2149
|
+
} catch (exc) {
|
|
2150
|
+
if (exc instanceof StorageError) throw exc;
|
|
2151
|
+
throw new CorruptedImageError(
|
|
2152
|
+
`Could not decode image: ${errMessage(exc)}`,
|
|
2153
|
+
{ cause: exc }
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
if (!width || !height)
|
|
2157
|
+
throw new CorruptedImageError("Image has no readable dimensions");
|
|
2158
|
+
if (width * height > maxPixels)
|
|
2159
|
+
throw new ImageTooLargeError(sizeBytes, maxBytes);
|
|
2160
|
+
try {
|
|
2161
|
+
await heicValidate(data);
|
|
2162
|
+
} catch (exc) {
|
|
2163
|
+
if (exc instanceof StorageError) throw exc;
|
|
2164
|
+
throw new CorruptedImageError(
|
|
2165
|
+
`Image data is truncated or corrupted: ${errMessage(exc)}`,
|
|
2166
|
+
{ cause: exc }
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
return { mimeType: FORMAT_TO_MIME.heic, width, height, sizeBytes };
|
|
2170
|
+
}
|
|
1950
2171
|
function isPixelLimit(exc) {
|
|
1951
2172
|
return /pixel limit/i.test(errMessage(exc));
|
|
1952
2173
|
}
|
|
@@ -1955,28 +2176,53 @@ function errMessage(exc) {
|
|
|
1955
2176
|
}
|
|
1956
2177
|
|
|
1957
2178
|
// src/optimize.ts
|
|
2179
|
+
var ENCODABLE_FORMATS = ["jpeg", "png", "webp"];
|
|
1958
2180
|
var DEFAULT_JPEG_QUALITY = 90;
|
|
1959
2181
|
var DEFAULT_WEBP_QUALITY = 88;
|
|
1960
|
-
var FORMAT_ALIASES = { jpg: "jpeg" };
|
|
1961
2182
|
async function optimizeImage(data, opts) {
|
|
1962
2183
|
const { maxWidth, quality, maxBytes, maxPixels = DEFAULT_MAX_PIXELS } = opts;
|
|
1963
2184
|
const sharp = await loadSharp();
|
|
1964
2185
|
const originalSizeBytes = data.length;
|
|
1965
|
-
const
|
|
1966
|
-
const
|
|
1967
|
-
|
|
1968
|
-
const target = FORMAT_ALIASES[requested] ?? requested;
|
|
1969
|
-
if (!SUPPORTED_FORMATS.includes(target)) {
|
|
2186
|
+
const requestedRaw = opts.format?.toLowerCase();
|
|
2187
|
+
const requested = requestedRaw === "jpg" ? "jpeg" : requestedRaw;
|
|
2188
|
+
if (requested === "heic" || requested === "heif") {
|
|
1970
2189
|
throw new ImageValidationError(
|
|
1971
|
-
|
|
1972
|
-
[...SUPPORTED_FORMATS].sort()
|
|
1973
|
-
)}`
|
|
2190
|
+
"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`."
|
|
1974
2191
|
);
|
|
1975
2192
|
}
|
|
1976
|
-
let
|
|
2193
|
+
let input = data;
|
|
2194
|
+
let inputOptions = {
|
|
1977
2195
|
limitInputPixels: maxPixels,
|
|
1978
2196
|
failOn: "error"
|
|
1979
|
-
}
|
|
2197
|
+
};
|
|
2198
|
+
let sourceFormat;
|
|
2199
|
+
if (looksLikeHeic(data)) {
|
|
2200
|
+
if (!heicAvailable()) {
|
|
2201
|
+
throw new ImageValidationError(
|
|
2202
|
+
`This looks like a HEIC/HEIF image, which requires an optional dependency. Install it with: ${HEIC_INSTALL_HINT}`
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
if (!requested) {
|
|
2206
|
+
throw new ImageValidationError(
|
|
2207
|
+
"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 })."
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
2210
|
+
const decoded = await decodeHeic(data);
|
|
2211
|
+
input = decoded.rgba;
|
|
2212
|
+
inputOptions = {
|
|
2213
|
+
raw: { width: decoded.width, height: decoded.height, channels: 4 }
|
|
2214
|
+
};
|
|
2215
|
+
sourceFormat = "heic";
|
|
2216
|
+
} else {
|
|
2217
|
+
sourceFormat = (await sharp(data, inputOptions).metadata()).format;
|
|
2218
|
+
}
|
|
2219
|
+
const target = requested ?? sourceFormat ?? "jpeg";
|
|
2220
|
+
if (!ENCODABLE_FORMATS.includes(target)) {
|
|
2221
|
+
throw new ImageValidationError(
|
|
2222
|
+
`Cannot re-encode to unsupported format ${JSON.stringify(target)}. Supported: ${JSON.stringify([...ENCODABLE_FORMATS].sort())}`
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
let pipeline = sharp(input, inputOptions).rotate();
|
|
1980
2226
|
if (maxWidth !== void 0) {
|
|
1981
2227
|
pipeline = pipeline.resize({
|
|
1982
2228
|
width: maxWidth,
|