splashy 6.0.45 → 6.0.47

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 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 and [Lokesh Dhakar](https://github.com/lokesh) for the original code implementation.
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.45",
5
+ "version": "6.0.47",
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
- async function getPixels ({ data, info }) {
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 raw = await sharp(buffer)
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 imgData = await getPixels(raw)
43
- const pixelCount = imgData.shape[0] * imgData.shape[1]
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
@@ -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
+ }