img-upload-kit 0.0.0-stage → 2.0.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/CHANGELOG.md +89 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +192 -0
- package/README.md +242 -2
- package/dist/bin.js +1186 -0
- package/dist/bin.js.map +1 -0
- package/dist/blur.cjs +41 -0
- package/dist/blur.cjs.map +1 -0
- package/dist/blur.d.cts +17 -0
- package/dist/blur.d.ts +17 -0
- package/dist/blur.js +41 -0
- package/dist/blur.js.map +1 -0
- package/dist/blurDataUtils-DveBRtqE.d.cts +43 -0
- package/dist/blurDataUtils-yx9Q6HlU.d.ts +43 -0
- package/dist/blurService-D5TpMNji.d.ts +83 -0
- package/dist/blurService-DacwylJx.d.cts +83 -0
- package/dist/chunk-2OVNXHTN.cjs +514 -0
- package/dist/chunk-2OVNXHTN.cjs.map +1 -0
- package/dist/chunk-3ICG2SLL.cjs +135 -0
- package/dist/chunk-3ICG2SLL.cjs.map +1 -0
- package/dist/chunk-4ASFG3KB.js +514 -0
- package/dist/chunk-4ASFG3KB.js.map +1 -0
- package/dist/chunk-AI3JJLSA.js +2370 -0
- package/dist/chunk-AI3JJLSA.js.map +1 -0
- package/dist/chunk-CV23O4YU.js +145 -0
- package/dist/chunk-CV23O4YU.js.map +1 -0
- package/dist/chunk-G7LLSKBV.js +158 -0
- package/dist/chunk-G7LLSKBV.js.map +1 -0
- package/dist/chunk-IB3TEDLR.js +294 -0
- package/dist/chunk-IB3TEDLR.js.map +1 -0
- package/dist/chunk-KJVYWKIG.cjs +158 -0
- package/dist/chunk-KJVYWKIG.cjs.map +1 -0
- package/dist/chunk-NEDBIIE3.cjs +226 -0
- package/dist/chunk-NEDBIIE3.cjs.map +1 -0
- package/dist/chunk-QHU7FUO4.cjs +37 -0
- package/dist/chunk-QHU7FUO4.cjs.map +1 -0
- package/dist/chunk-QQHZJKNA.js +135 -0
- package/dist/chunk-QQHZJKNA.js.map +1 -0
- package/dist/chunk-S2IFAWJK.js +37 -0
- package/dist/chunk-S2IFAWJK.js.map +1 -0
- package/dist/chunk-TT5C7K3Q.js +226 -0
- package/dist/chunk-TT5C7K3Q.js.map +1 -0
- package/dist/chunk-U2TWTEBY.cjs +2370 -0
- package/dist/chunk-U2TWTEBY.cjs.map +1 -0
- package/dist/chunk-WISG74VL.cjs +294 -0
- package/dist/chunk-WISG74VL.cjs.map +1 -0
- package/dist/chunk-YRXXQUDE.cjs +145 -0
- package/dist/chunk-YRXXQUDE.cjs.map +1 -0
- package/dist/cli.cjs +1241 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +99 -0
- package/dist/cli.d.ts +99 -0
- package/dist/cli.js +1194 -0
- package/dist/cli.js.map +1 -0
- package/dist/headless.cjs +27 -0
- package/dist/headless.cjs.map +1 -0
- package/dist/headless.d.cts +4 -0
- package/dist/headless.d.ts +4 -0
- package/dist/headless.js +27 -0
- package/dist/headless.js.map +1 -0
- package/dist/index-BIdnqtW8.d.ts +234 -0
- package/dist/index-Brk_uHs6.d.cts +234 -0
- package/dist/index-CPMlaQsC.d.ts +635 -0
- package/dist/index-DHINe-rA.d.cts +635 -0
- package/dist/index.cjs +65 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +43 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/memoryAdapter-Ck0ccZbF.d.cts +25 -0
- package/dist/memoryAdapter-DxEKaG0b.d.ts +25 -0
- package/dist/placeholders-C4ftwaII.d.ts +5 -0
- package/dist/placeholders-x_3rt65M.d.cts +5 -0
- package/dist/react.cjs +3779 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +476 -0
- package/dist/react.d.ts +476 -0
- package/dist/react.js +3779 -0
- package/dist/react.js.map +1 -0
- package/dist/server-handler.cjs +14 -0
- package/dist/server-handler.cjs.map +1 -0
- package/dist/server-handler.d.cts +3 -0
- package/dist/server-handler.d.ts +3 -0
- package/dist/server-handler.js +14 -0
- package/dist/server-handler.js.map +1 -0
- package/dist/server.cjs +1801 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +142 -0
- package/dist/server.d.ts +142 -0
- package/dist/server.js +1801 -0
- package/dist/server.js.map +1 -0
- package/dist/storage.cjs +447 -0
- package/dist/storage.cjs.map +1 -0
- package/dist/storage.d.cts +163 -0
- package/dist/storage.d.ts +163 -0
- package/dist/storage.js +447 -0
- package/dist/storage.js.map +1 -0
- package/dist/styles/animated-image.css +112 -0
- package/dist/styles/dropzone-and-tiles.css +247 -0
- package/dist/styles/modals-and-dock.css +248 -0
- package/dist/styles/primitives.css +197 -0
- package/dist/styles/quality-and-inputs.css +184 -0
- package/dist/styles/tokens.css +123 -0
- package/dist/styles.css +1122 -0
- package/dist/types-BrP-o9nd.d.cts +85 -0
- package/dist/types-sZ3KoFQY.d.ts +85 -0
- package/dist/types-yShGNQnW.d.cts +61 -0
- package/dist/types-yShGNQnW.d.ts +61 -0
- package/dist/upload-C5Trd6Dh.d.cts +154 -0
- package/dist/upload-C5Trd6Dh.d.ts +154 -0
- package/package.json +198 -4
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,1801 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2;
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
var _chunk2OVNXHTNcjs = require('./chunk-2OVNXHTN.cjs');
|
|
7
|
+
require('./chunk-QHU7FUO4.cjs');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
var _chunkNEDBIIE3cjs = require('./chunk-NEDBIIE3.cjs');
|
|
17
|
+
|
|
18
|
+
// src/server/optimization/types.ts
|
|
19
|
+
var DEFAULT_LIMIT_INPUT_PIXELS = 5e7;
|
|
20
|
+
var PRIMARY_AVIF_EFFORT = 6;
|
|
21
|
+
var RETRY_AVIF_EFFORT = 5;
|
|
22
|
+
var BLUR_PLACEHOLDER_WIDTH = 128;
|
|
23
|
+
var BLUR_PLACEHOLDER_FALLBACK_WIDTH = 96;
|
|
24
|
+
var BLUR_PLACEHOLDER_QUALITY = 80;
|
|
25
|
+
var BLUR_PLACEHOLDER_MAX_CHARS = 8e3;
|
|
26
|
+
var EDGE_COLOR_FALLBACK = "#6B7280";
|
|
27
|
+
var EDGE_WHITE_FALLBACK = "#F9FAFB";
|
|
28
|
+
var WHITE_DOMINANCE_RATIO = 1.35;
|
|
29
|
+
|
|
30
|
+
// src/server/security/sanitizeSvg.ts
|
|
31
|
+
var DEFAULT_MAX_SVG_BYTES = 1024 * 1024;
|
|
32
|
+
var FORBIDDEN_ELEMENTS = [
|
|
33
|
+
"script",
|
|
34
|
+
"foreignObject",
|
|
35
|
+
"iframe",
|
|
36
|
+
"embed",
|
|
37
|
+
"object",
|
|
38
|
+
"applet",
|
|
39
|
+
"meta",
|
|
40
|
+
"link",
|
|
41
|
+
"base",
|
|
42
|
+
"audio",
|
|
43
|
+
"video",
|
|
44
|
+
"canvas",
|
|
45
|
+
"animate",
|
|
46
|
+
"animateMotion",
|
|
47
|
+
"animateTransform",
|
|
48
|
+
"set",
|
|
49
|
+
"handler",
|
|
50
|
+
"listener",
|
|
51
|
+
"tbreak"
|
|
52
|
+
];
|
|
53
|
+
var SAFE_NAMED_ENTITIES = /* @__PURE__ */ new Set(["amp", "lt", "gt", "quot", "apos"]);
|
|
54
|
+
var SAFE_HREF_FRAGMENT_RE = /^#[a-zA-Z0-9_.:-]+$/;
|
|
55
|
+
var SAFE_DATA_IMAGE_RE = /^data:image\/(?:png|jpeg|webp);base64,[a-zA-Z0-9+/=\s]+$/i;
|
|
56
|
+
function stripControlAndSpace(str) {
|
|
57
|
+
let out = "";
|
|
58
|
+
for (let i = 0; i < str.length; i++) {
|
|
59
|
+
if (str.charCodeAt(i) > 32) out += str[i];
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function decodeHtmlEntitiesForCheck(value) {
|
|
64
|
+
const decoded = value.replace(
|
|
65
|
+
/&#x([0-9a-fA-F]+);?/g,
|
|
66
|
+
(_, hex) => String.fromCodePoint(Number.parseInt(hex, 16) || 0)
|
|
67
|
+
).replace(
|
|
68
|
+
/&#(\d+);?/g,
|
|
69
|
+
(_, dec) => String.fromCodePoint(Number.parseInt(dec, 10) || 0)
|
|
70
|
+
);
|
|
71
|
+
return stripControlAndSpace(decoded);
|
|
72
|
+
}
|
|
73
|
+
function isSafeSvgHref(rawValue) {
|
|
74
|
+
const trimmed = rawValue.trim();
|
|
75
|
+
if (SAFE_HREF_FRAGMENT_RE.test(trimmed)) return true;
|
|
76
|
+
const decoded = decodeHtmlEntitiesForCheck(trimmed);
|
|
77
|
+
if (SAFE_HREF_FRAGMENT_RE.test(decoded)) return true;
|
|
78
|
+
if (SAFE_DATA_IMAGE_RE.test(trimmed) && SAFE_DATA_IMAGE_RE.test(decoded)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function sanitizeCssBlock(css) {
|
|
84
|
+
return css.replace(/@import\b[^;{}]*(?:;|$)/gi, "").replace(/expression\s*\([^)]*\)/gi, "").replace(/(?:javascript|vbscript|data)\s*:[^;)"']*/gi, "").replace(/-moz-binding\s*:[^;}"']*/gi, "").replace(/behavior\s*:[^;}"']*/gi, "").replace(/url\s*\(\s*(['"]?)(.*?)\1\s*\)/gi, (full, _q, target) => {
|
|
85
|
+
const cleaned = decodeHtmlEntitiesForCheck(target.trim());
|
|
86
|
+
return SAFE_HREF_FRAGMENT_RE.test(cleaned) ? full : "none";
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function sanitizeTagAttributes(attrs) {
|
|
90
|
+
const attrRe = /([a-zA-Z_:][\w:.-]*)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
91
|
+
const kept = [];
|
|
92
|
+
for (const match of attrs.matchAll(attrRe)) {
|
|
93
|
+
const rawName = match[1];
|
|
94
|
+
const lowerName = rawName.toLowerCase();
|
|
95
|
+
const localName = lowerName.includes(":") ? lowerName.split(":")[1] : lowerName;
|
|
96
|
+
const rawValue = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(match[2], () => ( match[3])), () => ( match[4])), () => ( ""));
|
|
97
|
+
if (lowerName.startsWith("on") || localName.startsWith("on")) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (localName === "href" || lowerName === "xlink:href" || localName === "src" || localName === "action" || localName === "formaction" || localName === "data") {
|
|
101
|
+
if (!isSafeSvgHref(rawValue)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const escaped = rawValue.replace(/"/g, """);
|
|
105
|
+
kept.push(`${rawName}="${escaped}"`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (localName === "style") {
|
|
109
|
+
const cleanStyle = sanitizeCssBlock(rawValue).replace(/"/g, "'");
|
|
110
|
+
if (cleanStyle.trim()) {
|
|
111
|
+
kept.push(`style="${cleanStyle}"`);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const decodedVal = decodeHtmlEntitiesForCheck(rawValue).toLowerCase();
|
|
116
|
+
if (decodedVal.includes("javascript:") || decodedVal.includes("vbscript:") || decodedVal.includes("data:text/") || decodedVal.includes("data:image/svg")) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (/url\s*\(/i.test(rawValue)) {
|
|
120
|
+
const sanitizedUrlAttr = sanitizeCssBlock(rawValue).replace(/"/g, """);
|
|
121
|
+
kept.push(`${rawName}="${sanitizedUrlAttr}"`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (match[2] !== void 0 || match[3] !== void 0 || match[4] !== void 0) {
|
|
125
|
+
kept.push(`${rawName}="${rawValue.replace(/"/g, """)}"`);
|
|
126
|
+
} else {
|
|
127
|
+
kept.push(rawName);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return kept.length > 0 ? ` ${kept.join(" ")}` : "";
|
|
131
|
+
}
|
|
132
|
+
function sanitizeSvg(input, options = {}) {
|
|
133
|
+
const maxBytes = _nullishCoalesce(options.maxBytes, () => ( DEFAULT_MAX_SVG_BYTES));
|
|
134
|
+
const byteLength = typeof input === "string" ? Buffer.byteLength(input, "utf8") : input.length;
|
|
135
|
+
if (byteLength > maxBytes) {
|
|
136
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(
|
|
137
|
+
`SVG file size (${byteLength} bytes) exceeds maximum allowed SVG size of ${maxBytes} bytes`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
let svg = typeof input === "string" ? input : input.toString("utf8");
|
|
141
|
+
svg = svg.replace(/^\uFEFF/, "");
|
|
142
|
+
svg = svg.replace(/<!DOCTYPE[^[>]*(?:\[[\s\S]*?\]\s*)?>/gi, "");
|
|
143
|
+
svg = svg.replace(/<!ENTITY[\s\S]*?>/gi, "");
|
|
144
|
+
svg = svg.replace(/<!--[\s\S]*?-->/g, "");
|
|
145
|
+
svg = svg.replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "");
|
|
146
|
+
svg = svg.replace(/<\?(?!xml\b)[\s\S]*?\?>/gi, "");
|
|
147
|
+
svg = svg.replace(
|
|
148
|
+
/&([a-zA-Z_][\w.-]*);/g,
|
|
149
|
+
(full, name) => SAFE_NAMED_ENTITIES.has(name) ? full : ""
|
|
150
|
+
);
|
|
151
|
+
const tagPattern = FORBIDDEN_ELEMENTS.join("|");
|
|
152
|
+
const pairedRe = new RegExp(
|
|
153
|
+
`<\\s*(?:[a-zA-Z0-9_-]+:)?(?:${tagPattern})\\b[^>]*>[\\s\\S]*?<\\s*\\/\\s*(?:[a-zA-Z0-9_-]+:)?(?:${tagPattern})\\s*>`,
|
|
154
|
+
"gi"
|
|
155
|
+
);
|
|
156
|
+
const singleRe = new RegExp(
|
|
157
|
+
`<\\s*\\/?\\s*(?:[a-zA-Z0-9_-]+:)?(?:${tagPattern})\\b[^>]*\\/?>`,
|
|
158
|
+
"gi"
|
|
159
|
+
);
|
|
160
|
+
const htmlNamespaceRe = /<\s*\/?\s*(?:xhtml|html):[a-zA-Z0-9_-]+\b[^>]*\/?>/gi;
|
|
161
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
162
|
+
const prev = svg;
|
|
163
|
+
svg = svg.replace(pairedRe, "").replace(singleRe, "").replace(htmlNamespaceRe, "");
|
|
164
|
+
if (svg === prev) break;
|
|
165
|
+
}
|
|
166
|
+
svg = svg.replace(
|
|
167
|
+
/(<\s*style\b[^>]*>)([\s\S]*?)(<\s*\/\s*style\s*>)/gi,
|
|
168
|
+
(_m, open, body, close) => `${open}${sanitizeCssBlock(body)}${close}`
|
|
169
|
+
);
|
|
170
|
+
svg = svg.replace(
|
|
171
|
+
/<([a-zA-Z_:][\w:.-]*)(\s[^<>]*?)?(\s*\/?)>/g,
|
|
172
|
+
(_full, tagName, attrs, selfClose) => {
|
|
173
|
+
const cleanAttrs = attrs ? sanitizeTagAttributes(attrs) : "";
|
|
174
|
+
return `<${tagName}${cleanAttrs}${selfClose}>`;
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
if (!/<svg[\s>/]/i.test(svg)) {
|
|
178
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(
|
|
179
|
+
"Invalid SVG: missing root <svg> element after sanitization"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (pairedRe.test(svg) || singleRe.test(svg) || /<!(?:DOCTYPE|ENTITY)\b/i.test(svg) || /\bon[a-z]+\s*=/i.test(svg) || /(?:javascript|vbscript)\s*:/i.test(svg)) {
|
|
183
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(
|
|
184
|
+
"Invalid SVG: unsafe content remained after sanitization"
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return Buffer.from(svg.trim(), "utf8");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/server/optimization/loadSharp.ts
|
|
191
|
+
var SHARP_MISSING_MESSAGE = "img-upload-kit/server requires `sharp` \u2014 run `npm i sharp`";
|
|
192
|
+
var sharpPromise = null;
|
|
193
|
+
function unwrapDefault(mod) {
|
|
194
|
+
const record = mod;
|
|
195
|
+
return record && record.default !== void 0 ? record.default : mod;
|
|
196
|
+
}
|
|
197
|
+
function loadSharp() {
|
|
198
|
+
if (!sharpPromise) {
|
|
199
|
+
sharpPromise = Promise.resolve().then(() => _interopRequireWildcard(require("sharp"))).then(
|
|
200
|
+
(mod) => unwrapDefault(mod),
|
|
201
|
+
(error) => {
|
|
202
|
+
sharpPromise = null;
|
|
203
|
+
throw new Error(SHARP_MISSING_MESSAGE, { cause: error });
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return sharpPromise;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/server/optimization/imageOptimizer.ts
|
|
211
|
+
var PASSTHROUGH_FORMATS = /* @__PURE__ */ new Set(["jpeg", "png", "webp", "avif"]);
|
|
212
|
+
function extractSvgDimensions(svgText, metaWidth, metaHeight) {
|
|
213
|
+
let width = metaWidth;
|
|
214
|
+
let height = metaHeight;
|
|
215
|
+
if (!width || !height) {
|
|
216
|
+
const svgTagMatch = svgText.match(/<svg\b[^>]*>/i);
|
|
217
|
+
if (svgTagMatch) {
|
|
218
|
+
const tag = svgTagMatch[0];
|
|
219
|
+
const wMatch = tag.match(/\bwidth\s*=\s*["']\s*([\d.]+)(?:px)?\s*["']/i);
|
|
220
|
+
const hMatch = tag.match(/\bheight\s*=\s*["']\s*([\d.]+)(?:px)?\s*["']/i);
|
|
221
|
+
if (!width && wMatch) width = Math.round(parseFloat(wMatch[1]));
|
|
222
|
+
if (!height && hMatch) height = Math.round(parseFloat(hMatch[1]));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!width || !height) {
|
|
226
|
+
const viewBoxMatch = svgText.match(
|
|
227
|
+
/viewBox\s*=\s*["']\s*([\d.-]+)[\s,]+([\d.-]+)[\s,]+([\d.-]+)[\s,]+([\d.-]+)\s*["']/i
|
|
228
|
+
);
|
|
229
|
+
if (viewBoxMatch) {
|
|
230
|
+
if (!width) width = Math.round(parseFloat(viewBoxMatch[3]));
|
|
231
|
+
if (!height) height = Math.round(parseFloat(viewBoxMatch[4]));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
width: width && width > 0 ? width : 600,
|
|
236
|
+
height: height && height > 0 ? height : 600
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
async function optimizeImage(buffer, options = {}) {
|
|
240
|
+
const sharp = await loadSharp();
|
|
241
|
+
const maxDimensions = options.maxDimensions || _chunkNEDBIIE3cjs.DEFAULT_IMAGE_DIMENSIONS;
|
|
242
|
+
const { outputFormat, quality } = options;
|
|
243
|
+
const limitInputPixels = _nullishCoalesce(options.limitInputPixels, () => ( DEFAULT_LIMIT_INPUT_PIXELS));
|
|
244
|
+
const primaryEffort = _nullishCoalesce(options.effort, () => ( PRIMARY_AVIF_EFFORT));
|
|
245
|
+
const retryEffort = options.effort !== void 0 ? Math.max(1, options.effort - 1) : RETRY_AVIF_EFFORT;
|
|
246
|
+
let pipeline = sharp(buffer, {
|
|
247
|
+
failOn: "error",
|
|
248
|
+
limitInputPixels
|
|
249
|
+
}).rotate();
|
|
250
|
+
let metadata;
|
|
251
|
+
try {
|
|
252
|
+
metadata = await pipeline.metadata();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
255
|
+
if (msg.toLowerCase().includes("pixel limit") || msg.toLowerCase().includes("exceeds") || msg.toLowerCase().includes("unsupported image format")) {
|
|
256
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(msg);
|
|
257
|
+
}
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
const originalFormat = metadata.format;
|
|
261
|
+
if (originalFormat === "svg") {
|
|
262
|
+
const sanitizedBuffer = sanitizeSvg(buffer);
|
|
263
|
+
const svgText = sanitizedBuffer.toString("utf8");
|
|
264
|
+
const dims = extractSvgDimensions(svgText, metadata.width, metadata.height);
|
|
265
|
+
return {
|
|
266
|
+
buffer: sanitizedBuffer,
|
|
267
|
+
width: dims.width,
|
|
268
|
+
height: dims.height,
|
|
269
|
+
format: "svg",
|
|
270
|
+
size: sanitizedBuffer.length,
|
|
271
|
+
...quality !== void 0 ? { quality } : {}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const isPhoto = originalFormat === "jpeg" || Boolean(metadata.density && metadata.density > 150);
|
|
275
|
+
let baseQuality = _nullishCoalesce(quality, () => ( (isPhoto ? 85 : 88)));
|
|
276
|
+
baseQuality = Math.max(1, Math.min(100, baseQuality));
|
|
277
|
+
pipeline = pipeline.resize(maxDimensions.width, maxDimensions.height, {
|
|
278
|
+
fit: "inside",
|
|
279
|
+
withoutEnlargement: true,
|
|
280
|
+
kernel: "lanczos3"
|
|
281
|
+
});
|
|
282
|
+
if (outputFormat === "jpeg" || outputFormat === "png" || outputFormat === "webp") {
|
|
283
|
+
let cloned = pipeline.clone();
|
|
284
|
+
if (outputFormat === "jpeg") {
|
|
285
|
+
cloned = cloned.jpeg({ quality: baseQuality, mozjpeg: true });
|
|
286
|
+
} else if (outputFormat === "png") {
|
|
287
|
+
cloned = cloned.png();
|
|
288
|
+
} else if (outputFormat === "webp") {
|
|
289
|
+
cloned = cloned.webp({ quality: baseQuality, effort: 4 });
|
|
290
|
+
}
|
|
291
|
+
const outputBuffer = await cloned.toBuffer({ resolveWithObject: true });
|
|
292
|
+
return {
|
|
293
|
+
buffer: outputBuffer.data,
|
|
294
|
+
width: outputBuffer.info.width || maxDimensions.width,
|
|
295
|
+
height: outputBuffer.info.height || maxDimensions.height,
|
|
296
|
+
format: outputFormat,
|
|
297
|
+
size: outputBuffer.info.size,
|
|
298
|
+
quality: baseQuality
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const avifBuffer = await pipeline.clone().avif({
|
|
302
|
+
quality: baseQuality,
|
|
303
|
+
effort: primaryEffort,
|
|
304
|
+
chromaSubsampling: "4:2:0",
|
|
305
|
+
bitdepth: 8
|
|
306
|
+
}).toBuffer({ resolveWithObject: true });
|
|
307
|
+
const avifSize = avifBuffer.info.size;
|
|
308
|
+
const originalSize = buffer.length;
|
|
309
|
+
if (avifSize <= originalSize || outputFormat === "avif") {
|
|
310
|
+
return {
|
|
311
|
+
buffer: avifBuffer.data,
|
|
312
|
+
width: avifBuffer.info.width || maxDimensions.width,
|
|
313
|
+
height: avifBuffer.info.height || maxDimensions.height,
|
|
314
|
+
format: "avif",
|
|
315
|
+
size: avifSize,
|
|
316
|
+
quality: baseQuality
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const scaledQuality = Math.max(
|
|
320
|
+
40,
|
|
321
|
+
Math.round(baseQuality * Math.sqrt(originalSize / avifSize))
|
|
322
|
+
);
|
|
323
|
+
const avifRetry = await pipeline.clone().avif({
|
|
324
|
+
quality: scaledQuality,
|
|
325
|
+
effort: retryEffort,
|
|
326
|
+
chromaSubsampling: "4:2:0",
|
|
327
|
+
bitdepth: 8
|
|
328
|
+
}).toBuffer({ resolveWithObject: true });
|
|
329
|
+
if (avifRetry.info.size <= originalSize) {
|
|
330
|
+
return {
|
|
331
|
+
buffer: avifRetry.data,
|
|
332
|
+
width: avifRetry.info.width || maxDimensions.width,
|
|
333
|
+
height: avifRetry.info.height || maxDimensions.height,
|
|
334
|
+
format: "avif",
|
|
335
|
+
size: avifRetry.info.size,
|
|
336
|
+
quality: scaledQuality
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const isRotated90 = typeof metadata.orientation === "number" && metadata.orientation >= 5 && metadata.orientation <= 8;
|
|
340
|
+
const orientedWidth = isRotated90 ? metadata.height : metadata.width;
|
|
341
|
+
const orientedHeight = isRotated90 ? metadata.width : metadata.height;
|
|
342
|
+
const wasDimensionCapped = (_nullishCoalesce(orientedWidth, () => ( 0))) > maxDimensions.width || (_nullishCoalesce(orientedHeight, () => ( 0))) > maxDimensions.height;
|
|
343
|
+
if (!wasDimensionCapped && PASSTHROUGH_FORMATS.has(_nullishCoalesce(originalFormat, () => ( "")))) {
|
|
344
|
+
const srcFmt = originalFormat;
|
|
345
|
+
let passthrough = pipeline.clone();
|
|
346
|
+
if (srcFmt === "jpeg") {
|
|
347
|
+
passthrough = passthrough.jpeg({ quality: baseQuality, mozjpeg: true });
|
|
348
|
+
} else if (srcFmt === "png") {
|
|
349
|
+
passthrough = passthrough.png({ compressionLevel: 9 });
|
|
350
|
+
} else if (srcFmt === "webp") {
|
|
351
|
+
passthrough = passthrough.webp({ quality: baseQuality, effort: 4 });
|
|
352
|
+
} else {
|
|
353
|
+
passthrough = passthrough.avif({
|
|
354
|
+
quality: baseQuality,
|
|
355
|
+
effort: retryEffort
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
let passthroughOut = await passthrough.toBuffer({ resolveWithObject: true });
|
|
359
|
+
if (srcFmt === "jpeg" && passthroughOut.info.size > originalSize) {
|
|
360
|
+
passthroughOut = await pipeline.clone().jpeg({ quality: 10 }).toBuffer({ resolveWithObject: true });
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
buffer: passthroughOut.data,
|
|
364
|
+
width: passthroughOut.info.width || orientedWidth || maxDimensions.width,
|
|
365
|
+
height: passthroughOut.info.height || orientedHeight || maxDimensions.height,
|
|
366
|
+
format: srcFmt,
|
|
367
|
+
size: passthroughOut.info.size,
|
|
368
|
+
quality: baseQuality
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const webpBuffer = await pipeline.clone().webp({ quality: baseQuality, effort: 6, smartSubsample: true }).toBuffer({ resolveWithObject: true });
|
|
372
|
+
const useWebP = webpBuffer.info.size <= avifRetry.info.size;
|
|
373
|
+
const winner = useWebP ? {
|
|
374
|
+
buf: webpBuffer.data,
|
|
375
|
+
fmt: "webp",
|
|
376
|
+
sz: webpBuffer.info.size,
|
|
377
|
+
w: webpBuffer.info.width,
|
|
378
|
+
h: webpBuffer.info.height,
|
|
379
|
+
q: baseQuality
|
|
380
|
+
} : {
|
|
381
|
+
buf: avifRetry.data,
|
|
382
|
+
fmt: "avif",
|
|
383
|
+
sz: avifRetry.info.size,
|
|
384
|
+
w: avifRetry.info.width,
|
|
385
|
+
h: avifRetry.info.height,
|
|
386
|
+
q: scaledQuality
|
|
387
|
+
};
|
|
388
|
+
return {
|
|
389
|
+
buffer: winner.buf,
|
|
390
|
+
width: winner.w || maxDimensions.width,
|
|
391
|
+
height: winner.h || maxDimensions.height,
|
|
392
|
+
format: winner.fmt,
|
|
393
|
+
size: winner.sz,
|
|
394
|
+
quality: winner.q
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/server/optimization/blurGenerator.ts
|
|
399
|
+
var _blurhash = require('blurhash');
|
|
400
|
+
|
|
401
|
+
// src/server/optimization/colorHistogram.ts
|
|
402
|
+
async function extractEnhancedColorFromBuffer(buffer, mode = "balanced") {
|
|
403
|
+
const sharp = await loadSharp();
|
|
404
|
+
try {
|
|
405
|
+
const metadata = await sharp(buffer).metadata();
|
|
406
|
+
const maxSize = 400;
|
|
407
|
+
const needsResize = metadata.width && metadata.height && (metadata.width > maxSize || metadata.height > maxSize);
|
|
408
|
+
if (needsResize) {
|
|
409
|
+
const resized = await sharp(buffer).resize(maxSize, maxSize, {
|
|
410
|
+
fit: "inside",
|
|
411
|
+
kernel: "lanczos3",
|
|
412
|
+
withoutEnlargement: true
|
|
413
|
+
}).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
414
|
+
return extractDominantColorHistogramAdvanced(
|
|
415
|
+
resized.data,
|
|
416
|
+
resized.info.width,
|
|
417
|
+
resized.info.height,
|
|
418
|
+
mode
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
const raw = await sharp(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
422
|
+
return extractDominantColorHistogramAdvanced(
|
|
423
|
+
raw.data,
|
|
424
|
+
raw.info.width,
|
|
425
|
+
raw.info.height,
|
|
426
|
+
mode
|
|
427
|
+
);
|
|
428
|
+
} catch (e2) {
|
|
429
|
+
const stats = await sharp(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
430
|
+
return extractDominantColorHistogram(stats.data);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function extractDominantColorHistogramAdvanced(data, width, height, mode) {
|
|
434
|
+
const colorMap = /* @__PURE__ */ new Map();
|
|
435
|
+
const bucketSize = 16;
|
|
436
|
+
const centerWeight = 1.5;
|
|
437
|
+
const centerX = width / 2 || 1;
|
|
438
|
+
const centerY = height / 2 || 1;
|
|
439
|
+
let whiteWeight = 0;
|
|
440
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
441
|
+
const alpha = data[i + 3];
|
|
442
|
+
if (alpha !== void 0 && alpha < 8) continue;
|
|
443
|
+
const r2 = data[i];
|
|
444
|
+
const g2 = data[i + 1];
|
|
445
|
+
const b2 = data[i + 2];
|
|
446
|
+
const brightness = (r2 + g2 + b2) / 3;
|
|
447
|
+
if (brightness < 10) continue;
|
|
448
|
+
const pixelIndex = i / 4;
|
|
449
|
+
const x = pixelIndex % width;
|
|
450
|
+
const y = Math.floor(pixelIndex / width);
|
|
451
|
+
const dx = (x - centerX) / centerX;
|
|
452
|
+
const dy = (y - centerY) / centerY;
|
|
453
|
+
const distanceFromCenter = Math.sqrt(dx * dx + dy * dy);
|
|
454
|
+
let weight = 1;
|
|
455
|
+
if (mode === "edge") {
|
|
456
|
+
const edgeThicknessX = Math.max(1, Math.floor(width * 0.12));
|
|
457
|
+
const edgeThicknessY = Math.max(1, Math.floor(height * 0.12));
|
|
458
|
+
const onLeftEdge = x < edgeThicknessX;
|
|
459
|
+
const onRightEdge = x >= width - edgeThicknessX;
|
|
460
|
+
const onTopEdge = y < edgeThicknessY;
|
|
461
|
+
const onBottomEdge = y >= height - edgeThicknessY;
|
|
462
|
+
if (!(onLeftEdge || onRightEdge || onTopEdge || onBottomEdge)) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const distanceToNearestEdge = Math.min(
|
|
466
|
+
x,
|
|
467
|
+
y,
|
|
468
|
+
width - 1 - x,
|
|
469
|
+
height - 1 - y
|
|
470
|
+
);
|
|
471
|
+
const maxEdgeDistance = Math.max(edgeThicknessX, edgeThicknessY) || 1;
|
|
472
|
+
const edgeFactor = 1 - distanceToNearestEdge / maxEdgeDistance;
|
|
473
|
+
weight = 1 + edgeFactor * 3;
|
|
474
|
+
} else {
|
|
475
|
+
weight = distanceFromCenter < 0.5 ? centerWeight : 1;
|
|
476
|
+
}
|
|
477
|
+
const isNearWhite = r2 >= 245 && g2 >= 245 && b2 >= 245;
|
|
478
|
+
if (isNearWhite) {
|
|
479
|
+
whiteWeight += weight;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const bucketR = Math.floor(r2 / bucketSize) * bucketSize;
|
|
483
|
+
const bucketG = Math.floor(g2 / bucketSize) * bucketSize;
|
|
484
|
+
const bucketB = Math.floor(b2 / bucketSize) * bucketSize;
|
|
485
|
+
const key = `${bucketR},${bucketG},${bucketB}`;
|
|
486
|
+
colorMap.set(key, (colorMap.get(key) || 0) + weight);
|
|
487
|
+
}
|
|
488
|
+
if (!colorMap.size) {
|
|
489
|
+
return whiteWeight > 0 ? EDGE_WHITE_FALLBACK : EDGE_COLOR_FALLBACK;
|
|
490
|
+
}
|
|
491
|
+
let maxCount = 0;
|
|
492
|
+
let dominantColor = "64,64,80";
|
|
493
|
+
let totalColorWeight = 0;
|
|
494
|
+
colorMap.forEach((count, color) => {
|
|
495
|
+
totalColorWeight += count;
|
|
496
|
+
if (count > maxCount) {
|
|
497
|
+
maxCount = count;
|
|
498
|
+
dominantColor = color;
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
const whiteDominates = whiteWeight > 0 && whiteWeight >= totalColorWeight * WHITE_DOMINANCE_RATIO;
|
|
502
|
+
if (whiteDominates) {
|
|
503
|
+
return EDGE_WHITE_FALLBACK;
|
|
504
|
+
}
|
|
505
|
+
const [r, g, b] = dominantColor.split(",").map(Number);
|
|
506
|
+
return _chunkNEDBIIE3cjs.rgbToHex.call(void 0, r, g, b);
|
|
507
|
+
}
|
|
508
|
+
function extractDominantColorHistogram(data) {
|
|
509
|
+
const colorMap = /* @__PURE__ */ new Map();
|
|
510
|
+
const bucketSize = 16;
|
|
511
|
+
let whiteWeight = 0;
|
|
512
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
513
|
+
const alpha = data[i + 3];
|
|
514
|
+
if (alpha !== void 0 && alpha < 8) continue;
|
|
515
|
+
const r2 = Math.floor(data[i] / bucketSize) * bucketSize;
|
|
516
|
+
const g2 = Math.floor(data[i + 1] / bucketSize) * bucketSize;
|
|
517
|
+
const b2 = Math.floor(data[i + 2] / bucketSize) * bucketSize;
|
|
518
|
+
if (r2 >= 240 && g2 >= 240 && b2 >= 240) {
|
|
519
|
+
whiteWeight += 1;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const key = `${r2},${g2},${b2}`;
|
|
523
|
+
colorMap.set(key, (colorMap.get(key) || 0) + 1);
|
|
524
|
+
}
|
|
525
|
+
if (!colorMap.size) {
|
|
526
|
+
return whiteWeight > 0 ? EDGE_WHITE_FALLBACK : EDGE_COLOR_FALLBACK;
|
|
527
|
+
}
|
|
528
|
+
let maxCount = 0;
|
|
529
|
+
let dominantColor = "64,64,80";
|
|
530
|
+
let totalColorWeight = 0;
|
|
531
|
+
colorMap.forEach((count, color) => {
|
|
532
|
+
totalColorWeight += count;
|
|
533
|
+
if (count > maxCount) {
|
|
534
|
+
maxCount = count;
|
|
535
|
+
dominantColor = color;
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
const whiteDominates = whiteWeight > 0 && whiteWeight >= totalColorWeight * WHITE_DOMINANCE_RATIO;
|
|
539
|
+
if (whiteDominates) {
|
|
540
|
+
return EDGE_WHITE_FALLBACK;
|
|
541
|
+
}
|
|
542
|
+
const [r, g, b] = dominantColor.split(",").map(Number);
|
|
543
|
+
return _chunkNEDBIIE3cjs.rgbToHex.call(void 0, r, g, b);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/server/optimization/edgeColorExtractor.ts
|
|
547
|
+
async function extractEdgeColorsFromBuffer(buffer) {
|
|
548
|
+
const sharp = await loadSharp();
|
|
549
|
+
try {
|
|
550
|
+
const sample = await sharp(buffer).resize(400, 400, {
|
|
551
|
+
fit: "inside",
|
|
552
|
+
kernel: "lanczos3",
|
|
553
|
+
withoutEnlargement: true
|
|
554
|
+
}).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
555
|
+
return computeEdgeColorSet(
|
|
556
|
+
sample.data,
|
|
557
|
+
sample.info.width,
|
|
558
|
+
sample.info.height
|
|
559
|
+
);
|
|
560
|
+
} catch (e3) {
|
|
561
|
+
try {
|
|
562
|
+
const fallback = await extractEnhancedColorFromBuffer(buffer, "edge");
|
|
563
|
+
return {
|
|
564
|
+
top: fallback,
|
|
565
|
+
right: fallback,
|
|
566
|
+
bottom: fallback,
|
|
567
|
+
left: fallback
|
|
568
|
+
};
|
|
569
|
+
} catch (e4) {
|
|
570
|
+
return {
|
|
571
|
+
top: EDGE_COLOR_FALLBACK,
|
|
572
|
+
right: EDGE_COLOR_FALLBACK,
|
|
573
|
+
bottom: EDGE_COLOR_FALLBACK,
|
|
574
|
+
left: EDGE_COLOR_FALLBACK
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function computeEdgeColorSet(data, width, height) {
|
|
580
|
+
const thicknessX = Math.max(2, Math.floor(width * 0.12));
|
|
581
|
+
const thicknessY = Math.max(2, Math.floor(height * 0.12));
|
|
582
|
+
const bucketSize = 16;
|
|
583
|
+
const edgeMaps = {
|
|
584
|
+
top: /* @__PURE__ */ new Map(),
|
|
585
|
+
right: /* @__PURE__ */ new Map(),
|
|
586
|
+
bottom: /* @__PURE__ */ new Map(),
|
|
587
|
+
left: /* @__PURE__ */ new Map()
|
|
588
|
+
};
|
|
589
|
+
const whiteEdgeStrength = {
|
|
590
|
+
top: 0,
|
|
591
|
+
right: 0,
|
|
592
|
+
bottom: 0,
|
|
593
|
+
left: 0
|
|
594
|
+
};
|
|
595
|
+
const accumulateEdgeSample = (edge, weight, key, isWhiteSample) => {
|
|
596
|
+
if (weight <= 0) return;
|
|
597
|
+
if (isWhiteSample) {
|
|
598
|
+
whiteEdgeStrength[edge] += weight;
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
edgeMaps[edge].set(key, (edgeMaps[edge].get(key) || 0) + weight);
|
|
602
|
+
};
|
|
603
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
604
|
+
const r = data[i];
|
|
605
|
+
const g = data[i + 1];
|
|
606
|
+
const b = data[i + 2];
|
|
607
|
+
const alpha = data[i + 3];
|
|
608
|
+
if (alpha !== void 0 && alpha < 8) continue;
|
|
609
|
+
const brightness = (r + g + b) / 3;
|
|
610
|
+
if (brightness < 12) continue;
|
|
611
|
+
const isNearWhite = r >= 245 && g >= 245 && b >= 245;
|
|
612
|
+
const pixelIndex = i / 4;
|
|
613
|
+
const x = pixelIndex % width;
|
|
614
|
+
const y = Math.floor(pixelIndex / width);
|
|
615
|
+
const bucketR = Math.floor(r / bucketSize) * bucketSize;
|
|
616
|
+
const bucketG = Math.floor(g / bucketSize) * bucketSize;
|
|
617
|
+
const bucketB = Math.floor(b / bucketSize) * bucketSize;
|
|
618
|
+
const key = `${bucketR},${bucketG},${bucketB}`;
|
|
619
|
+
const topWeight = y < thicknessY ? 1 + (thicknessY - y) / thicknessY : 0;
|
|
620
|
+
if (topWeight > 0) {
|
|
621
|
+
accumulateEdgeSample("top", topWeight, key, isNearWhite);
|
|
622
|
+
}
|
|
623
|
+
const bottomWeight = y >= height - thicknessY ? 1 + (y - (height - thicknessY)) / thicknessY : 0;
|
|
624
|
+
if (bottomWeight > 0) {
|
|
625
|
+
accumulateEdgeSample("bottom", bottomWeight, key, isNearWhite);
|
|
626
|
+
}
|
|
627
|
+
const leftWeight = x < thicknessX ? 1 + (thicknessX - x) / thicknessX : 0;
|
|
628
|
+
if (leftWeight > 0) {
|
|
629
|
+
accumulateEdgeSample("left", leftWeight, key, isNearWhite);
|
|
630
|
+
}
|
|
631
|
+
const rightWeight = x >= width - thicknessX ? 1 + (x - (width - thicknessX)) / thicknessX : 0;
|
|
632
|
+
if (rightWeight > 0) {
|
|
633
|
+
accumulateEdgeSample("right", rightWeight, key, isNearWhite);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
top: resolveEdgeColor(edgeMaps.top, whiteEdgeStrength.top),
|
|
638
|
+
right: resolveEdgeColor(edgeMaps.right, whiteEdgeStrength.right),
|
|
639
|
+
bottom: resolveEdgeColor(edgeMaps.bottom, whiteEdgeStrength.bottom),
|
|
640
|
+
left: resolveEdgeColor(edgeMaps.left, whiteEdgeStrength.left)
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function resolveEdgeColor(map, whiteStrength) {
|
|
644
|
+
if (!map.size) {
|
|
645
|
+
return whiteStrength > 0 ? EDGE_WHITE_FALLBACK : EDGE_COLOR_FALLBACK;
|
|
646
|
+
}
|
|
647
|
+
let dominantColor = "";
|
|
648
|
+
let maxCount = 0;
|
|
649
|
+
let totalColorWeight = 0;
|
|
650
|
+
map.forEach((count, color) => {
|
|
651
|
+
totalColorWeight += count;
|
|
652
|
+
if (count > maxCount) {
|
|
653
|
+
maxCount = count;
|
|
654
|
+
dominantColor = color;
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
if (!dominantColor) {
|
|
658
|
+
return whiteStrength > 0 ? EDGE_WHITE_FALLBACK : EDGE_COLOR_FALLBACK;
|
|
659
|
+
}
|
|
660
|
+
const whiteDominates = whiteStrength > 0 && (totalColorWeight === 0 || whiteStrength >= totalColorWeight * WHITE_DOMINANCE_RATIO);
|
|
661
|
+
if (whiteDominates) {
|
|
662
|
+
return EDGE_WHITE_FALLBACK;
|
|
663
|
+
}
|
|
664
|
+
if (dominantColor.startsWith("#")) {
|
|
665
|
+
return dominantColor.toUpperCase();
|
|
666
|
+
}
|
|
667
|
+
const [r, g, b] = dominantColor.split(",").map(Number);
|
|
668
|
+
return _chunkNEDBIIE3cjs.rgbToHex.call(void 0, r, g, b);
|
|
669
|
+
}
|
|
670
|
+
function blendEdgeColors(edgeColors) {
|
|
671
|
+
const colors = Object.values(edgeColors);
|
|
672
|
+
if (!colors.length) {
|
|
673
|
+
return EDGE_COLOR_FALLBACK;
|
|
674
|
+
}
|
|
675
|
+
const totals = colors.reduce(
|
|
676
|
+
(acc, hex) => {
|
|
677
|
+
const { r, g, b } = _chunkNEDBIIE3cjs.hexToRgb.call(void 0, hex || EDGE_COLOR_FALLBACK);
|
|
678
|
+
acc.r += r;
|
|
679
|
+
acc.g += g;
|
|
680
|
+
acc.b += b;
|
|
681
|
+
return acc;
|
|
682
|
+
},
|
|
683
|
+
{ r: 0, g: 0, b: 0 }
|
|
684
|
+
);
|
|
685
|
+
const count = colors.length;
|
|
686
|
+
return _chunkNEDBIIE3cjs.rgbToHex.call(void 0, totals.r / count, totals.g / count, totals.b / count);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/server/optimization/blurGenerator.ts
|
|
690
|
+
function encodeRawBlurHash(data, width, height, componentX = 4, componentY = 3) {
|
|
691
|
+
const clampedX = Math.max(1, Math.min(Math.round(componentX), width, 9));
|
|
692
|
+
const clampedY = Math.max(1, Math.min(Math.round(componentY), height, 9));
|
|
693
|
+
return _blurhash.encode.call(void 0,
|
|
694
|
+
new Uint8ClampedArray(data),
|
|
695
|
+
width,
|
|
696
|
+
height,
|
|
697
|
+
clampedX,
|
|
698
|
+
clampedY
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
async function encodeBlurHashString(buffer, componentX = 4, componentY = 3) {
|
|
702
|
+
const sharp = await loadSharp();
|
|
703
|
+
const { data, info } = await sharp(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
704
|
+
return encodeRawBlurHash(
|
|
705
|
+
data,
|
|
706
|
+
info.width,
|
|
707
|
+
info.height,
|
|
708
|
+
componentX,
|
|
709
|
+
componentY
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
async function generateBlurPlaceholder(buffer, options = {}) {
|
|
713
|
+
const targetWidth = _nullishCoalesce(options.width, () => ( BLUR_PLACEHOLDER_WIDTH));
|
|
714
|
+
const fallbackWidth = _nullishCoalesce(options.fallbackWidth, () => ( BLUR_PLACEHOLDER_FALLBACK_WIDTH));
|
|
715
|
+
const quality = _nullishCoalesce(options.quality, () => ( BLUR_PLACEHOLDER_QUALITY));
|
|
716
|
+
const maxChars = _nullishCoalesce(options.maxDataUrlChars, () => ( BLUR_PLACEHOLDER_MAX_CHARS));
|
|
717
|
+
const sharp = await loadSharp();
|
|
718
|
+
try {
|
|
719
|
+
let processBuffer = buffer;
|
|
720
|
+
const isSvg = /<svg[\s>]/i.test(buffer.toString("utf8", 0, 1024));
|
|
721
|
+
if (isSvg) {
|
|
722
|
+
const normalizedSvg = buffer.toString("utf8").replace(/<svg\b/gi, "<svg").replace(/<\/svg\s*>/gi, "</svg>");
|
|
723
|
+
processBuffer = Buffer.from(normalizedSvg, "utf8");
|
|
724
|
+
}
|
|
725
|
+
const baseSize = Math.max(targetWidth, 128);
|
|
726
|
+
const rawSample = await sharp(processBuffer).rotate().resize(baseSize, baseSize, { fit: "inside", withoutEnlargement: true }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
727
|
+
const rawInput = {
|
|
728
|
+
raw: {
|
|
729
|
+
width: rawSample.info.width,
|
|
730
|
+
height: rawSample.info.height,
|
|
731
|
+
channels: 4
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
const buildPlaceholderDataUrl = async (width) => {
|
|
735
|
+
const webpBuffer = await sharp(rawSample.data, rawInput).resize(width, width, { fit: "inside", withoutEnlargement: true }).webp({ quality, effort: 6, smartSubsample: true }).toBuffer();
|
|
736
|
+
return `data:image/webp;base64,${webpBuffer.toString("base64")}`;
|
|
737
|
+
};
|
|
738
|
+
let dataURL = await buildPlaceholderDataUrl(targetWidth);
|
|
739
|
+
if (dataURL.length > maxChars) {
|
|
740
|
+
dataURL = await buildPlaceholderDataUrl(fallbackWidth);
|
|
741
|
+
}
|
|
742
|
+
const edgeColors = computeEdgeColorSet(
|
|
743
|
+
rawSample.data,
|
|
744
|
+
rawSample.info.width,
|
|
745
|
+
rawSample.info.height
|
|
746
|
+
);
|
|
747
|
+
const color = blendEdgeColors(edgeColors);
|
|
748
|
+
let blurHash;
|
|
749
|
+
if (options.includeBlurHash) {
|
|
750
|
+
try {
|
|
751
|
+
const hashSample = rawSample.info.width <= 32 && rawSample.info.height <= 32 ? rawSample : await sharp(rawSample.data, rawInput).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
752
|
+
blurHash = encodeRawBlurHash(
|
|
753
|
+
hashSample.data,
|
|
754
|
+
hashSample.info.width,
|
|
755
|
+
hashSample.info.height
|
|
756
|
+
);
|
|
757
|
+
} catch (e5) {
|
|
758
|
+
blurHash = void 0;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
dataURL,
|
|
763
|
+
color,
|
|
764
|
+
...blurHash && { blurHash }
|
|
765
|
+
};
|
|
766
|
+
} catch (e6) {
|
|
767
|
+
return generateFallbackBlur(buffer, options.includeBlurHash);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
var generateBlurHash = generateBlurPlaceholder;
|
|
771
|
+
async function generateFallbackBlur(buffer, includeBlurHash) {
|
|
772
|
+
const sharp = await loadSharp();
|
|
773
|
+
const tinyBuffer = await sharp(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).blur(1).webp({
|
|
774
|
+
quality: 75,
|
|
775
|
+
effort: 6,
|
|
776
|
+
smartSubsample: false
|
|
777
|
+
}).toBuffer();
|
|
778
|
+
const dataURL = `data:image/webp;base64,${tinyBuffer.toString("base64")}`;
|
|
779
|
+
const edgeColors = await extractEdgeColorsFromBuffer(buffer);
|
|
780
|
+
const color = blendEdgeColors(edgeColors);
|
|
781
|
+
let blurHash;
|
|
782
|
+
if (includeBlurHash) {
|
|
783
|
+
try {
|
|
784
|
+
blurHash = await encodeBlurHashString(tinyBuffer);
|
|
785
|
+
} catch (e7) {
|
|
786
|
+
blurHash = void 0;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return { dataURL, color, ...blurHash && { blurHash } };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// src/server/security/sniffFormat.ts
|
|
793
|
+
var AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
|
|
794
|
+
var HEIF_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "hevc", "hevx", "mif1", "msf1"]);
|
|
795
|
+
function readAscii(bytes, start, end) {
|
|
796
|
+
let out = "";
|
|
797
|
+
const limit = Math.min(bytes.length, end);
|
|
798
|
+
for (let i = start; i < limit; i++) {
|
|
799
|
+
out += String.fromCharCode(bytes[i]);
|
|
800
|
+
}
|
|
801
|
+
return out;
|
|
802
|
+
}
|
|
803
|
+
function sniffIsoBmff(bytes) {
|
|
804
|
+
if (bytes.length < 12) return null;
|
|
805
|
+
if (readAscii(bytes, 4, 8) !== "ftyp") return null;
|
|
806
|
+
const boxSize = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
|
|
807
|
+
const maxOffset = Math.min(
|
|
808
|
+
bytes.length,
|
|
809
|
+
boxSize >= 16 ? Math.min(boxSize, 128) : 64
|
|
810
|
+
);
|
|
811
|
+
const majorBrand = readAscii(bytes, 8, 12).toLowerCase();
|
|
812
|
+
if (AVIF_BRANDS.has(majorBrand)) {
|
|
813
|
+
return { format: "avif", mime: "image/avif" };
|
|
814
|
+
}
|
|
815
|
+
const brands = [majorBrand];
|
|
816
|
+
for (let offset = 16; offset + 4 <= maxOffset; offset += 4) {
|
|
817
|
+
brands.push(readAscii(bytes, offset, offset + 4).toLowerCase());
|
|
818
|
+
}
|
|
819
|
+
if (brands.some((b) => AVIF_BRANDS.has(b))) {
|
|
820
|
+
return { format: "avif", mime: "image/avif" };
|
|
821
|
+
}
|
|
822
|
+
if (brands.some((b) => HEIF_BRANDS.has(b))) {
|
|
823
|
+
return { format: "heif", mime: "image/heif" };
|
|
824
|
+
}
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
function sniffTextSvg(bytes) {
|
|
828
|
+
const sampleLength = Math.min(bytes.length, 4096);
|
|
829
|
+
for (let i = 0; i < Math.min(sampleLength, 512); i++) {
|
|
830
|
+
if (bytes[i] === 0) return null;
|
|
831
|
+
}
|
|
832
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(0, sampleLength)).replace(/^\uFEFF/, "").trimStart();
|
|
833
|
+
if (!text.startsWith("<")) return null;
|
|
834
|
+
const stripped = text.replace(/^<\?xml[\s\S]*?\?>\s*/i, "").replace(/<!--[\s\S]*?-->\s*/g, "").replace(/<!DOCTYPE\s+svg[\s\S]*?(?:\]>|>)\s*/i, "").replace(/<!--[\s\S]*?-->\s*/g, "").trimStart();
|
|
835
|
+
if (/^<svg[\s>/]/i.test(stripped) || /<svg[\s>/]/i.test(text)) {
|
|
836
|
+
if (/^<!doctype\s+html\b/i.test(text) || /^<html[\s>]/i.test(stripped)) {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
return { format: "svg", mime: "image/svg+xml" };
|
|
840
|
+
}
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
function sniffImageFormat(input) {
|
|
844
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
845
|
+
if (bytes.length < 4) return null;
|
|
846
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
|
|
847
|
+
return { format: "jpeg", mime: "image/jpeg" };
|
|
848
|
+
}
|
|
849
|
+
if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) {
|
|
850
|
+
return { format: "png", mime: "image/png" };
|
|
851
|
+
}
|
|
852
|
+
if (bytes.length >= 6) {
|
|
853
|
+
const header = readAscii(bytes, 0, 6);
|
|
854
|
+
if (header === "GIF87a" || header === "GIF89a") {
|
|
855
|
+
return { format: "gif", mime: "image/gif" };
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (bytes.length >= 12 && readAscii(bytes, 0, 4) === "RIFF" && readAscii(bytes, 8, 12) === "WEBP") {
|
|
859
|
+
return { format: "webp", mime: "image/webp" };
|
|
860
|
+
}
|
|
861
|
+
const bmff = sniffIsoBmff(bytes);
|
|
862
|
+
if (bmff) return bmff;
|
|
863
|
+
if (bytes[0] === 73 && bytes[1] === 73 && bytes[2] === 42 && bytes[3] === 0 || bytes[0] === 77 && bytes[1] === 77 && bytes[2] === 0 && bytes[3] === 42) {
|
|
864
|
+
return { format: "tiff", mime: "image/tiff" };
|
|
865
|
+
}
|
|
866
|
+
if (bytes.length >= 14 && bytes[0] === 66 && bytes[1] === 77) {
|
|
867
|
+
return { format: "bmp", mime: "image/bmp" };
|
|
868
|
+
}
|
|
869
|
+
if (bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 1 && bytes[3] === 0) {
|
|
870
|
+
return { format: "ico", mime: "image/x-icon" };
|
|
871
|
+
}
|
|
872
|
+
return sniffTextSvg(bytes);
|
|
873
|
+
}
|
|
874
|
+
function assertMagicBytesAllowed(buffer, allowedFormats, allowSvg = false) {
|
|
875
|
+
const sniffed = sniffImageFormat(buffer);
|
|
876
|
+
if (!sniffed) {
|
|
877
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(
|
|
878
|
+
"Unsupported image format: file content is not a recognized image"
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (sniffed.format === "svg" && !allowSvg) {
|
|
882
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(
|
|
883
|
+
"File type image/svg+xml is not allowed unless allowSvg: true is set"
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
if (allowedFormats.length > 0 && !allowedFormats.includes(sniffed.mime)) {
|
|
887
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(
|
|
888
|
+
`File type ${sniffed.mime} is not allowed. Allowed: ${allowedFormats.join(", ")}`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
return sniffed;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/server/semaphore.ts
|
|
895
|
+
var SemaphoreQueueFullError = (_class = class extends Error {
|
|
896
|
+
__init() {this.status = 503}
|
|
897
|
+
constructor(message = "Server upload queue is full. Please try again later.") {
|
|
898
|
+
super(message);_class.prototype.__init.call(this);;
|
|
899
|
+
this.name = "SemaphoreQueueFullError";
|
|
900
|
+
}
|
|
901
|
+
}, _class);
|
|
902
|
+
function createAbortError(signal) {
|
|
903
|
+
if (_optionalChain([signal, 'optionalAccess', _2 => _2.reason]) instanceof Error) {
|
|
904
|
+
return signal.reason;
|
|
905
|
+
}
|
|
906
|
+
const err = new Error(
|
|
907
|
+
typeof _optionalChain([signal, 'optionalAccess', _3 => _3.reason]) === "string" ? signal.reason : "Operation aborted"
|
|
908
|
+
);
|
|
909
|
+
err.name = "AbortError";
|
|
910
|
+
return err;
|
|
911
|
+
}
|
|
912
|
+
var AsyncSemaphore = (_class2 = class {
|
|
913
|
+
constructor(concurrency, options) {;_class2.prototype.__init2.call(this);_class2.prototype.__init3.call(this);
|
|
914
|
+
this.concurrency = concurrency;
|
|
915
|
+
if (concurrency < 1) {
|
|
916
|
+
throw new Error("AsyncSemaphore concurrency must be >= 1");
|
|
917
|
+
}
|
|
918
|
+
if (typeof options === "number") {
|
|
919
|
+
this.maxQueue = options;
|
|
920
|
+
} else if (options && typeof options === "object") {
|
|
921
|
+
this.maxQueue = options.maxQueue;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
__init2() {this.activeCount = 0}
|
|
926
|
+
|
|
927
|
+
__init3() {this.queue = []}
|
|
928
|
+
get active() {
|
|
929
|
+
return this.activeCount;
|
|
930
|
+
}
|
|
931
|
+
get pending() {
|
|
932
|
+
return this.queue.length;
|
|
933
|
+
}
|
|
934
|
+
async run(task, options = {}) {
|
|
935
|
+
const { signal, timeoutMs, label = "Semaphore task" } = options;
|
|
936
|
+
await this.acquire(signal);
|
|
937
|
+
let rawTaskPromise;
|
|
938
|
+
try {
|
|
939
|
+
rawTaskPromise = Promise.resolve().then(task);
|
|
940
|
+
} catch (syncError) {
|
|
941
|
+
this.release();
|
|
942
|
+
throw syncError;
|
|
943
|
+
}
|
|
944
|
+
let released = false;
|
|
945
|
+
const releaseOnce = () => {
|
|
946
|
+
if (!released) {
|
|
947
|
+
released = true;
|
|
948
|
+
this.release();
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
void rawTaskPromise.then(
|
|
952
|
+
() => releaseOnce(),
|
|
953
|
+
(err) => {
|
|
954
|
+
const pending = err && typeof err === "object" && "pendingTask" in err ? err.pendingTask : void 0;
|
|
955
|
+
if (pending && typeof pending.then === "function") {
|
|
956
|
+
void pending.then(releaseOnce, releaseOnce);
|
|
957
|
+
} else {
|
|
958
|
+
releaseOnce();
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
);
|
|
962
|
+
let callerPromise = rawTaskPromise;
|
|
963
|
+
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
964
|
+
callerPromise = withTimeout(callerPromise, timeoutMs, label);
|
|
965
|
+
}
|
|
966
|
+
if (signal) {
|
|
967
|
+
callerPromise = raceWithAbort(callerPromise, signal);
|
|
968
|
+
}
|
|
969
|
+
return callerPromise;
|
|
970
|
+
}
|
|
971
|
+
acquire(signal) {
|
|
972
|
+
if (_optionalChain([signal, 'optionalAccess', _4 => _4.aborted])) {
|
|
973
|
+
return Promise.reject(createAbortError(signal));
|
|
974
|
+
}
|
|
975
|
+
if (this.activeCount < this.concurrency) {
|
|
976
|
+
this.activeCount += 1;
|
|
977
|
+
return Promise.resolve();
|
|
978
|
+
}
|
|
979
|
+
if (this.maxQueue !== void 0 && this.queue.length >= this.maxQueue) {
|
|
980
|
+
return Promise.reject(new SemaphoreQueueFullError());
|
|
981
|
+
}
|
|
982
|
+
return new Promise((resolve, reject) => {
|
|
983
|
+
const onAbort = () => {
|
|
984
|
+
const idx = this.queue.indexOf(entry);
|
|
985
|
+
if (idx !== -1) {
|
|
986
|
+
this.queue.splice(idx, 1);
|
|
987
|
+
}
|
|
988
|
+
entry.cancel(createAbortError(signal));
|
|
989
|
+
};
|
|
990
|
+
const entry = {
|
|
991
|
+
grant: () => {
|
|
992
|
+
_optionalChain([signal, 'optionalAccess', _5 => _5.removeEventListener, 'call', _6 => _6("abort", onAbort)]);
|
|
993
|
+
this.activeCount += 1;
|
|
994
|
+
resolve();
|
|
995
|
+
},
|
|
996
|
+
cancel: (reason) => {
|
|
997
|
+
_optionalChain([signal, 'optionalAccess', _7 => _7.removeEventListener, 'call', _8 => _8("abort", onAbort)]);
|
|
998
|
+
reject(reason);
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
_optionalChain([signal, 'optionalAccess', _9 => _9.addEventListener, 'call', _10 => _10("abort", onAbort, { once: true })]);
|
|
1002
|
+
this.queue.push(entry);
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
release() {
|
|
1006
|
+
this.activeCount = Math.max(0, this.activeCount - 1);
|
|
1007
|
+
const next = this.queue.shift();
|
|
1008
|
+
_optionalChain([next, 'optionalAccess', _11 => _11.grant, 'call', _12 => _12()]);
|
|
1009
|
+
}
|
|
1010
|
+
}, _class2);
|
|
1011
|
+
function raceWithAbort(task, signal) {
|
|
1012
|
+
if (signal.aborted) {
|
|
1013
|
+
const err = createAbortError(signal);
|
|
1014
|
+
err.pendingTask = task.then(
|
|
1015
|
+
() => {
|
|
1016
|
+
},
|
|
1017
|
+
() => {
|
|
1018
|
+
}
|
|
1019
|
+
);
|
|
1020
|
+
return Promise.reject(err);
|
|
1021
|
+
}
|
|
1022
|
+
return new Promise((resolve, reject) => {
|
|
1023
|
+
const onAbort = () => {
|
|
1024
|
+
const err = createAbortError(signal);
|
|
1025
|
+
err.pendingTask = task.then(
|
|
1026
|
+
() => {
|
|
1027
|
+
},
|
|
1028
|
+
() => {
|
|
1029
|
+
}
|
|
1030
|
+
);
|
|
1031
|
+
reject(err);
|
|
1032
|
+
};
|
|
1033
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1034
|
+
task.then(
|
|
1035
|
+
(val) => {
|
|
1036
|
+
signal.removeEventListener("abort", onAbort);
|
|
1037
|
+
resolve(val);
|
|
1038
|
+
},
|
|
1039
|
+
(err) => {
|
|
1040
|
+
signal.removeEventListener("abort", onAbort);
|
|
1041
|
+
reject(err);
|
|
1042
|
+
}
|
|
1043
|
+
);
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
async function withTimeout(task, timeoutMs, label) {
|
|
1047
|
+
let timeoutHandle = null;
|
|
1048
|
+
const settledTask = task.then(
|
|
1049
|
+
() => {
|
|
1050
|
+
},
|
|
1051
|
+
() => {
|
|
1052
|
+
}
|
|
1053
|
+
);
|
|
1054
|
+
try {
|
|
1055
|
+
return await Promise.race([
|
|
1056
|
+
task,
|
|
1057
|
+
new Promise((_, reject) => {
|
|
1058
|
+
timeoutHandle = setTimeout(() => {
|
|
1059
|
+
const err = new Error(
|
|
1060
|
+
`${label} timed out after ${timeoutMs}ms`
|
|
1061
|
+
);
|
|
1062
|
+
err.pendingTask = settledTask;
|
|
1063
|
+
reject(err);
|
|
1064
|
+
}, timeoutMs);
|
|
1065
|
+
})
|
|
1066
|
+
]);
|
|
1067
|
+
} finally {
|
|
1068
|
+
if (timeoutHandle) {
|
|
1069
|
+
clearTimeout(timeoutHandle);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/server/uploader/bufferProcessor.ts
|
|
1075
|
+
async function computeSha256Hex(buffer) {
|
|
1076
|
+
const bytes = new Uint8Array(buffer.byteLength);
|
|
1077
|
+
bytes.set(buffer);
|
|
1078
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
1079
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1080
|
+
}
|
|
1081
|
+
function throwIfAborted(signal) {
|
|
1082
|
+
if (!_optionalChain([signal, 'optionalAccess', _13 => _13.aborted])) return;
|
|
1083
|
+
if (signal.reason instanceof Error) throw signal.reason;
|
|
1084
|
+
const err = new Error(
|
|
1085
|
+
typeof signal.reason === "string" ? signal.reason : "Operation aborted"
|
|
1086
|
+
);
|
|
1087
|
+
err.name = "AbortError";
|
|
1088
|
+
throw err;
|
|
1089
|
+
}
|
|
1090
|
+
async function processImageBuffer(originalBuffer, options = {}) {
|
|
1091
|
+
const optTimeout = _nullishCoalesce(options.optimizationTimeoutMs, () => ( 6e4));
|
|
1092
|
+
const blurTimeout = _nullishCoalesce(options.blurTimeoutMs, () => ( 3e4));
|
|
1093
|
+
throwIfAborted(options.signal);
|
|
1094
|
+
const contentHash = await computeSha256Hex(originalBuffer);
|
|
1095
|
+
throwIfAborted(options.signal);
|
|
1096
|
+
const optimized = await withTimeout(
|
|
1097
|
+
optimizeImage(originalBuffer, {
|
|
1098
|
+
maxDimensions: options.maxDimensions,
|
|
1099
|
+
outputFormat: options.outputFormat,
|
|
1100
|
+
quality: options.quality,
|
|
1101
|
+
limitInputPixels: options.limitInputPixels,
|
|
1102
|
+
effort: options.effort
|
|
1103
|
+
}),
|
|
1104
|
+
optTimeout,
|
|
1105
|
+
"Image optimization"
|
|
1106
|
+
);
|
|
1107
|
+
if (!optimized || !optimized.buffer) {
|
|
1108
|
+
throw new Error("Image optimization returned invalid result");
|
|
1109
|
+
}
|
|
1110
|
+
if (!optimized.width || !optimized.height) {
|
|
1111
|
+
throw new Error(
|
|
1112
|
+
`Image dimensions missing: width=${optimized.width}, height=${optimized.height}`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
throwIfAborted(options.signal);
|
|
1116
|
+
const blurData = await withTimeout(
|
|
1117
|
+
generateBlurPlaceholder(optimized.buffer, {
|
|
1118
|
+
includeBlurHash: options.includeBlurHash
|
|
1119
|
+
}),
|
|
1120
|
+
blurTimeout,
|
|
1121
|
+
"Blur generation"
|
|
1122
|
+
);
|
|
1123
|
+
return {
|
|
1124
|
+
optimized,
|
|
1125
|
+
blurData,
|
|
1126
|
+
contentHash,
|
|
1127
|
+
originalSize: originalBuffer.length
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/server/uploader/pathGenerator.ts
|
|
1132
|
+
function randomShortId() {
|
|
1133
|
+
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
|
|
1134
|
+
return globalThis.crypto.randomUUID().split("-")[0];
|
|
1135
|
+
}
|
|
1136
|
+
return Math.random().toString(16).slice(2, 10).padEnd(8, "0");
|
|
1137
|
+
}
|
|
1138
|
+
function formatToExtension(format) {
|
|
1139
|
+
switch (format) {
|
|
1140
|
+
case "svg":
|
|
1141
|
+
return "svg";
|
|
1142
|
+
case "png":
|
|
1143
|
+
return "png";
|
|
1144
|
+
case "webp":
|
|
1145
|
+
return "webp";
|
|
1146
|
+
case "jpeg":
|
|
1147
|
+
return "jpg";
|
|
1148
|
+
default:
|
|
1149
|
+
return "avif";
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function sanitizeSegment(value) {
|
|
1153
|
+
return value.replace(/\\/g, "/").split("/").map((part) => part.trim().replace(/[^a-zA-Z0-9._-]/g, "-")).filter((part) => part && part !== "." && part !== "..").join("/");
|
|
1154
|
+
}
|
|
1155
|
+
function stripFilenameExtension(rawFilename) {
|
|
1156
|
+
const leaf = rawFilename.replace(/^.*[\\/]/, "").trim();
|
|
1157
|
+
const dotIdx = leaf.lastIndexOf(".");
|
|
1158
|
+
const base = dotIdx > 0 ? leaf.slice(0, dotIdx) : leaf;
|
|
1159
|
+
const cleaned = base.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^[.-]+|[.-]+$/g, "");
|
|
1160
|
+
return cleaned || "image";
|
|
1161
|
+
}
|
|
1162
|
+
function createDefaultPathGenerator() {
|
|
1163
|
+
let imageCounter = 0;
|
|
1164
|
+
return (ctx) => {
|
|
1165
|
+
const cleanFolder = ctx.folder ? sanitizeSegment(ctx.folder) : "";
|
|
1166
|
+
if (_optionalChain([ctx, 'access', _14 => _14.customFilename, 'optionalAccess', _15 => _15.trim, 'call', _16 => _16()])) {
|
|
1167
|
+
const baseName = stripFilenameExtension(ctx.customFilename);
|
|
1168
|
+
const cleanName = `${baseName}.${ctx.extension}`;
|
|
1169
|
+
const fullPath2 = cleanFolder ? `${cleanFolder}/${cleanName}` : cleanName;
|
|
1170
|
+
return {
|
|
1171
|
+
bucket: ctx.bucket,
|
|
1172
|
+
path: fullPath2,
|
|
1173
|
+
filename: cleanName
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
const date = /* @__PURE__ */ new Date();
|
|
1177
|
+
const year = date.getFullYear();
|
|
1178
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1179
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1180
|
+
imageCounter = (imageCounter + 1) % 1e4;
|
|
1181
|
+
const seq = String(imageCounter).padStart(4, "0");
|
|
1182
|
+
const timestamp = Date.now();
|
|
1183
|
+
const randomSuffix = randomShortId();
|
|
1184
|
+
const prefix = sanitizeSegment(
|
|
1185
|
+
ctx.preset || cleanFolder.split("/")[0] || "img"
|
|
1186
|
+
);
|
|
1187
|
+
const filename = `${prefix}-${year}${month}${day}-${seq}-${timestamp}-${randomSuffix}.${ctx.extension}`;
|
|
1188
|
+
const fullPath = cleanFolder ? `${cleanFolder}/${year}/${month}/${filename}` : `${year}/${month}/${filename}`;
|
|
1189
|
+
return {
|
|
1190
|
+
bucket: ctx.bucket,
|
|
1191
|
+
path: fullPath,
|
|
1192
|
+
filename
|
|
1193
|
+
};
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/server/uploader/uploadRetry.ts
|
|
1198
|
+
var _promises = require('fs/promises'); var _promises2 = _interopRequireDefault(_promises);
|
|
1199
|
+
var _path = require('path'); var _path2 = _interopRequireDefault(_path);
|
|
1200
|
+
function randomRetrySuffix() {
|
|
1201
|
+
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
|
|
1202
|
+
return globalThis.crypto.randomUUID().split("-")[0];
|
|
1203
|
+
}
|
|
1204
|
+
return Math.random().toString(16).slice(2, 10);
|
|
1205
|
+
}
|
|
1206
|
+
function isCollisionError(e) {
|
|
1207
|
+
if (!e) return false;
|
|
1208
|
+
const code = typeof e === "object" && e !== null && "code" in e ? String(e.code).toUpperCase() : "";
|
|
1209
|
+
const statusCode = typeof e === "object" && e !== null && "$metadata" in e ? _optionalChain([e, 'access', _17 => _17.$metadata, 'optionalAccess', _18 => _18.httpStatusCode]) : typeof e === "object" && e !== null && "statusCode" in e ? Number(e.statusCode) : void 0;
|
|
1210
|
+
if (code === "EEXIST" || code === "PRECONDITIONFAILED" || statusCode === 412) {
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
if (code === "ENOENT") {
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
const msg = e instanceof Error ? e.message.toLowerCase() : String(e).toLowerCase();
|
|
1217
|
+
if (msg.includes("does not exist") || msg.includes("doesn't exist") || msg.includes("not exist") || msg.includes("enoent")) {
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
return msg.includes("already exists") || msg.includes("already_exists") || msg.includes("eexist") || msg.includes("file exists") || msg.includes("precondition failed");
|
|
1221
|
+
}
|
|
1222
|
+
async function uploadWithCollisionRetry(params) {
|
|
1223
|
+
const {
|
|
1224
|
+
storage,
|
|
1225
|
+
buildPath,
|
|
1226
|
+
pathContext,
|
|
1227
|
+
data,
|
|
1228
|
+
contentType,
|
|
1229
|
+
maxRetries = 3
|
|
1230
|
+
} = params;
|
|
1231
|
+
const isServerCustomName = Boolean(pathContext.customFilename) && !pathContext.isClientFilename;
|
|
1232
|
+
let storageTarget = buildPath(pathContext);
|
|
1233
|
+
let uploadResult;
|
|
1234
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1235
|
+
try {
|
|
1236
|
+
uploadResult = await storage.upload({
|
|
1237
|
+
bucket: storageTarget.bucket,
|
|
1238
|
+
path: storageTarget.path,
|
|
1239
|
+
data,
|
|
1240
|
+
contentType,
|
|
1241
|
+
upsert: isServerCustomName,
|
|
1242
|
+
contentHash: pathContext.contentHash,
|
|
1243
|
+
metadata: pathContext.metadata
|
|
1244
|
+
});
|
|
1245
|
+
break;
|
|
1246
|
+
} catch (e) {
|
|
1247
|
+
if (isCollisionError(e)) {
|
|
1248
|
+
if (isServerCustomName) {
|
|
1249
|
+
uploadResult = await storage.upload({
|
|
1250
|
+
bucket: storageTarget.bucket,
|
|
1251
|
+
path: storageTarget.path,
|
|
1252
|
+
data,
|
|
1253
|
+
contentType,
|
|
1254
|
+
upsert: true,
|
|
1255
|
+
contentHash: pathContext.contentHash,
|
|
1256
|
+
metadata: pathContext.metadata
|
|
1257
|
+
});
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
if (pathContext.customFilename && pathContext.isClientFilename) {
|
|
1261
|
+
const base = stripFilenameExtension(pathContext.customFilename);
|
|
1262
|
+
storageTarget = buildPath({
|
|
1263
|
+
...pathContext,
|
|
1264
|
+
customFilename: `${base}-${randomRetrySuffix()}`
|
|
1265
|
+
});
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
storageTarget = buildPath({
|
|
1269
|
+
...pathContext,
|
|
1270
|
+
customFilename: void 0
|
|
1271
|
+
});
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
throw e;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
if (!uploadResult) {
|
|
1278
|
+
throw new Error("Failed to upload image after multiple retries");
|
|
1279
|
+
}
|
|
1280
|
+
return { uploadResult, storageTarget };
|
|
1281
|
+
}
|
|
1282
|
+
async function mirrorBufferLocally(options) {
|
|
1283
|
+
const {
|
|
1284
|
+
buffer,
|
|
1285
|
+
filename,
|
|
1286
|
+
storagePath,
|
|
1287
|
+
fallbackUrl,
|
|
1288
|
+
mirrorDir,
|
|
1289
|
+
publicUrlBase
|
|
1290
|
+
} = options;
|
|
1291
|
+
if (!mirrorDir) {
|
|
1292
|
+
return fallbackUrl;
|
|
1293
|
+
}
|
|
1294
|
+
const relativeTarget = (storagePath || filename).replace(/^\/+/, "");
|
|
1295
|
+
try {
|
|
1296
|
+
const resolvedBase = _path2.default.resolve(mirrorDir);
|
|
1297
|
+
const destPath = _path2.default.resolve(resolvedBase, relativeTarget);
|
|
1298
|
+
const rel = _path2.default.relative(resolvedBase, destPath);
|
|
1299
|
+
if (rel.startsWith("..") || _path2.default.isAbsolute(rel)) {
|
|
1300
|
+
return fallbackUrl;
|
|
1301
|
+
}
|
|
1302
|
+
const destDir = _path2.default.dirname(destPath);
|
|
1303
|
+
await _promises2.default.mkdir(destDir, { recursive: true });
|
|
1304
|
+
await _promises2.default.writeFile(destPath, buffer);
|
|
1305
|
+
if (publicUrlBase) {
|
|
1306
|
+
return `${publicUrlBase.replace(/\/+$/, "")}/${relativeTarget}`;
|
|
1307
|
+
}
|
|
1308
|
+
} catch (e8) {
|
|
1309
|
+
}
|
|
1310
|
+
return fallbackUrl;
|
|
1311
|
+
}
|
|
1312
|
+
async function deleteMirroredFiles(mirrorDirs, storagePath) {
|
|
1313
|
+
if (!mirrorDirs.length || !storagePath) return;
|
|
1314
|
+
const relativeTarget = storagePath.replace(/^\/+/, "");
|
|
1315
|
+
if (!relativeTarget) return;
|
|
1316
|
+
await Promise.all(
|
|
1317
|
+
mirrorDirs.map(async (dir) => {
|
|
1318
|
+
try {
|
|
1319
|
+
const resolvedBase = _path2.default.resolve(dir);
|
|
1320
|
+
const destPath = _path2.default.resolve(resolvedBase, relativeTarget);
|
|
1321
|
+
const rel = _path2.default.relative(resolvedBase, destPath);
|
|
1322
|
+
if (rel.startsWith("..") || _path2.default.isAbsolute(rel)) return;
|
|
1323
|
+
await _promises2.default.unlink(destPath);
|
|
1324
|
+
} catch (e9) {
|
|
1325
|
+
}
|
|
1326
|
+
})
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/server/uploader/variantGenerator.ts
|
|
1331
|
+
function buildVariantStoragePath(basePath, width) {
|
|
1332
|
+
const dotIdx = basePath.lastIndexOf(".");
|
|
1333
|
+
const slashIdx = basePath.lastIndexOf("/");
|
|
1334
|
+
if (dotIdx > slashIdx && dotIdx > 0) {
|
|
1335
|
+
return `${basePath.slice(0, dotIdx)}-${width}w${basePath.slice(dotIdx)}`;
|
|
1336
|
+
}
|
|
1337
|
+
return `${basePath}-${width}w`;
|
|
1338
|
+
}
|
|
1339
|
+
function resolveEffectiveAllowedFormats(baseFormats, allowSvg, explicitAllowSvg) {
|
|
1340
|
+
if (!allowSvg) return baseFormats.filter((fmt) => fmt !== _chunkNEDBIIE3cjs.SVG_MIME_TYPE);
|
|
1341
|
+
if (explicitAllowSvg && !baseFormats.includes(_chunkNEDBIIE3cjs.SVG_MIME_TYPE)) {
|
|
1342
|
+
return [...baseFormats, _chunkNEDBIIE3cjs.SVG_MIME_TYPE];
|
|
1343
|
+
}
|
|
1344
|
+
return baseFormats;
|
|
1345
|
+
}
|
|
1346
|
+
async function generateImageVariants(params) {
|
|
1347
|
+
const {
|
|
1348
|
+
sourceBuffer,
|
|
1349
|
+
format,
|
|
1350
|
+
quality,
|
|
1351
|
+
effort,
|
|
1352
|
+
limitInputPixels,
|
|
1353
|
+
mainWidth,
|
|
1354
|
+
mainHeight,
|
|
1355
|
+
mainUrl,
|
|
1356
|
+
baseStoragePath,
|
|
1357
|
+
baseFilename,
|
|
1358
|
+
bucket,
|
|
1359
|
+
storage,
|
|
1360
|
+
variantWidths,
|
|
1361
|
+
upsert = false,
|
|
1362
|
+
contentHash,
|
|
1363
|
+
metadata,
|
|
1364
|
+
mirrorDir,
|
|
1365
|
+
publicUrlBase
|
|
1366
|
+
} = params;
|
|
1367
|
+
if (format === "svg" || !_optionalChain([variantWidths, 'optionalAccess', _19 => _19.length])) {
|
|
1368
|
+
return { variants: [] };
|
|
1369
|
+
}
|
|
1370
|
+
const targetWidths = [
|
|
1371
|
+
...new Set(
|
|
1372
|
+
variantWidths.map((w) => Math.floor(w)).filter((w) => Number.isFinite(w) && w > 0 && w < mainWidth)
|
|
1373
|
+
)
|
|
1374
|
+
].sort((a, b) => a - b);
|
|
1375
|
+
if (targetWidths.length === 0) {
|
|
1376
|
+
return { variants: [] };
|
|
1377
|
+
}
|
|
1378
|
+
const sharp = await loadSharp();
|
|
1379
|
+
const baseQuality = Math.max(1, Math.min(100, _nullishCoalesce(quality, () => ( 85))));
|
|
1380
|
+
const avifEffort = _nullishCoalesce(effort, () => ( PRIMARY_AVIF_EFFORT));
|
|
1381
|
+
const basePipeline = sharp(sourceBuffer, {
|
|
1382
|
+
failOn: "error",
|
|
1383
|
+
limitInputPixels: _nullishCoalesce(limitInputPixels, () => ( DEFAULT_LIMIT_INPUT_PIXELS))
|
|
1384
|
+
}).rotate();
|
|
1385
|
+
const variants = await Promise.all(
|
|
1386
|
+
targetWidths.map(async (w) => {
|
|
1387
|
+
let branch = basePipeline.clone().resize({
|
|
1388
|
+
width: w,
|
|
1389
|
+
withoutEnlargement: true,
|
|
1390
|
+
kernel: "lanczos3"
|
|
1391
|
+
});
|
|
1392
|
+
if (format === "jpeg") {
|
|
1393
|
+
branch = branch.jpeg({ quality: baseQuality, mozjpeg: true });
|
|
1394
|
+
} else if (format === "png") {
|
|
1395
|
+
branch = branch.png({ compressionLevel: 9 });
|
|
1396
|
+
} else if (format === "webp") {
|
|
1397
|
+
branch = branch.webp({ quality: baseQuality, effort: 4 });
|
|
1398
|
+
} else {
|
|
1399
|
+
branch = branch.avif({
|
|
1400
|
+
quality: baseQuality,
|
|
1401
|
+
effort: avifEffort,
|
|
1402
|
+
chromaSubsampling: "4:2:0",
|
|
1403
|
+
bitdepth: 8
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
const out = await branch.toBuffer({ resolveWithObject: true });
|
|
1407
|
+
const variantPath = buildVariantStoragePath(baseStoragePath, w);
|
|
1408
|
+
const variantFilename = buildVariantStoragePath(baseFilename, w);
|
|
1409
|
+
const uploaded = await storage.upload({
|
|
1410
|
+
bucket,
|
|
1411
|
+
path: variantPath,
|
|
1412
|
+
data: out.data,
|
|
1413
|
+
contentType: `image/${format}`,
|
|
1414
|
+
upsert,
|
|
1415
|
+
contentHash,
|
|
1416
|
+
metadata
|
|
1417
|
+
});
|
|
1418
|
+
const resolvedUrl = await mirrorBufferLocally({
|
|
1419
|
+
buffer: out.data,
|
|
1420
|
+
filename: variantFilename,
|
|
1421
|
+
storagePath: uploaded.path,
|
|
1422
|
+
fallbackUrl: uploaded.url,
|
|
1423
|
+
mirrorDir,
|
|
1424
|
+
publicUrlBase
|
|
1425
|
+
});
|
|
1426
|
+
return {
|
|
1427
|
+
width: out.info.width || w,
|
|
1428
|
+
height: out.info.height || Math.max(1, Math.round(mainHeight / Math.max(1, mainWidth) * w)),
|
|
1429
|
+
url: resolvedUrl,
|
|
1430
|
+
size: out.info.size,
|
|
1431
|
+
storagePath: uploaded.path
|
|
1432
|
+
};
|
|
1433
|
+
})
|
|
1434
|
+
);
|
|
1435
|
+
const srcSet = [
|
|
1436
|
+
...variants.map((v) => `${v.url} ${v.width}w`),
|
|
1437
|
+
`${mainUrl} ${mainWidth}w`
|
|
1438
|
+
].join(", ");
|
|
1439
|
+
return { variants, srcSet };
|
|
1440
|
+
}
|
|
1441
|
+
function collectCandidateVariantWidths(presets, extraWidths) {
|
|
1442
|
+
const widths = new Set(extraWidths);
|
|
1443
|
+
for (const preset of Object.values(presets)) {
|
|
1444
|
+
if (_optionalChain([preset, 'optionalAccess', _20 => _20.variantWidths])) {
|
|
1445
|
+
for (const w of preset.variantWidths) {
|
|
1446
|
+
if (Number.isFinite(w) && w > 0) widths.add(Math.floor(w));
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return [...widths];
|
|
1451
|
+
}
|
|
1452
|
+
async function deleteStoredVariants(params) {
|
|
1453
|
+
const {
|
|
1454
|
+
storage,
|
|
1455
|
+
bucket,
|
|
1456
|
+
basePath,
|
|
1457
|
+
mirrorDirs,
|
|
1458
|
+
knownVariantPaths = [],
|
|
1459
|
+
candidateWidths = []
|
|
1460
|
+
} = params;
|
|
1461
|
+
const paths = new Set(knownVariantPaths);
|
|
1462
|
+
for (const w of candidateWidths) {
|
|
1463
|
+
paths.add(buildVariantStoragePath(basePath, w));
|
|
1464
|
+
}
|
|
1465
|
+
if (paths.size === 0) return;
|
|
1466
|
+
await Promise.all(
|
|
1467
|
+
[...paths].map(async (vPath) => {
|
|
1468
|
+
await storage.delete(bucket, vPath).catch(() => {
|
|
1469
|
+
});
|
|
1470
|
+
await deleteMirroredFiles(mirrorDirs, vPath);
|
|
1471
|
+
})
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
async function invokeRecordHookWithRollback(params) {
|
|
1475
|
+
const {
|
|
1476
|
+
onUploadRecord,
|
|
1477
|
+
awaitRecordHook,
|
|
1478
|
+
record,
|
|
1479
|
+
storage,
|
|
1480
|
+
bucket,
|
|
1481
|
+
mainPath,
|
|
1482
|
+
variantPaths,
|
|
1483
|
+
mirrorDirs,
|
|
1484
|
+
onError
|
|
1485
|
+
} = params;
|
|
1486
|
+
if (!onUploadRecord) return;
|
|
1487
|
+
if (awaitRecordHook) {
|
|
1488
|
+
try {
|
|
1489
|
+
await onUploadRecord(record);
|
|
1490
|
+
} catch (hookError) {
|
|
1491
|
+
await storage.delete(bucket, mainPath).catch(() => {
|
|
1492
|
+
});
|
|
1493
|
+
await deleteMirroredFiles(mirrorDirs, mainPath);
|
|
1494
|
+
await deleteStoredVariants({
|
|
1495
|
+
storage,
|
|
1496
|
+
bucket,
|
|
1497
|
+
basePath: mainPath,
|
|
1498
|
+
mirrorDirs,
|
|
1499
|
+
knownVariantPaths: variantPaths
|
|
1500
|
+
});
|
|
1501
|
+
throw hookError;
|
|
1502
|
+
}
|
|
1503
|
+
} else {
|
|
1504
|
+
Promise.resolve(onUploadRecord(record)).catch((err) => {
|
|
1505
|
+
_optionalChain([onError, 'optionalCall', _21 => _21(err, { action: "recordHook" })]);
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/server/uploader/createImageUploader.ts
|
|
1511
|
+
function createImageUploader(options) {
|
|
1512
|
+
const {
|
|
1513
|
+
storage,
|
|
1514
|
+
defaultBucket = "images",
|
|
1515
|
+
defaultFolder = "",
|
|
1516
|
+
defaultMaxSizeMB = 10,
|
|
1517
|
+
defaultAllowedFormats = _chunkNEDBIIE3cjs.DEFAULT_ALLOWED_FORMATS,
|
|
1518
|
+
defaultMaxDimensions = _chunkNEDBIIE3cjs.DEFAULT_IMAGE_DIMENSIONS,
|
|
1519
|
+
allowSvg: defaultAllowSvg = false,
|
|
1520
|
+
limitInputPixels: defaultLimitInputPixels,
|
|
1521
|
+
effort: defaultEffort,
|
|
1522
|
+
presets: customPresets,
|
|
1523
|
+
includeStandardPresets = true,
|
|
1524
|
+
pathGenerator,
|
|
1525
|
+
onUploadRecord,
|
|
1526
|
+
awaitRecordHook = false,
|
|
1527
|
+
onError,
|
|
1528
|
+
concurrency = 2,
|
|
1529
|
+
maxQueue,
|
|
1530
|
+
optimizationTimeoutMs = 6e4,
|
|
1531
|
+
blurTimeoutMs = 3e4,
|
|
1532
|
+
includeBlurHash: defaultIncludeBlurHash = false
|
|
1533
|
+
} = options;
|
|
1534
|
+
const mergedPresets = {
|
|
1535
|
+
...includeStandardPresets ? _chunkNEDBIIE3cjs.STANDARD_PRESETS : {},
|
|
1536
|
+
...customPresets || {}
|
|
1537
|
+
};
|
|
1538
|
+
const semaphore = new AsyncSemaphore(concurrency, { maxQueue });
|
|
1539
|
+
const buildPath = pathGenerator || createDefaultPathGenerator();
|
|
1540
|
+
const trackedVariants = /* @__PURE__ */ new Map();
|
|
1541
|
+
const observedWidths = /* @__PURE__ */ new Set();
|
|
1542
|
+
const resolvePreset = (name) => name ? mergedPresets[name] : void 0;
|
|
1543
|
+
const getAllowedBuckets = () => {
|
|
1544
|
+
const buckets = /* @__PURE__ */ new Set([defaultBucket]);
|
|
1545
|
+
for (const preset of Object.values(mergedPresets)) {
|
|
1546
|
+
if (_optionalChain([preset, 'optionalAccess', _22 => _22.bucket])) buckets.add(preset.bucket);
|
|
1547
|
+
}
|
|
1548
|
+
return [...buckets];
|
|
1549
|
+
};
|
|
1550
|
+
const getMirrorDirs = () => [...new Set(Object.values(mergedPresets).map((p) => _optionalChain([p, 'optionalAccess', _23 => _23.mirrorDir])).filter(Boolean))];
|
|
1551
|
+
const getMaxSizeMB = (allowedPresets) => {
|
|
1552
|
+
let maxMB = defaultMaxSizeMB;
|
|
1553
|
+
const keys = _optionalChain([allowedPresets, 'optionalAccess', _24 => _24.length]) ? allowedPresets : Object.keys(mergedPresets);
|
|
1554
|
+
for (const key of keys) {
|
|
1555
|
+
const p = mergedPresets[key];
|
|
1556
|
+
if (_optionalChain([p, 'optionalAccess', _25 => _25.maxSizeMB]) && p.maxSizeMB > maxMB) maxMB = p.maxSizeMB;
|
|
1557
|
+
}
|
|
1558
|
+
return maxMB;
|
|
1559
|
+
};
|
|
1560
|
+
const cleanupVariantsFor = async (bucket, basePath) => {
|
|
1561
|
+
const key = `${bucket}:${basePath}`;
|
|
1562
|
+
const knownVariantPaths = trackedVariants.get(key);
|
|
1563
|
+
trackedVariants.delete(key);
|
|
1564
|
+
await deleteStoredVariants({
|
|
1565
|
+
storage,
|
|
1566
|
+
bucket,
|
|
1567
|
+
basePath,
|
|
1568
|
+
mirrorDirs: getMirrorDirs(),
|
|
1569
|
+
knownVariantPaths,
|
|
1570
|
+
candidateWidths: collectCandidateVariantWidths(mergedPresets, observedWidths)
|
|
1571
|
+
});
|
|
1572
|
+
};
|
|
1573
|
+
return {
|
|
1574
|
+
defaultBucket,
|
|
1575
|
+
resolvePreset,
|
|
1576
|
+
getAllowedBuckets,
|
|
1577
|
+
getMaxSizeMB,
|
|
1578
|
+
parseUrl: (url) => storage.parseUrl ? storage.parseUrl(url) : null,
|
|
1579
|
+
async processBuffer(buffer, procOptions = {}) {
|
|
1580
|
+
return semaphore.run(
|
|
1581
|
+
() => processImageBuffer(buffer, {
|
|
1582
|
+
maxDimensions: defaultMaxDimensions,
|
|
1583
|
+
optimizationTimeoutMs,
|
|
1584
|
+
blurTimeoutMs,
|
|
1585
|
+
includeBlurHash: defaultIncludeBlurHash,
|
|
1586
|
+
limitInputPixels: defaultLimitInputPixels,
|
|
1587
|
+
effort: defaultEffort,
|
|
1588
|
+
...procOptions
|
|
1589
|
+
}),
|
|
1590
|
+
{ signal: procOptions.signal }
|
|
1591
|
+
);
|
|
1592
|
+
},
|
|
1593
|
+
async uploadImage(input, uploadOptions = {}) {
|
|
1594
|
+
const presetConfig = resolvePreset(uploadOptions.preset);
|
|
1595
|
+
if (uploadOptions.preset && !presetConfig) {
|
|
1596
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(`Invalid preset "${uploadOptions.preset}".`);
|
|
1597
|
+
}
|
|
1598
|
+
const bucket = uploadOptions.bucket || _optionalChain([presetConfig, 'optionalAccess', _26 => _26.bucket]) || defaultBucket;
|
|
1599
|
+
const folder = _nullishCoalesce(_nullishCoalesce(uploadOptions.folder, () => ( _optionalChain([presetConfig, 'optionalAccess', _27 => _27.folder]))), () => ( defaultFolder));
|
|
1600
|
+
const maxSizeMB = _nullishCoalesce(_nullishCoalesce(uploadOptions.maxSizeMB, () => ( _optionalChain([presetConfig, 'optionalAccess', _28 => _28.maxSizeMB]))), () => ( defaultMaxSizeMB));
|
|
1601
|
+
const allowSvg = _nullishCoalesce(_nullishCoalesce(uploadOptions.allowSvg, () => ( _optionalChain([presetConfig, 'optionalAccess', _29 => _29.allowSvg]))), () => ( defaultAllowSvg));
|
|
1602
|
+
const explicitAllowSvg = Boolean(
|
|
1603
|
+
_nullishCoalesce(_nullishCoalesce(uploadOptions.allowSvg, () => ( _optionalChain([presetConfig, 'optionalAccess', _30 => _30.allowSvg]))), () => ( (defaultAllowSvg && !uploadOptions.allowedFormats && !_optionalChain([presetConfig, 'optionalAccess', _31 => _31.allowedFormats]))))
|
|
1604
|
+
);
|
|
1605
|
+
const allowedFormats = resolveEffectiveAllowedFormats(
|
|
1606
|
+
_nullishCoalesce(_nullishCoalesce(uploadOptions.allowedFormats, () => ( _optionalChain([presetConfig, 'optionalAccess', _32 => _32.allowedFormats]))), () => ( defaultAllowedFormats)),
|
|
1607
|
+
allowSvg,
|
|
1608
|
+
explicitAllowSvg
|
|
1609
|
+
);
|
|
1610
|
+
const maxDimensions = _nullishCoalesce(_nullishCoalesce(uploadOptions.maxDimensions, () => ( _optionalChain([presetConfig, 'optionalAccess', _33 => _33.maxDimensions]))), () => ( defaultMaxDimensions));
|
|
1611
|
+
const outputFormat = _nullishCoalesce(uploadOptions.outputFormat, () => ( _optionalChain([presetConfig, 'optionalAccess', _34 => _34.outputFormat])));
|
|
1612
|
+
const quality = _nullishCoalesce(uploadOptions.quality, () => ( _optionalChain([presetConfig, 'optionalAccess', _35 => _35.quality])));
|
|
1613
|
+
const includeBlurHash = _nullishCoalesce(_nullishCoalesce(uploadOptions.includeBlurHash, () => ( _optionalChain([presetConfig, 'optionalAccess', _36 => _36.includeBlurHash]))), () => ( defaultIncludeBlurHash));
|
|
1614
|
+
const limitInputPixels = _nullishCoalesce(_nullishCoalesce(uploadOptions.limitInputPixels, () => ( _optionalChain([presetConfig, 'optionalAccess', _37 => _37.limitInputPixels]))), () => ( defaultLimitInputPixels));
|
|
1615
|
+
const variantWidths = _nullishCoalesce(uploadOptions.variantWidths, () => ( _optionalChain([presetConfig, 'optionalAccess', _38 => _38.variantWidths])));
|
|
1616
|
+
const isWebFile = typeof input.arrayBuffer === "function";
|
|
1617
|
+
if (isWebFile) {
|
|
1618
|
+
const pre = _chunkNEDBIIE3cjs.validateImageFileStrict.call(void 0, { size: input.size, type: input.type }, maxSizeMB, allowedFormats);
|
|
1619
|
+
if (!pre.valid) throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(pre.error);
|
|
1620
|
+
}
|
|
1621
|
+
try {
|
|
1622
|
+
let originalBuffer = isWebFile ? Buffer.from(await input.arrayBuffer()) : input.buffer;
|
|
1623
|
+
if (!originalBuffer || originalBuffer.length === 0) throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)("File is empty");
|
|
1624
|
+
const validation = _chunkNEDBIIE3cjs.validateImageFileStrict.call(void 0, { size: originalBuffer.length, type: input.type }, maxSizeMB, allowedFormats);
|
|
1625
|
+
if (!validation.valid) throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(validation.error);
|
|
1626
|
+
const sniffed = assertMagicBytesAllowed(originalBuffer, allowedFormats, allowSvg);
|
|
1627
|
+
if (sniffed.format === "svg") originalBuffer = sanitizeSvg(originalBuffer);
|
|
1628
|
+
const { optimized, blurData, contentHash, originalSize } = await semaphore.run(
|
|
1629
|
+
() => processImageBuffer(originalBuffer, {
|
|
1630
|
+
maxDimensions,
|
|
1631
|
+
outputFormat,
|
|
1632
|
+
quality,
|
|
1633
|
+
includeBlurHash,
|
|
1634
|
+
optimizationTimeoutMs,
|
|
1635
|
+
blurTimeoutMs,
|
|
1636
|
+
limitInputPixels,
|
|
1637
|
+
effort: defaultEffort,
|
|
1638
|
+
allowSvg,
|
|
1639
|
+
signal: uploadOptions.signal
|
|
1640
|
+
}),
|
|
1641
|
+
{ signal: uploadOptions.signal }
|
|
1642
|
+
);
|
|
1643
|
+
const mainFormat = optimized.format;
|
|
1644
|
+
const { uploadResult, storageTarget } = await uploadWithCollisionRetry({
|
|
1645
|
+
storage,
|
|
1646
|
+
buildPath,
|
|
1647
|
+
pathContext: {
|
|
1648
|
+
bucket,
|
|
1649
|
+
folder,
|
|
1650
|
+
preset: uploadOptions.preset,
|
|
1651
|
+
format: mainFormat,
|
|
1652
|
+
extension: formatToExtension(mainFormat),
|
|
1653
|
+
customFilename: uploadOptions.customFilename,
|
|
1654
|
+
isClientFilename: uploadOptions.isClientFilename,
|
|
1655
|
+
contentHash,
|
|
1656
|
+
originalName: input.name,
|
|
1657
|
+
metadata: uploadOptions.metadata
|
|
1658
|
+
},
|
|
1659
|
+
data: optimized.buffer,
|
|
1660
|
+
contentType: mainFormat === "svg" ? _chunkNEDBIIE3cjs.SVG_MIME_TYPE : `image/${mainFormat}`
|
|
1661
|
+
});
|
|
1662
|
+
const resolvedUrl = await mirrorBufferLocally({
|
|
1663
|
+
buffer: optimized.buffer,
|
|
1664
|
+
filename: storageTarget.filename,
|
|
1665
|
+
storagePath: uploadResult.path,
|
|
1666
|
+
fallbackUrl: uploadResult.url,
|
|
1667
|
+
mirrorDir: _optionalChain([presetConfig, 'optionalAccess', _39 => _39.mirrorDir]),
|
|
1668
|
+
publicUrlBase: _optionalChain([presetConfig, 'optionalAccess', _40 => _40.publicUrlBase])
|
|
1669
|
+
});
|
|
1670
|
+
const { variants, srcSet } = _optionalChain([variantWidths, 'optionalAccess', _41 => _41.length]) && mainFormat !== "svg" ? await semaphore.run(
|
|
1671
|
+
() => generateImageVariants({
|
|
1672
|
+
sourceBuffer: originalBuffer,
|
|
1673
|
+
format: mainFormat,
|
|
1674
|
+
quality: _nullishCoalesce(optimized.quality, () => ( quality)),
|
|
1675
|
+
effort: defaultEffort,
|
|
1676
|
+
limitInputPixels,
|
|
1677
|
+
mainWidth: optimized.width,
|
|
1678
|
+
mainHeight: optimized.height,
|
|
1679
|
+
mainUrl: resolvedUrl,
|
|
1680
|
+
baseStoragePath: uploadResult.path,
|
|
1681
|
+
baseFilename: storageTarget.filename,
|
|
1682
|
+
bucket: storageTarget.bucket,
|
|
1683
|
+
storage,
|
|
1684
|
+
variantWidths,
|
|
1685
|
+
upsert: Boolean(uploadOptions.customFilename) && !uploadOptions.isClientFilename,
|
|
1686
|
+
contentHash,
|
|
1687
|
+
metadata: uploadOptions.metadata,
|
|
1688
|
+
mirrorDir: _optionalChain([presetConfig, 'optionalAccess', _42 => _42.mirrorDir]),
|
|
1689
|
+
publicUrlBase: _optionalChain([presetConfig, 'optionalAccess', _43 => _43.publicUrlBase])
|
|
1690
|
+
}),
|
|
1691
|
+
{ signal: uploadOptions.signal }
|
|
1692
|
+
) : { variants: [], srcSet: void 0 };
|
|
1693
|
+
const variantPaths = variants.map((v) => v.storagePath).filter(Boolean);
|
|
1694
|
+
if (variantPaths.length > 0) {
|
|
1695
|
+
trackedVariants.set(`${storageTarget.bucket}:${uploadResult.path}`, variantPaths);
|
|
1696
|
+
variants.forEach((v) => observedWidths.add(v.width));
|
|
1697
|
+
}
|
|
1698
|
+
const result = {
|
|
1699
|
+
url: resolvedUrl,
|
|
1700
|
+
blurDataUrl: blurData.dataURL,
|
|
1701
|
+
blurColor: blurData.color,
|
|
1702
|
+
...blurData.blurHash && { blurHash: blurData.blurHash },
|
|
1703
|
+
altText: uploadOptions.altText || "",
|
|
1704
|
+
width: optimized.width,
|
|
1705
|
+
height: optimized.height,
|
|
1706
|
+
format: optimized.format,
|
|
1707
|
+
size: optimized.size,
|
|
1708
|
+
originalSize,
|
|
1709
|
+
contentHash,
|
|
1710
|
+
storagePath: uploadResult.path,
|
|
1711
|
+
bucket: storageTarget.bucket,
|
|
1712
|
+
...uploadOptions.preset && { preset: uploadOptions.preset },
|
|
1713
|
+
...folder && { folder },
|
|
1714
|
+
...uploadOptions.metadata && { metadata: uploadOptions.metadata },
|
|
1715
|
+
...variants.length > 0 && { variants },
|
|
1716
|
+
...srcSet && { srcSet },
|
|
1717
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1718
|
+
};
|
|
1719
|
+
const record = { ...result, quality: _nullishCoalesce(_nullishCoalesce(optimized.quality, () => ( quality)), () => ( 85)) };
|
|
1720
|
+
await invokeRecordHookWithRollback({
|
|
1721
|
+
onUploadRecord,
|
|
1722
|
+
awaitRecordHook,
|
|
1723
|
+
record,
|
|
1724
|
+
storage,
|
|
1725
|
+
bucket: storageTarget.bucket,
|
|
1726
|
+
mainPath: uploadResult.path,
|
|
1727
|
+
variantPaths,
|
|
1728
|
+
mirrorDirs: getMirrorDirs(),
|
|
1729
|
+
onError
|
|
1730
|
+
});
|
|
1731
|
+
return result;
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
if (error instanceof _chunk2OVNXHTNcjs.UploadValidationError || error instanceof SemaphoreQueueFullError) {
|
|
1734
|
+
throw error;
|
|
1735
|
+
}
|
|
1736
|
+
throw new Error(`Image upload failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1737
|
+
}
|
|
1738
|
+
},
|
|
1739
|
+
async deleteImage(targetPath, bucket) {
|
|
1740
|
+
const targetBucket = bucket || defaultBucket;
|
|
1741
|
+
try {
|
|
1742
|
+
await storage.delete(targetBucket, targetPath);
|
|
1743
|
+
await deleteMirroredFiles(getMirrorDirs(), targetPath);
|
|
1744
|
+
await cleanupVariantsFor(targetBucket, targetPath);
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
throw new Error(`Image deletion failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
async deleteByUrl(url) {
|
|
1750
|
+
const parsed = _optionalChain([storage, 'access', _44 => _44.parseUrl, 'optionalCall', _45 => _45(url)]);
|
|
1751
|
+
const deleted = storage.deleteByUrl ? await storage.deleteByUrl(url) : false;
|
|
1752
|
+
if (deleted && _optionalChain([parsed, 'optionalAccess', _46 => _46.path])) {
|
|
1753
|
+
await deleteMirroredFiles(getMirrorDirs(), parsed.path);
|
|
1754
|
+
await cleanupVariantsFor(parsed.bucket || defaultBucket, parsed.path);
|
|
1755
|
+
}
|
|
1756
|
+
return deleted;
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
|
|
1772
|
+
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
|
|
1791
|
+
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
exports.AsyncSemaphore = AsyncSemaphore; exports.BLUR_PLACEHOLDER_FALLBACK_WIDTH = BLUR_PLACEHOLDER_FALLBACK_WIDTH; exports.BLUR_PLACEHOLDER_MAX_CHARS = BLUR_PLACEHOLDER_MAX_CHARS; exports.BLUR_PLACEHOLDER_QUALITY = BLUR_PLACEHOLDER_QUALITY; exports.BLUR_PLACEHOLDER_WIDTH = BLUR_PLACEHOLDER_WIDTH; exports.DEFAULT_LIMIT_INPUT_PIXELS = DEFAULT_LIMIT_INPUT_PIXELS; exports.DEFAULT_MAX_SVG_BYTES = DEFAULT_MAX_SVG_BYTES; exports.EDGE_COLOR_FALLBACK = EDGE_COLOR_FALLBACK; exports.EDGE_WHITE_FALLBACK = EDGE_WHITE_FALLBACK; exports.PRIMARY_AVIF_EFFORT = PRIMARY_AVIF_EFFORT; exports.RETRY_AVIF_EFFORT = RETRY_AVIF_EFFORT; exports.SemaphoreQueueFullError = SemaphoreQueueFullError; exports.UploadValidationError = _chunk2OVNXHTNcjs.UploadValidationError; exports.WHITE_DOMINANCE_RATIO = WHITE_DOMINANCE_RATIO; exports.assertMagicBytesAllowed = assertMagicBytesAllowed; exports.blendEdgeColors = blendEdgeColors; exports.buildVariantStoragePath = buildVariantStoragePath; exports.computeEdgeColorSet = computeEdgeColorSet; exports.createDefaultPathGenerator = createDefaultPathGenerator; exports.createImageUploader = createImageUploader; exports.createUploadHandler = _chunk2OVNXHTNcjs.createUploadHandler; exports.createUploadHttpHandler = _chunk2OVNXHTNcjs.createUploadHttpHandler; exports.encodeBlurHashString = encodeBlurHashString; exports.extractEdgeColorsFromBuffer = extractEdgeColorsFromBuffer; exports.extractEnhancedColorFromBuffer = extractEnhancedColorFromBuffer; exports.formatToExtension = formatToExtension; exports.generateBlurHash = generateBlurHash; exports.generateBlurPlaceholder = generateBlurPlaceholder; exports.generateImageVariants = generateImageVariants; exports.isClientValidationError = _chunk2OVNXHTNcjs.isClientValidationError; exports.isCollisionError = isCollisionError; exports.mirrorBufferLocally = mirrorBufferLocally; exports.optimizeImage = optimizeImage; exports.processImageBuffer = processImageBuffer; exports.sanitizeSegment = sanitizeSegment; exports.sanitizeSvg = sanitizeSvg; exports.sniffImageFormat = sniffImageFormat; exports.uploadWithCollisionRetry = uploadWithCollisionRetry; exports.withTimeout = withTimeout;
|
|
1801
|
+
//# sourceMappingURL=server.cjs.map
|