@trishchuk/coolors-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +39 -0
- package/.env +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +73 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +71 -0
- package/.github/pull_request_template.md +97 -0
- package/.github/workflows/ci.yml +127 -0
- package/.github/workflows/deploy-docs.yml +56 -0
- package/.github/workflows/release.yml +99 -0
- package/.mcp.json +12 -0
- package/.prettierignore +1 -0
- package/CLAUDE.md +201 -0
- package/DOCUMENTATION.md +274 -0
- package/GEMINI.md +54 -0
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/demo/content_based_color.png +0 -0
- package/demo/music-player.html +621 -0
- package/demo/podcast-player.html +903 -0
- package/dist/bin/coolors-mcp.d.ts +1 -0
- package/dist/bin/coolors-mcp.js +154 -0
- package/dist/bin/coolors-mcp.js.map +1 -0
- package/dist/bin/server.d.ts +1 -0
- package/dist/bin/server.js +3292 -0
- package/dist/bin/server.js.map +1 -0
- package/dist/chunk-IQ7NN26V.js +114 -0
- package/dist/chunk-IQ7NN26V.js.map +1 -0
- package/dist/chunk-P3ARRKLS.js +1214 -0
- package/dist/chunk-P3ARRKLS.js.map +1 -0
- package/dist/color/index.d.ts +716 -0
- package/dist/color/index.js +153 -0
- package/dist/color/index.js.map +1 -0
- package/dist/coolors-mcp.d.ts +136 -0
- package/dist/coolors-mcp.js +7 -0
- package/dist/coolors-mcp.js.map +1 -0
- package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
- package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
- package/docs/.vitepress/cache/deps/_metadata.json +127 -0
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +12683 -0
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +7 -0
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +9719 -0
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +7 -0
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
- package/docs/.vitepress/cache/deps/cytoscape.js +30278 -0
- package/docs/.vitepress/cache/deps/cytoscape.js.map +7 -0
- package/docs/.vitepress/cache/deps/dayjs.js +285 -0
- package/docs/.vitepress/cache/deps/dayjs.js.map +7 -0
- package/docs/.vitepress/cache/deps/debug.js +468 -0
- package/docs/.vitepress/cache/deps/debug.js.map +7 -0
- package/docs/.vitepress/cache/deps/package.json +3 -0
- package/docs/.vitepress/cache/deps/prismjs.js +1466 -0
- package/docs/.vitepress/cache/deps/prismjs.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +228 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +142 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +27 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +65 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +53 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +73 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1146 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1814 -0
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
- package/docs/.vitepress/cache/deps/vue.js +344 -0
- package/docs/.vitepress/cache/deps/vue.js.map +7 -0
- package/docs/.vitepress/components/ClientGrid.vue +125 -0
- package/docs/.vitepress/components/CodeBlock.vue +231 -0
- package/docs/.vitepress/components/ConfigModal.vue +477 -0
- package/docs/.vitepress/components/DiagramModal.vue +528 -0
- package/docs/.vitepress/components/TroubleshootingModal.vue +472 -0
- package/docs/.vitepress/config.js +162 -0
- package/docs/.vitepress/theme/FundingLayout.vue +251 -0
- package/docs/.vitepress/theme/Layout.vue +134 -0
- package/docs/.vitepress/theme/components/AdBanner.vue +317 -0
- package/docs/.vitepress/theme/components/AdPlaceholder.vue +78 -0
- package/docs/.vitepress/theme/components/FundingEffects.vue +322 -0
- package/docs/.vitepress/theme/components/FundingHero.vue +345 -0
- package/docs/.vitepress/theme/components/SupportSection.vue +511 -0
- package/docs/.vitepress/theme/custom-app.css +339 -0
- package/docs/.vitepress/theme/custom.css +699 -0
- package/docs/.vitepress/theme/index.js +25 -0
- package/docs/README.md +198 -0
- package/docs/concepts/accessibility.md +473 -0
- package/docs/concepts/color-spaces.md +222 -0
- package/docs/concepts/distance-metrics.md +384 -0
- package/docs/concepts/hct.md +261 -0
- package/docs/concepts/image-analysis.md +396 -0
- package/docs/concepts/material-design.md +306 -0
- package/docs/concepts/theme-matching.md +399 -0
- package/docs/examples/basic-colors.md +490 -0
- package/docs/examples/creating-themes.md +898 -0
- package/docs/examples/css-refactoring.md +824 -0
- package/docs/examples/image-extraction.md +882 -0
- package/docs/getting-started.md +366 -0
- package/docs/index.md +190 -0
- package/docs/installation.md +157 -0
- package/docs/tools/README.md +234 -0
- package/docs/tools/accessibility.md +614 -0
- package/docs/tools/color-operations.md +374 -0
- package/docs/tools/image-extraction.md +624 -0
- package/docs/tools/material-design.md +347 -0
- package/docs/tools/theme-matching.md +552 -0
- package/eslint.config.ts +14 -0
- package/examples/theme-matching.md +113 -0
- package/jsr.json +7 -0
- package/mcp-config.json +8 -0
- package/note.md +35 -0
- package/package.json +122 -0
- package/research_results.md +53 -0
- package/src/bin/coolors-mcp.ts +194 -0
- package/src/bin/server.ts +61 -0
- package/src/color/__tests__/conversions-argb.test.ts +198 -0
- package/src/color/__tests__/extract-colors.test.ts +360 -0
- package/src/color/__tests__/image-utils.test.ts +242 -0
- package/src/color/__tests__/reference-colors.test.ts +278 -0
- package/src/color/__tests__/round-trip.test.ts +197 -0
- package/src/color/conversions.test.ts +402 -0
- package/src/color/conversions.ts +393 -0
- package/src/color/dislike/__tests__/dislike-analyzer.test.ts +248 -0
- package/src/color/dislike/dislike-analyzer.ts +114 -0
- package/src/color/extract-colors.ts +228 -0
- package/src/color/hct/__tests__/hct-class.test.ts +232 -0
- package/src/color/hct/harmonization.ts +204 -0
- package/src/color/hct/hct-class.ts +109 -0
- package/src/color/hct/hct-solver.ts +168 -0
- package/src/color/hct/index.ts +39 -0
- package/src/color/hct/tonal-palette.ts +211 -0
- package/src/color/hct/types.ts +88 -0
- package/src/color/image-utils.ts +79 -0
- package/src/color/index.ts +87 -0
- package/src/color/material-theme.ts +157 -0
- package/src/color/metrics.test.ts +276 -0
- package/src/color/metrics.ts +281 -0
- package/src/color/quantize/__tests__/quantizer_celebi.test.ts +195 -0
- package/src/color/quantize/lab_point_provider.ts +55 -0
- package/src/color/quantize/point_provider.ts +27 -0
- package/src/color/quantize/quantizer_celebi.ts +51 -0
- package/src/color/quantize/quantizer_celebi_test.ts +71 -0
- package/src/color/quantize/quantizer_map.ts +47 -0
- package/src/color/quantize/quantizer_wsmeans.ts +232 -0
- package/src/color/quantize/quantizer_wu.ts +472 -0
- package/src/color/score/__tests__/score.test.ts +224 -0
- package/src/color/score/score.ts +175 -0
- package/src/color/types.ts +151 -0
- package/src/color/utils/color_utils.ts +292 -0
- package/src/color/utils/math_utils.ts +145 -0
- package/src/color/utils.test.ts +403 -0
- package/src/color/utils.ts +315 -0
- package/src/constants.ts +5 -0
- package/src/coolors-mcp.ts +37 -0
- package/src/examples/addition.ts +333 -0
- package/src/examples/color-demo.ts +125 -0
- package/src/examples/custom-logger.ts +201 -0
- package/src/examples/oauth-server.ts +113 -0
- package/src/examples/session-context.ts +269 -0
- package/src/session.ts +116 -0
- package/src/theme/__tests__/matcher.test.ts +180 -0
- package/src/theme/__tests__/parser.test.ts +148 -0
- package/src/theme/__tests__/refactor.test.ts +224 -0
- package/src/theme/index.ts +34 -0
- package/src/theme/matcher.ts +395 -0
- package/src/theme/parser.ts +392 -0
- package/src/theme/refactor.ts +360 -0
- package/src/theme/types.ts +152 -0
- package/src/tools/__tests__/gradient-generator.test.ts +206 -0
- package/src/tools/__tests__/palette-with-locks.test.ts +109 -0
- package/src/tools/color-conversion.tool.ts +54 -0
- package/src/tools/color-distance.tool.ts +41 -0
- package/src/tools/colors.ts +31 -0
- package/src/tools/contrast-checker.tool.ts +37 -0
- package/src/tools/dislike-analyzer.tool.ts +247 -0
- package/src/tools/gradient-generator.tool.ts +250 -0
- package/src/tools/image-extraction.tools.ts +289 -0
- package/src/tools/index.ts +39 -0
- package/src/tools/material-theme.tools.ts +250 -0
- package/src/tools/palette-generator.tool.ts +135 -0
- package/src/tools/palette-with-locks.tool.ts +221 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/simple-tools.ts +37 -0
- package/src/tools/theme-matching.tools.ts +334 -0
- package/src/types.ts +182 -0
- package/src/utils.ts +22 -0
- package/tsconfig.json +8 -0
- package/vitest.config.js +15 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for color extraction from images
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { extractColors, extractThemePalette } from "../extract-colors.js";
|
|
8
|
+
import * as utils from "../utils/color_utils.js";
|
|
9
|
+
|
|
10
|
+
// Mock the quantizer and scorer for predictable tests
|
|
11
|
+
vi.mock("../quantize/quantizer_celebi", () => ({
|
|
12
|
+
QuantizerCelebi: {
|
|
13
|
+
quantize: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("../score/score", () => ({
|
|
18
|
+
Score: {
|
|
19
|
+
score: vi.fn(),
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe("Color Extraction", () => {
|
|
24
|
+
describe("extractColors", () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should extract colors with default options", async () => {
|
|
30
|
+
const { QuantizerCelebi } = await import(
|
|
31
|
+
"../quantize/quantizer_celebi.js"
|
|
32
|
+
);
|
|
33
|
+
const { Score } = await import("../score/score.js");
|
|
34
|
+
|
|
35
|
+
// Mock quantizer to return test colors
|
|
36
|
+
const mockQuantized = new Map([
|
|
37
|
+
[utils.argbFromRgb(0, 0, 255), 60], // Blue
|
|
38
|
+
[utils.argbFromRgb(0, 255, 0), 80], // Green
|
|
39
|
+
[utils.argbFromRgb(255, 0, 0), 100], // Red
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(mockQuantized);
|
|
43
|
+
vi.mocked(Score.score).mockReturnValue([
|
|
44
|
+
utils.argbFromRgb(255, 0, 0),
|
|
45
|
+
utils.argbFromRgb(0, 255, 0),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const imageData = {
|
|
49
|
+
data: new Uint8ClampedArray([
|
|
50
|
+
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255,
|
|
51
|
+
]),
|
|
52
|
+
height: 1,
|
|
53
|
+
width: 3,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const colors = extractColors(imageData);
|
|
57
|
+
|
|
58
|
+
expect(colors).toHaveLength(2);
|
|
59
|
+
expect(colors[0].hex).toBe("#ff0000");
|
|
60
|
+
expect(colors[0].rgb).toEqual({ b: 0, g: 0, r: 255 });
|
|
61
|
+
expect(colors[0].population).toBe(100);
|
|
62
|
+
expect(colors[0].percentage).toBeCloseTo(41.67, 1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should extract colors with low quality setting", async () => {
|
|
66
|
+
const { QuantizerCelebi } = await import("../quantize/quantizer_celebi");
|
|
67
|
+
|
|
68
|
+
const mockQuantized = new Map([[utils.argbFromRgb(128, 128, 128), 50]]);
|
|
69
|
+
|
|
70
|
+
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(mockQuantized);
|
|
71
|
+
|
|
72
|
+
const imageData = {
|
|
73
|
+
data: new Uint8ClampedArray(100 * 100 * 4), // Large image
|
|
74
|
+
height: 100,
|
|
75
|
+
width: 100,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
extractColors(imageData, { quality: "low" });
|
|
79
|
+
|
|
80
|
+
// Check that quantizer was called with correct max colors for low quality
|
|
81
|
+
expect(QuantizerCelebi.quantize).toHaveBeenCalledWith(
|
|
82
|
+
expect.any(Array),
|
|
83
|
+
64, // Low quality uses 64 colors
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should extract colors with high quality setting", async () => {
|
|
88
|
+
const { QuantizerCelebi } = await import("../quantize/quantizer_celebi");
|
|
89
|
+
|
|
90
|
+
const mockQuantized = new Map([[utils.argbFromRgb(128, 128, 128), 50]]);
|
|
91
|
+
|
|
92
|
+
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(mockQuantized);
|
|
93
|
+
|
|
94
|
+
const imageData = {
|
|
95
|
+
data: new Uint8ClampedArray(100 * 100 * 4),
|
|
96
|
+
height: 100,
|
|
97
|
+
width: 100,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
extractColors(imageData, { quality: "high" });
|
|
101
|
+
|
|
102
|
+
expect(QuantizerCelebi.quantize).toHaveBeenCalledWith(
|
|
103
|
+
expect.any(Array),
|
|
104
|
+
256, // High quality uses 256 colors
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should extract colors without scoring", async () => {
|
|
109
|
+
const { QuantizerCelebi } = await import(
|
|
110
|
+
"../quantize/quantizer_celebi.js"
|
|
111
|
+
);
|
|
112
|
+
const { Score } = await import("../score/score.js");
|
|
113
|
+
|
|
114
|
+
const mockQuantized = new Map([
|
|
115
|
+
[utils.argbFromRgb(0, 0, 255), 60],
|
|
116
|
+
[utils.argbFromRgb(0, 255, 0), 80],
|
|
117
|
+
[utils.argbFromRgb(255, 0, 0), 100],
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(mockQuantized);
|
|
121
|
+
|
|
122
|
+
const imageData = {
|
|
123
|
+
data: new Uint8ClampedArray([255, 0, 0, 255]),
|
|
124
|
+
height: 1,
|
|
125
|
+
width: 1,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const colors = extractColors(imageData, {
|
|
129
|
+
maxColors: 2,
|
|
130
|
+
scoringEnabled: false,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Should not call Score.score
|
|
134
|
+
expect(Score.score).not.toHaveBeenCalled();
|
|
135
|
+
|
|
136
|
+
// Should return top 2 by population
|
|
137
|
+
expect(colors).toHaveLength(2);
|
|
138
|
+
expect(colors[0].hex).toBe("#ff0000");
|
|
139
|
+
expect(colors[1].hex).toBe("#00ff00");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should handle empty image data", async () => {
|
|
143
|
+
const { QuantizerCelebi } = await import(
|
|
144
|
+
"../quantize/quantizer_celebi.js"
|
|
145
|
+
);
|
|
146
|
+
const { Score } = await import("../score/score.js");
|
|
147
|
+
|
|
148
|
+
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(new Map());
|
|
149
|
+
vi.mocked(Score.score).mockReturnValue([]);
|
|
150
|
+
|
|
151
|
+
const imageData = {
|
|
152
|
+
data: new Uint8ClampedArray([]),
|
|
153
|
+
height: 0,
|
|
154
|
+
width: 0,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const colors = extractColors(imageData);
|
|
158
|
+
expect(colors).toHaveLength(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should calculate correct percentages", async () => {
|
|
162
|
+
const { QuantizerCelebi } = await import(
|
|
163
|
+
"../quantize/quantizer_celebi.js"
|
|
164
|
+
);
|
|
165
|
+
const { Score } = await import("../score/score.js");
|
|
166
|
+
|
|
167
|
+
const mockQuantized = new Map([
|
|
168
|
+
[utils.argbFromRgb(0, 255, 0), 25],
|
|
169
|
+
[utils.argbFromRgb(255, 0, 0), 75],
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(mockQuantized);
|
|
173
|
+
vi.mocked(Score.score).mockReturnValue([
|
|
174
|
+
utils.argbFromRgb(255, 0, 0),
|
|
175
|
+
utils.argbFromRgb(0, 255, 0),
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
const imageData = {
|
|
179
|
+
data: new Uint8ClampedArray([255, 0, 0, 255]),
|
|
180
|
+
height: 1,
|
|
181
|
+
width: 1,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const colors = extractColors(imageData);
|
|
185
|
+
|
|
186
|
+
expect(colors[0].percentage).toBe(75);
|
|
187
|
+
expect(colors[1].percentage).toBe(25);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("extractThemePalette", () => {
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
vi.clearAllMocks();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should extract theme palette with primary color", async () => {
|
|
197
|
+
const { QuantizerCelebi } = await import(
|
|
198
|
+
"../quantize/quantizer_celebi.js"
|
|
199
|
+
);
|
|
200
|
+
const { Score } = await import("../score/score.js");
|
|
201
|
+
|
|
202
|
+
const mockQuantized = new Map([
|
|
203
|
+
[utils.argbFromRgb(103, 80, 164), 100], // Primary purple
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(mockQuantized);
|
|
207
|
+
vi.mocked(Score.score).mockReturnValue([utils.argbFromRgb(103, 80, 164)]);
|
|
208
|
+
|
|
209
|
+
const imageData = {
|
|
210
|
+
data: new Uint8ClampedArray([103, 80, 164, 255]),
|
|
211
|
+
height: 1,
|
|
212
|
+
width: 1,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const palette = extractThemePalette(imageData);
|
|
216
|
+
|
|
217
|
+
expect(palette.primary).toBeDefined();
|
|
218
|
+
expect(palette.primary.hex).toBe("#6750a4");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should extract secondary with different hue", async () => {
|
|
222
|
+
const { QuantizerCelebi } = await import(
|
|
223
|
+
"../quantize/quantizer_celebi.js"
|
|
224
|
+
);
|
|
225
|
+
const { Score } = await import("../score/score.js");
|
|
226
|
+
|
|
227
|
+
const mockQuantized = new Map([
|
|
228
|
+
[utils.argbFromRgb(103, 80, 164), 100], // Purple
|
|
229
|
+
[utils.argbFromRgb(110, 80, 164), 60], // Similar purple
|
|
230
|
+
[utils.argbFromRgb(255, 0, 0), 80], // Red (very different hue)
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(mockQuantized);
|
|
234
|
+
vi.mocked(Score.score).mockReturnValue([
|
|
235
|
+
utils.argbFromRgb(103, 80, 164),
|
|
236
|
+
utils.argbFromRgb(255, 0, 0),
|
|
237
|
+
utils.argbFromRgb(110, 80, 164),
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
const imageData = {
|
|
241
|
+
data: new Uint8ClampedArray([103, 80, 164, 255]),
|
|
242
|
+
height: 1,
|
|
243
|
+
width: 1,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const palette = extractThemePalette(imageData);
|
|
247
|
+
|
|
248
|
+
expect(palette.primary).toBeDefined();
|
|
249
|
+
expect(palette.secondary).toBeDefined();
|
|
250
|
+
expect(palette.secondary?.hex).toBe("#ff0000"); // Red should be secondary
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should find neutral color with low chroma", async () => {
|
|
254
|
+
const { QuantizerCelebi } = await import(
|
|
255
|
+
"../quantize/quantizer_celebi.js"
|
|
256
|
+
);
|
|
257
|
+
const { Score } = await import("../score/score.js");
|
|
258
|
+
|
|
259
|
+
const mockQuantized = new Map([
|
|
260
|
+
[utils.argbFromRgb(103, 80, 164), 100], // Primary
|
|
261
|
+
[utils.argbFromRgb(128, 128, 128), 50], // Gray (low chroma)
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(mockQuantized);
|
|
265
|
+
vi.mocked(Score.score).mockReturnValue([
|
|
266
|
+
utils.argbFromRgb(103, 80, 164),
|
|
267
|
+
utils.argbFromRgb(128, 128, 128),
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
const imageData = {
|
|
271
|
+
data: new Uint8ClampedArray([103, 80, 164, 255]),
|
|
272
|
+
height: 1,
|
|
273
|
+
width: 1,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const palette = extractThemePalette(imageData);
|
|
277
|
+
|
|
278
|
+
expect(palette.neutral).toBeDefined();
|
|
279
|
+
expect(palette.neutral?.hct.c).toBeLessThan(20); // Low chroma
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("should find error color in red hue range", async () => {
|
|
283
|
+
const { QuantizerCelebi } = await import(
|
|
284
|
+
"../quantize/quantizer_celebi.js"
|
|
285
|
+
);
|
|
286
|
+
const { Score } = await import("../score/score.js");
|
|
287
|
+
|
|
288
|
+
const primaryArgb = utils.argbFromRgb(103, 80, 164);
|
|
289
|
+
const redArgb = utils.argbFromRgb(255, 20, 20);
|
|
290
|
+
|
|
291
|
+
const mockQuantized = new Map([
|
|
292
|
+
[primaryArgb, 100], // Primary
|
|
293
|
+
[redArgb, 50], // Red for error
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(mockQuantized);
|
|
297
|
+
vi.mocked(Score.score).mockReturnValue([primaryArgb, redArgb]);
|
|
298
|
+
|
|
299
|
+
const imageData = {
|
|
300
|
+
data: new Uint8ClampedArray([103, 80, 164, 255]),
|
|
301
|
+
height: 1,
|
|
302
|
+
width: 1,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const palette = extractThemePalette(imageData);
|
|
306
|
+
|
|
307
|
+
expect(palette.error).toBeDefined();
|
|
308
|
+
// Red/orange hue should be 0-40 or 350-360
|
|
309
|
+
const errorHue = palette.error?.hct.h || 0;
|
|
310
|
+
expect(errorHue <= 40 || errorHue >= 350).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("should throw error for empty image", async () => {
|
|
314
|
+
const { QuantizerCelebi } = await import(
|
|
315
|
+
"../quantize/quantizer_celebi.js"
|
|
316
|
+
);
|
|
317
|
+
const { Score } = await import("../score/score.js");
|
|
318
|
+
|
|
319
|
+
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(new Map());
|
|
320
|
+
vi.mocked(Score.score).mockReturnValue([]);
|
|
321
|
+
|
|
322
|
+
const imageData = {
|
|
323
|
+
data: new Uint8ClampedArray([]),
|
|
324
|
+
height: 0,
|
|
325
|
+
width: 0,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
expect(() => extractThemePalette(imageData)).toThrow(
|
|
329
|
+
"No colors could be extracted from image",
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("should handle single color image", async () => {
|
|
334
|
+
const { QuantizerCelebi } = await import(
|
|
335
|
+
"../quantize/quantizer_celebi.js"
|
|
336
|
+
);
|
|
337
|
+
const { Score } = await import("../score/score.js");
|
|
338
|
+
|
|
339
|
+
const mockQuantized = new Map([
|
|
340
|
+
[utils.argbFromRgb(0, 128, 255), 100], // Single blue color
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
vi.mocked(QuantizerCelebi.quantize).mockReturnValue(mockQuantized);
|
|
344
|
+
vi.mocked(Score.score).mockReturnValue([utils.argbFromRgb(0, 128, 255)]);
|
|
345
|
+
|
|
346
|
+
const imageData = {
|
|
347
|
+
data: new Uint8ClampedArray([0, 128, 255, 255]),
|
|
348
|
+
height: 1,
|
|
349
|
+
width: 1,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const palette = extractThemePalette(imageData);
|
|
353
|
+
|
|
354
|
+
expect(palette.primary).toBeDefined();
|
|
355
|
+
expect(palette.primary.hex).toBe("#0080ff");
|
|
356
|
+
expect(palette.secondary).toBeUndefined();
|
|
357
|
+
expect(palette.tertiary).toBeUndefined();
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for image processing utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
filterExtremeTones,
|
|
9
|
+
imageDataToPixels,
|
|
10
|
+
samplePixels,
|
|
11
|
+
} from "../image-utils";
|
|
12
|
+
import * as utils from "../utils/color_utils";
|
|
13
|
+
|
|
14
|
+
describe("Image Processing Utilities", () => {
|
|
15
|
+
describe("imageDataToPixels", () => {
|
|
16
|
+
it("should convert RGBA data to ARGB pixels", () => {
|
|
17
|
+
const imageData = {
|
|
18
|
+
data: new Uint8ClampedArray([
|
|
19
|
+
255,
|
|
20
|
+
0,
|
|
21
|
+
0,
|
|
22
|
+
255, // Red pixel
|
|
23
|
+
0,
|
|
24
|
+
255,
|
|
25
|
+
0,
|
|
26
|
+
255, // Green pixel
|
|
27
|
+
0,
|
|
28
|
+
0,
|
|
29
|
+
255,
|
|
30
|
+
255, // Blue pixel
|
|
31
|
+
]),
|
|
32
|
+
height: 1,
|
|
33
|
+
width: 3,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const pixels = imageDataToPixels(imageData);
|
|
37
|
+
|
|
38
|
+
expect(pixels).toHaveLength(3);
|
|
39
|
+
expect(pixels[0]).toBe(utils.argbFromRgb(255, 0, 0));
|
|
40
|
+
expect(pixels[1]).toBe(utils.argbFromRgb(0, 255, 0));
|
|
41
|
+
expect(pixels[2]).toBe(utils.argbFromRgb(0, 0, 255));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should skip transparent pixels", () => {
|
|
45
|
+
const imageData = {
|
|
46
|
+
data: new Uint8ClampedArray([
|
|
47
|
+
255,
|
|
48
|
+
0,
|
|
49
|
+
0,
|
|
50
|
+
255, // Opaque red
|
|
51
|
+
0,
|
|
52
|
+
255,
|
|
53
|
+
0,
|
|
54
|
+
0, // Fully transparent green
|
|
55
|
+
0,
|
|
56
|
+
0,
|
|
57
|
+
255,
|
|
58
|
+
128, // Semi-transparent blue
|
|
59
|
+
255,
|
|
60
|
+
255,
|
|
61
|
+
0,
|
|
62
|
+
2, // Nearly transparent yellow (< 1%)
|
|
63
|
+
]),
|
|
64
|
+
height: 2,
|
|
65
|
+
width: 2,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const pixels = imageDataToPixels(imageData);
|
|
69
|
+
|
|
70
|
+
// Should keep opaque and semi-transparent, skip fully and nearly transparent
|
|
71
|
+
expect(pixels).toHaveLength(2);
|
|
72
|
+
expect(pixels[0]).toBe(utils.argbFromRgb(255, 0, 0));
|
|
73
|
+
expect(pixels[1]).toBe(utils.argbFromRgb(0, 0, 255));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should handle regular number array input", () => {
|
|
77
|
+
const imageData = {
|
|
78
|
+
data: [255, 255, 255, 255, 0, 0, 0, 255],
|
|
79
|
+
height: 1,
|
|
80
|
+
width: 2,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const pixels = imageDataToPixels(imageData);
|
|
84
|
+
|
|
85
|
+
expect(pixels).toHaveLength(2);
|
|
86
|
+
expect(pixels[0]).toBe(utils.argbFromRgb(255, 255, 255));
|
|
87
|
+
expect(pixels[1]).toBe(utils.argbFromRgb(0, 0, 0));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should handle empty image data", () => {
|
|
91
|
+
const imageData = {
|
|
92
|
+
data: new Uint8ClampedArray([]),
|
|
93
|
+
height: 0,
|
|
94
|
+
width: 0,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const pixels = imageDataToPixels(imageData);
|
|
98
|
+
expect(pixels).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should handle single pixel", () => {
|
|
102
|
+
const imageData = {
|
|
103
|
+
data: new Uint8ClampedArray([128, 64, 192, 255]),
|
|
104
|
+
height: 1,
|
|
105
|
+
width: 1,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const pixels = imageDataToPixels(imageData);
|
|
109
|
+
expect(pixels).toHaveLength(1);
|
|
110
|
+
expect(pixels[0]).toBe(utils.argbFromRgb(128, 64, 192));
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("samplePixels", () => {
|
|
115
|
+
it("should return all pixels when under limit", () => {
|
|
116
|
+
const pixels = [1, 2, 3, 4, 5];
|
|
117
|
+
const sampled = samplePixels(pixels, 10);
|
|
118
|
+
|
|
119
|
+
expect(sampled).toEqual(pixels);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should sample evenly when over limit", () => {
|
|
123
|
+
const pixels = Array.from({ length: 100 }, (_, i) => i);
|
|
124
|
+
const sampled = samplePixels(pixels, 10);
|
|
125
|
+
|
|
126
|
+
expect(sampled).toHaveLength(10);
|
|
127
|
+
expect(sampled[0]).toBe(0);
|
|
128
|
+
expect(sampled[1]).toBe(10);
|
|
129
|
+
expect(sampled[2]).toBe(20);
|
|
130
|
+
expect(sampled[9]).toBe(90);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should handle exact limit", () => {
|
|
134
|
+
const pixels = [1, 2, 3, 4, 5];
|
|
135
|
+
const sampled = samplePixels(pixels, 5);
|
|
136
|
+
|
|
137
|
+
expect(sampled).toEqual(pixels);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should handle single pixel with limit 1", () => {
|
|
141
|
+
const pixels = [42];
|
|
142
|
+
const sampled = samplePixels(pixels, 1);
|
|
143
|
+
|
|
144
|
+
expect(sampled).toEqual([42]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should use default maxPixels of 10000", () => {
|
|
148
|
+
const pixels = Array.from({ length: 5000 }, (_, i) => i);
|
|
149
|
+
const sampled = samplePixels(pixels);
|
|
150
|
+
|
|
151
|
+
expect(sampled).toHaveLength(5000); // Under default limit
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should sample large arrays with default limit", () => {
|
|
155
|
+
const pixels = Array.from({ length: 20000 }, (_, i) => i);
|
|
156
|
+
const sampled = samplePixels(pixels);
|
|
157
|
+
|
|
158
|
+
expect(sampled.length).toBeLessThanOrEqual(10000);
|
|
159
|
+
expect(sampled[0]).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("filterExtremeTones", () => {
|
|
164
|
+
it("should filter near-black pixels", () => {
|
|
165
|
+
const blackPixel = utils.argbFromRgb(10, 10, 10); // Very dark
|
|
166
|
+
const midPixel = utils.argbFromRgb(128, 128, 128); // Middle gray
|
|
167
|
+
|
|
168
|
+
const pixels = [blackPixel, midPixel];
|
|
169
|
+
const filtered = filterExtremeTones(pixels);
|
|
170
|
+
|
|
171
|
+
expect(filtered).toHaveLength(1);
|
|
172
|
+
expect(filtered[0]).toBe(midPixel);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should filter near-white pixels", () => {
|
|
176
|
+
const whitePixel = utils.argbFromRgb(250, 250, 250); // Very light
|
|
177
|
+
const midPixel = utils.argbFromRgb(128, 128, 128); // Middle gray
|
|
178
|
+
|
|
179
|
+
const pixels = [whitePixel, midPixel];
|
|
180
|
+
const filtered = filterExtremeTones(pixels);
|
|
181
|
+
|
|
182
|
+
expect(filtered).toHaveLength(1);
|
|
183
|
+
expect(filtered[0]).toBe(midPixel);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should keep mid-tone pixels", () => {
|
|
187
|
+
const pixels = [
|
|
188
|
+
utils.argbFromRgb(50, 50, 50), // Dark but not too dark
|
|
189
|
+
utils.argbFromRgb(100, 100, 100), // Mid-dark
|
|
190
|
+
utils.argbFromRgb(150, 150, 150), // Mid-light
|
|
191
|
+
utils.argbFromRgb(200, 200, 200), // Light but not too light
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
const filtered = filterExtremeTones(pixels);
|
|
195
|
+
expect(filtered).toHaveLength(4);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should handle colored pixels correctly", () => {
|
|
199
|
+
const pixels = [
|
|
200
|
+
utils.argbFromRgb(255, 0, 0), // Pure red - should be kept
|
|
201
|
+
utils.argbFromRgb(0, 255, 0), // Pure green - should be kept
|
|
202
|
+
utils.argbFromRgb(0, 0, 255), // Pure blue - should be kept
|
|
203
|
+
utils.argbFromRgb(5, 5, 5), // Near black - should be filtered
|
|
204
|
+
utils.argbFromRgb(250, 250, 250), // Near white - should be filtered
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
const filtered = filterExtremeTones(pixels);
|
|
208
|
+
expect(filtered).toHaveLength(3);
|
|
209
|
+
|
|
210
|
+
// Check that primary colors are kept
|
|
211
|
+
const filteredRgbs = filtered.map((pixel) => ({
|
|
212
|
+
b: utils.blueFromArgb(pixel),
|
|
213
|
+
g: utils.greenFromArgb(pixel),
|
|
214
|
+
r: utils.redFromArgb(pixel),
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
expect(filteredRgbs).toContainEqual({ b: 0, g: 0, r: 255 });
|
|
218
|
+
expect(filteredRgbs).toContainEqual({ b: 0, g: 255, r: 0 });
|
|
219
|
+
expect(filteredRgbs).toContainEqual({ b: 255, g: 0, r: 0 });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("should handle empty array", () => {
|
|
223
|
+
const filtered = filterExtremeTones([]);
|
|
224
|
+
expect(filtered).toHaveLength(0);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should filter based on luminance calculation", () => {
|
|
228
|
+
// Test edge cases for luminance calculation
|
|
229
|
+
// Luminance = 0.299 * R + 0.587 * G + 0.114 * B
|
|
230
|
+
|
|
231
|
+
// Just above 5% threshold (12.75)
|
|
232
|
+
const justAbove = utils.argbFromRgb(13, 13, 13);
|
|
233
|
+
// Just below 95% threshold (242.25)
|
|
234
|
+
const justBelow = utils.argbFromRgb(242, 242, 242);
|
|
235
|
+
|
|
236
|
+
const pixels = [justAbove, justBelow];
|
|
237
|
+
const filtered = filterExtremeTones(pixels);
|
|
238
|
+
|
|
239
|
+
expect(filtered).toHaveLength(2); // Both should be kept
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|