@xprite/pixelate 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +12 -0
  3. package/README.md +270 -2
  4. package/README.zh-CN.md +240 -0
  5. package/dist/analyze.d.ts +8 -0
  6. package/dist/analyze.js +217 -0
  7. package/dist/base.d.ts +18 -0
  8. package/dist/base.js +42 -0
  9. package/dist/cell-color-picker.d.ts +14 -0
  10. package/dist/cell-color-picker.js +278 -0
  11. package/dist/color-lab.d.ts +3 -0
  12. package/dist/color-lab.js +31 -0
  13. package/dist/color-step.d.ts +33 -0
  14. package/dist/color-step.js +62 -0
  15. package/dist/contours.d.ts +4 -0
  16. package/dist/contours.js +66 -0
  17. package/dist/denoise.d.ts +3 -0
  18. package/dist/denoise.js +37 -0
  19. package/dist/deskew.d.ts +24 -0
  20. package/dist/deskew.js +202 -0
  21. package/dist/grid-ac-rl-consensus.d.ts +127 -0
  22. package/dist/grid-ac-rl-consensus.js +497 -0
  23. package/dist/grid-arbitrate.d.ts +52 -0
  24. package/dist/grid-arbitrate.js +366 -0
  25. package/dist/grid-cut-snap.d.ts +15 -0
  26. package/dist/grid-cut-snap.js +77 -0
  27. package/dist/grid-description-length.d.ts +47 -0
  28. package/dist/grid-description-length.js +697 -0
  29. package/dist/grid-detect.d.ts +8 -0
  30. package/dist/grid-detect.js +665 -0
  31. package/dist/grid-edge-coherence.d.ts +4 -0
  32. package/dist/grid-edge-coherence.js +158 -0
  33. package/dist/grid-edge-path.d.ts +9 -0
  34. package/dist/grid-edge-path.js +201 -0
  35. package/dist/grid-frame-lattice.d.ts +25 -0
  36. package/dist/grid-frame-lattice.js +420 -0
  37. package/dist/grid-frequency-spectrum.d.ts +11 -0
  38. package/dist/grid-frequency-spectrum.js +89 -0
  39. package/dist/grid-period-boundary.d.ts +13 -0
  40. package/dist/grid-period-boundary.js +255 -0
  41. package/dist/grid-period-comb.d.ts +4 -0
  42. package/dist/grid-period-comb.js +230 -0
  43. package/dist/grid-period-distance.d.ts +7 -0
  44. package/dist/grid-period-distance.js +233 -0
  45. package/dist/grid-phase-field.d.ts +11 -0
  46. package/dist/grid-phase-field.js +446 -0
  47. package/dist/grid-pitch-lattice.d.ts +4 -0
  48. package/dist/grid-pitch-lattice.js +255 -0
  49. package/dist/grid-pitch-search.d.ts +11 -0
  50. package/dist/grid-pitch-search.js +233 -0
  51. package/dist/grid-sample.d.ts +4 -0
  52. package/dist/grid-sample.js +164 -0
  53. package/dist/grid-sampling.d.ts +13 -0
  54. package/dist/grid-sampling.js +132 -0
  55. package/dist/grid-strip-shift.d.ts +44 -0
  56. package/dist/grid-strip-shift.js +223 -0
  57. package/dist/grid-test-images.d.ts +69 -0
  58. package/dist/grid-test-images.js +547 -0
  59. package/dist/grid-warp.d.ts +38 -0
  60. package/dist/grid-warp.js +252 -0
  61. package/dist/grid.d.ts +8 -0
  62. package/dist/grid.js +43 -0
  63. package/dist/index.d.ts +16 -0
  64. package/dist/index.js +14 -0
  65. package/dist/line-guided/cv-lab.d.ts +4 -0
  66. package/dist/line-guided/cv-lab.js +134 -0
  67. package/dist/line-guided/cv-ops.d.ts +29 -0
  68. package/dist/line-guided/cv-ops.js +759 -0
  69. package/dist/line-guided/pixeloe-entry.d.ts +15 -0
  70. package/dist/line-guided/pixeloe-entry.js +122 -0
  71. package/dist/line-guided/pixeloe-legacy.d.ts +31 -0
  72. package/dist/line-guided/pixeloe-legacy.js +301 -0
  73. package/dist/line-guided/xdog.d.ts +7 -0
  74. package/dist/line-guided/xdog.js +84 -0
  75. package/dist/most-frequent.d.ts +3 -0
  76. package/dist/most-frequent.js +56 -0
  77. package/dist/outline-prepass.d.ts +37 -0
  78. package/dist/outline-prepass.js +56 -0
  79. package/dist/palette-snap.d.ts +4 -0
  80. package/dist/palette-snap.js +371 -0
  81. package/dist/palette.d.ts +21 -0
  82. package/dist/palette.js +280 -0
  83. package/dist/pixelate.d.ts +15 -0
  84. package/dist/pixelate.js +214 -0
  85. package/dist/pixelation-algorithm-registry.d.ts +34 -0
  86. package/dist/pixelation-algorithm-registry.js +716 -0
  87. package/dist/pixeloe-prepass.d.ts +12 -0
  88. package/dist/pixeloe-prepass.js +527 -0
  89. package/dist/quantization.d.ts +6 -0
  90. package/dist/quantization.js +145 -0
  91. package/dist/remove-background.d.ts +40 -0
  92. package/dist/remove-background.js +229 -0
  93. package/dist/resize.d.ts +3 -0
  94. package/dist/resize.js +86 -0
  95. package/dist/sampling.d.ts +7 -0
  96. package/dist/sampling.js +301 -0
  97. package/dist/settings.d.ts +36 -0
  98. package/dist/settings.js +181 -0
  99. package/dist/types.d.ts +255 -0
  100. package/dist/types.js +87 -0
  101. package/licenses/PixelOE-Apache-2.0.txt +201 -0
  102. package/licenses/pixel-art-fixer-MIT.txt +21 -0
  103. package/licenses/proper-pixel-art-MIT.txt +21 -0
  104. package/licenses/tensorhead-dithering-MIT.txt +21 -0
  105. package/package.json +62 -4
  106. package/wasm/ac-rl-detect-browser-worker.js +45 -0
  107. package/wasm/ac-rl-detect-worker.mjs +51 -0
  108. package/wasm/pixelfixer_detect.d.ts +65 -0
  109. package/wasm/pixelfixer_detect.js +334 -0
  110. package/wasm/pixelfixer_detect_bg.wasm +0 -0
