@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +57 -0
  4. package/dist/blurhash/index.d.ts +35 -0
  5. package/dist/blurhash/index.js +40 -0
  6. package/dist/browser/exif-orientation.d.ts +12 -0
  7. package/dist/browser/index.d.ts +7 -0
  8. package/dist/browser/index.js +160 -0
  9. package/dist/browser/orientation-semantics.d.ts +37 -0
  10. package/dist/browser/orientation.d.ts +25 -0
  11. package/dist/browser/prepare-upload.d.ts +100 -0
  12. package/dist/budget/index.d.ts +73 -0
  13. package/dist/budget/index.js +157 -0
  14. package/dist/calibrate/index.d.ts +129 -0
  15. package/dist/calibrate/index.js +206 -0
  16. package/dist/composite/index.d.ts +50 -0
  17. package/dist/composite/index.js +56 -0
  18. package/dist/exif/index.d.ts +41 -0
  19. package/dist/exif/index.js +100 -0
  20. package/dist/heif/index.d.ts +139 -0
  21. package/dist/heif/index.js +275 -0
  22. package/dist/index.d.ts +14 -0
  23. package/dist/index.js +68 -0
  24. package/dist/ogp/index.d.ts +72 -0
  25. package/dist/ogp/index.js +117 -0
  26. package/dist/presets-PTxNoMdy.js +630 -0
  27. package/dist/product-photo/index.d.ts +50 -0
  28. package/dist/product-photo/index.js +189 -0
  29. package/dist/product-photo/shadow.d.ts +48 -0
  30. package/dist/run-BIjGA2Fu.js +22 -0
  31. package/dist/square/index.d.ts +98 -0
  32. package/dist/square/index.js +89 -0
  33. package/dist/variants/engine.d.ts +38 -0
  34. package/dist/variants/feature-detect.d.ts +8 -0
  35. package/dist/variants/hash-cache.d.ts +63 -0
  36. package/dist/variants/heic.d.ts +15 -0
  37. package/dist/variants/index.d.ts +5 -0
  38. package/dist/variants/index.js +19 -0
  39. package/dist/variants/presets.d.ts +18 -0
  40. package/dist/variants/repair.d.ts +28 -0
  41. package/dist/variants/run.d.ts +36 -0
  42. package/dist/variants/tags.d.ts +13 -0
  43. package/dist/variants/types.d.ts +208 -0
  44. package/package.json +126 -0
