smart-downscaler 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,9 +4,39 @@ A sophisticated Rust library for intelligent image downscaling with focus on pix
4
4
 
5
5
  **Available as both a native Rust library and a WebAssembly module for browser/Node.js usage.**
6
6
 
7
+ ## What's New in v0.2
8
+
9
+ ### 🎨 Oklab Color Space
10
+
11
+ Version 0.2 introduces **Oklab color space** for all color operations, solving the common problem of desaturated, muddy colors:
12
+
13
+ | Before (RGB Median Cut) | After (Oklab Median Cut) |
14
+ |-------------------------|--------------------------|
15
+ | Colors appear tanned/darkened | True color preservation |
16
+ | Saturated colors become muddy | Vibrant colors maintained |
17
+ | RGB averaging loses chroma | Perceptually uniform blending |
18
+
19
+ ### Palette Strategies
20
+
21
+ Choose the best strategy for your use case:
22
+
23
+ ```javascript
24
+ const config = new WasmDownscaleConfig();
25
+ config.palette_strategy = 'saturation'; // For vibrant pixel art
26
+ ```
27
+
28
+ | Strategy | Best For | Description |
29
+ |----------|----------|-------------|
30
+ | `oklab` | General use | Default, best overall quality |
31
+ | `saturation` | Vibrant art | Preserves highly saturated colors |
32
+ | `medoid` | Exact colors | Only uses colors from source image |
33
+ | `kmeans` | Small palettes | K-Means++ clustering |
34
+ | `legacy` | Comparison | Original RGB (not recommended) |
35
+
7
36
  ## Features
8
37
 
9
- - **Global Palette Extraction**: Median Cut + K-Means++ refinement in Lab color space for perceptually optimal palettes
38
+ - **Oklab Color Space**: Modern perceptual color space with superior hue linearity
39
+ - **Global Palette Extraction**: Median Cut + K-Means++ refinement
10
40
  - **Multiple Segmentation Methods**:
11
41
  - SLIC superpixels for fast, balanced regions
12
42
  - VTracer-style hierarchical clustering for content-aware boundaries
@@ -14,7 +44,6 @@ A sophisticated Rust library for intelligent image downscaling with focus on pix
14
44
  - **Edge-Aware Processing**: Sobel/Scharr edge detection to preserve boundaries
15
45
  - **Neighbor-Coherent Assignment**: Spatial coherence through neighbor and region voting
16
46
  - **Two-Pass Refinement**: Iterative optimization for smooth results
17
- - **Graph-Cut Optimization**: Optional MRF energy minimization for advanced refinement
18
47
  - **WebAssembly Support**: Run in browsers with full performance
19
48
 
20
49
  ## Installation
@@ -25,7 +54,7 @@ Add to your `Cargo.toml`:
25
54
 
26
55
  ```toml
27
56
  [dependencies]
28
- smart-downscaler = "0.1"
57
+ smart-downscaler = "0.2"
29
58
  ```
30
59
 
31
60
  ### WebAssembly (npm)
@@ -49,11 +78,24 @@ Or use directly in browser:
49
78
 
50
79
  ```rust
51
80
  use smart_downscaler::prelude::*;
81
+ use smart_downscaler::palette::PaletteStrategy;
52
82
 
53
83
  fn main() {
54
84
  let img = image::open("input.png").unwrap().to_rgb8();
85
+
86
+ // Simple usage
55
87
  let result = downscale(&img, 64, 64, 16);
56
88
  result.save("output.png").unwrap();
89
+
90
+ // Advanced: preserve vibrant colors
91
+ let config = DownscaleConfig {
92
+ palette_size: 24,
93
+ palette_strategy: PaletteStrategy::SaturationWeighted,
94
+ ..Default::default()
95
+ };
96
+
97
+ let pixels: Vec<Rgb> = img.pixels().map(|&p| p.into()).collect();
98
+ let result = smart_downscale(&pixels, img.width() as usize, img.height() as usize, 64, 64, &config);
57
99
  }
58
100
  ```
59
101
 