@@ -0,0 +1,280 @@
1
+ import { UINT8_MAX } from "./base.js";
2
+ import { assertPixelBuffer, clampByte } from "./base.js";
3
+ import { optimizePalette, paletteCoordinates } from "./quantization.js";
4
+ const RGB_HISTOGRAM_BINS = 32 * 32 * 32;
5
+ const HISTOGRAM_BINS = RGB_HISTOGRAM_BINS * 16 + 1;
6
+ const MAX_EXACT_COLORS = 16_384;
7
+ const REDUCTION_CACHE_SIZE = 65_536;
8
+ function colorKey(data, offset, opaqueVisible = false) {
9
+ if (data[offset + 3] === 0)
10
+ return 0;
11
+ return ((data[offset] << 24) |
12
+ (data[offset + 1] << 16) |
13
+ (data[offset + 2] << 8) |
14
+ (opaqueVisible ? UINT8_MAX : data[offset + 3]) |
15
+ 0);
16
+ }
17
+ function colorFromKey(key) {
18
+ return [
19
+ (key >>> 24) & UINT8_MAX,
20
+ (key >>> 16) & UINT8_MAX,
21
+ (key >>> 8) & UINT8_MAX,
22
+ key & UINT8_MAX,
23
+ ];
24
+ }
25
+ function paletteLimit(value) {
26
+ if (value === undefined)
27
+ return 256;
28
+ if (!Number.isSafeInteger(value) || value < 1) {
29
+ throw new RangeError("maxColors must be a positive integer");
30
+ }
31
+ return Math.min(256, value);
32
+ }
33
+ function histogramIndex(r, g, b, a) {
34
+ if (!a)
35
+ return HISTOGRAM_BINS - 1;
36
+ return (r >>> 3) | ((g >>> 3) << 5) | ((b >>> 3) << 10) | ((a >>> 4) << 15);
37
+ }
38
+ /** The visible-alpha palette uses the same RGB bins without the constant alpha-plane offset. */
39
+ function opaqueHistogramPalette(image, maxColors) {
40
+ const counts = new Uint32Array(RGB_HISTOGRAM_BINS);
41
+ const sumR = new Float64Array(RGB_HISTOGRAM_BINS);
42
+ const sumG = new Float64Array(RGB_HISTOGRAM_BINS);
43
+ const sumB = new Float64Array(RGB_HISTOGRAM_BINS);
44
+ const touched = [];
45
+ const { data } = image;
46
+ let total = 0;
47
+ for (let offset = 0; offset < data.length; offset += 4) {
48
+ if (!data[offset + 3])
49
+ continue;
50
+ total++;
51
+ const r = data[offset];
52
+ const g = data[offset + 1];
53
+ const b = data[offset + 2];
54
+ const index = (r >>> 3) | ((g >>> 3) << 5) | ((b >>> 3) << 10);
55
+ if (counts[index] === 0)
56
+ touched.push(index);
57
+ counts[index] += 1;
58
+ sumR[index] += r;
59
+ sumG[index] += g;
60
+ sumB[index] += b;
61
+ }
62
+ touched.sort((left, right) => counts[right] - counts[left] || left - right);
63
+ const entries = [];
64
+ for (let i = 0; i < touched.length; i++) {
65
+ const index = touched[i];
66
+ const count = counts[index];
67
+ entries.push({
68
+ color: [
69
+ clampByte(sumR[index] / count),
70
+ clampByte(sumG[index] / count),
71
+ clampByte(sumB[index] / count),
72
+ UINT8_MAX,
73
+ ],
74
+ count,
75
+ coverage: count / total,
76
+ });
77
+ }
78
+ return optimizePalette(entries, maxColors);
79
+ }
80
+ function histogramPalette(image, maxColors, ignoreTransparent, opaqueVisible) {
81
+ if (opaqueVisible && ignoreTransparent)
82
+ return opaqueHistogramPalette(image, maxColors);
83
+ const counts = new Uint32Array(HISTOGRAM_BINS);
84
+ const sumR = new Float64Array(HISTOGRAM_BINS);
85
+ const sumG = new Float64Array(HISTOGRAM_BINS);
86
+ const sumB = new Float64Array(HISTOGRAM_BINS);
87
+ const sumA = new Float64Array(HISTOGRAM_BINS);
88
+ const touched = [];
89
+ const { data } = image;
90
+ let total = 0;
91
+ for (let offset = 0; offset < data.length; offset += 4) {
92
+ const a = opaqueVisible && data[offset + 3] ? UINT8_MAX : data[offset + 3];
93
+ if (ignoreTransparent && !a)
94
+ continue;
95
+ total++;
96
+ const r = a ? data[offset] : 0;
97
+ const g = a ? data[offset + 1] : 0;
98
+ const b = a ? data[offset + 2] : 0;
99
+ const index = histogramIndex(r, g, b, a);
100
+ if (counts[index] === 0)
101
+ touched.push(index);
102
+ counts[index] += 1;
103
+ sumR[index] += r;
104
+ sumG[index] += g;
105
+ sumB[index] += b;
106
+ sumA[index] += a;
107
+ }
108
+ touched.sort((left, right) => counts[right] - counts[left] || left - right);
109
+ const entries = [];
110
+ for (let i = 0; i < touched.length; i += 1) {
111
+ const index = touched[i];
112
+ const count = counts[index];
113
+ entries.push({
114
+ color: [
115
+ clampByte(sumR[index] / count),
116
+ clampByte(sumG[index] / count),
117
+ clampByte(sumB[index] / count),
118
+ clampByte(sumA[index] / count),
119
+ ],
120
+ count,
121
+ coverage: count / total,
122
+ });
123
+ }
124
+ return optimizePalette(entries, maxColors);
125
+ }
126
+ /**
127
+ * Extract a deterministic, bounded palette. Exact colors are retained for
128
+ * ordinary sprites; a fixed 32x32x32x16 histogram is used for noisy images so
129
+ * memory stays bounded instead of growing with every photograph pixel.
130
+ */
131
+ export function extractPalette(image, options = {}) {
132
+ return extractPaletteWithAlpha(image, options, false);
133
+ }
134
+ function extractPaletteWithAlpha(image, options, opaqueVisible) {
135
+ assertPixelBuffer(image);
136
+ const maxColors = paletteLimit(options.maxColors);
137
+ const exact = new Map();
138
+ const data = image.data;
139
+ let overflowed = false;
140
+ let total = 0;
141
+ for (let offset = 0; offset < data.length; offset += 4) {
142
+ if (options.ignoreTransparent && !data[offset + 3])
143
+ continue;
144
+ total++;
145
+ const key = colorKey(data, offset, opaqueVisible);
146
+ const previous = exact.get(key);
147
+ if (previous !== undefined) {
148
+ exact.set(key, previous + 1);
149
+ }
150
+ else if (exact.size < MAX_EXACT_COLORS) {
151
+ exact.set(key, 1);
152
+ }
153
+ else {
154
+ overflowed = true;
155
+ break;
156
+ }
157
+ }
158
+ if (overflowed)
159
+ return histogramPalette(image, maxColors, options.ignoreTransparent ?? false, opaqueVisible);
160
+ const entries = [...exact.entries()]
161
+ .sort(([keyA, countA], [keyB, countB]) => countB - countA || (keyA >>> 0) - (keyB >>> 0))
162
+ .map(([key, count]) => ({
163
+ color: colorFromKey(key),
164
+ count,
165
+ coverage: count / total,
166
+ }));
167
+ return optimizePalette(entries, maxColors);
168
+ }
169
+ /** Count distinct RGBA values with a bounded result for large images. */
170
+ export function countDistinctColors(image, limit = MAX_EXACT_COLORS) {
171
+ assertPixelBuffer(image);
172
+ if (!Number.isSafeInteger(limit) || limit < 1)
173
+ throw new RangeError("limit must be a positive integer");
174
+ const colors = new Set();
175
+ for (let offset = 0; offset < image.data.length; offset += 4) {
176
+ colors.add(colorKey(image.data, offset));
177
+ if (colors.size > limit)
178
+ return limit + 1;
179
+ }
180
+ return colors.size;
181
+ }
182
+ function validatePalette(palette) {
183
+ if (!Array.isArray(palette) || palette.length < 1 || palette.length > 256) {
184
+ throw new RangeError("palette must contain between 1 and 256 colors");
185
+ }
186
+ return palette.map((color, index) => {
187
+ if (!Array.isArray(color) ||
188
+ color.length !== 4 ||
189
+ color.some((channel) => !Number.isFinite(channel))) {
190
+ throw new TypeError(`palette[${index}] must be an RGBA tuple`);
191
+ }
192
+ return [
193
+ clampByte(color[0]),
194
+ clampByte(color[1]),
195
+ clampByte(color[2]),
196
+ clampByte(color[3]),
197
+ ];
198
+ });
199
+ }
200
+ function paletteMatcher(palette, preserveAlpha = false) {
201
+ const colors = validatePalette(palette);
202
+ const transparent = colors.some((color) => color[3] === 0);
203
+ const cache = new Map();
204
+ const coordinates = colors.map((color) => paletteCoordinates(preserveAlpha && color[3] ? [color[0], color[1], color[2], UINT8_MAX] : color));
205
+ const nearest = (r, g, b, a) => {
206
+ const opacity = preserveAlpha && a ? UINT8_MAX : a;
207
+ const cacheKey = opacity ? (r << 24) | (g << 16) | (b << 8) | opacity : 0;
208
+ const cached = cache.get(cacheKey);
209
+ if (cached !== undefined)
210
+ return cached;
211
+ const sample = paletteCoordinates([r, g, b, opacity]);
212
+ let best = 0, bestDistance = Number.POSITIVE_INFINITY;
213
+ for (let i = 0; i < coordinates.length; i++) {
214
+ if (preserveAlpha && a && !colors[i][3])
215
+ continue;
216
+ const color = coordinates[i];
217
+ const dl = sample[0] - color[0], da = sample[1] - color[1], db = sample[2] - color[2], opacityDifference = sample[3] - color[3];
218
+ const distance = dl * dl + da * da + db * db + opacityDifference * opacityDifference;
219
+ if (distance < bestDistance) {
220
+ bestDistance = distance;
221
+ best = i;
222
+ }
223
+ }
224
+ if (cache.size < REDUCTION_CACHE_SIZE)
225
+ cache.set(cacheKey, best);
226
+ return best;
227
+ };
228
+ return { colors, transparent, nearest };
229
+ }
230
+ /** Perceptual matching with exact RGBA cache keys; colors within a coarse bin may differ. */
231
+ export function reduceToPalette(image, palette, options = {}) {
232
+ assertPixelBuffer(image);
233
+ const { colors, transparent, nearest } = paletteMatcher(palette, options.preserveAlpha);
234
+ const output = new Uint8ClampedArray(image.data.length);
235
+ const source = image.data;
236
+ for (let offset = 0; offset < source.length; offset += 4) {
237
+ if ((transparent || options.preserveAlpha) && source[offset + 3] === 0)
238
+ continue;
239
+ const index = nearest(source[offset], source[offset + 1], source[offset + 2], source[offset + 3]);
240
+ const color = colors[index];
241
+ output[offset] = color[0];
242
+ output[offset + 1] = color[1];
243
+ output[offset + 2] = color[2];
244
+ output[offset + 3] = options.preserveAlpha ? source[offset + 3] : color[3];
245
+ }
246
+ return { width: image.width, height: image.height, data: output };
247
+ }
248
+ /** Palette generation counts visible RGB independently of coverage. */
249
+ export function extractPixelationPalette(image, options = {}) {
250
+ const entries = extractPaletteWithAlpha(image, { ...options, ignoreTransparent: true }, true);
251
+ return entries.length
252
+ ? entries
253
+ : [{ color: [0, 0, 0, 0], count: image.width * image.height, coverage: 1 }];
254
+ }
255
+ /** Compact mapped-RGB groups for the bounded Structure/TwoStage palettes. */
256
+ export function reduceToPixelationLabels(image, palette) {
257
+ assertPixelBuffer(image);
258
+ const { colors, nearest } = paletteMatcher(palette, true);
259
+ const keys = [0];
260
+ const groups = new Map([[0, 0]]);
261
+ const paletteGroups = colors.map((color) => {
262
+ const key = (color[0] << 16) | (color[1] << 8) | color[2];
263
+ let group = groups.get(key);
264
+ if (group === undefined) {
265
+ group = keys.length;
266
+ groups.set(key, group);
267
+ keys.push(key);
268
+ }
269
+ return group;
270
+ });
271
+ const data = new Uint8Array(image.width * image.height);
272
+ const source = image.data;
273
+ for (let offset = 0; offset < source.length; offset += 4) {
274
+ if (source[offset + 3] === 0)
275
+ continue;
276
+ data[offset / 4] =
277
+ paletteGroups[nearest(source[offset], source[offset + 1], source[offset + 2], source[offset + 3])];
278
+ }
279
+ return { data, keys };
280
+ }
@@ -0,0 +1,15 @@
1
+ import type { PixelBuffer } from "./base.ts";
2
+ import type { PixelateOptions } from "./types.ts";
3
+ /**
4
+ * Pixelate an RGBA buffer without touching the source. Nearest mode follows
5
+ * nearest-neighbor `floor(dst * src / dst)` mapping. Box mode averages the
6
+ * source area covered by each destination pixel. Local dominant and structural
7
+ * sampling preserve contrasting regions; grid recovery restores one RGBA pixel
8
+ * per detected cell. Limiting colours keeps each pixel's own alpha.
9
+ */
10
+ export declare function pixelateImage(image: PixelBuffer, options?: PixelateOptions): PixelBuffer;
11
+ /**
12
+ * Async pixelate for product Auto path (import-worker). Same output as pixelateImage;
13
+ * Auto detect overlaps WASM AC+RL with proposers via Promise join (no COOP/COEP).
14
+ */
15
+ export declare function pixelateImageAsync(image: PixelBuffer, options?: PixelateOptions): Promise<PixelBuffer>;
@@ -0,0 +1,214 @@
1
+ import { assertDimension, assertPixelBuffer } from "./base.js";
2
+ import { protectPixelationContours } from "./contours.js";
3
+ import { smoothPixelation } from "./denoise.js";
4
+ import { blockPartition } from "./grid.js";
5
+ import { prepareGridSampling } from "./grid-sampling.js";
6
+ import { runPixeloe } from "./line-guided/pixeloe-entry.js";
7
+ import { resizeMostFrequent } from "./most-frequent.js";
8
+ import { applyOutlinePrepass, DEFAULT_LINE_GUIDE, runsStockPixeloe, } from "./outline-prepass.js";
9
+ import { snapPalette } from "./palette-snap.js";
10
+ import { detectWithAlgorithm, detectWithAlgorithmAsync, recoverWithGrid, } from "./pixelation-algorithm-registry.js";
11
+ import { resizeBox, resizeNearest } from "./resize.js";
12
+ import { samplePixelation } from "./sampling.js";
13
+ import { OutlinePrepass, PixelationMethod, PixelationMode, PixelationAlgorithm, } from "./types.js";
14
+ const WORKING_SCALE = 4;
15
+ const MAX_WORKING_PIXELS = 1_048_576;
16
+ const MAX_OUTLINE_STRENGTH = 100;
17
+ function prepareSource(image, target, outlineStrength) {
18
+ const scale = Math.min(WORKING_SCALE, Math.sqrt(MAX_WORKING_PIXELS / (target.width * target.height)));
19
+ const width = Math.min(image.width, Math.max(1, Math.ceil(target.width * scale)));
20
+ const height = Math.min(image.height, Math.max(1, Math.ceil(target.height * scale)));
21
+ const working = image.width === width && image.height === height
22
+ ? image
23
+ : resizeBox(image, width, height, "premultiplied");
24
+ return protectPixelationContours(working, outlineStrength);
25
+ }
26
+ function uniformBlocks(image, target) {
27
+ return {
28
+ ...blockPartition(image, target.width, target.height),
29
+ confidence: 1,
30
+ reliable: true,
31
+ algorithm: PixelationAlgorithm.UniformGrid,
32
+ };
33
+ }
34
+ function sampleImage(image, options, method) {
35
+ // Grid recovery runs when asked for, or when only an algorithm was named and it finds a reliable grid.
36
+ const wantsGrid = options.mode === PixelationMode.RecoverGrid ||
37
+ (options.mode === undefined && options.algorithm !== undefined);
38
+ const grid = options.grid ??
39
+ (wantsGrid
40
+ ? detectWithAlgorithm(image, options.algorithm ?? PixelationAlgorithm.Auto)
41
+ : undefined);
42
+ const mode = options.mode ??
43
+ (grid && (!("reliable" in grid) || grid.reliable)
44
+ ? PixelationMode.RecoverGrid
45
+ : PixelationMode.Convert);
46
+ if (mode !== PixelationMode.Convert && mode !== PixelationMode.RecoverGrid)
47
+ throw new RangeError("Unknown pixelation mode.");
48
+ const prepass = options.prepass ?? OutlinePrepass.None;
49
+ const guide = prepass === OutlinePrepass.PixeloeLines ? (options.lineGuide ?? DEFAULT_LINE_GUIDE) : undefined;
50
+ if (mode === PixelationMode.RecoverGrid) {
51
+ const found = grid ?? detectWithAlgorithm(image, options.algorithm ?? PixelationAlgorithm.Auto);
52
+ if ("reliable" in found && !found.reliable)
53
+ throw new Error("No consistent pixel grid was detected. Turn off Automatic grid and choose cell sizes.");
54
+ const cell = (found.cellWidth + found.cellHeight) / 2;
55
+ const analysis = found;
56
+ const upright = prepareGridSampling(image, analysis);
57
+ const gridForRecover = { ...analysis, rotation: undefined, sampling: undefined };
58
+ // Custom size's even blocks with PixelOE + Contrast: stock PixelOE to that size.
59
+ if (runsStockPixeloe({ ...options, mode }, found))
60
+ return runPixeloe(upright, { width: found.width, height: found.height }, guide);
61
+ return recoverWithGrid(applyOutlinePrepass(upright, cell, prepass, options.lineGuide), gridForRecover, options.colorPicker);
62
+ }
63
+ const target = resolveTargetSize(image, options);
64
+ // PixelOE + Contrast without a recovered grid is stock PixelOE: the reference result.
65
+ if (runsStockPixeloe({ ...options, mode }, undefined))
66
+ return runPixeloe(image, target, guide);
67
+ // Otherwise an independent pre-pass; whatever sampler or colour picker follows works on its result.
68
+ image = applyOutlinePrepass(image, (image.width / target.width + image.height / target.height) / 2, prepass, options.lineGuide);
69
+ const strength = options.outlineStrength ?? 0;
70
+ if (!Number.isFinite(strength) || strength < 0 || strength > MAX_OUTLINE_STRENGTH)
71
+ throw new RangeError("Outline protection must be between 0 and 100.");
72
+ if (options.blockPicker)
73
+ return recoverWithGrid(image, uniformBlocks(image, target), options.colorPicker);
74
+ const alphaMode = options.alphaMode ?? "straight";
75
+ if (alphaMode !== "straight" && alphaMode !== "premultiplied")
76
+ throw new RangeError("alphaMode must be 'straight' or 'premultiplied'");
77
+ const prepared = strength || method === PixelationMethod.Dominant || method === PixelationMethod.Structure
78
+ ? prepareSource(image, target, strength)
79
+ : image;
80
+ if (method === PixelationMethod.Nearest)
81
+ return resizeNearest(prepared, target.width, target.height);
82
+ if (method === PixelationMethod.Box)
83
+ return resizeBox(prepared, target.width, target.height, alphaMode);
84
+ if (method === PixelationMethod.MostFrequent)
85
+ return resizeMostFrequent(prepared, target.width, target.height);
86
+ return samplePixelation(prepared, target.width, target.height, method);
87
+ }
88
+ function sizeFromBlock(blockSize, image) {
89
+ if (typeof blockSize === "number") {
90
+ assertDimension(blockSize, "blockSize");
91
+ return {
92
+ width: Math.max(1, Math.ceil(image.width / blockSize)),
93
+ height: Math.max(1, Math.ceil(image.height / blockSize)),
94
+ };
95
+ }
96
+ if (!blockSize || typeof blockSize !== "object")
97
+ throw new TypeError("blockSize must be a positive number or size");
98
+ assertDimension(blockSize.width, "blockSize.width");
99
+ assertDimension(blockSize.height, "blockSize.height");
100
+ return {
101
+ width: Math.max(1, Math.ceil(image.width / blockSize.width)),
102
+ height: Math.max(1, Math.ceil(image.height / blockSize.height)),
103
+ };
104
+ }
105
+ function resolveTargetSize(image, options) {
106
+ const hasTargetFields = options.targetWidth !== undefined || options.targetHeight !== undefined;
107
+ const hasTargetObject = options.targetSize !== undefined;
108
+ if (hasTargetFields && hasTargetObject)
109
+ throw new RangeError("Use a target size object or targetWidth/targetHeight, not both");
110
+ if ((options.targetWidth !== undefined || options.targetHeight !== undefined) &&
111
+ options.blockSize !== undefined) {
112
+ throw new RangeError("Use target dimensions or blockSize, not both");
113
+ }
114
+ if (options.targetSize && options.blockSize !== undefined) {
115
+ throw new RangeError("Use target dimensions or blockSize, not both");
116
+ }
117
+ if (options.blockSize !== undefined)
118
+ return sizeFromBlock(options.blockSize, image);
119
+ const targetObject = options.targetSize;
120
+ const targetWidth = targetObject?.width ?? options.targetWidth;
121
+ const targetHeight = targetObject?.height ?? options.targetHeight;
122
+ if (targetWidth === undefined && targetHeight === undefined)
123
+ return { width: image.width, height: image.height };
124
+ if (targetWidth !== undefined)
125
+ assertDimension(targetWidth, "targetWidth");
126
+ if (targetHeight !== undefined)
127
+ assertDimension(targetHeight, "targetHeight");
128
+ if (targetWidth === undefined) {
129
+ return {
130
+ width: Math.max(1, Math.round((image.width * targetHeight) / image.height)),
131
+ height: targetHeight,
132
+ };
133
+ }
134
+ if (targetHeight === undefined) {
135
+ return {
136
+ width: targetWidth,
137
+ height: Math.max(1, Math.round((image.height * targetWidth) / image.width)),
138
+ };
139
+ }
140
+ return { width: targetWidth, height: targetHeight };
141
+ }
142
+ function validateMethod(value) {
143
+ const method = value ?? PixelationMethod.Nearest;
144
+ if (!Object.values(PixelationMethod).includes(method))
145
+ throw new RangeError("Unknown pixelation method.");
146
+ return method;
147
+ }
148
+ /**
149
+ * Pixelate an RGBA buffer without touching the source. Nearest mode follows
150
+ * nearest-neighbor `floor(dst * src / dst)` mapping. Box mode averages the
151
+ * source area covered by each destination pixel. Local dominant and structural
152
+ * sampling preserve contrasting regions; grid recovery restores one RGBA pixel
153
+ * per detected cell. Limiting colours keeps each pixel's own alpha.
154
+ */
155
+ export function pixelateImage(image, options = {}) {
156
+ assertPixelBuffer(image);
157
+ const method = validateMethod(options.method);
158
+ const sampled = sampleImage(image, options, method);
159
+ const smoothed = options.removeNoise ? smoothPixelation(sampled) : sampled;
160
+ // Limit colors: Lab k-means that keeps rare colours, or snapping to a loaded palette.
161
+ let result = options.paletteSnap ? snapPalette(smoothed, options.paletteSnap) : smoothed;
162
+ if (options.preserveDimensions &&
163
+ (result.width !== image.width || result.height !== image.height)) {
164
+ result = resizeNearest(result, image.width, image.height);
165
+ }
166
+ return result;
167
+ }
168
+ async function sampleImageAsync(image, options, method) {
169
+ const wantsGrid = options.mode === PixelationMode.RecoverGrid ||
170
+ (options.mode === undefined && options.algorithm !== undefined);
171
+ const grid = options.grid ??
172
+ (wantsGrid
173
+ ? await detectWithAlgorithmAsync(image, options.algorithm ?? PixelationAlgorithm.Auto)
174
+ : undefined);
175
+ const mode = options.mode ??
176
+ (grid && (!("reliable" in grid) || grid.reliable)
177
+ ? PixelationMode.RecoverGrid
178
+ : PixelationMode.Convert);
179
+ if (mode !== PixelationMode.Convert && mode !== PixelationMode.RecoverGrid)
180
+ throw new RangeError("Unknown pixelation mode.");
181
+ const prepass = options.prepass ?? OutlinePrepass.None;
182
+ const guide = prepass === OutlinePrepass.PixeloeLines ? (options.lineGuide ?? DEFAULT_LINE_GUIDE) : undefined;
183
+ if (mode === PixelationMode.RecoverGrid) {
184
+ const found = grid ??
185
+ (await detectWithAlgorithmAsync(image, options.algorithm ?? PixelationAlgorithm.Auto));
186
+ if ("reliable" in found && !found.reliable)
187
+ throw new Error("No consistent pixel grid was detected. Turn off Automatic grid and choose cell sizes.");
188
+ const cell = (found.cellWidth + found.cellHeight) / 2;
189
+ const analysis = found;
190
+ const upright = prepareGridSampling(image, analysis);
191
+ const gridForRecover = { ...analysis, rotation: undefined, sampling: undefined };
192
+ if (runsStockPixeloe({ ...options, mode }, found))
193
+ return runPixeloe(upright, { width: found.width, height: found.height }, guide);
194
+ return recoverWithGrid(applyOutlinePrepass(upright, cell, prepass, options.lineGuide), gridForRecover, options.colorPicker);
195
+ }
196
+ // Convert path is sync — reuse sampleImage body for non-recover.
197
+ return sampleImage(image, { ...options, grid: undefined, mode: PixelationMode.Convert }, method);
198
+ }
199
+ /**
200
+ * Async pixelate for product Auto path (import-worker). Same output as pixelateImage;
201
+ * Auto detect overlaps WASM AC+RL with proposers via Promise join (no COOP/COEP).
202
+ */
203
+ export async function pixelateImageAsync(image, options = {}) {
204
+ assertPixelBuffer(image);
205
+ const method = validateMethod(options.method);
206
+ const sampled = await sampleImageAsync(image, options, method);
207
+ const smoothed = options.removeNoise ? smoothPixelation(sampled) : sampled;
208
+ let result = options.paletteSnap ? snapPalette(smoothed, options.paletteSnap) : smoothed;
209
+ if (options.preserveDimensions &&
210
+ (result.width !== image.width || result.height !== image.height)) {
211
+ result = resizeNearest(result, image.width, image.height);
212
+ }
213
+ return result;
214
+ }
@@ -0,0 +1,34 @@
1
+ import type { PixelBuffer } from "./base.ts";
2
+ import { CellColorPicker, PixelationAlgorithm, type PixelGridAnalysis, type PixelGridGeometry, type PixelationSize } from "./types.ts";
3
+ export interface PixelationAlgorithmEntry {
4
+ /** i18n key of the label shown in the import dialog and preferences. */
5
+ readonly label: string;
6
+ /**
7
+ * Whether "Find pixels by" offers this entry; the plain resize is only Auto's fallback, and the
8
+ * single proposers are Fixed grid's sources (see getGridSources).
9
+ */
10
+ readonly listed: boolean;
11
+ /**
12
+ * Sync detect for most algorithms. Auto also exposes detectAsync for product overlap
13
+ * (browser nested Worker join without COOP/COEP).
14
+ */
15
+ detect(image: PixelBuffer, size?: PixelationSize): PixelGridAnalysis;
16
+ /** Optional async detect (Auto): awaitable WASM||proposers overlap. */
17
+ detectAsync?(image: PixelBuffer, size?: PixelationSize): Promise<PixelGridAnalysis>;
18
+ /** One output pixel per grid cell. */
19
+ run(image: PixelBuffer, grid: PixelGridGeometry, picker: CellColorPicker): PixelBuffer;
20
+ }
21
+ export declare function getAlgorithmEntry(algorithm: PixelationAlgorithm): PixelationAlgorithmEntry;
22
+ export declare function getAllAlgorithms(): readonly PixelationAlgorithm[];
23
+ /** Entries the import dialog offers, in registry order. */
24
+ export declare function getListedAlgorithms(): readonly PixelationAlgorithm[];
25
+ export declare function getGridSources(): readonly PixelationAlgorithm[];
26
+ /** The "Find pixels by" entry an algorithm belongs to: a grid source is a kind of Fixed grid. */
27
+ export declare function pixelationMethodOf(algorithm: PixelationAlgorithm): PixelationAlgorithm;
28
+ export declare function detectWithAlgorithm(image: PixelBuffer, algorithm?: PixelationAlgorithm, size?: PixelationSize): PixelGridAnalysis;
29
+ /**
30
+ * Async detect for product Auto path: uses detectAsync when present (WASM||proposers
31
+ * overlap via nested Worker Promise join). Other algorithms resolve sync detect.
32
+ */
33
+ export declare function detectWithAlgorithmAsync(image: PixelBuffer, algorithm?: PixelationAlgorithm, size?: PixelationSize): Promise<PixelGridAnalysis>;
34
+ export declare function recoverWithGrid(image: PixelBuffer, grid: PixelGridGeometry, picker?: CellColorPicker): PixelBuffer;