@@ -0,0 +1,157 @@
1
+ import b from "node:fs/promises";
2
+ import m from "sharp";
3
+ const T = 400, _ = 0.85, L = 3e4, A = 100 * 1024 * 1024, y = [90, 82, 75, 65, 55, 45, 35], W = 95, S = [95, 85, 75, 65, 50], B = [100, 90, 80, 65, 50];
4
+ async function Q(t, n) {
5
+ const {
6
+ maxBytes: e,
7
+ format: r,
8
+ exactWidth: c,
9
+ qualityLadder: s,
10
+ minWidth: h = T,
11
+ widthStepFactor: o = _,
12
+ paletteQuantization: f = !0,
13
+ fetchTimeoutMs: u = L,
14
+ maxInputBytes: a = A
15
+ } = n, i = await I(t, { fetchTimeoutMs: u, maxInputBytes: a }), d = await m(i).metadata();
16
+ if (N(d.format, d.pages))
17
+ return { ok: !1, reason: "animated-gif-skipped", steps: [] };
18
+ if (!d.width || !d.height)
19
+ throw new Error("Could not read image dimensions from source");
20
+ const l = k(r, !!d.hasAlpha, c !== void 0);
21
+ if (c !== void 0)
22
+ return D(i, {
23
+ format: l,
24
+ exactWidth: c,
25
+ maxBytes: e,
26
+ qualityLadder: s ?? (l === "jpeg" ? y : S),
27
+ paletteQuantization: f
28
+ });
29
+ const p = U(d.orientation) ? d.height : d.width;
30
+ return $(i, {
31
+ format: l,
32
+ sourceWidth: p,
33
+ maxBytes: e,
34
+ qualityLadder: s ?? (l === "jpeg" ? y : B),
35
+ minWidth: h,
36
+ widthStepFactor: o,
37
+ paletteQuantization: f
38
+ });
39
+ }
40
+ async function D(t, n) {
41
+ const { format: e, exactWidth: r, maxBytes: c, qualityLadder: s, paletteQuantization: h } = n, o = [], f = /* @__PURE__ */ new Set();
42
+ for (const u of s) {
43
+ const a = E(e, u, h);
44
+ if (f.has(a))
45
+ continue;
46
+ f.add(a);
47
+ const i = await F(t, e, r, u, h);
48
+ if (o.push({ width: r, quality: u, bytes: i.length }), i.length <= c)
49
+ return { ok: !0, buffer: i, format: e, width: r, quality: u, bytes: i.length, steps: o };
50
+ }
51
+ return { ok: !1, reason: "unreachable-budget", steps: o };
52
+ }
53
+ async function F(t, n, e, r, c) {
54
+ const s = m(t).rotate().resize(e, null, { withoutEnlargement: !1 });
55
+ return n === "png" ? s.png(w(r, c)).toBuffer() : s.jpeg({ quality: r, mozjpeg: !0 }).toBuffer();
56
+ }
57
+ async function $(t, n) {
58
+ const { format: e, sourceWidth: r, maxBytes: c, qualityLadder: s, minWidth: h, widthStepFactor: o, paletteQuantization: f } = n, u = [];
59
+ let a = r;
60
+ for (; ; ) {
61
+ const i = /* @__PURE__ */ new Set();
62
+ for (const l of s) {
63
+ const p = E(e, l, f);
64
+ if (i.has(p))
65
+ continue;
66
+ i.add(p);
67
+ const g = await x(t, e, a === r ? null : a, l, f);
68
+ if (u.push({ width: a, quality: l, bytes: g.length }), g.length <= c)
69
+ return { ok: !0, buffer: g, format: e, width: a, quality: l, bytes: g.length, steps: u };
70
+ }
71
+ const d = Math.round(a * o);
72
+ if (d < h || d >= a) {
73
+ if (a > h) {
74
+ a = h;
75
+ continue;
76
+ }
77
+ break;
78
+ }
79
+ a = d;
80
+ }
81
+ return { ok: !1, reason: "unreachable-budget", steps: u };
82
+ }
83
+ async function x(t, n, e, r, c) {
84
+ let s = m(t).rotate();
85
+ return e && (s = s.resize(e, null, { fit: "inside", withoutEnlargement: !0 })), n === "png" ? s.png(w(r, c)).toBuffer() : s.jpeg({ quality: r, mozjpeg: !0 }).toBuffer();
86
+ }
87
+ function w(t, n) {
88
+ const e = { compressionLevel: 9, effort: 10 };
89
+ return n && t < W ? (e.palette = !0, e.colors = t >= 70 ? 256 : t >= 45 ? 16 : 4, e.dither = 1) : e.palette = !1, e;
90
+ }
91
+ function E(t, n, e) {
92
+ return JSON.stringify(t === "png" ? w(n, e) : { quality: n });
93
+ }
94
+ function U(t) {
95
+ return t !== void 0 && t >= 5 && t <= 8;
96
+ }
97
+ function N(t, n) {
98
+ return t === "gif" && n !== void 0 && n > 1;
99
+ }
100
+ function k(t, n, e) {
101
+ return t === "jpeg" || t === "png" ? t : t === void 0 && e || n ? "png" : "jpeg";
102
+ }
103
+ function z(t) {
104
+ return /^https?:\/\//i.test(t);
105
+ }
106
+ async function I(t, n) {
107
+ if (Buffer.isBuffer(t))
108
+ return t;
109
+ if (z(t))
110
+ return R(t, n);
111
+ try {
112
+ return await b.readFile(t);
113
+ } catch (e) {
114
+ throw new Error(`Failed to read local file "${t}": ${e.message}`, { cause: e });
115
+ }
116
+ }
117
+ async function R(t, { fetchTimeoutMs: n, maxInputBytes: e }) {
118
+ const r = new AbortController(), c = new Error(`Failed to fetch "${t}": timed out after ${n}ms`), s = setTimeout(() => r.abort(c), n), h = (o) => {
119
+ throw r.signal.reason === c ? c : o instanceof Error && o.message.includes("exceeds the maximum input size") ? o : new Error(`Failed to fetch "${t}": ${o.message}`, { cause: o });
120
+ };
121
+ try {
122
+ let o;
123
+ try {
124
+ o = await fetch(t, { signal: r.signal });
125
+ } catch (i) {
126
+ throw h(i), i;
127
+ }
128
+ if (!o.ok)
129
+ throw r.abort(), new Error(`Failed to fetch "${t}": HTTP ${o.status} ${o.statusText}`);
130
+ const f = Number(o.headers.get("content-length"));
131
+ if (Number.isFinite(f) && f > e)
132
+ throw r.abort(), new Error(
133
+ `Remote input "${t}" declares ${f} bytes, which exceeds the maximum input size of ${e} bytes`
134
+ );
135
+ if (!o.body)
136
+ return Buffer.alloc(0);
137
+ const u = [];
138
+ let a = 0;
139
+ try {
140
+ for await (const i of o.body) {
141
+ if (a += i.byteLength, a > e)
142
+ throw r.abort(), new Error(
143
+ `Remote input "${t}" exceeds the maximum input size of ${e} bytes (aborted after ${a} bytes)`
144
+ );
145
+ u.push(i);
146
+ }
147
+ } catch (i) {
148
+ h(i);
149
+ }
150
+ return Buffer.concat(u);
151
+ } finally {
152
+ clearTimeout(s);
153
+ }
154
+ }
155
+ export {
156
+ Q as encodeUnderByteBudget
157
+ };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Background-color calibration: sample the background color from an
3
+ * image's four corners, derive a target color by averaging that sample
4
+ * across a set of reference images, and normalize a new image's
5
+ * background toward that target.
6
+ *
7
+ * Correction is selective: each pixel is blended toward its target-scaled
8
+ * value by a weight built from a saturation-gate ramp times a circular
9
+ * hue-distance falloff around the *sampled* background hue (not the
10
+ * target hue), so neutrals, grays, and dark pixels are left untouched
11
+ * while only background-colored pixels move. When the sampled background
12
+ * is itself achromatic (its saturation falls below `saturationGate.from`)
13
+ * there is no coherent hue to match against, so every pixel gets zero
14
+ * weight rather than being compared to a meaningless hue reading of 0.
15
+ *
16
+ * EXIF orientation is applied before any pixel is read or written (via
17
+ * sharp's `.rotate()` with no arguments) — camera photos with a
18
+ * non-default orientation tag are sampled and corrected in their
19
+ * upright, displayed orientation, not their as-stored one.
20
+ *
21
+ * HEIC/HEIF decoding is feature-detected via macOS `sips` (invoked with
22
+ * `execFile` argument arrays, never a shell string) because this module
23
+ * carries no Node-native HEIC decoder. `sampleBackgroundColor` and
24
+ * `normalizeBackgroundColor` throw a descriptive error for `.heic`/`.heif`
25
+ * input on any platform where `sips` is unavailable (Linux, Windows) —
26
+ * pre-convert such inputs to JPEG/PNG (e.g. via the sibling `/heif`
27
+ * module, which does carry a Node-native fallback) before calling into
28
+ * this module. Buffer input bypasses HEIC handling entirely; only a
29
+ * string path is checked for a `.heic`/`.heif` extension. A `sips` failure
30
+ * for any reason other than being absent (e.g. an HDR "gain map" HEIC
31
+ * whose auxiliary-image references exceed the system libheif's limit —
32
+ * see `/heif`'s module doc for the full explanation) is rethrown wrapped
33
+ * with the same pre-convert-via-`/heif` guidance (issue #34).
34
+ *
35
+ * ICC profile (issue #35): `normalizeBackgroundColor` samples and corrects
36
+ * pixels via a raw decode, which yields the source's literal device-space
37
+ * sample values without any colour-space conversion (verified empirically:
38
+ * a Display-P3-tagged source's raw pixels are numerically unchanged from
39
+ * its stored bytes). All arithmetic — sampling, target-ratio scaling,
40
+ * hue/saturation gating — therefore stays self-consistent in that same
41
+ * device space throughout. To avoid re-interpreting those device-space
42
+ * values as sRGB (which would shift the rendered colour for any
43
+ * wide-gamut source), the source's ICC profile, if present, is re-attached
44
+ * byte-identically to the output via `sharp#withIccProfile` pointed at a
45
+ * temp file holding the extracted profile bytes — this only tags the
46
+ * output, it does not run a colour transform, because the pipeline is
47
+ * built from a raw pixel buffer with no profile of its own to transform
48
+ * from (see the `withIccProfile` call site below for the same reasoning
49
+ * `/heif` documents for its APP2 splice). EXIF and all other metadata are
50
+ * always dropped: the raw-buffer re-encode carries no EXIF forward and
51
+ * this module never re-attaches any (unlike ICC, which is deliberately
52
+ * restored) — there is no `stripMetadata`-style opt-out.
53
+ */
54
+ export interface RgbColor {
55
+ r: number;
56
+ g: number;
57
+ b: number;
58
+ }
59
+ export interface SaturationGate {
60
+ /** Saturation (0-1) below which a pixel is treated as neutral. */
61
+ from: number;
62
+ /** Saturation (0-1) at and above which a pixel gets full correction weight. */
63
+ to: number;
64
+ }
65
+ export interface SampleBackgroundColorOptions {
66
+ /** Size (px) of each square corner patch sampled. @default 50 */
67
+ patchSize?: number;
68
+ /**
69
+ * HEIC/HEIF input only: kill the `sips` conversion subprocess if it
70
+ * hasn't exited after this many ms. @default 60000
71
+ */
72
+ sipsTimeoutMs?: number;
73
+ }
74
+ export interface NormalizeBackgroundColorOptions {
75
+ /** Color to move the sampled background toward. */
76
+ target: RgbColor;
77
+ /** Size (px) of each corner patch used to sample the current background. @default 50 */
78
+ patchSize?: number;
79
+ /** Hue-distance falloff radius, in degrees, around the sampled background hue. @default 60 */
80
+ hueRadiusDeg?: number;
81
+ /** Saturation ramp gating which pixels are eligible for correction. @default { from: 0.08, to: 0.25 } */
82
+ saturationGate?: SaturationGate;
83
+ /** Per-channel scale-factor clamp [min, max] applied to target/current ratios. @default [0.5, 2.0] */
84
+ channelClamp?: [number, number];
85
+ /** Output encoding. Inferred from `input`'s extension when it's a path (default jpeg); defaults to jpeg for Buffer input. */
86
+ format?: 'jpeg' | 'png' | 'webp' | 'tiff';
87
+ /**
88
+ * HEIC/HEIF input only: kill the `sips` conversion subprocess if it
89
+ * hasn't exited after this many ms. @default 60000
90
+ */
91
+ sipsTimeoutMs?: number;
92
+ }
93
+ export interface NormalizeBackgroundColorResult {
94
+ buffer: Buffer;
95
+ applied: {
96
+ scaleR: number;
97
+ scaleG: number;
98
+ scaleB: number;
99
+ };
100
+ }
101
+ /**
102
+ * Sample the background color of an image by averaging four square
103
+ * patches, one from each corner (with a small margin so patches don't
104
+ * touch the exact edge).
105
+ */
106
+ export declare function sampleBackgroundColor(input: string | Buffer, opts?: SampleBackgroundColorOptions): Promise<RgbColor>;
107
+ /**
108
+ * Derive a target background color by sampling each reference image's
109
+ * background (see {@link sampleBackgroundColor}) and averaging the
110
+ * results.
111
+ */
112
+ export declare function calibrateTargetFromSamples(inputs: Array<string | Buffer>, opts?: SampleBackgroundColorOptions): Promise<RgbColor>;
113
+ /**
114
+ * Normalize an image's background color toward `opts.target`. The image is
115
+ * converted (if HEIC/HEIF) and decoded to raw pixels exactly once; the
116
+ * current background is sampled directly from that raw buffer (see
117
+ * {@link sampleBackgroundFromRawBuffer}) rather than via a second decode.
118
+ * Each channel's target/current ratio becomes that channel's scale factor,
119
+ * clamped to `opts.channelClamp`. Per pixel, the scaled value is blended
120
+ * in proportional to {@link computeCorrectionWeight}, so only pixels close
121
+ * in hue to the *sampled* background — and saturated enough to carry that
122
+ * hue reliably — move; neutrals and dark pixels are returned unchanged.
123
+ *
124
+ * ICC: the source's ICC profile, if present, is re-attached byte-identically
125
+ * to the output (see the module doc comment for why re-attaching rather
126
+ * than transforming is correct here). EXIF and all other metadata are
127
+ * always dropped — see the module doc comment's "EXIF" note.
128
+ */
129
+ export declare function normalizeBackgroundColor(input: string | Buffer, opts: NormalizeBackgroundColorOptions): Promise<NormalizeBackgroundColorResult>;
@@ -0,0 +1,206 @@
1
+ import { randomUUID as O } from "node:crypto";
2
+ import T from "node:fs/promises";
3
+ import R from "node:os";
4
+ import P from "node:path";
5
+ import y from "sharp";
6
+ import { r as Z, a as z, i as Q } from "../run-BIjGA2Fu.js";
7
+ const U = 6e4, j = 50, s = 5, V = 60, Y = { from: 0.08, to: 0.25 }, tt = [0.5, 2], et = 30;
8
+ function rt(t) {
9
+ return /\.hei[cf]$/i.test(t);
10
+ }
11
+ async function nt(t, r) {
12
+ const e = P.join(R.tmpdir(), `zit-calibrate-${process.pid}-${O()}.jpg`);
13
+ try {
14
+ await Z(
15
+ "sips",
16
+ [
17
+ "-s",
18
+ "format",
19
+ "jpeg",
20
+ "-s",
21
+ "formatOptions",
22
+ "95",
23
+ z(t),
24
+ "--out",
25
+ e
26
+ ],
27
+ { timeoutMs: r }
28
+ );
29
+ } catch (n) {
30
+ if (await T.rm(e, { force: !0 }), Q(n))
31
+ return null;
32
+ throw new Error(
33
+ `HEIC/HEIF conversion via 'sips' failed for '${t}': ${n.message}. This can happen with HDR "gain map" HEIC files, which the system libheif that \`sips\` relies on may reject via a strict auxiliary-image-reference limit. Pre-convert '` + t + "' via the sibling /heif module (whose Node fallback does not carry that limit) before calling this function.",
34
+ { cause: n }
35
+ );
36
+ }
37
+ return e;
38
+ }
39
+ async function L(t, r) {
40
+ if (typeof t == "string" && rt(t)) {
41
+ const e = await nt(t, r);
42
+ if (!e)
43
+ throw new Error(
44
+ `HEIC/HEIF decoding requires macOS 'sips', which is unavailable on this platform. Pre-convert '${t}' to JPEG/PNG (e.g. via the sibling /heif module) before calling this function.`
45
+ );
46
+ return { sharpInput: e, cleanup: () => T.rm(e, { force: !0 }) };
47
+ }
48
+ return { sharpInput: t, cleanup: async () => {
49
+ } };
50
+ }
51
+ async function ot(t) {
52
+ const { width: r = 0, height: e = 0, orientation: n } = await y(t).metadata();
53
+ return n !== void 0 && n >= 5 && n <= 8 ? { width: e, height: r } : { width: r, height: e };
54
+ }
55
+ function at(t) {
56
+ let r = 0, e = 0, n = 0;
57
+ const o = t.length / 3;
58
+ for (let a = 0; a < t.length; a += 3)
59
+ r += t[a], e += t[a + 1], n += t[a + 2];
60
+ return {
61
+ r: Math.round(r / o),
62
+ g: Math.round(e / o),
63
+ b: Math.round(n / o)
64
+ };
65
+ }
66
+ function $(t) {
67
+ const r = t.length;
68
+ return {
69
+ r: Math.round(t.reduce((e, n) => e + n.r, 0) / r),
70
+ g: Math.round(t.reduce((e, n) => e + n.g, 0) / r),
71
+ b: Math.round(t.reduce((e, n) => e + n.b, 0) / r)
72
+ };
73
+ }
74
+ async function it(t, r = {}) {
75
+ const e = r.patchSize ?? j, n = r.sipsTimeoutMs ?? U, o = e + s * 2, { sharpInput: a, cleanup: i } = await L(t, n);
76
+ try {
77
+ const { width: u, height: f } = await ot(a);
78
+ if (!u || !f || u < o || f < o)
79
+ throw new Error(
80
+ `Image too small (${u ?? 0}x${f ?? 0}). Minimum ${o}x${o} required.`
81
+ );
82
+ const h = [
83
+ { left: s, top: s },
84
+ { left: u - e - s, top: s },
85
+ { left: s, top: f - e - s },
86
+ { left: u - e - s, top: f - e - s }
87
+ ], c = [];
88
+ for (const m of h) {
89
+ const { data: l } = await y(a).rotate().extract({ left: m.left, top: m.top, width: e, height: e }).removeAlpha().raw().toBuffer({ resolveWithObject: !0 });
90
+ c.push(at(l));
91
+ }
92
+ return $(c);
93
+ } finally {
94
+ await i();
95
+ }
96
+ }
97
+ async function bt(t, r = {}) {
98
+ if (t.length === 0)
99
+ throw new Error("calibrateTargetFromSamples requires at least one reference image.");
100
+ const e = await Promise.all(t.map((n) => it(n, r)));
101
+ return $(e);
102
+ }
103
+ function N(t, r, e) {
104
+ return Math.min(Math.max(t, r), e);
105
+ }
106
+ function v(t) {
107
+ return t < 0 ? 0 : t > 255 ? 255 : Math.round(t);
108
+ }
109
+ function q(t, r, e) {
110
+ const n = Math.max(t, r, e), o = Math.min(t, r, e), a = n - o;
111
+ if (a === 0) return 0;
112
+ let i;
113
+ return n === t ? i = (r - e) / a % 6 : n === r ? i = (e - t) / a + 2 : i = (t - r) / a + 4, i *= 60, i < 0 && (i += 360), i;
114
+ }
115
+ function k(t, r, e) {
116
+ const n = Math.max(t, r, e), o = Math.min(t, r, e);
117
+ return n === 0 ? 0 : (n - o) / n;
118
+ }
119
+ function st(t, r, e, n, o, a, i) {
120
+ if (Math.max(t, r, e) < et || o < i.from) return 0;
121
+ const f = k(t, r, e);
122
+ if (f < i.from) return 0;
123
+ const h = q(t, r, e);
124
+ let c = Math.abs(h - n);
125
+ c > 180 && (c = 360 - c);
126
+ const m = c < a ? 1 - c / a : 0, l = i.to - i.from;
127
+ return (l > 0 ? N((f - i.from) / l, 0, 1) : f >= i.from ? 1 : 0) * m;
128
+ }
129
+ function ct(t) {
130
+ if (typeof t != "string") return "jpeg";
131
+ const r = P.extname(t).toLowerCase();
132
+ return r === ".png" ? "png" : r === ".webp" ? "webp" : r === ".tiff" || r === ".tif" ? "tiff" : "jpeg";
133
+ }
134
+ function ft(t, r) {
135
+ switch (r) {
136
+ case "png":
137
+ return t.png();
138
+ case "webp":
139
+ return t.webp({ quality: 95 });
140
+ case "tiff":
141
+ return t.tiff({ quality: 95 });
142
+ default:
143
+ return t.jpeg({ quality: 95 });
144
+ }
145
+ }
146
+ function lt(t, r, e, n, o, a) {
147
+ let i = 0, u = 0, f = 0;
148
+ for (let c = 0; c < a; c++) {
149
+ let m = ((o + c) * r + n) * e;
150
+ for (let l = 0; l < a; l++)
151
+ i += t[m], u += t[m + 1], f += t[m + 2], m += e;
152
+ }
153
+ const h = a * a;
154
+ return {
155
+ r: Math.round(i / h),
156
+ g: Math.round(u / h),
157
+ b: Math.round(f / h)
158
+ };
159
+ }
160
+ function ht(t, r, e, n, o) {
161
+ const a = [
162
+ { left: s, top: s },
163
+ { left: r - o - s, top: s },
164
+ { left: s, top: e - o - s },
165
+ { left: r - o - s, top: e - o - s }
166
+ ];
167
+ return $(
168
+ a.map((i) => lt(t, r, n, i.left, i.top, o))
169
+ );
170
+ }
171
+ async function yt(t, r) {
172
+ const {
173
+ target: e,
174
+ patchSize: n = j,
175
+ hueRadiusDeg: o = V,
176
+ saturationGate: a = Y,
177
+ channelClamp: i = tt,
178
+ format: u,
179
+ sipsTimeoutMs: f = U
180
+ } = r;
181
+ if (!Number.isInteger(n) || n <= 0)
182
+ throw new Error(`patchSize must be a positive integer, got ${n}`);
183
+ const h = n + s * 2, { sharpInput: c, cleanup: m } = await L(t, f);
184
+ let l = null;
185
+ try {
186
+ const { hasAlpha: D, icc: _ } = await y(c).metadata(), F = y(c).rotate(), { data: W, info: p } = await (D ? F.ensureAlpha() : F.removeAlpha()).raw().toBuffer({ resolveWithObject: !0 }), A = p.channels, w = Buffer.from(W);
187
+ if (!p.width || !p.height || p.width < h || p.height < h)
188
+ throw new Error(
189
+ `Image too small (${p.width ?? 0}x${p.height ?? 0}). Minimum ${h}x${h} required.`
190
+ );
191
+ const d = ht(w, p.width, p.height, A, n), [G, J] = i, C = (g, M) => M === 0 ? 1 : N(g / M, G, J), S = C(e.r, d.r), H = C(e.g, d.g), B = C(e.b, d.b), K = q(d.r, d.g, d.b), X = k(d.r, d.g, d.b);
192
+ for (let g = 0; g < w.length; g += A) {
193
+ const M = w[g], E = w[g + 1], x = w[g + 2], I = st(M, E, x, K, X, o, a);
194
+ I > 0.01 && (w[g] = v(M + I * (M * S - M)), w[g + 1] = v(E + I * (E * H - E)), w[g + 2] = v(x + I * (x * B - x)));
195
+ }
196
+ let b = y(w, { raw: { width: p.width, height: p.height, channels: A } });
197
+ return _ && (l = P.join(R.tmpdir(), `zit-calibrate-icc-${process.pid}-${O()}.icc`), await T.writeFile(l, _), b = b.withIccProfile(l)), b = ft(b, u ?? ct(t)), { buffer: await b.toBuffer(), applied: { scaleR: S, scaleG: H, scaleB: B } };
198
+ } finally {
199
+ l && await T.rm(l, { force: !0 }), await m();
200
+ }
201
+ }
202
+ export {
203
+ bt as calibrateTargetFromSamples,
204
+ yt as normalizeBackgroundColor,
205
+ it as sampleBackgroundColor
206
+ };
@@ -0,0 +1,50 @@
1
+ import { type SharpInput } from 'sharp';
2
+ /** Anything sharp() itself accepts as an image source. */
3
+ export type ImageInput = SharpInput;
4
+ /** Corner the overlay is anchored to. Only 'bottom-right' ships in v1. */
5
+ export type CompositePosition = 'bottom-right';
6
+ export interface CompositeOverlayOptions {
7
+ /**
8
+ * Overlay width as a percentage (0-100) of the base image's SHORTER side
9
+ * (min(width, height)). The overlay is always forced to a 1:1 square at
10
+ * this size. Sizing off the shorter side (rather than width alone) keeps
11
+ * the badge from overflowing the base on its narrow axis for landscape
12
+ * (or portrait) images.
13
+ */
14
+ widthPercent: number;
15
+ /**
16
+ * Gap from the base image's edges, as a percentage of the base image's
17
+ * SHORTER side (used for both the horizontal and vertical offset) — same
18
+ * basis as `widthPercent`, for the same reason.
19
+ */
20
+ paddingPercent: number;
21
+ /** @default 'bottom-right' */
22
+ position?: CompositePosition;
23
+ }
24
+ export interface CompositeOverlayResult {
25
+ buffer: Buffer;
26
+ width: number;
27
+ height: number;
28
+ }
29
+ /**
30
+ * Composites `overlay` onto a corner of `base` and returns the result.
31
+ *
32
+ * The output buffer is always re-encoded as PNG, regardless of `base`'s
33
+ * original format.
34
+ */
35
+ export declare function compositeOverlay(base: ImageInput, overlay: ImageInput, options: CompositeOverlayOptions): Promise<CompositeOverlayResult>;
36
+ export interface CompositeBatchItem {
37
+ /** Caller-chosen identifier echoed back in the result (e.g. a filename or slug). */
38
+ ref: string;
39
+ image: ImageInput;
40
+ }
41
+ export interface CompositeBatchOptions extends CompositeOverlayOptions {
42
+ /** Max number of composites running at once. @default 4 */
43
+ concurrency?: number;
44
+ }
45
+ export interface CompositeBatchResult {
46
+ baseRef: string;
47
+ overlayRef: string;
48
+ result: CompositeOverlayResult;
49
+ }
50
+ export declare function compositeBatch(bases: CompositeBatchItem[], overlays: CompositeBatchItem[], options: CompositeBatchOptions): Promise<CompositeBatchResult[]>;
@@ -0,0 +1,56 @@
1
+ import l from "sharp";
2
+ async function g(r, n, i) {
3
+ const { widthPercent: t, paddingPercent: a, position: o = "bottom-right" } = i, f = l(r).rotate(), u = await f.metadata(), { width: c, height: h } = y(u);
4
+ if (!c || !h)
5
+ throw new Error("compositeOverlay: could not read base image dimensions");
6
+ const s = Math.min(c, h), e = Math.round(s * (t / 100));
7
+ if (!Number.isFinite(e) || e <= 0)
8
+ throw new Error(
9
+ `compositeOverlay: widthPercent ${t} of base shorter side ${s}px rounds to a non-positive overlay size`
10
+ );
11
+ const d = Math.round(s * (a / 100)), { left: p, top: w } = b(o, c, h, e, d);
12
+ if (p < 0 || w < 0)
13
+ throw new Error(
14
+ `compositeOverlay: overlay (${e}px) plus padding (${d}px) does not fit within the ${c}x${h} base image at position "${o}" — reduce widthPercent/paddingPercent`
15
+ );
16
+ const m = await l(n).rotate().resize(e, e, { fit: "cover" }).toBuffer();
17
+ return { buffer: await f.composite([{ input: m, left: p, top: w }]).png().toBuffer(), width: c, height: h };
18
+ }
19
+ function y(r) {
20
+ const { width: n, height: i, orientation: t } = r;
21
+ return t !== void 0 && t >= 5 && t <= 8 ? { width: i, height: n } : { width: n, height: i };
22
+ }
23
+ function b(r, n, i, t, a) {
24
+ if (r === "bottom-right")
25
+ return {
26
+ left: n - t - a,
27
+ top: i - t - a
28
+ };
29
+ {
30
+ const o = r;
31
+ throw new Error(`compositeOverlay: unsupported position "${String(o)}"`);
32
+ }
33
+ }
34
+ async function x(r, n, i) {
35
+ const { concurrency: t = 4, ...a } = i;
36
+ if (!Number.isFinite(t) || t < 1)
37
+ throw new Error(`compositeBatch: concurrency must be a finite number >= 1, received ${t}`);
38
+ const o = [];
39
+ for (const s of r)
40
+ for (const e of n)
41
+ o.push({ base: s, overlay: e });
42
+ const f = new Array(o.length);
43
+ let u = 0;
44
+ async function c() {
45
+ for (; u < o.length; ) {
46
+ const s = u++, { base: e, overlay: d } = o[s], p = await g(e.image, d.image, a);
47
+ f[s] = { baseRef: e.ref, overlayRef: d.ref, result: p };
48
+ }
49
+ }
50
+ const h = Math.max(1, Math.min(Math.floor(t), o.length));
51
+ return await Promise.all(Array.from({ length: h }, () => c())), f;
52
+ }
53
+ export {
54
+ x as compositeBatch,
55
+ g as compositeOverlay
56
+ };
@@ -0,0 +1,41 @@
1
+ import { type SharpInput } from 'sharp';
2
+ /**
3
+ * Extract the capture date from a raw EXIF buffer. A bounded TIFF/IFD parse
4
+ * resolves the tags in preference order — `DateTimeOriginal`, then
5
+ * `DateTimeDigitized`, then IFD0 `DateTime` — so edited photos (where
6
+ * `DateTime` is the modification date) still return when they were shot.
7
+ * Buffers that aren't TIFF-parseable fall back to scanning the bytes for the
8
+ * first timestamp-shaped text, preserving the previous behavior. Timestamps
9
+ * are interpreted as UTC (EXIF dates carry no time zone). Returns `null`
10
+ * when no usable date is found; never throws on malformed input.
11
+ */
12
+ export declare function parseExifDate(buffer: Buffer | Uint8Array | null | undefined): Date | null;
13
+ /**
14
+ * Physically rotate/flip pixel data to match the source's EXIF `Orientation`
15
+ * tag, then re-encode without metadata — except the ICC profile, which is
16
+ * retained with the pixel values untouched (`keepIccProfile`). Use this
17
+ * before serving an image so viewers that ignore EXIF orientation still
18
+ * display it upright.
19
+ *
20
+ * Retaining the profile keeps this usable as a mid-pipeline step (issue
21
+ * #71): a downstream encode can still choose to carry the profile through
22
+ * or to drop it — sharp's default pipeline genuinely converts pixels to
23
+ * sRGB via the embedded profile before dropping it. Stripping the profile
24
+ * HERE would lock a wide-gamut source into an early sRGB conversion.
25
+ */
26
+ export declare function bakeOrientation(input: SharpInput): Promise<Buffer>;
27
+ /**
28
+ * Re-encode an image with all metadata (EXIF, ICC profile, XMP, …) removed,
29
+ * without altering pixel orientation.
30
+ */
31
+ export declare function stripExif(input: SharpInput): Promise<Buffer>;
32
+ export type Orientation = 'landscape' | 'portrait' | 'square';
33
+ /**
34
+ * Classify a pixel dimension pair as landscape, portrait, or square.
35
+ */
36
+ export declare function deriveOrientation(width: number, height: number): Orientation;
37
+ /**
38
+ * Filter a list of candidate variant widths down to those that don't exceed
39
+ * the source's width, preserving input order — never upscale.
40
+ */
41
+ export declare function pickVariantWidths(srcWidth: number, widths: number[]): number[];