splashy 6.0.45 → 6.0.46
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/LICENSE +27 -0
- package/README.md +1 -1
- package/package.json +2 -5
- package/src/index.js +4 -32
- package/src/quantize.js +243 -0
package/LICENSE
CHANGED
|
@@ -19,3 +19,30 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
21
|
THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
src/quantize.js is ported from quantize.js (Copyright 2008 Nick Rabinowitz) and its
|
|
26
|
+
Node.js port @lokesh.dhakar/quantize (Copyright (c) 2014 Olivier Lesnicki), used under
|
|
27
|
+
the following license:
|
|
28
|
+
|
|
29
|
+
The MIT License (MIT)
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2014 Olivier Lesnicki
|
|
32
|
+
|
|
33
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
34
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
35
|
+
the Software without restriction, including without limitation the rights to
|
|
36
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
37
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
38
|
+
subject to the following conditions:
|
|
39
|
+
|
|
40
|
+
The above copyright notice and this permission notice shall be included in all
|
|
41
|
+
copies or substantial portions of the Software.
|
|
42
|
+
|
|
43
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
44
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
45
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
46
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
47
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
48
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -74,6 +74,6 @@ The raw content for detecting the color information.
|
|
|
74
74
|
|
|
75
75
|
Authored and maintained by [Kiko Beats](https://kikobeats.com) with help from [contributors](https://github.com/microlink/microlink-function/contributors).
|
|
76
76
|
|
|
77
|
-
Special thanks to [Tim Carry](https://github.com/pixelastic) for writing the benchmark
|
|
77
|
+
Special thanks to [Tim Carry](https://github.com/pixelastic) for writing the benchmark, [Lokesh Dhakar](https://github.com/lokesh) for the original code implementation, and [Nick Rabinowitz](https://github.com/nrabinowitz) for [quantize.js](https://gist.github.com/nrabinowitz/1104622) (MIT), the median cut quantization `src/quantize.js` is ported from.
|
|
78
78
|
|
|
79
79
|
> [microlink.io](https://microlink.io) · GitHub [microlinkhq](https://github.com/microlinkhq) · X [@microlinkhq](https://x.com/microlinkhq)
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "splashy",
|
|
3
3
|
"description": "Given an image, extract predominant & palette colors",
|
|
4
4
|
"homepage": "https://github.com/microlinkhq/splashy",
|
|
5
|
-
"version": "6.0.
|
|
5
|
+
"version": "6.0.46",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Kiko Beats",
|
|
@@ -46,10 +46,6 @@
|
|
|
46
46
|
"url"
|
|
47
47
|
],
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lokesh.dhakar/quantize": "~1.4.0",
|
|
50
|
-
"ndarray": "~1.1.1",
|
|
51
|
-
"node-addon-api": "~8.9.0",
|
|
52
|
-
"node-gyp": "~13.1.0",
|
|
53
49
|
"sharp": "0.35.5"
|
|
54
50
|
},
|
|
55
51
|
"devDependencies": {
|
|
@@ -75,6 +71,7 @@
|
|
|
75
71
|
"src"
|
|
76
72
|
],
|
|
77
73
|
"scripts": {
|
|
74
|
+
"benchmark": "node benchmark/index.js",
|
|
78
75
|
"clean": "rm -rf node_modules",
|
|
79
76
|
"contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
|
|
80
77
|
"formats:update": "node test/supported-formats.mjs --update",
|
package/src/index.js
CHANGED
|
@@ -1,49 +1,21 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const quantize = require('@lokesh.dhakar/quantize').default || require('@lokesh.dhakar/quantize')
|
|
4
|
-
const ndarray = require('ndarray')
|
|
5
3
|
const sharp = require('sharp')
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
return ndarray(
|
|
9
|
-
new Uint8Array(data.buffer, data.byteOffset, data.length),
|
|
10
|
-
[info.width, info.height, 4],
|
|
11
|
-
[4, (4 * info.width) | 0, 1],
|
|
12
|
-
0
|
|
13
|
-
)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function createPixelArray (pixels, pixelCount, quality = 10) {
|
|
17
|
-
const pixelArray = []
|
|
18
|
-
for (let i = 0, offset; i < pixelCount; i += quality) {
|
|
19
|
-
offset = i * 4
|
|
20
|
-
const r = pixels[offset]
|
|
21
|
-
const g = pixels[offset + 1]
|
|
22
|
-
const b = pixels[offset + 2]
|
|
23
|
-
const a = pixels[offset + 3]
|
|
24
|
-
const isOpaqueEnough = a >= 125
|
|
25
|
-
const isWhite = r > 250 && g > 250 && b > 250
|
|
26
|
-
if (isOpaqueEnough && !isWhite) pixelArray.push([r, g, b])
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return pixelArray
|
|
30
|
-
}
|
|
5
|
+
const quantize = require('./quantize')
|
|
31
6
|
|
|
32
7
|
const toHex = ([r, g, b]) => '#' + (b | (g << 8) | (r << 16) | (1 << 24)).toString(16).slice(1)
|
|
33
8
|
|
|
34
9
|
module.exports = async function (buffer) {
|
|
35
|
-
const
|
|
10
|
+
const { data, info } = await sharp(buffer)
|
|
36
11
|
// resizing the image before processing leads to more consistent (and much shorter) processing times.
|
|
37
12
|
// .resize(200, 200, { fit: 'inside', withoutEnlargement: true })
|
|
38
13
|
.ensureAlpha()
|
|
39
14
|
.raw()
|
|
40
15
|
.toBuffer({ resolveWithObject: true })
|
|
41
16
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const pixelArray = createPixelArray(imgData.data, pixelCount)
|
|
45
|
-
const cmap = quantize(pixelArray, 10) // internal tuning
|
|
46
|
-
return cmap.palette().slice(0, 6).map(toHex)
|
|
17
|
+
const palette = quantize(data, info.width * info.height, { step: 10, maxColors: 10 })
|
|
18
|
+
return palette.slice(0, 6).map(toHex)
|
|
47
19
|
}
|
|
48
20
|
|
|
49
21
|
module.exports.toHex = toHex
|
package/src/quantize.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const SIGBITS = 5
|
|
4
|
+
const RSHIFT = 8 - SIGBITS
|
|
5
|
+
const HISTO_SIZE = 1 << (3 * SIGBITS)
|
|
6
|
+
const MAX_ITERATIONS = 1000
|
|
7
|
+
const FRACT_BY_POPULATIONS = 0.75
|
|
8
|
+
const MIN_ALPHA = 125
|
|
9
|
+
const WHITE_THRESHOLD = 250
|
|
10
|
+
|
|
11
|
+
const getColorIndex = (r, g, b) => (r << (2 * SIGBITS)) + (g << SIGBITS) + b
|
|
12
|
+
|
|
13
|
+
class PQueue {
|
|
14
|
+
constructor (comparator) {
|
|
15
|
+
this.comparator = comparator
|
|
16
|
+
this.contents = []
|
|
17
|
+
this.sorted = false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
push (item) {
|
|
21
|
+
this.contents.push(item)
|
|
22
|
+
this.sorted = false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pop () {
|
|
26
|
+
if (!this.sorted) {
|
|
27
|
+
this.contents.sort(this.comparator)
|
|
28
|
+
this.sorted = true
|
|
29
|
+
}
|
|
30
|
+
return this.contents.pop()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
size () {
|
|
34
|
+
return this.contents.length
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class VBox {
|
|
39
|
+
constructor (r1, r2, g1, g2, b1, b2, histo) {
|
|
40
|
+
this.r1 = r1
|
|
41
|
+
this.r2 = r2
|
|
42
|
+
this.g1 = g1
|
|
43
|
+
this.g2 = g2
|
|
44
|
+
this.b1 = b1
|
|
45
|
+
this.b2 = b2
|
|
46
|
+
this.histo = histo
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
volume () {
|
|
50
|
+
if (!this._volume) {
|
|
51
|
+
this._volume = (this.r2 - this.r1 + 1) * (this.g2 - this.g1 + 1) * (this.b2 - this.b1 + 1)
|
|
52
|
+
}
|
|
53
|
+
return this._volume
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
count () {
|
|
57
|
+
if (!this._countSet) {
|
|
58
|
+
let npix = 0
|
|
59
|
+
for (let i = this.r1; i <= this.r2; i++) {
|
|
60
|
+
for (let j = this.g1; j <= this.g2; j++) {
|
|
61
|
+
for (let k = this.b1; k <= this.b2; k++) {
|
|
62
|
+
npix += this.histo[getColorIndex(i, j, k)]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this._count = npix
|
|
67
|
+
this._countSet = true
|
|
68
|
+
}
|
|
69
|
+
return this._count
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
copy () {
|
|
73
|
+
return new VBox(this.r1, this.r2, this.g1, this.g2, this.b1, this.b2, this.histo)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
avg () {
|
|
77
|
+
if (!this._avg) {
|
|
78
|
+
const mult = 1 << RSHIFT
|
|
79
|
+
if (this.r1 === this.r2 && this.g1 === this.g2 && this.b1 === this.b2) {
|
|
80
|
+
this._avg = [this.r1 << RSHIFT, this.g1 << RSHIFT, this.b1 << RSHIFT]
|
|
81
|
+
} else {
|
|
82
|
+
let ntot = 0
|
|
83
|
+
let rsum = 0
|
|
84
|
+
let gsum = 0
|
|
85
|
+
let bsum = 0
|
|
86
|
+
for (let i = this.r1; i <= this.r2; i++) {
|
|
87
|
+
for (let j = this.g1; j <= this.g2; j++) {
|
|
88
|
+
for (let k = this.b1; k <= this.b2; k++) {
|
|
89
|
+
const hval = this.histo[getColorIndex(i, j, k)]
|
|
90
|
+
ntot += hval
|
|
91
|
+
rsum += hval * (i + 0.5) * mult
|
|
92
|
+
gsum += hval * (j + 0.5) * mult
|
|
93
|
+
bsum += hval * (k + 0.5) * mult
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
this._avg = ntot
|
|
98
|
+
? [~~(rsum / ntot), ~~(gsum / ntot), ~~(bsum / ntot)]
|
|
99
|
+
: [
|
|
100
|
+
~~((mult * (this.r1 + this.r2 + 1)) / 2),
|
|
101
|
+
~~((mult * (this.g1 + this.g2 + 1)) / 2),
|
|
102
|
+
~~((mult * (this.b1 + this.b2 + 1)) / 2)
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return this._avg
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const STRIDE = { r: getColorIndex(1, 0, 0), g: getColorIndex(0, 1, 0), b: getColorIndex(0, 0, 1) }
|
|
111
|
+
const SWEEP_ORDER = { r: ['r', 'g', 'b'], g: ['g', 'r', 'b'], b: ['b', 'r', 'g'] }
|
|
112
|
+
|
|
113
|
+
const partialSums = (histo, vbox, axis) => {
|
|
114
|
+
const [outer, middle, inner] = SWEEP_ORDER[axis]
|
|
115
|
+
const partialsum = []
|
|
116
|
+
let total = 0
|
|
117
|
+
for (let i = vbox[outer + '1']; i <= vbox[outer + '2']; i++) {
|
|
118
|
+
let sum = 0
|
|
119
|
+
for (let j = vbox[middle + '1']; j <= vbox[middle + '2']; j++) {
|
|
120
|
+
for (let k = vbox[inner + '1']; k <= vbox[inner + '2']; k++) {
|
|
121
|
+
sum += histo[i * STRIDE[outer] + j * STRIDE[middle] + k * STRIDE[inner]]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
total += sum
|
|
125
|
+
partialsum[i] = total
|
|
126
|
+
}
|
|
127
|
+
return { partialsum, total }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const medianCutApply = (histo, vbox) => {
|
|
131
|
+
const rw = vbox.r2 - vbox.r1 + 1
|
|
132
|
+
const gw = vbox.g2 - vbox.g1 + 1
|
|
133
|
+
const bw = vbox.b2 - vbox.b1 + 1
|
|
134
|
+
const maxw = Math.max(rw, gw, bw)
|
|
135
|
+
if (vbox.count() === 1) return [vbox.copy()]
|
|
136
|
+
|
|
137
|
+
const axis = maxw === rw ? 'r' : maxw === gw ? 'g' : 'b'
|
|
138
|
+
const { partialsum, total } = partialSums(histo, vbox, axis)
|
|
139
|
+
const lookaheadsum = []
|
|
140
|
+
partialsum.forEach((d, i) => {
|
|
141
|
+
lookaheadsum[i] = total - d
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const dim1 = axis + '1'
|
|
145
|
+
const dim2 = axis + '2'
|
|
146
|
+
for (let i = vbox[dim1]; i <= vbox[dim2]; i++) {
|
|
147
|
+
if (partialsum[i] > total / 2) {
|
|
148
|
+
const vbox1 = vbox.copy()
|
|
149
|
+
const vbox2 = vbox.copy()
|
|
150
|
+
const left = i - vbox[dim1]
|
|
151
|
+
const right = vbox[dim2] - i
|
|
152
|
+
let d2 =
|
|
153
|
+
left <= right
|
|
154
|
+
? Math.min(vbox[dim2] - 1, ~~(i + right / 2))
|
|
155
|
+
: Math.max(vbox[dim1], ~~(i - 1 - left / 2))
|
|
156
|
+
while (!partialsum[d2]) d2++
|
|
157
|
+
let count2 = lookaheadsum[d2]
|
|
158
|
+
while (!count2 && partialsum[d2 - 1]) count2 = lookaheadsum[--d2]
|
|
159
|
+
vbox1[dim2] = d2
|
|
160
|
+
vbox2[dim1] = vbox1[dim2] + 1
|
|
161
|
+
return [vbox1, vbox2]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const iterate = (histo, queue, target) => {
|
|
167
|
+
let ncolors = queue.size()
|
|
168
|
+
let niters = 0
|
|
169
|
+
while (niters++ < MAX_ITERATIONS) {
|
|
170
|
+
if (ncolors >= target) return
|
|
171
|
+
const vbox = queue.pop()
|
|
172
|
+
if (!vbox.count()) {
|
|
173
|
+
queue.push(vbox)
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
const [vbox1, vbox2] = medianCutApply(histo, vbox)
|
|
177
|
+
queue.push(vbox1)
|
|
178
|
+
if (vbox2) {
|
|
179
|
+
queue.push(vbox2)
|
|
180
|
+
ncolors++
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const isSampled = (r, g, b, a) =>
|
|
186
|
+
a >= MIN_ALPHA && !(r > WHITE_THRESHOLD && g > WHITE_THRESHOLD && b > WHITE_THRESHOLD)
|
|
187
|
+
|
|
188
|
+
const sample = (rgba, pixelCount, step, maxColors) => {
|
|
189
|
+
const histo = new Uint32Array(HISTO_SIZE)
|
|
190
|
+
const uniqueKeys = []
|
|
191
|
+
const uniqueColors = []
|
|
192
|
+
let count = 0
|
|
193
|
+
let rmin = 1000000
|
|
194
|
+
let rmax = 0
|
|
195
|
+
let gmin = 1000000
|
|
196
|
+
let gmax = 0
|
|
197
|
+
let bmin = 1000000
|
|
198
|
+
let bmax = 0
|
|
199
|
+
for (let i = 0; i < pixelCount; i += step) {
|
|
200
|
+
const offset = i * 4
|
|
201
|
+
const r = rgba[offset]
|
|
202
|
+
const g = rgba[offset + 1]
|
|
203
|
+
const b = rgba[offset + 2]
|
|
204
|
+
if (!isSampled(r, g, b, rgba[offset + 3])) continue
|
|
205
|
+
count++
|
|
206
|
+
if (uniqueKeys.length <= maxColors) {
|
|
207
|
+
const key = (r << 16) | (g << 8) | b
|
|
208
|
+
if (!uniqueKeys.includes(key)) {
|
|
209
|
+
uniqueKeys.push(key)
|
|
210
|
+
uniqueColors.push([r, g, b])
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const rval = r >> RSHIFT
|
|
214
|
+
const gval = g >> RSHIFT
|
|
215
|
+
const bval = b >> RSHIFT
|
|
216
|
+
histo[getColorIndex(rval, gval, bval)]++
|
|
217
|
+
if (rval < rmin) rmin = rval
|
|
218
|
+
else if (rval > rmax) rmax = rval
|
|
219
|
+
if (gval < gmin) gmin = gval
|
|
220
|
+
else if (gval > gmax) gmax = gval
|
|
221
|
+
if (bval < bmin) bmin = bval
|
|
222
|
+
else if (bval > bmax) bmax = bval
|
|
223
|
+
}
|
|
224
|
+
return { count, uniqueColors, histo, vbox: new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo) }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = (rgba, pixelCount, { step, maxColors }) => {
|
|
228
|
+
const { count, uniqueColors, histo, vbox } = sample(rgba, pixelCount, step, maxColors)
|
|
229
|
+
if (!count) return false
|
|
230
|
+
if (uniqueColors.length <= maxColors) return uniqueColors
|
|
231
|
+
|
|
232
|
+
const byCount = new PQueue((a, b) => a.count() - b.count())
|
|
233
|
+
byCount.push(vbox)
|
|
234
|
+
iterate(histo, byCount, FRACT_BY_POPULATIONS * maxColors)
|
|
235
|
+
|
|
236
|
+
const byVolume = new PQueue((a, b) => a.count() * a.volume() - b.count() * b.volume())
|
|
237
|
+
while (byCount.size()) byVolume.push(byCount.pop())
|
|
238
|
+
iterate(histo, byVolume, maxColors)
|
|
239
|
+
|
|
240
|
+
const palette = []
|
|
241
|
+
while (byVolume.size()) palette.push(byVolume.pop().avg())
|
|
242
|
+
return palette
|
|
243
|
+
}
|