@takazudo/zudo-image-tweaker 0.1.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 +1 -0
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/blurhash/index.d.ts +35 -0
- package/dist/blurhash/index.js +40 -0
- package/dist/browser/exif-orientation.d.ts +12 -0
- package/dist/browser/index.d.ts +7 -0
- package/dist/browser/index.js +160 -0
- package/dist/browser/orientation-semantics.d.ts +37 -0
- package/dist/browser/orientation.d.ts +25 -0
- package/dist/browser/prepare-upload.d.ts +100 -0
- package/dist/budget/index.d.ts +73 -0
- package/dist/budget/index.js +157 -0
- package/dist/calibrate/index.d.ts +129 -0
- package/dist/calibrate/index.js +206 -0
- package/dist/composite/index.d.ts +50 -0
- package/dist/composite/index.js +56 -0
- package/dist/exif/index.d.ts +41 -0
- package/dist/exif/index.js +100 -0
- package/dist/heif/index.d.ts +139 -0
- package/dist/heif/index.js +275 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +68 -0
- package/dist/ogp/index.d.ts +72 -0
- package/dist/ogp/index.js +117 -0
- package/dist/presets-PTxNoMdy.js +630 -0
- package/dist/product-photo/index.d.ts +50 -0
- package/dist/product-photo/index.js +189 -0
- package/dist/product-photo/shadow.d.ts +48 -0
- package/dist/run-BIjGA2Fu.js +22 -0
- package/dist/square/index.d.ts +98 -0
- package/dist/square/index.js +89 -0
- package/dist/variants/engine.d.ts +38 -0
- package/dist/variants/feature-detect.d.ts +8 -0
- package/dist/variants/hash-cache.d.ts +63 -0
- package/dist/variants/heic.d.ts +15 -0
- package/dist/variants/index.d.ts +5 -0
- package/dist/variants/index.js +19 -0
- package/dist/variants/presets.d.ts +18 -0
- package/dist/variants/repair.d.ts +28 -0
- package/dist/variants/run.d.ts +36 -0
- package/dist/variants/tags.d.ts +13 -0
- package/dist/variants/types.d.ts +208 -0
- package/package.json +126 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import P from "node:path";
|
|
2
|
+
import { pathToFileURL as L } from "node:url";
|
|
3
|
+
import m from "sharp";
|
|
4
|
+
const Y = { r: 0, g: 0, b: 0, alpha: 0 }, D = { lightX: 0.3, lightY: 0.25, strength: 0.12, spread: 1.3 }, j = [
|
|
5
|
+
{ blur: 100, opacity: 0.25, offsetX: 10, offsetY: 8, fadeStartRatio: -0.1 },
|
|
6
|
+
{ blur: 40, opacity: 0.35, offsetX: 8, offsetY: 6, fadeStartRatio: 0 }
|
|
7
|
+
], x = { blur: 8, opacity: 0.55, offsetX: 4, offsetY: 3 }, F = {
|
|
8
|
+
squash: 0.25,
|
|
9
|
+
offsetX: 65,
|
|
10
|
+
offsetY: 35,
|
|
11
|
+
blur: 75,
|
|
12
|
+
opacity: 0.45
|
|
13
|
+
}, G = [
|
|
14
|
+
{ blur: 120, opacity: 0.3, offsetX: 62, offsetY: 55, fadeStartRatio: -0.1 },
|
|
15
|
+
{ blur: 52, opacity: 0.45, offsetX: 58, offsetY: 50, fadeStartRatio: 0 }
|
|
16
|
+
], X = { blur: 12, opacity: 0.5, offsetX: 55, offsetY: 45 };
|
|
17
|
+
async function q(o, t = {}) {
|
|
18
|
+
const { mode: n = "grounded", vignette: e, contactShadow: f, bottomShadows: s, projectedShadow: u } = t, { width: r, height: a } = await m(o).metadata();
|
|
19
|
+
if (!r || !a)
|
|
20
|
+
throw new Error("generateShadowLayers: could not read alphaImage dimensions.");
|
|
21
|
+
const c = await m(o).extractChannel(3).toColourspace("b-w").raw().toBuffer(), l = U(c, r, a), w = { ...D, ...e }, d = W(r, a, w);
|
|
22
|
+
if (n === "floating") {
|
|
23
|
+
const p = { ...F, ...u }, [g, B] = _(G, s), i = { ...X, ...f };
|
|
24
|
+
return Promise.all([
|
|
25
|
+
d,
|
|
26
|
+
H(c, r, a, l, p),
|
|
27
|
+
h(c, r, a, l, g),
|
|
28
|
+
h(c, r, a, l, B),
|
|
29
|
+
M(c, r, a, i)
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
const [y, b] = _(j, s), S = { ...x, ...f };
|
|
33
|
+
return Promise.all([
|
|
34
|
+
d,
|
|
35
|
+
h(c, r, a, l, y),
|
|
36
|
+
h(c, r, a, l, b),
|
|
37
|
+
M(c, r, a, S)
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
function _(o, t) {
|
|
41
|
+
return o.map((n, e) => ({ ...n, ...t?.[e] }));
|
|
42
|
+
}
|
|
43
|
+
function A(o) {
|
|
44
|
+
return Number.isFinite(o) ? Math.min(1, Math.max(0, o)) : 0;
|
|
45
|
+
}
|
|
46
|
+
function U(o, t, n) {
|
|
47
|
+
let e = n, f = 0, s = t, u = 0;
|
|
48
|
+
for (let r = 0; r < n; r++)
|
|
49
|
+
for (let a = 0; a < t; a++)
|
|
50
|
+
o[r * t + a] > 0 && (r < e && (e = r), r > f && (f = r), a < s && (s = a), a > u && (u = a));
|
|
51
|
+
return e > f ? null : { top: e, bottom: f + 1, left: s, right: u + 1 };
|
|
52
|
+
}
|
|
53
|
+
async function M(o, t, n, { blur: e, opacity: f, offsetX: s, offsetY: u }) {
|
|
54
|
+
const r = k(o, t, n, s, u), a = await m(r, { raw: { width: t, height: n, channels: 1 } }).toColourspace("b-w").blur(Math.max(e, 0.3)).linear(A(f), 0).png().toBuffer();
|
|
55
|
+
return C(a, t, n);
|
|
56
|
+
}
|
|
57
|
+
async function H(o, t, n, e, { squash: f, offsetX: s, offsetY: u, blur: r, opacity: a }) {
|
|
58
|
+
if (!e) return I(t, n);
|
|
59
|
+
const c = e.right - e.left, l = e.bottom - e.top, w = Buffer.alloc(c * l);
|
|
60
|
+
for (let B = 0; B < l; B++)
|
|
61
|
+
for (let i = 0; i < c; i++)
|
|
62
|
+
w[B * c + i] = o[(e.top + B) * t + (e.left + i)];
|
|
63
|
+
const d = Math.max(1, Math.round(l * f)), y = await m(w, { raw: { width: c, height: l, channels: 1 } }).toColourspace("b-w").resize(c, d, { fit: "fill" }).raw().toBuffer(), b = Buffer.alloc(t * n, 0), S = e.left + s, p = e.bottom + u;
|
|
64
|
+
for (let B = 0; B < d; B++) {
|
|
65
|
+
const i = p + B;
|
|
66
|
+
if (!(i < 0 || i >= n))
|
|
67
|
+
for (let T = 0; T < c; T++) {
|
|
68
|
+
const E = S + T;
|
|
69
|
+
E < 0 || E >= t || (b[i * t + E] = y[B * c + T]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const g = await m(b, { raw: { width: t, height: n, channels: 1 } }).toColourspace("b-w").blur(Math.max(r, 0.3)).linear(A(a), 0).png().toBuffer();
|
|
73
|
+
return C(g, t, n);
|
|
74
|
+
}
|
|
75
|
+
async function h(o, t, n, e, { blur: f, opacity: s, offsetX: u, offsetY: r, fadeStartRatio: a }) {
|
|
76
|
+
if (!e) return I(t, n);
|
|
77
|
+
const c = A(s), l = k(o, t, n, u, r), w = await m(l, { raw: { width: t, height: n, channels: 1 } }).toColourspace("b-w").blur(Math.max(f, 0.3)).raw().toBuffer(), d = e.top + (e.bottom - e.top) * (0.5 + a), y = (e.bottom - d) * 0.5, b = Buffer.alloc(t * n, 0);
|
|
78
|
+
for (let p = 0; p < n; p++) {
|
|
79
|
+
let g;
|
|
80
|
+
p <= d ? g = 0 : y > 0 && p <= d + y ? g = (p - d) / y : g = 1;
|
|
81
|
+
const B = g * c;
|
|
82
|
+
for (let i = 0; i < t; i++)
|
|
83
|
+
b[p * t + i] = Math.round(w[p * t + i] * B);
|
|
84
|
+
}
|
|
85
|
+
const S = await m(b, { raw: { width: t, height: n, channels: 1 } }).toColourspace("b-w").png().toBuffer();
|
|
86
|
+
return C(S, t, n);
|
|
87
|
+
}
|
|
88
|
+
async function W(o, t, { lightX: n, lightY: e, strength: f, spread: s }) {
|
|
89
|
+
const u = A(f), r = Buffer.alloc(o * t, 0), a = o * n, c = t * e, l = Math.min(o, t) / 2, w = s * s;
|
|
90
|
+
for (let y = 0; y < t; y++) {
|
|
91
|
+
const b = (y - c) / l, S = b * b;
|
|
92
|
+
for (let p = 0; p < o; p++) {
|
|
93
|
+
const g = (p - a) / l, B = g * g + S, i = Math.min(1, (B / w) ** 0.75) * u * 255;
|
|
94
|
+
r[y * o + p] = Math.round(i);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const d = await m(r, { raw: { width: o, height: t, channels: 1 } }).toColourspace("b-w").png().toBuffer();
|
|
98
|
+
return C(d, o, t);
|
|
99
|
+
}
|
|
100
|
+
function k(o, t, n, e, f) {
|
|
101
|
+
const s = Buffer.alloc(t * n, 0);
|
|
102
|
+
for (let u = 0; u < n; u++) {
|
|
103
|
+
const r = u + f;
|
|
104
|
+
if (!(r < 0 || r >= n))
|
|
105
|
+
for (let a = 0; a < t; a++) {
|
|
106
|
+
const c = a + e;
|
|
107
|
+
c < 0 || c >= t || (s[r * t + c] = o[u * t + a]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return s;
|
|
111
|
+
}
|
|
112
|
+
const R = /* @__PURE__ */ new Map();
|
|
113
|
+
function $(o, t) {
|
|
114
|
+
const n = `${o}x${t}`;
|
|
115
|
+
let e = R.get(n);
|
|
116
|
+
return e || (e = m({ create: { width: o, height: t, channels: 3, background: { r: 0, g: 0, b: 0 } } }).png().toBuffer(), R.set(n, e)), e;
|
|
117
|
+
}
|
|
118
|
+
async function C(o, t, n) {
|
|
119
|
+
const e = await $(t, n);
|
|
120
|
+
return { buffer: await m(e).joinChannel(o).png().toBuffer(), blend: "multiply", offset: { x: 0, y: 0 } };
|
|
121
|
+
}
|
|
122
|
+
async function I(o, t) {
|
|
123
|
+
return { buffer: await m({ create: { width: o, height: t, channels: 4, background: Y } }).png().toBuffer(), blend: "multiply", offset: { x: 0, y: 0 } };
|
|
124
|
+
}
|
|
125
|
+
const v = 1600, V = 1440, Z = 90, N = { r: 0, g: 0, b: 0, alpha: 0 }, O = "@imgly/background-removal-node";
|
|
126
|
+
async function ct(o) {
|
|
127
|
+
const t = await z(), n = Q(o), f = await (await t.removeBackground(n)).arrayBuffer();
|
|
128
|
+
return Buffer.from(f);
|
|
129
|
+
}
|
|
130
|
+
async function z() {
|
|
131
|
+
try {
|
|
132
|
+
return await import("@imgly/background-removal-node");
|
|
133
|
+
} catch (o) {
|
|
134
|
+
if (!K(o)) throw o;
|
|
135
|
+
const t = `Optional peer dependency "${O}" is not installed. Install it with: npm install ${O} (or pnpm add / yarn add ${O}).`, n = new Error(t);
|
|
136
|
+
throw n.code = "ERR_OPTIONAL_PEER_MISSING", n;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const J = /cannot find (?:package|module)\s+['"]([^'"]+)['"]/i;
|
|
140
|
+
function K(o) {
|
|
141
|
+
let t = o;
|
|
142
|
+
for (let n = 0; n < 5 && t instanceof Error; n++) {
|
|
143
|
+
if (t.message.match(J)?.[1] === O) return !0;
|
|
144
|
+
t = t.cause;
|
|
145
|
+
}
|
|
146
|
+
return !1;
|
|
147
|
+
}
|
|
148
|
+
function Q(o) {
|
|
149
|
+
return Buffer.isBuffer(o) || o instanceof URL ? o : L(P.resolve(o)).href;
|
|
150
|
+
}
|
|
151
|
+
async function ft(o, t = {}) {
|
|
152
|
+
const { threshold: n = 10 } = t;
|
|
153
|
+
return m(o).trim({ background: N, threshold: n }).png().toBuffer();
|
|
154
|
+
}
|
|
155
|
+
async function st(o, t) {
|
|
156
|
+
const { background: n, size: e = v, quality: f = Z, shadow: s = !1 } = t, u = t.fit ?? Math.round(e * (V / v));
|
|
157
|
+
if (n == null)
|
|
158
|
+
throw new Error(
|
|
159
|
+
"composeProductPhoto requires a `background`: an image path/Buffer, or a flat `{ color }` fill."
|
|
160
|
+
);
|
|
161
|
+
const r = Math.round((e - u) / 2), a = await m(o).resize(u, u, { fit: "contain", background: N }).png().toBuffer(), c = nt(n, e);
|
|
162
|
+
let l;
|
|
163
|
+
if (s) {
|
|
164
|
+
const y = await m({
|
|
165
|
+
create: { width: e, height: e, channels: 4, background: N }
|
|
166
|
+
}).composite([{ input: a, left: r, top: r }]).png().toBuffer();
|
|
167
|
+
l = [...(await q(y, s === !0 ? {} : s)).map(tt), { input: y, left: 0, top: 0 }];
|
|
168
|
+
} else
|
|
169
|
+
l = [{ input: a, left: r, top: r }];
|
|
170
|
+
const { data: w, info: d } = await c.composite(l).jpeg({ quality: f }).toBuffer({ resolveWithObject: !0 });
|
|
171
|
+
return { buffer: w, width: d.width, height: d.height, format: "jpeg" };
|
|
172
|
+
}
|
|
173
|
+
function tt(o) {
|
|
174
|
+
return { input: o.buffer, left: o.offset.x, top: o.offset.y, blend: o.blend };
|
|
175
|
+
}
|
|
176
|
+
function ot(o) {
|
|
177
|
+
return typeof o == "object" && !Buffer.isBuffer(o) && "color" in o;
|
|
178
|
+
}
|
|
179
|
+
function nt(o, t) {
|
|
180
|
+
return ot(o) ? m({
|
|
181
|
+
create: { width: t, height: t, channels: 3, background: o.color }
|
|
182
|
+
}) : m(o).resize(t, t, { fit: "cover" });
|
|
183
|
+
}
|
|
184
|
+
export {
|
|
185
|
+
ft as alphaTrim,
|
|
186
|
+
st as composeProductPhoto,
|
|
187
|
+
q as generateShadowLayers,
|
|
188
|
+
ct as removeBackground
|
|
189
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type ShadowMode = 'grounded' | 'floating';
|
|
2
|
+
export type ShadowBlend = 'multiply' | 'over';
|
|
3
|
+
export interface ShadowLayer {
|
|
4
|
+
buffer: Buffer;
|
|
5
|
+
blend: ShadowBlend;
|
|
6
|
+
offset: {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface ShadowTuning {
|
|
12
|
+
blur: number;
|
|
13
|
+
/** Valid range 0..1. Out-of-range values are clamped (prevents 8-bit raw-buffer wraparound). */
|
|
14
|
+
opacity: number;
|
|
15
|
+
offsetX: number;
|
|
16
|
+
offsetY: number;
|
|
17
|
+
}
|
|
18
|
+
export interface BottomShadowTuning extends ShadowTuning {
|
|
19
|
+
fadeStartRatio: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ProjectedShadowTuning extends ShadowTuning {
|
|
22
|
+
squash: number;
|
|
23
|
+
}
|
|
24
|
+
export interface VignetteTuning {
|
|
25
|
+
lightX: number;
|
|
26
|
+
lightY: number;
|
|
27
|
+
/** Valid range 0..1. Out-of-range values are clamped (prevents 8-bit raw-buffer wraparound). */
|
|
28
|
+
strength: number;
|
|
29
|
+
spread: number;
|
|
30
|
+
}
|
|
31
|
+
export interface GenerateShadowLayersOptions {
|
|
32
|
+
mode?: ShadowMode;
|
|
33
|
+
vignette?: Partial<VignetteTuning>;
|
|
34
|
+
contactShadow?: Partial<ShadowTuning>;
|
|
35
|
+
bottomShadows?: Array<Partial<BottomShadowTuning> | undefined>;
|
|
36
|
+
projectedShadow?: Partial<ProjectedShadowTuning>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Generate shadow/vignette layers for a product image that already has
|
|
40
|
+
* transparent background and sits at its final position on a
|
|
41
|
+
* canvas-sized (fully transparent elsewhere) RGBA image.
|
|
42
|
+
*
|
|
43
|
+
* Returns layers in back-to-front composite order. Each layer is already
|
|
44
|
+
* sized to match `alphaImage` and pre-positioned, so `offset` is always
|
|
45
|
+
* `{ x: 0, y: 0 }` — callers composite with `left/top: offset.x/y` and the
|
|
46
|
+
* given `blend` mode.
|
|
47
|
+
*/
|
|
48
|
+
export declare function generateShadowLayers(alphaImage: string | Buffer, options?: GenerateShadowLayersOptions): Promise<ShadowLayer[]>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { execFile as a } from "node:child_process";
|
|
2
|
+
import c from "node:path";
|
|
3
|
+
import { promisify as u } from "node:util";
|
|
4
|
+
const m = u(a), f = 6e4;
|
|
5
|
+
async function E(r, t, o = {}) {
|
|
6
|
+
const { timeoutMs: i = f, killSignal: n = "SIGKILL" } = o, { stdout: e, stderr: s } = await m(r, t, {
|
|
7
|
+
timeout: i,
|
|
8
|
+
killSignal: n
|
|
9
|
+
});
|
|
10
|
+
return { stdout: e, stderr: s };
|
|
11
|
+
}
|
|
12
|
+
function M(r) {
|
|
13
|
+
return c.resolve(r);
|
|
14
|
+
}
|
|
15
|
+
function T(r) {
|
|
16
|
+
return r?.code === "ENOENT";
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
M as a,
|
|
20
|
+
T as i,
|
|
21
|
+
E as r
|
|
22
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type Color } from 'sharp';
|
|
2
|
+
export interface SquareResult {
|
|
3
|
+
buffer: Buffer;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
export type SquareAnchor = 'top' | 'center' | 'bottom';
|
|
8
|
+
export interface CropToSquareOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Which part of the image to keep.
|
|
11
|
+
* For PORTRAIT images (h > w): top=keep top rows, bottom=keep bottom rows.
|
|
12
|
+
* For LANDSCAPE images (w > h): top=keep left columns, bottom=keep right columns
|
|
13
|
+
* (the anchor names refer to the crop axis edge, not an absolute screen position).
|
|
14
|
+
*/
|
|
15
|
+
anchor?: SquareAnchor;
|
|
16
|
+
}
|
|
17
|
+
export interface PadToSquareOptions {
|
|
18
|
+
background?: Color;
|
|
19
|
+
}
|
|
20
|
+
export interface PadToSquareCenteredOptions {
|
|
21
|
+
background?: Color;
|
|
22
|
+
}
|
|
23
|
+
export interface InsetOnSquareOptions {
|
|
24
|
+
/** Fractional border per edge (0 <= margin < 0.5). Default 0.10. */
|
|
25
|
+
margin?: number;
|
|
26
|
+
background?: Color;
|
|
27
|
+
}
|
|
28
|
+
export interface TrimPadSquareOptions {
|
|
29
|
+
/** Fractional border on the content's longer axis (0 <= margin < 0.5). Default 0.10. */
|
|
30
|
+
margin?: number;
|
|
31
|
+
/** 0..255; pixels with all of R,G,B >= this are treated as background. Default 245. */
|
|
32
|
+
threshold?: number;
|
|
33
|
+
background?: Color;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Crop an image to a square using sharp extract.
|
|
37
|
+
*
|
|
38
|
+
* @returns output dimensions (always square)
|
|
39
|
+
*/
|
|
40
|
+
export declare function cropToSquare(input: Buffer, opts?: CropToSquareOptions): Promise<SquareResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Extend canvas to square adding background color ONLY on the narrow axis:
|
|
43
|
+
* portrait (h > w): pad LEFT by (h−w) — content becomes right-aligned
|
|
44
|
+
* landscape (w > h): pad TOP by (w−h) — content becomes bottom-aligned
|
|
45
|
+
* square (h = w): no-op
|
|
46
|
+
*
|
|
47
|
+
* Right and bottom are NEVER extended.
|
|
48
|
+
*
|
|
49
|
+
* @returns output dimensions (always square)
|
|
50
|
+
*/
|
|
51
|
+
export declare function padToSquare(input: Buffer, opts?: PadToSquareOptions): Promise<SquareResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Extend canvas to square adding background color SYMMETRICALLY on BOTH sides
|
|
54
|
+
* of the narrow axis, keeping the original content CENTERED (no crop):
|
|
55
|
+
* portrait (h > w): pad LEFT and RIGHT by (h−w)/2 each — content stays horizontally centered
|
|
56
|
+
* landscape (w > h): pad TOP and BOTTOM by (w−h)/2 each — content stays vertically centered
|
|
57
|
+
* square (h = w): no-op
|
|
58
|
+
*
|
|
59
|
+
* When the difference is odd, the extra single pixel is added to the
|
|
60
|
+
* right (portrait) / bottom (landscape) so the output stays square.
|
|
61
|
+
*
|
|
62
|
+
* @returns output dimensions (always square)
|
|
63
|
+
*/
|
|
64
|
+
export declare function padToSquareCentered(input: Buffer, opts?: PadToSquareCenteredOptions): Promise<SquareResult>;
|
|
65
|
+
/**
|
|
66
|
+
* Shrink a SQUARE image's content and center it on a background-color square canvas
|
|
67
|
+
* of the SAME side length, leaving a `margin*N` border on every edge:
|
|
68
|
+
* content scales to round(N·(1−2·margin)), centered on an N×N canvas.
|
|
69
|
+
* margin 0.10 (default) → content ~80%, with a 10% border all around.
|
|
70
|
+
*
|
|
71
|
+
* Intended for square inputs. For a non-square input the SHORTER side is treated
|
|
72
|
+
* as N, the content is scaled to fit inside the inset box preserving aspect
|
|
73
|
+
* ratio, and centered on the N×N canvas.
|
|
74
|
+
*
|
|
75
|
+
* NOTE: insets the WHOLE input, so any pre-existing border STACKS on top of the
|
|
76
|
+
* new one (a 10% inset of an image already framed at 10% yields ~18%). Use
|
|
77
|
+
* trimPadSquare instead when an EXACT margin is required regardless of input.
|
|
78
|
+
*
|
|
79
|
+
* @returns output dimensions (always square N×N)
|
|
80
|
+
*/
|
|
81
|
+
export declare function insetOnSquare(input: Buffer, opts?: InsetOnSquareOptions): Promise<SquareResult>;
|
|
82
|
+
/**
|
|
83
|
+
* Trim the near-background border down to the content bounding box, then pad
|
|
84
|
+
* a SQUARE canvas leaving an EXACT `margin` fraction on the content's LONGER
|
|
85
|
+
* axis.
|
|
86
|
+
*
|
|
87
|
+
* This differs from insetOnSquare in one decisive way: the existing border is
|
|
88
|
+
* REMOVED first, so a pre-framed image does not stack margins. A portrait subject
|
|
89
|
+
* ends up with `margin` border top + bottom (the binding axis) and larger symmetric
|
|
90
|
+
* left/right margins; landscape is the reverse. The trimmed content is composited
|
|
91
|
+
* 1:1 (never resampled) onto the square, so no detail is lost.
|
|
92
|
+
*
|
|
93
|
+
* Background detection: a pixel counts as background when all of R,G,B are >=
|
|
94
|
+
* `threshold`. Tune `threshold` down if subtle shadows must be kept as content.
|
|
95
|
+
*
|
|
96
|
+
* @returns square output dimensions
|
|
97
|
+
*/
|
|
98
|
+
export declare function trimPadSquare(input: Buffer, opts?: TrimPadSquareOptions): Promise<SquareResult>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import l from "sharp";
|
|
2
|
+
const p = { r: 255, g: 255, b: 255, alpha: 1 };
|
|
3
|
+
async function m(h) {
|
|
4
|
+
const s = await l(h).rotate().png().toBuffer(), e = l(s), i = await e.metadata();
|
|
5
|
+
if (!i.width || !i.height)
|
|
6
|
+
throw new Error("Could not read dimensions from input image");
|
|
7
|
+
return { img: e, width: i.width, height: i.height };
|
|
8
|
+
}
|
|
9
|
+
async function E(h, s = {}) {
|
|
10
|
+
const e = s.anchor ?? "center";
|
|
11
|
+
if (!["top", "center", "bottom"].includes(e))
|
|
12
|
+
throw new Error(`Invalid anchor "${e}". Must be top | center | bottom.`);
|
|
13
|
+
const { img: i, width: n, height: o } = await m(h), c = Math.min(n, o);
|
|
14
|
+
let a = 0, f = 0;
|
|
15
|
+
if (n >= o) {
|
|
16
|
+
const t = n - c;
|
|
17
|
+
e === "top" ? a = 0 : e === "center" ? a = Math.floor(t / 2) : a = t;
|
|
18
|
+
} else {
|
|
19
|
+
const t = o - c;
|
|
20
|
+
e === "top" ? f = 0 : e === "center" ? f = Math.floor(t / 2) : f = t;
|
|
21
|
+
}
|
|
22
|
+
return { buffer: await i.extract({ left: a, top: f, width: c, height: c }).toBuffer(), width: c, height: c };
|
|
23
|
+
}
|
|
24
|
+
async function I(h, s = {}) {
|
|
25
|
+
const e = s.background ?? p, { img: i, width: n, height: o } = await m(h);
|
|
26
|
+
if (n === o)
|
|
27
|
+
return { buffer: await i.toBuffer(), width: n, height: o };
|
|
28
|
+
let c;
|
|
29
|
+
o > n ? c = { top: 0, bottom: 0, left: o - n, right: 0, background: e } : c = { top: n - o, bottom: 0, left: 0, right: 0, background: e };
|
|
30
|
+
const a = await i.ensureAlpha().extend(c).toBuffer(), f = Math.max(n, o);
|
|
31
|
+
return { buffer: a, width: f, height: f };
|
|
32
|
+
}
|
|
33
|
+
async function S(h, s = {}) {
|
|
34
|
+
const e = s.background ?? p, { img: i, width: n, height: o } = await m(h);
|
|
35
|
+
if (n === o)
|
|
36
|
+
return { buffer: await i.toBuffer(), width: n, height: o };
|
|
37
|
+
let c;
|
|
38
|
+
if (o > n) {
|
|
39
|
+
const r = o - n, t = Math.floor(r / 2), u = r - t;
|
|
40
|
+
c = { top: 0, bottom: 0, left: t, right: u, background: e };
|
|
41
|
+
} else {
|
|
42
|
+
const r = n - o, t = Math.floor(r / 2), u = r - t;
|
|
43
|
+
c = { top: t, bottom: u, left: 0, right: 0, background: e };
|
|
44
|
+
}
|
|
45
|
+
const a = await i.ensureAlpha().extend(c).toBuffer(), f = Math.max(n, o);
|
|
46
|
+
return { buffer: a, width: f, height: f };
|
|
47
|
+
}
|
|
48
|
+
async function X(h, s = {}) {
|
|
49
|
+
const e = s.margin ?? 0.1, i = s.background ?? p;
|
|
50
|
+
if (!(e >= 0 && e < 0.5))
|
|
51
|
+
throw new Error(`Invalid margin "${e}". Must be 0 ≤ margin < 0.5.`);
|
|
52
|
+
const { img: n, width: o, height: c } = await m(h), a = Math.min(o, c), f = Math.max(1, Math.round(a * (1 - 2 * e))), { data: r, info: t } = await n.resize(f, f, { fit: "inside" }).toBuffer({ resolveWithObject: !0 }), u = Math.round((a - t.width) / 2), g = Math.round((a - t.height) / 2);
|
|
53
|
+
return { buffer: await l({
|
|
54
|
+
create: { width: a, height: a, channels: 4, background: i }
|
|
55
|
+
}).composite([{ input: r, top: g, left: u }]).png().toBuffer(), width: a, height: a };
|
|
56
|
+
}
|
|
57
|
+
const k = 10;
|
|
58
|
+
function A(h, s, e, i, n) {
|
|
59
|
+
let o = s, c = e, a = -1, f = -1;
|
|
60
|
+
for (let r = 0; r < e; r++)
|
|
61
|
+
for (let t = 0; t < s; t++) {
|
|
62
|
+
const u = (r * s + t) * i, g = h[u], w = h[u + 1], d = h[u + 2];
|
|
63
|
+
(i >= 4 ? h[u + 3] : 255) <= k || g >= n && w >= n && d >= n || (t < o && (o = t), t > a && (a = t), r < c && (c = r), r > f && (f = r));
|
|
64
|
+
}
|
|
65
|
+
return a < 0 ? null : { minX: o, minY: c, maxX: a, maxY: f };
|
|
66
|
+
}
|
|
67
|
+
async function q(h, s = {}) {
|
|
68
|
+
const e = s.margin ?? 0.1, i = s.threshold ?? 245, n = s.background ?? p;
|
|
69
|
+
if (!(e >= 0 && e < 0.5))
|
|
70
|
+
throw new Error(`Invalid margin "${e}". Must be 0 ≤ margin < 0.5.`);
|
|
71
|
+
if (!Number.isInteger(i) || i < 0 || i > 255)
|
|
72
|
+
throw new Error(`Invalid threshold "${i}". Must be an integer 0 ≤ threshold ≤ 255.`);
|
|
73
|
+
const { img: o, width: c, height: a } = await m(h), { data: f, info: r } = await o.clone().ensureAlpha().raw().toBuffer({ resolveWithObject: !0 }), t = A(f, r.width, r.height, r.channels, i);
|
|
74
|
+
if (!t) {
|
|
75
|
+
const b = Math.max(c, a);
|
|
76
|
+
return { buffer: await l({
|
|
77
|
+
create: { width: b, height: b, channels: 4, background: n }
|
|
78
|
+
}).png().toBuffer(), width: b, height: b };
|
|
79
|
+
}
|
|
80
|
+
const u = t.maxX - t.minX + 1, g = t.maxY - t.minY + 1, w = Math.max(u, g), d = Math.round(w / (1 - 2 * e)), M = await o.extract({ left: t.minX, top: t.minY, width: u, height: g }).toBuffer(), x = Math.round((d - u) / 2), B = Math.round((d - g) / 2);
|
|
81
|
+
return { buffer: await l({ create: { width: d, height: d, channels: 4, background: n } }).composite([{ input: M, top: B, left: x }]).png().toBuffer(), width: d, height: d };
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
E as cropToSquare,
|
|
85
|
+
X as insetOnSquare,
|
|
86
|
+
I as padToSquare,
|
|
87
|
+
S as padToSquareCentered,
|
|
88
|
+
q as trimPadSquare
|
|
89
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type ImageEntry, type ItemResult, type ProcessFailure, type ProcessImagesConfig, type ProcessOneConfig, type ProcessSummary } from './types.js';
|
|
2
|
+
/** Source file extensions the directory scanner treats as images. */
|
|
3
|
+
export declare const SUPPORTED_IMAGE_EXTENSIONS: string[];
|
|
4
|
+
export declare const IMAGE_EXTENSION_REGEX: RegExp;
|
|
5
|
+
/**
|
|
6
|
+
* Thrown for a stage-specific processing failure. `processImages` catches
|
|
7
|
+
* it to record which pipeline stage failed; a caller invoking `processOne`
|
|
8
|
+
* directly can read `stage` and the underlying `cause`.
|
|
9
|
+
*/
|
|
10
|
+
export declare class VariantProcessingError extends Error {
|
|
11
|
+
readonly stage: ProcessFailure['stage'];
|
|
12
|
+
constructor(stage: ProcessFailure['stage'], cause: unknown);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The widths actually emitted for a source: every configured width that
|
|
16
|
+
* doesn't upscale it (via `/exif`'s pickVariantWidths), or — when the
|
|
17
|
+
* source is smaller than every configured width — a single variant at the
|
|
18
|
+
* source's own width so even a tiny image gets one representation.
|
|
19
|
+
*/
|
|
20
|
+
export declare function selectVariantWidths(sourceWidth: number, widths: number[]): number[];
|
|
21
|
+
/**
|
|
22
|
+
* Process a single image end-to-end: HEIC conversion, content-hash cache
|
|
23
|
+
* check, corruption repair, blurhash, width variants (or byte-for-byte
|
|
24
|
+
* passthrough for animated GIF/WebP/APNG — see ANIMATED_PAGE_FORMATS), and
|
|
25
|
+
* optional OGP — dispatched by the entry's tag.
|
|
26
|
+
*
|
|
27
|
+
* Throws {@link VariantProcessingError} on a genuine processing failure
|
|
28
|
+
* (carrying the failing `stage`); returns a `skipped` result for a
|
|
29
|
+
* deliberate skip (cache hit, or a non-image guard trip).
|
|
30
|
+
*/
|
|
31
|
+
export declare function processOne(entry: ImageEntry, config: ProcessOneConfig): Promise<ItemResult>;
|
|
32
|
+
/**
|
|
33
|
+
* Process every source image from `inputDir` and/or an explicit `files`
|
|
34
|
+
* list, with bounded concurrency and a collect-and-continue error policy:
|
|
35
|
+
* one bad file lands in `ProcessSummary.failed[]` and never aborts the run.
|
|
36
|
+
* Orphan output directories are pruned only when `cleanupOrphans` is set.
|
|
37
|
+
*/
|
|
38
|
+
export declare function processImages(config: ProcessImagesConfig): Promise<ProcessSummary>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Whether the `file` type-identification binary is available. */
|
|
2
|
+
export declare function hasFileBinary(): Promise<boolean>;
|
|
3
|
+
/** Whether ImageMagick's `magick` binary is available. */
|
|
4
|
+
export declare function hasMagick(): Promise<boolean>;
|
|
5
|
+
/** Whether `ffmpeg` is available. */
|
|
6
|
+
export declare function hasFfmpeg(): Promise<boolean>;
|
|
7
|
+
/** Clear the memoised detection results. For tests only. */
|
|
8
|
+
export declare function resetFeatureDetectionCache(): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { TagMode, VariantMetadata } from './types.js';
|
|
2
|
+
/** The engine's per-image cache sidecar. Purely internal — not a metadata DB. */
|
|
3
|
+
export interface CacheEntry {
|
|
4
|
+
/** Content hash of the source file the cached outputs were produced from. */
|
|
5
|
+
hash: string;
|
|
6
|
+
/**
|
|
7
|
+
* Fingerprint of the output-affecting config (quality, widths, OGP options,
|
|
8
|
+
* …) the cached outputs were produced with. A change invalidates the cache
|
|
9
|
+
* even when the source bytes are unchanged.
|
|
10
|
+
*/
|
|
11
|
+
configHash: string;
|
|
12
|
+
/**
|
|
13
|
+
* The tag mode the cached outputs were produced under. Renaming a source
|
|
14
|
+
* to change only its tag (e.g. `photo__og.jpg` → `photo__ogonly.jpg`)
|
|
15
|
+
* keeps the same bytes and slug, so the mode is compared to stop the old
|
|
16
|
+
* outputs from satisfying the new mode's expected-output subset.
|
|
17
|
+
*/
|
|
18
|
+
mode: TagMode;
|
|
19
|
+
/** Whether the source was handled as an animated passthrough (GIF/WebP/APNG). */
|
|
20
|
+
animated: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The exact filenames this run emitted into the slug directory. Compared
|
|
23
|
+
* against the current config's expected filenames so a changed custom
|
|
24
|
+
* `outputName` scheme — including at the sub-min-width fallback, which the
|
|
25
|
+
* config fingerprint can't capture pre-probe — is treated as a miss rather
|
|
26
|
+
* than a stale hit on a lingering older-scheme file.
|
|
27
|
+
*/
|
|
28
|
+
outputs: string[];
|
|
29
|
+
/**
|
|
30
|
+
* On-disk byte size of every filename in `outputs`, keyed by filename.
|
|
31
|
+
* Validated against the actual file size on a cache hit so a truncated
|
|
32
|
+
* output — e.g. one left by a crash mid-write under an older, non-atomic
|
|
33
|
+
* version — is treated as a miss instead of a poisoned hit. An entry
|
|
34
|
+
* written before this field existed lacks it entirely, which the cache
|
|
35
|
+
* check reads as "unverifiable" and reprocesses once.
|
|
36
|
+
*/
|
|
37
|
+
outputSizes: Record<string, number>;
|
|
38
|
+
/** Last emitted metadata record (null for OGP-only images), replayed on a cache hit. */
|
|
39
|
+
metadata: VariantMetadata | null;
|
|
40
|
+
}
|
|
41
|
+
/** Content-hash a file's bytes with xxhash (64-bit), returned as a hex string. */
|
|
42
|
+
export declare function hashFile(filePath: string): Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Produce `filePath` atomically: `produce` writes to a sibling temp path in
|
|
45
|
+
* the SAME directory, which is then renamed over the target. rename(2) is
|
|
46
|
+
* atomic within a filesystem, so a reader never observes a half-written file
|
|
47
|
+
* and a crash before the rename leaves the previous version intact — never a
|
|
48
|
+
* truncated one at the final path. On any failure the temp file is removed.
|
|
49
|
+
*
|
|
50
|
+
* Invariant this establishes for every cached output: a file that exists at
|
|
51
|
+
* its final path is complete. `produce`'s return value (e.g. sharp's
|
|
52
|
+
* OutputInfo) is passed through so callers keep the size/metadata they need.
|
|
53
|
+
*/
|
|
54
|
+
export declare function writeAtomicVia<T>(filePath: string, produce: (tmpPath: string) => Promise<T>): Promise<T>;
|
|
55
|
+
/**
|
|
56
|
+
* Write `data` to `filePath` atomically via {@link writeAtomicVia}: staged in
|
|
57
|
+
* a sibling temp file, then renamed over the target.
|
|
58
|
+
*/
|
|
59
|
+
export declare function writeFileAtomic(filePath: string, data: string | Buffer): Promise<void>;
|
|
60
|
+
/** Read and parse a cache sidecar, or null when it's absent or unparseable. */
|
|
61
|
+
export declare function readCache(cachePath: string): Promise<CacheEntry | null>;
|
|
62
|
+
/** Atomically write a cache sidecar. */
|
|
63
|
+
export declare function writeCache(cachePath: string, entry: CacheEntry): Promise<void>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a source should be treated as HEIC/HEIF. Trusts the extension
|
|
3
|
+
* for `.heic`/`.heif`, and additionally sniffs `.jpg`/`.jpeg` files with
|
|
4
|
+
* the `file` binary because cameras and messaging apps routinely hand out
|
|
5
|
+
* HEIF payloads under a `.jpg` name. When `file` is unavailable, only the
|
|
6
|
+
* extension is consulted.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isHeicSource(inputPath: string): Promise<boolean>;
|
|
9
|
+
/**
|
|
10
|
+
* Heuristic guard against non-image files that were saved with an image
|
|
11
|
+
* extension — most commonly an HTML error page or a plain-text response
|
|
12
|
+
* downloaded to `photo.jpg`. Returns false (don't skip) whenever `file`
|
|
13
|
+
* can't make the determination.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isNonImageFile(inputPath: string): Promise<boolean>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { IMAGE_EXTENSION_REGEX, SUPPORTED_IMAGE_EXTENSIONS, processImages, processOne, selectVariantWidths, VariantProcessingError, } from './engine.js';
|
|
2
|
+
export { defaultTagParser, noTagParser } from './tags.js';
|
|
3
|
+
export { PHOTO_VARIANT_WIDTHS, photoVariantsPreset } from './presets.js';
|
|
4
|
+
export { DEFAULT_CACHE_FILENAME, DEFAULT_CONCURRENCY, DEFAULT_FORMATS, DEFAULT_OGP_FILENAME, DEFAULT_QUALITY, DEFAULT_WIDTHS, } from './types.js';
|
|
5
|
+
export type { ImageEntry, ItemResult, ItemStatus, OgpOutput, OutputNameFn, ParsedTag, ProcessFailure, ProcessImagesConfig, ProcessOneConfig, ProcessSummary, TagMode, TagParser, VariantMetadata, VariantOutput, } from './types.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { D as E, a as T, b as r, c as A, d as e, e as _, I, P as D, S as o, V as t, f as F, n as L, p as N, g as O, h as P, s as U } from "../presets-PTxNoMdy.js";
|
|
2
|
+
export {
|
|
3
|
+
E as DEFAULT_CACHE_FILENAME,
|
|
4
|
+
T as DEFAULT_CONCURRENCY,
|
|
5
|
+
r as DEFAULT_FORMATS,
|
|
6
|
+
A as DEFAULT_OGP_FILENAME,
|
|
7
|
+
e as DEFAULT_QUALITY,
|
|
8
|
+
_ as DEFAULT_WIDTHS,
|
|
9
|
+
I as IMAGE_EXTENSION_REGEX,
|
|
10
|
+
D as PHOTO_VARIANT_WIDTHS,
|
|
11
|
+
o as SUPPORTED_IMAGE_EXTENSIONS,
|
|
12
|
+
t as VariantProcessingError,
|
|
13
|
+
F as defaultTagParser,
|
|
14
|
+
L as noTagParser,
|
|
15
|
+
N as photoVariantsPreset,
|
|
16
|
+
O as processImages,
|
|
17
|
+
P as processOne,
|
|
18
|
+
U as selectVariantWidths
|
|
19
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Width ladder the photo preset emits. */
|
|
2
|
+
export declare const PHOTO_VARIANT_WIDTHS: number[];
|
|
3
|
+
/**
|
|
4
|
+
* Config fragment reproducing the source photo pipeline on top of the same
|
|
5
|
+
* engine: a smaller [400, 800, 1600] width ladder, EXIF orientation baked
|
|
6
|
+
* into pixels and all metadata stripped (see the `stripMetadata` behavior
|
|
7
|
+
* matrix in ProcessOneConfig), and no OGP or filename-tag dispatch (every
|
|
8
|
+
* file is a plain full-variant image).
|
|
9
|
+
*
|
|
10
|
+
* Spread it into a call and add the paths, e.g.
|
|
11
|
+
* `processImages({ ...photoVariantsPreset, inputDir, outputDir })`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const photoVariantsPreset: {
|
|
14
|
+
widths: number[];
|
|
15
|
+
bakeExifOrientation: true;
|
|
16
|
+
stripMetadata: true;
|
|
17
|
+
tagParser: import("./types.js").TagParser;
|
|
18
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Whether an error looks like recoverable bitstream corruption rather than a non-image. */
|
|
2
|
+
export declare function isCorruptionError(error: unknown): boolean;
|
|
3
|
+
export interface RepairCorruptedImageOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Before attempting repair, copy the untouched corrupt source to a sibling
|
|
6
|
+
* `<name>.corrupted.bak`. Off by default — repair otherwise never touches the
|
|
7
|
+
* caller's file at all. An existing `.bak` is kept, never overwritten.
|
|
8
|
+
* @default false
|
|
9
|
+
*/
|
|
10
|
+
backupCorrupted?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Attempt to recover a corrupt image by re-encoding it through an external
|
|
14
|
+
* tool, returning a clean image buffer or null if repair isn't possible.
|
|
15
|
+
*
|
|
16
|
+
* Repair is best-effort and entirely optional. When ImageMagick (`magick`) is
|
|
17
|
+
* available it is tried twice — first a strip + colourspace normalise, then a
|
|
18
|
+
* format-swap re-decode through the sibling raster format (jpg<->png), which
|
|
19
|
+
* recovers a bitstream the same-format pass can't. `ffmpeg` is tried last.
|
|
20
|
+
* Each strategy runs only if its binary is feature-detected; on a machine with
|
|
21
|
+
* neither (e.g. plain Linux without ImageMagick) this warns and returns null
|
|
22
|
+
* rather than failing — a missing optional binary must never break a run.
|
|
23
|
+
*
|
|
24
|
+
* The caller's source file is never modified. With `backupCorrupted` set, a
|
|
25
|
+
* sibling `<name>.corrupted.bak` copy is written before the attempt (the source
|
|
26
|
+
* itself is still left untouched); by default no backup is made.
|
|
27
|
+
*/
|
|
28
|
+
export declare function repairCorruptedImage(inputPath: string, options?: RepairCorruptedImageOptions): Promise<Buffer | null>;
|