@@ -62,111 +104,128 @@ fn main() {
62
104
  ```javascript
63
105
  import init, { WasmDownscaleConfig, downscale_rgba } from 'smart-downscaler';
64
106
 
65
- // Initialize WASM
66
107
  await init();
67
108
 
68
- // Get image data from canvas
69
109
  const ctx = canvas.getContext('2d');
70
110
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
71
111
 
72
- // Configure
73
- const config = new WasmDownscaleConfig();
112
+ // Use vibrant preset for saturated colors
113
+ const config = WasmDownscaleConfig.vibrant();
74
114
  config.palette_size = 16;
75
- config.neighbor_weight = 0.3;
76
- config.segmentation_method = 'hierarchy_fast';
77
115
 
78
- // Downscale
116
+ // Or configure manually
117
+ const config2 = new WasmDownscaleConfig();
118
+ config2.palette_size = 16;
119
+ config2.palette_strategy = 'saturation';
120
+ config2.neighbor_weight = 0.3;
121
+
79
122
  const result = downscale_rgba(
80
123
  imageData.data,
81
124
  canvas.width,
82
125
  canvas.height,
83
- 64, 64, // target size
126
+ 64, 64,
84
127
  config
85
128
  );
86
129
 
87
- // Use result
88
130
  const outputData = new ImageData(result.data, result.width, result.height);
89
131
  outputCtx.putImageData(outputData, 0, 0);
90
-
91
- // Access palette
92
- console.log(`Used ${result.palette_size} colors`);
93
132
  ```
94
133
 
95
- ### Using the JavaScript Wrapper
96
-
97
- For an even simpler API, use the included JavaScript wrapper:
134
+ ### Configuration Presets
98
135
 
99
136
  ```javascript
100
- import { init, downscale, extractPalette, getPresetPalette } from './smart-downscaler.js';
101
-
102
- await init();
137
+ // Speed optimized
138
+ const fast = WasmDownscaleConfig.fast();
103
139
 
104
- // Simple downscale
105
- const result = downscale(canvas, 64, 64, {
106
- paletteSize: 16,
107
- segmentation: 'hierarchy_fast'
108
- });
140
+ // Best quality
141
+ const quality = WasmDownscaleConfig.quality();
109
142
 
110
- // With preset palette (Game Boy, NES, PICO-8, CGA)
111
- const gbPalette = getPresetPalette('gameboy');
112
- const gbResult = downscaleWithPalette(canvas, 64, 64, gbPalette);
143
+ // Preserve vibrant colors
144
+ const vibrant = WasmDownscaleConfig.vibrant();
113
145
 
114
- // Extract palette only
115
- const palette = extractPalette(canvas, 16);
146
+ // Use only exact source colors
147
+ const exact = WasmDownscaleConfig.exact_colors();
116
148
  ```
117
149
 
118
- ## Building WebAssembly
150
+ ## Why Oklab?
119
151
 
120
- Requirements: [wasm-pack](https://rustwasm.github.io/wasm-pack/)
152
+ Traditional RGB-based palette extraction has fundamental problems:
121
153
 
122
- ```bash
123
- # Install wasm-pack
124
- cargo install wasm-pack
154
+ ### The Problem: RGB Averaging Desaturates Colors
155
+
156
+ ```
157
+ Red [255, 0, 0] + Cyan [0, 255, 255]
158
+ RGB Average → Gray [127, 127, 127] ❌
159
+ ```
160
+
161
+ When you average colors in RGB space, saturated colors get pulled toward gray. This is why downscaled images often look "washed out" or "tanned."
125
162
 
126
- # Build all targets
127
- ./build-wasm.sh
163
+ ### The Solution: Oklab Color Space
128
164
 
129
- # Or manually:
130
- wasm-pack build --target web --features wasm --no-default-features --out-dir pkg/web
165
+ Oklab is a **perceptually uniform** color space where:
166
+ - Euclidean distance = perceived color difference
167
+ - Averaging preserves hue and saturation
168
+ - Interpolations look natural
169
+
170
+ ```
171
+ Red (Oklab) + Cyan (Oklab)
172
+ Oklab Average → Preserves colorfulness ✓
131
173
  ```
132
174
 
133
- ## WASM API Reference
175
+ ### Visual Comparison
134
176
 
135
- ### Functions
177
+ | Issue | RGB Median Cut | Oklab Median Cut |
178
+ |-------|----------------|------------------|
179
+ | Saturated colors | Become muddy | Stay vibrant |
180
+ | Gradients | Shift in hue | Stay consistent |
181
+ | Dark colors | Get darker | Accurate lightness |
182
+ | Overall look | Desaturated | True to source |
136
183
 
137
- | Function | Description |
138
- |----------|-------------|
139
- | `downscale(data, w, h, tw, th, config?)` | Downscale RGBA image data |
140
- | `downscale_rgba(data, w, h, tw, th, config?)` | Same, for Uint8ClampedArray |
141
- | `downscale_simple(data, w, h, tw, th, colors)` | Simple API with color count |
142
- | `downscale_with_palette(data, w, h, tw, th, palette, config?)` | Use custom palette |
143
- | `extract_palette_from_image(data, w, h, colors, iters)` | Extract palette only |
144
- | `quantize_to_palette(data, w, h, palette)` | Quantize without resizing |
184
+ ## API Reference
145
185
 
146
186
  ### WasmDownscaleConfig
147
187
 
148
188
  ```javascript
149
189
  const config = new WasmDownscaleConfig();
150
- config.palette_size = 16; // Output colors
151
- config.kmeans_iterations = 5; // Palette refinement
152
- config.neighbor_weight = 0.3; // Spatial coherence [0-1]
153
- config.region_weight = 0.2; // Region coherence [0-1]
154
- config.two_pass_refinement = true; // Enable refinement
155
- config.refinement_iterations = 3; // Refinement passes
156
- config.edge_weight = 0.5; // Edge detection weight
157
- config.segmentation_method = 'hierarchy_fast'; // 'none'|'slic'|'hierarchy'|'hierarchy_fast'
158
-
159
- // Presets
160
- const fast = WasmDownscaleConfig.fast();
161
- const quality = WasmDownscaleConfig.quality();
190
+
191
+ // Palette settings
192
+ config.palette_size = 16; // Number of output colors
193
+ config.palette_strategy = 'oklab'; // 'oklab', 'saturation', 'medoid', 'kmeans', 'legacy'
194
+ config.kmeans_iterations = 5; // Refinement iterations
195
+
196
+ // Spatial coherence
197
+ config.neighbor_weight = 0.3; // [0-1] Prefer neighbor colors
198
+ config.region_weight = 0.2; // [0-1] Prefer region colors
199
+
200
+ // Refinement
201
+ config.two_pass_refinement = true;
202
+ config.refinement_iterations = 3;
203
+
204
+ // Edge detection
205
+ config.edge_weight = 0.5;
206
+
207
+ // Segmentation
208
+ config.segmentation_method = 'hierarchy_fast'; // 'none', 'slic', 'hierarchy', 'hierarchy_fast'
162
209
  ```
163
210
 
211
+ ### Available Functions
212
+
213
+ | Function | Description |
214
+ |----------|-------------|
215
+ | `downscale(data, w, h, tw, th, config?)` | Main downscale function |
216
+ | `downscale_rgba(data, w, h, tw, th, config?)` | For Uint8ClampedArray input |
217
+ | `downscale_simple(data, w, h, tw, th, colors)` | Simple API |
218
+ | `downscale_with_palette(...)` | Use custom palette |
219
+ | `extract_palette_from_image(data, w, h, colors, iters, strategy?)` | Extract palette only |
220
+ | `quantize_to_palette(data, w, h, palette)` | Quantize without resizing |
221
+ | `get_palette_strategies()` | List available strategies |
222
+
164
223
  ### WasmDownscaleResult
165
224
 
166
225
  ```javascript
167
226
  result.width // Output width
168
- result.height // Output height
169
- result.data // Uint8ClampedArray (RGBA for ImageData)
227
+ result.height // Output height
228
+ result.data // Uint8ClampedArray (RGBA)
170
229
  result.rgb_data() // Uint8Array (RGB only)
171
230
  result.palette // Uint8Array (RGB, 3 bytes per color)
172
231
  result.indices // Uint8Array (palette index per pixel)
@@ -179,143 +238,129 @@ result.palette_size // Number of colors
179
238
  # Basic usage
180
239
  smart-downscaler input.png output.png -w 64 -h 64
181
240
 
182
- # With custom palette size
183
- smart-downscaler input.png output.png -w 128 -h 128 -p 32
241
+ # With saturation preservation
242
+ smart-downscaler input.png output.png -w 64 -h 64 --palette-strategy saturation
184
243
 
185
- # Using scale factor
186
- smart-downscaler input.png output.png -s 0.125 -p 16
244
+ # Using exact source colors only
245
+ smart-downscaler input.png output.png -w 64 -h 64 --palette-strategy medoid -p 24
187
246
 
188
- # With SLIC segmentation
189
- smart-downscaler input.png output.png -w 64 -h 64 --segmentation slic
247
+ # Full options
248
+ smart-downscaler input.png output.png \
249
+ -w 128 -h 128 \
250
+ -p 32 \
251
+ --palette-strategy saturation \
252
+ --segmentation hierarchy-fast \
253
+ --neighbor-weight 0.4
190
254
  ```
191
255
 
192
256
  ## Algorithm Details
193
257
 
194
- ### 1. Global Palette Extraction
258
+ ### 1. Palette Extraction (Oklab Median Cut)
195
259
 
196
- The traditional per-tile k-means approach causes color drift across the image. We instead:
260
+ 1. Convert all pixels to Oklab color space
261
+ 2. Build weighted color histogram
262
+ 3. Apply Median Cut to partition Oklab space
263
+ 4. For each bucket, compute centroid in Oklab
264
+ 5. Convert centroids back to RGB
265
+ 6. Refine with K-Means++ in Oklab space
197
266
 
198
- 1. Build a weighted color histogram from all source pixels
199
- 2. Apply Median Cut to partition the color space, finding initial centroids with good distribution
200
- 3. Refine centroids using K-Means++ in CIE Lab space for perceptual accuracy
267
+ ### 2. Saturation-Weighted Strategy
201
268
 
202
- ### 2. Region Pre-Segmentation
269
+ When using `saturation` strategy:
270
+ ```
271
+ effective_weight = pixel_count × (1 + chroma × 2)
272
+ ```
203
273
 
204
- Before downscaling, we identify coherent regions to preserve:
274
+ This boosts the influence of highly saturated colors during Median Cut partitioning.
205
275
 
206
- **SLIC Superpixels:**
207
- - Iteratively clusters pixels by color and spatial proximity
208
- - Produces compact, regular regions
209
- - Fast and predictable
276
+ ### 3. Medoid Strategy
210
277
 
211
- **Hierarchical Clustering (VTracer-style):**
212
- - Bottom-up merging of similar adjacent pixels
213
- - Content-aware boundaries that follow natural edges
214
- - Configurable merge threshold and minimum region size
278
+ Instead of computing centroids (averages), selects the actual image color closest to the centroid. Guarantees output palette contains only exact source colors.
215
279
 
216
- **Fast Hierarchical (Union-Find):**
217
- - O(α(n)) per operation using union by rank + path compression
218
- - Best for large images where full hierarchical is too slow
280
+ ### 4. Region Pre-Segmentation
219
281
 
220
- ### 3. Edge-Aware Tile Computation
282
+ Before downscaling, identifies coherent regions:
283
+ - **SLIC**: Fast, regular superpixels
284
+ - **Hierarchy**: Content-aware boundaries
285
+ - **Hierarchy Fast**: O(α(n)) union-find
221
286
 
222
- Each output tile's color is computed as a weighted average of source pixels:
287
+ ### 5. Edge-Aware Tile Computation
223
288
 
224
289
  ```
225
- weight(pixel) = 1 / (1 + edge_strength * edge_weight)
290
+ weight(pixel) = 1 / (1 + edge_strength × edge_weight)
226
291
  ```
227
292
 
228
- This reduces the influence of transitional edge pixels, avoiding muddy colors from averaging across boundaries.
229
-
230
- ### 4. Neighbor-Coherent Assignment
293
+ Reduces influence of transitional edge pixels.
231
294
 
232
- When assigning each tile to a palette color, we consider:
295
+ ### 6. Neighbor-Coherent Assignment
233
296
 
234
- - **Color distance** to the tile's weighted average (primary factor)
235
- - **Neighbor votes**: already-assigned neighbors bias toward their colors
236
- - **Region membership**: tiles in the same source region prefer consistent colors
237
-
238
- The scoring function:
239
297
  ```
240
- score(color) = distance(color, tile_avg) * (1 - neighbor_bias - region_bias)
298
+ score(color) = oklab_distance(color, tile_avg) × (1 - neighbor_bias - region_bias)
241
299
  ```
242
300
 
243
- ### 5. Two-Pass Refinement
244
-
245
- After initial assignment, we iteratively refine:
246
-
247
- 1. For each pixel, gather all 8 neighbors
248
- 2. Re-evaluate the best palette color considering neighbor votes
249
- 3. Update if a better assignment is found
250
- 4. Repeat until convergence or max iterations
251
-
252
- This smooths isolated outliers while preserving intentional edges.
253
-
254
- ### 6. Graph-Cut Optimization (Optional)
255
-
256
- For highest quality, we offer MRF energy minimization:
257
-
258
- - **Data term**: color distance between tile and palette color
259
- - **Smoothness term**: penalty for different labels between neighbors
260
- - **Alpha-expansion**: iteratively try changing each pixel to each label
261
-
262
- ## Comparison with Existing Tools
263
-
264
- | Feature | Smart Downscaler | Per-Tile K-Means | Simple Resize |
265
- |---------|------------------|------------------|---------------|
266
- | Global color consistency | ✓ | ✗ | ✗ |
267
- | Edge preservation | ✓ | Partial | ✗ |
268
- | Region awareness | ✓ | ✗ | ✗ |
269
- | Spatial coherence | ✓ | ✗ | ✗ |
270
- | Two-pass refinement | ✓ | ✗ | ✗ |
271
- | Custom palette support | ✓ | ✓ | ✗ |
272
- | Perceptual color space | ✓ (Lab) | Often RGB | N/A |
273
-
274
301
  ## Performance
275
302
 
276
- Typical performance on a modern CPU (single-threaded):
303
+ Typical performance (single-threaded):
277
304
 
278
305
  | Image Size | Target Size | Palette | Time |
279
306
  |------------|-------------|---------|------|
280
307
  | 256×256 | 32×32 | 16 | ~50ms |
281
308
  | 512×512 | 64×64 | 32 | ~200ms |
282
309
  | 1024×1024 | 128×128 | 32 | ~800ms |
283
- | 2048×2048 | 256×256 | 64 | ~3s |
284
310
 
285
- Enable the `parallel` feature for multi-threaded processing on large images.
311
+ Enable `parallel` feature for multi-threaded processing.
286
312
 
287
313
  ## Configuration Reference
288
314
 
289
- ### DownscaleConfig
315
+ ### DownscaleConfig (Rust)
290
316
 
291
317
  | Field | Type | Default | Description |
292
318
  |-------|------|---------|-------------|
293
- | `palette_size` | usize | 16 | Number of colors in output palette |
294
- | `kmeans_iterations` | usize | 5 | K-Means refinement iterations |
295
- | `neighbor_weight` | f32 | 0.3 | Weight for neighbor coherence [0-1] |
296
- | `region_weight` | f32 | 0.2 | Weight for region coherence [0-1] |
297
- | `two_pass_refinement` | bool | true | Enable iterative refinement |
298
- | `refinement_iterations` | usize | 3 | Max refinement iterations |
299
- | `segmentation` | SegmentationMethod | Hierarchy | Pre-segmentation method |
300
- | `edge_weight` | f32 | 0.5 | Edge influence in tile averaging |
319
+ | `palette_size` | usize | 16 | Output colors |
320
+ | `palette_strategy` | PaletteStrategy | OklabMedianCut | Extraction method |
321
+ | `kmeans_iterations` | usize | 5 | Refinement iterations |
322
+ | `neighbor_weight` | f32 | 0.3 | Neighbor coherence |
323
+ | `region_weight` | f32 | 0.2 | Region coherence |
324
+ | `two_pass_refinement` | bool | true | Enable refinement |
325
+ | `refinement_iterations` | usize | 3 | Max iterations |
326
+ | `segmentation` | SegmentationMethod | Hierarchy | Pre-segmentation |
327
+ | `edge_weight` | f32 | 0.5 | Edge influence |
328
+
329
+ ### PaletteStrategy
330
+
331
+ | Value | Description |
332
+ |-------|-------------|
333
+ | `OklabMedianCut` | Default, best general quality |
334
+ | `SaturationWeighted` | Preserves vibrant colors |
335
+ | `Medoid` | Exact source colors only |
336
+ | `KMeansPlusPlus` | K-Means++ clustering |
337
+ | `LegacyRgb` | Original RGB (not recommended) |
338
+
339
+ ## Troubleshooting
340
+
341
+ ### Colors still look desaturated
342
+
343
+ Try increasing palette size or using `saturation` strategy:
344
+ ```javascript
345
+ config.palette_size = 24; // Up from 16
346
+ config.palette_strategy = 'saturation';
347
+ ```
301
348
 
302
- ### HierarchyConfig
349
+ ### Want exact source colors
303
350
 
304
- | Field | Type | Default | Description |
305
- |-------|------|---------|-------------|
306
- | `merge_threshold` | f32 | 15.0 | Max color distance for merging |
307
- | `min_region_size` | usize | 4 | Minimum pixels per region |
308
- | `max_regions` | usize | 0 | Max regions (0 = unlimited) |
309
- | `spatial_weight` | f32 | 0.1 | Spatial proximity influence |
351
+ Use medoid strategy with no K-Means refinement:
352
+ ```javascript
353
+ config.palette_strategy = 'medoid';
354
+ config.kmeans_iterations = 0;
355
+ ```
310
356
 
311
- ### SlicConfig
357
+ ### Output looks noisy
312
358
 
313
- | Field | Type | Default | Description |
314
- |-------|------|---------|-------------|
315
- | `num_superpixels` | usize | 100 | Approximate superpixel count |
316
- | `compactness` | f32 | 10.0 | Shape regularity (higher = more compact) |
317
- | `max_iterations` | usize | 10 | SLIC iterations |
318
- | `convergence_threshold` | f32 | 1.0 | Early termination threshold |
359
+ Increase neighbor weight for smoother results:
360
+ ```javascript
361
+ config.neighbor_weight = 0.5; // Up from 0.3
362
+ config.two_pass_refinement = true;
363
+ ```
319
364
 
320
365
  ## License
321
366
 
@@ -323,8 +368,7 @@ MIT
323
368
 
324
369
  ## Credits
325
370
 
326
- Inspired by:
327
- - VTracer's hierarchical clustering approach
371
+ - Oklab color space by Björn Ottosson
328
372
  - SLIC superpixel algorithm
329
373
  - K-Means++ initialization
330
- - CIE Lab perceptual color space
374
+ - VTracer hierarchical clustering approach
package/package.json CHANGED
@@ -5,11 +5,11 @@
5
5
  "Pixagram"
6
6
  ],
7
7
  "description": "Intelligent pixel art downscaler with region-aware color quantization",
8
- "version": "0.2.1",
8
+ "version": "0.3.1",
9
9
  "license": "MIT",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "https://github.com/pixagram-blockchain/smart-downscaler"
12
+ "url": "https://github.com/pixagram/smart-downscaler"
13
13
  },
14
14
  "files": [
15
15
  "smart_downscaler_bg.wasm",
@@ -30,4 +30,4 @@
30
30
  "quantization",
31
31
  "wasm"
32
32
  ]
33
- }
33
+ }
@@ -1,6 +1,40 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
 
4
+ export class ColorAnalysisResult {
5
+ private constructor();
6
+ free(): void;
7
+ [Symbol.dispose](): void;
8
+ /**
9
+ * Get color at index
10
+ */
11
+ get_color(index: number): ColorEntry | undefined;
12
+ /**
13
+ * Get all colors as a flat array: [r, g, b, count(4 bytes), percentage(4 bytes), ...]
14
+ * Each color is 11 bytes
15
+ */
16
+ get_colors_flat(): Uint8Array;
17
+ /**
18
+ * Get colors as JSON-compatible array
19
+ */
20
+ to_json(): any;
21
+ readonly success: boolean;
22
+ readonly color_count: number;
23
+ readonly total_pixels: number;
24
+ }
25
+
26
+ export class ColorEntry {
27
+ private constructor();
28
+ free(): void;
29
+ [Symbol.dispose](): void;
30
+ readonly r: number;
31
+ readonly g: number;
32
+ readonly b: number;
33
+ readonly count: number;
34
+ readonly percentage: number;
35
+ readonly hex: string;
36
+ }
37
+
4
38
  export class WasmDownscaleConfig {
5
39
  free(): void;
6
40
  [Symbol.dispose](): void;
@@ -16,6 +50,14 @@ export class WasmDownscaleConfig {
16
50
  * Create configuration optimized for quality
17
51
  */
18
52
  static quality(): WasmDownscaleConfig;
53
+ /**
54
+ * Create configuration optimized for vibrant colors
55
+ */
56
+ static vibrant(): WasmDownscaleConfig;
57
+ /**
58
+ * Create configuration that uses only exact image colors (medoid)
59
+ */
60
+ static exact_colors(): WasmDownscaleConfig;
19
61
  /**
20
62
  * Number of colors in the output palette (default: 16)
21
63
  */
@@ -64,6 +106,10 @@ export class WasmDownscaleConfig {
64
106
  * Get the segmentation method
65
107
  */
66
108
  segmentation_method: string;
109
+ /**
110
+ * Get the palette extraction strategy
111
+ */
112
+ palette_strategy: string;
67
113
  }
68
114
 
69
115
  export class WasmDownscaleResult {
@@ -105,15 +151,23 @@ export class WasmDownscaleResult {
105
151
  }
106
152
 
107
153
  /**
108
- * Highly optimized color analysis using a custom linear-probing hash table.
109
- *
110
- * OPTIMIZATIONS:
111
- * 1. Casts u8 slice to u32 slice (Zero Copy, 4x read speed)
112
- * 2. Uses Power-of-Two capacity for bitwise masking (vs modulo)
113
- * 3. FxHash-style integer mixer
114
- * 4. Linear probing with localized memory access
154
+ * Analyze colors in an image
155
+ *
156
+ * # Arguments
157
+ * * `image_data` - RGBA pixel data
158
+ * * `max_colors` - Maximum number of unique colors to track (stops if exceeded)
159
+ * * `sort_method` - Sorting method: "frequency", "morton", or "hilbert"
160
+ *
161
+ * # Returns
162
+ * ColorAnalysisResult with array of colors (r, g, b, count, percentage, hex)
163
+ * If unique colors exceed max_colors, returns early with success=false
115
164
  */
116
- export function analyze_colors(image_data: Uint8Array, max_colors: number, sort_method: string): any;
165
+ export function analyze_colors(image_data: Uint8Array, max_colors: number, sort_method: string): ColorAnalysisResult;
166
+
167
+ /**
168
+ * Compute perceptual color distance between two RGB colors
169
+ */
170
+ export function color_distance(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number;
117
171
 
118
172
  /**
119
173
  * Main downscaling function for WebAssembly
@@ -149,7 +203,22 @@ export function downscale_with_palette(image_data: Uint8Array, width: number, he
149
203
  /**
150
204
  * Extract a color palette from an image without downscaling
151
205
  */
152
- export function extract_palette_from_image(image_data: Uint8Array, _width: number, _height: number, num_colors: number, kmeans_iterations: number): Uint8Array;
206
+ export function extract_palette_from_image(image_data: Uint8Array, _width: number, _height: number, num_colors: number, kmeans_iterations: number, strategy?: string | null): Uint8Array;
207
+
208
+ /**
209
+ * Get chroma (saturation) of an RGB color
210
+ */
211
+ export function get_chroma(r: number, g: number, b: number): number;
212
+
213
+ /**
214
+ * Get lightness of an RGB color in Oklab space
215
+ */
216
+ export function get_lightness(r: number, g: number, b: number): number;
217
+
218
+ /**
219
+ * Get available palette strategies
220
+ */
221
+ export function get_palette_strategies(): Array<any>;
153
222
 
154
223
  /**
155
224
  * Initialize panic hook for better error messages in browser console
@@ -161,11 +230,21 @@ export function init(): void;
161
230
  */
162
231
  export function log(message: string): void;
163
232
 
233
+ /**
234
+ * Convert Oklab to RGB (utility function for JS)
235
+ */
236
+ export function oklab_to_rgb(l: number, a: number, b: number): Uint8Array;
237
+
164
238
  /**
165
239
  * Quantize an image to a specific palette without resizing
166
240
  */
167
241
  export function quantize_to_palette(image_data: Uint8Array, width: number, height: number, palette_data: Uint8Array): WasmDownscaleResult;
168
242
 
243
+ /**
244
+ * Convert RGB to Oklab (utility function for JS)
245
+ */
246
+ export function rgb_to_oklab(r: number, g: number, b: number): Float32Array;
247
+
169
248
  /**
170
249
  * Get library version
171
250
  */
@@ -3,6 +3,12 @@ export function __wbg_set_wasm(val) {
3
3
  wasm = val;
4
4
  }
5
5
 
6
+ function addToExternrefTable0(obj) {
7
+ const idx = wasm.__externref_table_alloc();
8
+ wasm.__wbindgen_externrefs.set(idx, obj);
9
+ return idx;
10
+ }
11
+
6
12
  function _assertClass(instance, klass) {
7
13
  if (!(instance instanceof klass)) {
8
14
  throw new Error(`expected instance of ${klass.name}`);
@@ -27,15 +33,17 @@ function getUint8ArrayMemory0() {
27
33
  return cachedUint8ArrayMemory0;
28
34
  }
29
35
 
30
- function isLikeNone(x) {
31
- return x === undefined || x === null;
36
+ function handleError(f, args) {
37
+ try {
38
+ return f.apply(this, args);
39
+ } catch (e) {
40
+ const idx = addToExternrefTable0(e);
41
+ wasm.__wbindgen_exn_store(idx);
42
+ }
32
43
  }
33
44
 
34
- function passArray8ToWasm0(arg, malloc) {
35
- const ptr = malloc(arg.length * 1, 1) >>> 0;
36
- getUint8ArrayMemory0().set(arg, ptr / 1);
37
- WASM_VECTOR_LEN = arg.length;
38
- return ptr;
45
+ function isLikeNone(x) {
46
+ return x === undefined || x === null;
39
47
  }
40
48
 
41
49
  function passStringToWasm0(arg, malloc, realloc) {
@@ -110,6 +118,14 @@ if (!('encodeInto' in cachedTextEncoder)) {
110
118
 
111
119
  let WASM_VECTOR_LEN = 0;
112
120
 
121
+ const ColorAnalysisResultFinalization = (typeof FinalizationRegistry === 'undefined')
122
+ ? { register: () => {}, unregister: () => {} }
123
+ : new FinalizationRegistry(ptr => wasm.__wbg_coloranalysisresult_free(ptr >>> 0, 1));
124
+
125
+ const ColorEntryFinalization = (typeof FinalizationRegistry === 'undefined')
126
+ ? { register: () => {}, unregister: () => {} }
127
+ : new FinalizationRegistry(ptr => wasm.__wbg_colorentry_free(ptr >>> 0, 1));
128
+
113
129
  const WasmDownscaleConfigFinalization = (typeof FinalizationRegistry === 'undefined')
114
130
  ? { register: () => {}, unregister: () => {} }
115
131
  : new FinalizationRegistry(ptr => wasm.__wbg_wasmdownscaleconfig_free(ptr >>> 0, 1));
@@ -118,6 +134,154 @@ const WasmDownscaleResultFinalization = (typeof FinalizationRegistry === 'undefi
118
134
  ? { register: () => {}, unregister: () => {} }
119
135
  : new FinalizationRegistry(ptr => wasm.__wbg_wasmdownscaleresult_free(ptr >>> 0, 1));
120
136
 
137
+ /**
138
+ * Result of color analysis
139
+ */
140
+ export class ColorAnalysisResult {
141
+ static __wrap(ptr) {
142
+ ptr = ptr >>> 0;
143
+ const obj = Object.create(ColorAnalysisResult.prototype);
144
+ obj.__wbg_ptr = ptr;
145
+ ColorAnalysisResultFinalization.register(obj, obj.__wbg_ptr, obj);
146
+ return obj;
147
+ }
148
+ __destroy_into_raw() {
149
+ const ptr = this.__wbg_ptr;
150
+ this.__wbg_ptr = 0;
151
+ ColorAnalysisResultFinalization.unregister(this);
152
+ return ptr;
153
+ }
154
+ free() {
155
+ const ptr = this.__destroy_into_raw();
156
+ wasm.__wbg_coloranalysisresult_free(ptr, 0);
157
+ }
158
+ /**
159
+ * @returns {boolean}
160
+ */
161
+ get success() {
162
+ const ret = wasm.coloranalysisresult_success(this.__wbg_ptr);
163
+ return ret !== 0;
164
+ }
165
+ /**
166
+ * @returns {number}
167
+ */
168
+ get color_count() {
169
+ const ret = wasm.coloranalysisresult_color_count(this.__wbg_ptr);
170
+ return ret >>> 0;
171
+ }
172
+ /**
173
+ * @returns {number}
174
+ */
175
+ get total_pixels() {
176
+ const ret = wasm.coloranalysisresult_total_pixels(this.__wbg_ptr);
177
+ return ret >>> 0;
178
+ }
179
+ /**
180
+ * Get color at index
181
+ * @param {number} index
182
+ * @returns {ColorEntry | undefined}
183
+ */
184
+ get_color(index) {
185
+ const ret = wasm.coloranalysisresult_get_color(this.__wbg_ptr, index);
186
+ return ret === 0 ? undefined : ColorEntry.__wrap(ret);
187
+ }
188
+ /**
189
+ * Get all colors as a flat array: [r, g, b, count(4 bytes), percentage(4 bytes), ...]
190
+ * Each color is 11 bytes
191
+ * @returns {Uint8Array}
192
+ */
193
+ get_colors_flat() {
194
+ const ret = wasm.coloranalysisresult_get_colors_flat(this.__wbg_ptr);
195
+ return ret;
196
+ }
197
+ /**
198
+ * Get colors as JSON-compatible array
199
+ * @returns {any}
200
+ */
201
+ to_json() {
202
+ const ret = wasm.coloranalysisresult_to_json(this.__wbg_ptr);
203
+ if (ret[2]) {
204
+ throw takeFromExternrefTable0(ret[1]);
205
+ }
206
+ return takeFromExternrefTable0(ret[0]);
207
+ }
208
+ }
209
+ if (Symbol.dispose) ColorAnalysisResult.prototype[Symbol.dispose] = ColorAnalysisResult.prototype.free;
210
+
211
+ /**
212
+ * A single color entry with statistics
213
+ */
214
+ export class ColorEntry {
215
+ static __wrap(ptr) {
216
+ ptr = ptr >>> 0;
217
+ const obj = Object.create(ColorEntry.prototype);
218
+ obj.__wbg_ptr = ptr;
219
+ ColorEntryFinalization.register(obj, obj.__wbg_ptr, obj);
220
+ return obj;
221
+ }
222
+ __destroy_into_raw() {
223
+ const ptr = this.__wbg_ptr;
224
+ this.__wbg_ptr = 0;
225
+ ColorEntryFinalization.unregister(this);
226
+ return ptr;
227
+ }
228
+ free() {
229
+ const ptr = this.__destroy_into_raw();
230
+ wasm.__wbg_colorentry_free(ptr, 0);
231
+ }
232
+ /**
233
+ * @returns {number}
234
+ */
235
+ get r() {
236
+ const ret = wasm.colorentry_r(this.__wbg_ptr);
237
+ return ret;
238
+ }
239
+ /**
240
+ * @returns {number}
241
+ */
242
+ get g() {
243
+ const ret = wasm.colorentry_g(this.__wbg_ptr);
244
+ return ret;
245
+ }
246
+ /**
247
+ * @returns {number}
248
+ */
249
+ get b() {
250
+ const ret = wasm.colorentry_b(this.__wbg_ptr);
251
+ return ret;
252
+ }
253
+ /**
254
+ * @returns {number}
255
+ */
256
+ get count() {
257
+ const ret = wasm.colorentry_count(this.__wbg_ptr);
258
+ return ret >>> 0;
259
+ }
260
+ /**
261
+ * @returns {number}
262
+ */
263
+ get percentage() {
264
+ const ret = wasm.colorentry_percentage(this.__wbg_ptr);
265
+ return ret;
266
+ }
267
+ /**
268
+ * @returns {string}
269
+ */
270
+ get hex() {
271
+ let deferred1_0;
272
+ let deferred1_1;
273
+ try {
274
+ const ret = wasm.colorentry_hex(this.__wbg_ptr);
275
+ deferred1_0 = ret[0];
276
+ deferred1_1 = ret[1];
277
+ return getStringFromWasm0(ret[0], ret[1]);
278
+ } finally {
279
+ wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
280
+ }
281
+ }
282
+ }
283
+ if (Symbol.dispose) ColorEntry.prototype[Symbol.dispose] = ColorEntry.prototype.free;
284
+
121
285
  /**
122
286
  * Configuration options for the downscaler (JavaScript-compatible)
123
287
  */
@@ -338,6 +502,31 @@ export class WasmDownscaleConfig {
338
502
  wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
339
503
  }
340
504
  }
505
+ /**
506
+ * Set the palette extraction strategy
507
+ * @param {string} strategy
508
+ */
509
+ set palette_strategy(strategy) {
510
+ const ptr0 = passStringToWasm0(strategy, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
511
+ const len0 = WASM_VECTOR_LEN;
512
+ wasm.wasmdownscaleconfig_set_palette_strategy(this.__wbg_ptr, ptr0, len0);
513
+ }
514
+ /**
515
+ * Get the palette extraction strategy
516
+ * @returns {string}
517
+ */
518
+ get palette_strategy() {
519
+ let deferred1_0;
520
+ let deferred1_1;
521
+ try {
522
+ const ret = wasm.wasmdownscaleconfig_palette_strategy(this.__wbg_ptr);
523
+ deferred1_0 = ret[0];
524
+ deferred1_1 = ret[1];
525
+ return getStringFromWasm0(ret[0], ret[1]);
526
+ } finally {
527
+ wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
528
+ }
529
+ }
341
530
  /**
342
531
  * Create configuration optimized for speed
343
532
  * @returns {WasmDownscaleConfig}
@@ -354,6 +543,22 @@ export class WasmDownscaleConfig {
354
543
  const ret = wasm.wasmdownscaleconfig_quality();
355
544
  return WasmDownscaleConfig.__wrap(ret);
356
545
  }
546
+ /**
547
+ * Create configuration optimized for vibrant colors
548
+ * @returns {WasmDownscaleConfig}
549
+ */
550
+ static vibrant() {
551
+ const ret = wasm.wasmdownscaleconfig_vibrant();
552
+ return WasmDownscaleConfig.__wrap(ret);
553
+ }
554
+ /**
555
+ * Create configuration that uses only exact image colors (medoid)
556
+ * @returns {WasmDownscaleConfig}
557
+ */
558
+ static exact_colors() {
559
+ const ret = wasm.wasmdownscaleconfig_exact_colors();
560
+ return WasmDownscaleConfig.__wrap(ret);
561
+ }
357
562
  }
358
563
  if (Symbol.dispose) WasmDownscaleConfig.prototype[Symbol.dispose] = WasmDownscaleConfig.prototype.free;
359
564
 
@@ -447,28 +652,44 @@ export class WasmDownscaleResult {
447
652
  if (Symbol.dispose) WasmDownscaleResult.prototype[Symbol.dispose] = WasmDownscaleResult.prototype.free;
448
653
 
449
654
  /**
450
- * Highly optimized color analysis using a custom linear-probing hash table.
655
+ * Analyze colors in an image
656
+ *
657
+ * # Arguments
658
+ * * `image_data` - RGBA pixel data
659
+ * * `max_colors` - Maximum number of unique colors to track (stops if exceeded)
660
+ * * `sort_method` - Sorting method: "frequency", "morton", or "hilbert"
451
661
  *
452
- * OPTIMIZATIONS:
453
- * 1. Casts u8 slice to u32 slice (Zero Copy, 4x read speed)
454
- * 2. Uses Power-of-Two capacity for bitwise masking (vs modulo)
455
- * 3. FxHash-style integer mixer
456
- * 4. Linear probing with localized memory access
662
+ * # Returns
663
+ * ColorAnalysisResult with array of colors (r, g, b, count, percentage, hex)
664
+ * If unique colors exceed max_colors, returns early with success=false
457
665
  * @param {Uint8Array} image_data
458
666
  * @param {number} max_colors
459
667
  * @param {string} sort_method
460
- * @returns {any}
668
+ * @returns {ColorAnalysisResult}
461
669
  */
462
670
  export function analyze_colors(image_data, max_colors, sort_method) {
463
- const ptr0 = passArray8ToWasm0(image_data, wasm.__wbindgen_malloc);
671
+ const ptr0 = passStringToWasm0(sort_method, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
464
672
  const len0 = WASM_VECTOR_LEN;
465
- const ptr1 = passStringToWasm0(sort_method, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
466
- const len1 = WASM_VECTOR_LEN;
467
- const ret = wasm.analyze_colors(ptr0, len0, max_colors, ptr1, len1);
673
+ const ret = wasm.analyze_colors(image_data, max_colors, ptr0, len0);
468
674
  if (ret[2]) {
469
675
  throw takeFromExternrefTable0(ret[1]);
470
676
  }
471
- return takeFromExternrefTable0(ret[0]);
677
+ return ColorAnalysisResult.__wrap(ret[0]);
678
+ }
679
+
680
+ /**
681
+ * Compute perceptual color distance between two RGB colors
682
+ * @param {number} r1
683
+ * @param {number} g1
684
+ * @param {number} b1
685
+ * @param {number} r2
686
+ * @param {number} g2
687
+ * @param {number} b2
688
+ * @returns {number}
689
+ */
690
+ export function color_distance(r1, g1, b1, r2, g2, b2) {
691
+ const ret = wasm.color_distance(r1, g1, b1, r2, g2, b2);
692
+ return ret;
472
693
  }
473
694
 
474
695
  /**
@@ -577,16 +798,52 @@ export function downscale_with_palette(image_data, width, height, target_width,
577
798
  * @param {number} _height
578
799
  * @param {number} num_colors
579
800
  * @param {number} kmeans_iterations
801
+ * @param {string | null} [strategy]
580
802
  * @returns {Uint8Array}
581
803
  */
582
- export function extract_palette_from_image(image_data, _width, _height, num_colors, kmeans_iterations) {
583
- const ret = wasm.extract_palette_from_image(image_data, _width, _height, num_colors, kmeans_iterations);
804
+ export function extract_palette_from_image(image_data, _width, _height, num_colors, kmeans_iterations, strategy) {
805
+ var ptr0 = isLikeNone(strategy) ? 0 : passStringToWasm0(strategy, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
806
+ var len0 = WASM_VECTOR_LEN;
807
+ const ret = wasm.extract_palette_from_image(image_data, _width, _height, num_colors, kmeans_iterations, ptr0, len0);
584
808
  if (ret[2]) {
585
809
  throw takeFromExternrefTable0(ret[1]);
586
810
  }
587
811
  return takeFromExternrefTable0(ret[0]);
588
812
  }
589
813
 
814
+ /**
815
+ * Get chroma (saturation) of an RGB color
816
+ * @param {number} r
817
+ * @param {number} g
818
+ * @param {number} b
819
+ * @returns {number}
820
+ */
821
+ export function get_chroma(r, g, b) {
822
+ const ret = wasm.get_chroma(r, g, b);
823
+ return ret;
824
+ }
825
+
826
+ /**
827
+ * Get lightness of an RGB color in Oklab space
828
+ * @param {number} r
829
+ * @param {number} g
830
+ * @param {number} b
831
+ * @returns {number}
832
+ */
833
+ export function get_lightness(r, g, b) {
834
+ const ret = wasm.get_lightness(r, g, b);
835
+ return ret;
836
+ }
837
+
838
+ /**
839
+ * Get available palette strategies
840
+ * @returns {Array<any>}
841
+ */
842
+ export function get_palette_strategies() {
843
+ const ret = wasm.get_palette_strategies();
844
+ return ret;
845
+ }
846
+
590
847
  /**
591
848
  * Initialize panic hook for better error messages in browser console
592
849
  */
@@ -604,6 +861,18 @@ export function log(message) {
604
861
  wasm.log(ptr0, len0);
605
862
  }
606
863
 
864
+ /**
865
+ * Convert Oklab to RGB (utility function for JS)
866
+ * @param {number} l
867
+ * @param {number} a
868
+ * @param {number} b
869
+ * @returns {Uint8Array}
870
+ */
871
+ export function oklab_to_rgb(l, a, b) {
872
+ const ret = wasm.oklab_to_rgb(l, a, b);
873
+ return ret;
874
+ }
875
+
607
876
  /**
608
877
  * Quantize an image to a specific palette without resizing
609
878
  * @param {Uint8Array} image_data
@@ -620,6 +889,18 @@ export function quantize_to_palette(image_data, width, height, palette_data) {
620
889
  return WasmDownscaleResult.__wrap(ret[0]);
621
890
  }
622
891
 
892
+ /**
893
+ * Convert RGB to Oklab (utility function for JS)
894
+ * @param {number} r
895
+ * @param {number} g
896
+ * @param {number} b
897
+ * @returns {Float32Array}
898
+ */
899
+ export function rgb_to_oklab(r, g, b) {
900
+ const ret = wasm.rgb_to_oklab(r, g, b);
901
+ return ret;
902
+ }
903
+
623
904
  /**
624
905
  * Get library version
625
906
  * @returns {string}
@@ -675,6 +956,11 @@ export function __wbg_new_from_slice_f9c22b9153b26992(arg0, arg1) {
675
956
  return ret;
676
957
  };
677
958
 
959
+ export function __wbg_new_with_length_95ba657dfb7d3dfb(arg0) {
960
+ const ret = new Float32Array(arg0 >>> 0);
961
+ return ret;
962
+ };
963
+
678
964
  export function __wbg_new_with_length_aa5eaf41d35235e5(arg0) {
679
965
  const ret = new Uint8Array(arg0 >>> 0);
680
966
  return ret;
@@ -684,11 +970,21 @@ export function __wbg_prototypesetcall_dfe9b766cdc1f1fd(arg0, arg1, arg2) {
684
970
  Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
685
971
  };
686
972
 
687
- export function __wbg_set_3f1d0b984ed272ed(arg0, arg1, arg2) {
688
- arg0[arg1] = arg2;
973
+ export function __wbg_push_7d9be8f38fc13975(arg0, arg1) {
974
+ const ret = arg0.push(arg1);
975
+ return ret;
976
+ };
977
+
978
+ export function __wbg_set_781438a03c0c3c81() { return handleError(function (arg0, arg1, arg2) {
979
+ const ret = Reflect.set(arg0, arg1, arg2);
980
+ return ret;
981
+ }, arguments) };
982
+
983
+ export function __wbg_set_index_04c4b93e64d08a52(arg0, arg1, arg2) {
984
+ arg0[arg1 >>> 0] = arg2;
689
985
  };
690
986
 
691
- export function __wbg_set_7df433eea03a5c14(arg0, arg1, arg2) {
987
+ export function __wbg_set_index_165b46b0114d368c(arg0, arg1, arg2) {
692
988
  arg0[arg1 >>> 0] = arg2;
693
989
  };
694
990
 
Binary file