atoopub commited on
Commit
9fa1f67
·
verified ·
1 Parent(s): 547b07c

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +21 -0
  2. extract-colors.js +329 -0
  3. fast-average-color.js +510 -0
  4. index.js +94 -0
  5. package.json +20 -0
  6. phash.js +99 -0
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Menggunakan image Node.js yang lebih stabil
2
+ FROM node:16-slim
3
+
4
+
5
+ # Tentukan direktori kerja di dalam container
6
+ WORKDIR /app
7
+
8
+ # Salin file package.json dan package-lock.json terlebih dahulu
9
+ COPY package*.json ./
10
+
11
+ # Install dependensi Node.js
12
+ RUN npm install
13
+
14
+ # Salin seluruh kode aplikasi ke dalam container
15
+ COPY . .
16
+
17
+ # Ekspose port 3000 yang akan digunakan aplikasi Express
18
+ EXPOSE 3000
19
+
20
+ # Tentukan perintah untuk menjalankan aplikasi Express.js
21
+ CMD ["npm", "start"]
extract-colors.js ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Color {
2
+ constructor(red, green, blue, hex = red << 16 | green << 8 | blue) {
3
+ this._count = 1
4
+ this.__saturation = -1
5
+ this.__hue = -1
6
+ this.__lightness = -1
7
+ this.__intensity = -1
8
+ this._red = red
9
+ this._green = green
10
+ this._blue = blue
11
+ this._hex = hex
12
+ }
13
+ static distance(colorA, colorB) {
14
+ return (Math.abs(colorB._red - colorA._red) + Math.abs(colorB._green - colorA._green) + Math.abs(colorB._blue - colorA._blue)) / (3 * 255)
15
+ }
16
+ updateHSL() {
17
+ const red = this._red / 255
18
+ const green = this._green / 255
19
+ const blue = this._blue / 255
20
+ const max = Math.max(red, green, blue)
21
+ const min = Math.min(red, green, blue)
22
+ this.__lightness = (max + min) / 2
23
+ if (max === min) {
24
+ this.__hue = 0
25
+ this.__saturation = 0
26
+ this.__intensity = 0
27
+ } else {
28
+ const distance2 = max - min
29
+ this.__saturation = this.__lightness > 0.5 ? distance2 / (2 - max - min) : distance2 / (max + min)
30
+ this.__intensity = this.__saturation * ((0.5 - Math.abs(0.5 - this.__lightness)) * 2)
31
+ switch (max) {
32
+ case red:
33
+ this.__hue = ((green - blue) / distance2 + (green < blue ? 6 : 0)) / 6
34
+ break
35
+ case green:
36
+ this.__hue = ((blue - red) / distance2 + 2) / 6
37
+ break
38
+ case blue:
39
+ this.__hue = ((red - green) / distance2 + 4) / 6
40
+ break
41
+ }
42
+ }
43
+ }
44
+ get _hue() {
45
+ if (this.__hue === -1) {
46
+ this.updateHSL()
47
+ }
48
+ return this.__hue
49
+ }
50
+ get _saturation() {
51
+ if (this.__saturation === -1) {
52
+ this.updateHSL()
53
+ }
54
+ return this.__saturation
55
+ }
56
+ get _lightness() {
57
+ if (this.__lightness === -1) {
58
+ this.updateHSL()
59
+ }
60
+ return this.__lightness
61
+ }
62
+ get _intensity() {
63
+ if (this.__intensity === -1) {
64
+ this.updateHSL()
65
+ }
66
+ return this.__intensity
67
+ }
68
+ }
69
+ const distance = (a, b) => Math.abs(a - b)
70
+ const hueDistance = (a, b) => Math.min(distance(a, b), distance((a + 0.5) % 1, (b + 0.5) % 1))
71
+ class AverageGroup {
72
+ constructor() {
73
+ this.colors = []
74
+ this._average = null
75
+ }
76
+ addColor(color) {
77
+ this.colors.push(color)
78
+ this._average = null
79
+ }
80
+ isSamePalette(color, hue, saturation, lightness) {
81
+ for (let i = 0; i < this.colors.length; i++) {
82
+ const currentColor = this.colors[i]
83
+ const isSame = hueDistance(currentColor._hue, color._hue) < hue && distance(currentColor._saturation, color._saturation) < saturation && distance(currentColor._lightness, color._lightness) < lightness
84
+ if (!isSame) {
85
+ return false
86
+ }
87
+ }
88
+ return true
89
+ }
90
+ get average() {
91
+ if (!this._average) {
92
+ const { r, g, b } = this.colors.reduce((total2, color) => {
93
+ total2.r += color._red
94
+ total2.g += color._green
95
+ total2.b += color._blue
96
+ return total2
97
+ }, { r: 0, g: 0, b: 0 })
98
+ const total = this.colors.reduce((_count, color) => _count + color._count, 0)
99
+ this._average = new Color(
100
+ Math.round(r / this.colors.length),
101
+ Math.round(g / this.colors.length),
102
+ Math.round(b / this.colors.length)
103
+ )
104
+ this._average._count = total
105
+ }
106
+ return this._average
107
+ }
108
+ }
109
+ class AverageManager {
110
+ constructor(hue, saturation, lightness) {
111
+ this._groups = []
112
+ this._hue = hue
113
+ this._saturation = saturation
114
+ this._lightness = lightness
115
+ }
116
+ addColor(color) {
117
+ const samePalette = this._groups.find((averageGroup) => averageGroup.isSamePalette(color, this._hue, this._saturation, this._lightness))
118
+ if (samePalette) {
119
+ samePalette.addColor(color)
120
+ } else {
121
+ const averageGroup = new AverageGroup()
122
+ averageGroup.addColor(color)
123
+ this._groups.push(averageGroup)
124
+ }
125
+ }
126
+ getGroups() {
127
+ return this._groups.map((averageGroup) => averageGroup.average)
128
+ }
129
+ }
130
+ var sortColors = (list, _pixels, _hueDistance, _saturationDistance, _lightnessDistance) => {
131
+ const averageManager = new AverageManager(_hueDistance, _saturationDistance, _lightnessDistance)
132
+ list.forEach((color) => averageManager.addColor(color))
133
+ const sorted = averageManager.getGroups()
134
+ sorted.sort((a, b) => {
135
+ const bPower = (b._intensity + 0.1) * (0.9 - b._count / _pixels)
136
+ const aPower = (a._intensity + 0.1) * (0.9 - a._count / _pixels)
137
+ return bPower - aPower
138
+ })
139
+ return sorted
140
+ }
141
+ const createFinalColor = (color, pixels) => {
142
+ return {
143
+ hex: `#${'0'.repeat(6 - color._hex.toString(16).length)}${color._hex.toString(16)}`,
144
+ red: color._red,
145
+ green: color._green,
146
+ blue: color._blue,
147
+ area: color._count / pixels,
148
+ hue: color._hue,
149
+ saturation: color._saturation,
150
+ lightness: color._lightness,
151
+ intensity: color._intensity
152
+ }
153
+ }
154
+ const testUint = (label, val, min = 0, max = Number.MAX_SAFE_INTEGER) => {
155
+ if (!Number.isInteger(val) || val < min || val > max) {
156
+ throw new Error(`${label} is invalid (${val})`)
157
+ }
158
+ return val
159
+ }
160
+ const testNumber = (label, val, min = 0, max = Number.MAX_VALUE) => {
161
+ if (Number(val) !== val || val < min || val > max) {
162
+ throw new Error(`${label} is invalid (${val})`)
163
+ }
164
+ return val
165
+ }
166
+ const testFunction = (label, val) => {
167
+ if (!val || {}.toString.call(val) !== '[object Function]') {
168
+ throw new Error(`${label} is invalid (${val})`)
169
+ }
170
+ return val
171
+ }
172
+ var cleanInputs = (options) => {
173
+ var _a, _b, _c, _d, _e, _f, _g, _h
174
+ return [
175
+ testUint('pixels', (_a = options.pixels) != null ? _a : 1e4, 1),
176
+ testNumber('distance', (_b = options.distance) != null ? _b : 0.12, 0, 1),
177
+ testUint('splitPower', (_c = options.splitPower) != null ? _c : 10, 2, 15),
178
+ testFunction('colorValidator', (_d = options.colorValidator) != null ? _d : (_red, _green, _blue, _alpha) => (_alpha != null ? _alpha : 255) > 250),
179
+ testNumber('hueDistance', (_e = options.hueDistance) != null ? _e : 0.08333333333333333, 0, 1),
180
+ testNumber('saturationDistance', (_f = options.saturationDistance) != null ? _f : 0.2, 0, 1),
181
+ testNumber('lightnessDistance', (_g = options.lightnessDistance) != null ? _g : 0.2, 0, 1),
182
+ (_h = options.crossOrigin) != null ? _h : null
183
+ ]
184
+ }
185
+ class BudGroup {
186
+ constructor() {
187
+ this._count = 1
188
+ this._children = {}
189
+ }
190
+ addColor(_hex, _red, _green, _blue) {
191
+ if (this._children[_hex]) {
192
+ this._children[_hex]._count++
193
+ } else {
194
+ this._children[_hex] = new Color(_red, _green, _blue, _hex)
195
+ }
196
+ return this._children[_hex]
197
+ }
198
+ getList() {
199
+ return Object.keys(this._children).map((key) => this._children[key])
200
+ }
201
+ getMaxWeight(_count) {
202
+ if (this._maxWeight === void 0) {
203
+ const list = this.getList().map((child) => child._count / _count)
204
+ list.sort((a, b) => b - a)
205
+ this._maxWeight = list[0] || 0
206
+ }
207
+ return this._maxWeight
208
+ }
209
+ getMaxWeightColor(_count) {
210
+ const list = this.getList()
211
+ list.sort((a, b) => {
212
+ return b._count / _count - a._count / _count
213
+ })
214
+ return list[0]
215
+ }
216
+ getMaxCountColor() {
217
+ const list = this.getList()
218
+ const biggest = list.reduce((a, b) => a._count >= b._count ? a : b)
219
+ return biggest
220
+ }
221
+ }
222
+ class RootGroup {
223
+ constructor() {
224
+ this._count = 1
225
+ this._children = {}
226
+ }
227
+ addRootGroup(key) {
228
+ if (this._children[key]) {
229
+ this._children[key]._count++
230
+ } else {
231
+ this._children[key] = new RootGroup()
232
+ }
233
+ return this._children[key]
234
+ }
235
+ getList() {
236
+ return Object.keys(this._children).map((key) => this._children[key])
237
+ }
238
+ addBudGroup(key) {
239
+ if (this._children[key]) {
240
+ this._children[key]._count++
241
+ } else {
242
+ this._children[key] = new BudGroup()
243
+ }
244
+ return this._children[key]
245
+ }
246
+ getMaxWeight(_count) {
247
+ if (this._maxWeight === void 0) {
248
+ const list = this.getList().map((child) => child._count / _count)
249
+ list.sort((a, b) => b - a)
250
+ this._maxWeight = list[0] || 0
251
+ }
252
+ return this._maxWeight
253
+ }
254
+ getMaxWeightColor(_count) {
255
+ const list = this.getList()
256
+ list.sort((a, b) => {
257
+ return b._count / _count - a._count / _count
258
+ })
259
+ return list[0].getMaxWeightColor(_count)
260
+ }
261
+ getMaxCountColor() {
262
+ const list = this.getList()
263
+ const biggest = list.reduce((a, b) => a.getMaxCountColor()._count >= b.getMaxCountColor()._count ? a : b)
264
+ return biggest.getMaxCountColor()
265
+ }
266
+ getColors(_distance, _count) {
267
+ const list = this.getList().map((child) => {
268
+ const { _count: _count2 } = child
269
+ const color = child.getMaxCountColor()
270
+ color._count = _count2
271
+ return color
272
+ })
273
+ list.sort((a, b) => b._count / _count - a._count / _count)
274
+ const newList = []
275
+ list.forEach((color) => {
276
+ const near = newList.find((col) => Color.distance(col, color) < _distance)
277
+ if (!near) {
278
+ newList.push(color)
279
+ } else {
280
+ near._count += color._count
281
+ }
282
+ })
283
+ return newList
284
+ }
285
+ }
286
+ var extractor = ({ data, width, height }, _pixels, _distance, _splitPower, _colorValidator) => {
287
+ const rootGroup = new RootGroup()
288
+ const reducer = width && height ? Math.floor(width * height / _pixels) || 1 : 1
289
+ for (let i = 0; i < data.length; i += 4 * reducer) {
290
+ const r = data[i]
291
+ const g = data[i + 1]
292
+ const b = data[i + 2]
293
+ const a = data[i + 3]
294
+ if (_colorValidator(r, g, b, a)) {
295
+ const real = r << 16 | g << 8 | b
296
+ const medium = (r >> 4 & 15) << 2 | (g >> 4 & 15) << 1 | b >> 4 & 15
297
+ const small = Math.round(r * (_splitPower - 1) / 255) * (_splitPower * _splitPower) + Math.round(g * (_splitPower - 1) / 255) * _splitPower + Math.round(b * (_splitPower - 1) / 255)
298
+ const smallGroup = rootGroup.addRootGroup(small)
299
+ const mediumGroup = smallGroup.addBudGroup(medium)
300
+ mediumGroup.addColor(real, r, g, b)
301
+ }
302
+ }
303
+ return rootGroup.getColors(_distance, _pixels)
304
+ }
305
+ const sortFinalColors = (_colors, _pixels, _hueDistance, _saturationDistance, _lightnessDistance) => {
306
+ const list = sortColors(_colors, _pixels, _hueDistance, _saturationDistance, _lightnessDistance)
307
+ return list.map((color) => createFinalColor(color, _pixels))
308
+ }
309
+ const extractColorsFromImageData = (imageData, options = {}) => {
310
+ const [_pixels, _distance, _splitPower, _colorValidator, _hueDistance, _saturationDistance, _lightnessDistance] = cleanInputs(options)
311
+ const colors = extractor(imageData, _pixels, _distance, _splitPower, _colorValidator)
312
+ const px = imageData.width && imageData.height ? Math.min(imageData.width * imageData.height, _pixels) : _pixels
313
+ return sortFinalColors(colors, px, _hueDistance, _saturationDistance, _lightnessDistance)
314
+ }
315
+ const extractColors = (imageData, options) => {
316
+ if (imageData.data) {
317
+ return new Promise((resolve) => {
318
+ resolve(extractColorsFromImageData(imageData, options))
319
+ })
320
+ }
321
+ throw new Error('Send imageData to extractColors')
322
+ }
323
+ const extractColorsFromImage = () => {
324
+ throw new Error('Can not use extractColorsFromImage for Node.js')
325
+ }
326
+ const extractColorsFromSrc = () => {
327
+ throw new Error('Can not use extractColorsFromSrc for Node.js')
328
+ }
329
+ export { extractColors as default, extractColors, extractColorsFromImage, extractColorsFromImageData, extractColorsFromSrc }
fast-average-color.js ADDED
@@ -0,0 +1,510 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*! Fast Average Color | © 2022 Denis Seleznev | MIT License | https://github.com/fast-average-color/fast-average-color */
2
+ function toHex(num) {
3
+ let str = num.toString(16)
4
+ return str.length === 1 ? '0' + str : str
5
+ }
6
+ function arrayToHex(arr) {
7
+ return '#' + arr.map(toHex).join('')
8
+ }
9
+ function isDark(color) {
10
+ // http://www.w3.org/TR/AERT#color-contrast
11
+ let result = (color[0] * 299 + color[1] * 587 + color[2] * 114) / 1000
12
+ return result < 128
13
+ }
14
+ function prepareIgnoredColor(color) {
15
+ if (!color) {
16
+ return []
17
+ }
18
+ return isRGBArray(color) ? color : [color]
19
+ }
20
+ function isRGBArray(value) {
21
+ return Array.isArray(value[0])
22
+ }
23
+ function isIgnoredColor(data, index, ignoredColor) {
24
+ for (let i = 0; i < ignoredColor.length; i++) {
25
+ if (isIgnoredColorAsNumbers(data, index, ignoredColor[i])) {
26
+ return true
27
+ }
28
+ }
29
+ return false
30
+ }
31
+ function isIgnoredColorAsNumbers(data, index, ignoredColor) {
32
+ switch (ignoredColor.length) {
33
+ case 3:
34
+ // [red, green, blue]
35
+ if (isIgnoredRGBColor(data, index, ignoredColor)) {
36
+ return true
37
+ }
38
+ break
39
+ case 4:
40
+ // [red, green, blue, alpha]
41
+ if (isIgnoredRGBAColor(data, index, ignoredColor)) {
42
+ return true
43
+ }
44
+ break
45
+ case 5:
46
+ // [red, green, blue, alpha, threshold]
47
+ if (isIgnoredRGBAColorWithThreshold(data, index, ignoredColor)) {
48
+ return true
49
+ }
50
+ break
51
+ default:
52
+ return false
53
+ }
54
+ }
55
+ function isIgnoredRGBColor(data, index, ignoredColor) {
56
+ // Ignore if the pixel are transparent.
57
+ if (data[index + 3] !== 255) {
58
+ return true
59
+ }
60
+ if (data[index] === ignoredColor[0] &&
61
+ data[index + 1] === ignoredColor[1] &&
62
+ data[index + 2] === ignoredColor[2]) {
63
+ return true
64
+ }
65
+ return false
66
+ }
67
+ function isIgnoredRGBAColor(data, index, ignoredColor) {
68
+ if (data[index + 3] && ignoredColor[3]) {
69
+ return data[index] === ignoredColor[0] &&
70
+ data[index + 1] === ignoredColor[1] &&
71
+ data[index + 2] === ignoredColor[2] &&
72
+ data[index + 3] === ignoredColor[3]
73
+ }
74
+ // Ignore rgb components if the pixel are fully transparent.
75
+ return data[index + 3] === ignoredColor[3]
76
+ }
77
+ function inRange(colorComponent, ignoredColorComponent, value) {
78
+ return colorComponent >= (ignoredColorComponent - value) &&
79
+ colorComponent <= (ignoredColorComponent + value)
80
+ }
81
+ function isIgnoredRGBAColorWithThreshold(data, index, ignoredColor) {
82
+ let redIgnored = ignoredColor[0]
83
+ let greenIgnored = ignoredColor[1]
84
+ let blueIgnored = ignoredColor[2]
85
+ let alphaIgnored = ignoredColor[3]
86
+ let threshold = ignoredColor[4]
87
+ let alphaData = data[index + 3]
88
+ let alphaInRange = inRange(alphaData, alphaIgnored, threshold)
89
+ if (!alphaIgnored) {
90
+ return alphaInRange
91
+ }
92
+ if (!alphaData && alphaInRange) {
93
+ return true
94
+ }
95
+ if (inRange(data[index], redIgnored, threshold) &&
96
+ inRange(data[index + 1], greenIgnored, threshold) &&
97
+ inRange(data[index + 2], blueIgnored, threshold) &&
98
+ alphaInRange) {
99
+ return true
100
+ }
101
+ return false
102
+ }
103
+
104
+ function dominantAlgorithm(arr, len, options) {
105
+ let colorHash = {}
106
+ let divider = 24
107
+ let ignoredColor = options.ignoredColor
108
+ let step = options.step
109
+ let max = [0, 0, 0, 0, 0]
110
+ for (let i = 0; i < len; i += step) {
111
+ let red = arr[i]
112
+ let green = arr[i + 1]
113
+ let blue = arr[i + 2]
114
+ let alpha = arr[i + 3]
115
+ if (ignoredColor && isIgnoredColor(arr, i, ignoredColor)) {
116
+ continue
117
+ }
118
+ let key = Math.round(red / divider) + ',' +
119
+ Math.round(green / divider) + ',' +
120
+ Math.round(blue / divider)
121
+ if (colorHash[key]) {
122
+ colorHash[key] = [
123
+ colorHash[key][0] + red * alpha,
124
+ colorHash[key][1] + green * alpha,
125
+ colorHash[key][2] + blue * alpha,
126
+ colorHash[key][3] + alpha,
127
+ colorHash[key][4] + 1
128
+ ]
129
+ }
130
+ else {
131
+ colorHash[key] = [red * alpha, green * alpha, blue * alpha, alpha, 1]
132
+ }
133
+ if (max[4] < colorHash[key][4]) {
134
+ max = colorHash[key]
135
+ }
136
+ }
137
+ let redTotal = max[0]
138
+ let greenTotal = max[1]
139
+ let blueTotal = max[2]
140
+ let alphaTotal = max[3]
141
+ let count = max[4]
142
+ return alphaTotal ? [
143
+ Math.round(redTotal / alphaTotal),
144
+ Math.round(greenTotal / alphaTotal),
145
+ Math.round(blueTotal / alphaTotal),
146
+ Math.round(alphaTotal / count)
147
+ ] : options.defaultColor
148
+ }
149
+
150
+ function simpleAlgorithm(arr, len, options) {
151
+ let redTotal = 0
152
+ let greenTotal = 0
153
+ let blueTotal = 0
154
+ let alphaTotal = 0
155
+ let count = 0
156
+ let ignoredColor = options.ignoredColor
157
+ let step = options.step
158
+ for (let i = 0; i < len; i += step) {
159
+ let alpha = arr[i + 3]
160
+ let red = arr[i] * alpha
161
+ let green = arr[i + 1] * alpha
162
+ let blue = arr[i + 2] * alpha
163
+ if (ignoredColor && isIgnoredColor(arr, i, ignoredColor)) {
164
+ continue
165
+ }
166
+ redTotal += red
167
+ greenTotal += green
168
+ blueTotal += blue
169
+ alphaTotal += alpha
170
+ count++
171
+ }
172
+ return alphaTotal ? [
173
+ Math.round(redTotal / alphaTotal),
174
+ Math.round(greenTotal / alphaTotal),
175
+ Math.round(blueTotal / alphaTotal),
176
+ Math.round(alphaTotal / count)
177
+ ] : options.defaultColor
178
+ }
179
+
180
+ function sqrtAlgorithm(arr, len, options) {
181
+ let redTotal = 0
182
+ let greenTotal = 0
183
+ let blueTotal = 0
184
+ let alphaTotal = 0
185
+ let count = 0
186
+ let ignoredColor = options.ignoredColor
187
+ let step = options.step
188
+ for (let i = 0; i < len; i += step) {
189
+ let red = arr[i]
190
+ let green = arr[i + 1]
191
+ let blue = arr[i + 2]
192
+ let alpha = arr[i + 3]
193
+ if (ignoredColor && isIgnoredColor(arr, i, ignoredColor)) {
194
+ continue
195
+ }
196
+ redTotal += red * red * alpha
197
+ greenTotal += green * green * alpha
198
+ blueTotal += blue * blue * alpha
199
+ alphaTotal += alpha
200
+ count++
201
+ }
202
+ return alphaTotal ? [
203
+ Math.round(Math.sqrt(redTotal / alphaTotal)),
204
+ Math.round(Math.sqrt(greenTotal / alphaTotal)),
205
+ Math.round(Math.sqrt(blueTotal / alphaTotal)),
206
+ Math.round(alphaTotal / count)
207
+ ] : options.defaultColor
208
+ }
209
+
210
+ function getDefaultColor(options) {
211
+ return getOption(options, 'defaultColor', [0, 0, 0, 0])
212
+ }
213
+ function getOption(options, name, defaultValue) {
214
+ return (options[name] === undefined ? defaultValue : options[name])
215
+ }
216
+
217
+ let MIN_SIZE = 10
218
+ let MAX_SIZE = 100
219
+ function isSvg(filename) {
220
+ return filename.search(/\.svg(\?|$)/i) !== -1
221
+ }
222
+ function getOriginalSize(resource) {
223
+ if (isInstanceOfHTMLImageElement(resource)) {
224
+ let width = resource.naturalWidth
225
+ let height = resource.naturalHeight
226
+ // For SVG images with only viewBox attribute
227
+ if (!resource.naturalWidth && isSvg(resource.src)) {
228
+ width = height = MAX_SIZE
229
+ }
230
+ return {
231
+ width: width,
232
+ height: height,
233
+ }
234
+ }
235
+ if (isInstanceOfHTMLVideoElement(resource)) {
236
+ return {
237
+ width: resource.videoWidth,
238
+ height: resource.videoHeight
239
+ }
240
+ }
241
+ return {
242
+ width: resource.width,
243
+ height: resource.height
244
+ }
245
+ }
246
+ function getSrc(resource) {
247
+ if (isInstanceOfHTMLCanvasElement(resource)) {
248
+ return 'canvas'
249
+ }
250
+ if (isInstanceOfOffscreenCanvas(resource)) {
251
+ return 'offscreencanvas'
252
+ }
253
+ if (isInstanceOfImageBitmap(resource)) {
254
+ return 'imagebitmap'
255
+ }
256
+ return resource.src
257
+ }
258
+ function isInstanceOfHTMLImageElement(resource) {
259
+ return typeof HTMLImageElement !== 'undefined' && resource instanceof HTMLImageElement
260
+ }
261
+ let hasOffscreenCanvas = typeof OffscreenCanvas !== 'undefined'
262
+ function isInstanceOfOffscreenCanvas(resource) {
263
+ return hasOffscreenCanvas && resource instanceof OffscreenCanvas
264
+ }
265
+ function isInstanceOfHTMLVideoElement(resource) {
266
+ return typeof HTMLVideoElement !== 'undefined' && resource instanceof HTMLVideoElement
267
+ }
268
+ function isInstanceOfHTMLCanvasElement(resource) {
269
+ return typeof HTMLCanvasElement !== 'undefined' && resource instanceof HTMLCanvasElement
270
+ }
271
+ function isInstanceOfImageBitmap(resource) {
272
+ return typeof ImageBitmap !== 'undefined' && resource instanceof ImageBitmap
273
+ }
274
+ function prepareSizeAndPosition(originalSize, options) {
275
+ let srcLeft = getOption(options, 'left', 0)
276
+ let srcTop = getOption(options, 'top', 0)
277
+ let srcWidth = getOption(options, 'width', originalSize.width)
278
+ let srcHeight = getOption(options, 'height', originalSize.height)
279
+ let destWidth = srcWidth
280
+ let destHeight = srcHeight
281
+ if (options.mode === 'precision') {
282
+ return {
283
+ srcLeft: srcLeft,
284
+ srcTop: srcTop,
285
+ srcWidth: srcWidth,
286
+ srcHeight: srcHeight,
287
+ destWidth: destWidth,
288
+ destHeight: destHeight
289
+ }
290
+ }
291
+ let factor
292
+ if (srcWidth > srcHeight) {
293
+ factor = srcWidth / srcHeight
294
+ destWidth = MAX_SIZE
295
+ destHeight = Math.round(destWidth / factor)
296
+ }
297
+ else {
298
+ factor = srcHeight / srcWidth
299
+ destHeight = MAX_SIZE
300
+ destWidth = Math.round(destHeight / factor)
301
+ }
302
+ if (destWidth > srcWidth || destHeight > srcHeight ||
303
+ destWidth < MIN_SIZE || destHeight < MIN_SIZE) {
304
+ destWidth = srcWidth
305
+ destHeight = srcHeight
306
+ }
307
+ return {
308
+ srcLeft: srcLeft,
309
+ srcTop: srcTop,
310
+ srcWidth: srcWidth,
311
+ srcHeight: srcHeight,
312
+ destWidth: destWidth,
313
+ destHeight: destHeight
314
+ }
315
+ }
316
+ let isWebWorkers = typeof window === 'undefined'
317
+ function makeCanvas() {
318
+ if (isWebWorkers) {
319
+ return hasOffscreenCanvas ? new OffscreenCanvas(1, 1) : null
320
+ }
321
+ return document.createElement('canvas')
322
+ }
323
+
324
+ let ERROR_PREFIX = 'FastAverageColor: '
325
+ function getError(message) {
326
+ return Error(ERROR_PREFIX + message)
327
+ }
328
+ function outputError(error, silent) {
329
+ if (!silent) {
330
+ console.error(error)
331
+ }
332
+ }
333
+
334
+ let FastAverageColor = /** @class */ (function () {
335
+ function FastAverageColor() {
336
+ this.canvas = null
337
+ this.ctx = null
338
+ }
339
+ /**
340
+ * Get asynchronously the average color from not loaded image.
341
+ */
342
+ FastAverageColor.prototype.getColorAsync = function (resource, options) {
343
+ if (!resource) {
344
+ return Promise.reject(getError('call .getColorAsync() without resource.'))
345
+ }
346
+ if (typeof resource === 'string') {
347
+ // Web workers
348
+ if (typeof Image === 'undefined') {
349
+ return Promise.reject(getError('resource as string is not supported in this environment'))
350
+ }
351
+ let img = new Image()
352
+ img.crossOrigin = options && options.crossOrigin || ''
353
+ img.src = resource
354
+ return this.bindImageEvents(img, options)
355
+ }
356
+ else if (isInstanceOfHTMLImageElement(resource) && !resource.complete) {
357
+ return this.bindImageEvents(resource, options)
358
+ }
359
+ else {
360
+ let result = this.getColor(resource, options)
361
+ return result.error ? Promise.reject(result.error) : Promise.resolve(result)
362
+ }
363
+ }
364
+ /**
365
+ * Get the average color from images, videos and canvas.
366
+ */
367
+ FastAverageColor.prototype.getColor = function (resource, options) {
368
+ options = options || {}
369
+ let defaultColor = getDefaultColor(options)
370
+ if (!resource) {
371
+ let error = getError('call .getColor(null) without resource')
372
+ outputError(error, options.silent)
373
+ return this.prepareResult(defaultColor, error)
374
+ }
375
+ let originalSize = getOriginalSize(resource)
376
+ let size = prepareSizeAndPosition(originalSize, options)
377
+ if (!size.srcWidth || !size.srcHeight || !size.destWidth || !size.destHeight) {
378
+ let error = getError("incorrect sizes for resource \"".concat(getSrc(resource), "\""))
379
+ outputError(error, options.silent)
380
+ return this.prepareResult(defaultColor, error)
381
+ }
382
+ if (!this.canvas) {
383
+ this.canvas = makeCanvas()
384
+ if (!this.canvas) {
385
+ let error = getError('OffscreenCanvas is not supported in this browser')
386
+ outputError(error, options.silent)
387
+ return this.prepareResult(defaultColor, error)
388
+ }
389
+ }
390
+ if (!this.ctx) {
391
+ this.ctx = this.canvas.getContext('2d', { willReadFrequently: true })
392
+ if (!this.ctx) {
393
+ let error = getError('Canvas Context 2D is not supported in this browser')
394
+ outputError(error, options.silent)
395
+ return this.prepareResult(defaultColor)
396
+ }
397
+ this.ctx.imageSmoothingEnabled = false
398
+ }
399
+ this.canvas.width = size.destWidth
400
+ this.canvas.height = size.destHeight
401
+ try {
402
+ this.ctx.clearRect(0, 0, size.destWidth, size.destHeight)
403
+ this.ctx.drawImage(resource, size.srcLeft, size.srcTop, size.srcWidth, size.srcHeight, 0, 0, size.destWidth, size.destHeight)
404
+ let bitmapData = this.ctx.getImageData(0, 0, size.destWidth, size.destHeight).data
405
+ return this.prepareResult(this.getColorFromArray4(bitmapData, options))
406
+ }
407
+ catch (originalError) {
408
+ let error = getError("security error (CORS) for resource ".concat(getSrc(resource), ".\nDetails: https://developer.mozilla.org/en/docs/Web/HTML/CORS_enabled_image"))
409
+ outputError(error, options.silent)
410
+ !options.silent && console.error(originalError)
411
+ return this.prepareResult(defaultColor, error)
412
+ }
413
+ }
414
+ /**
415
+ * Get the average color from a array when 1 pixel is 4 bytes.
416
+ */
417
+ FastAverageColor.prototype.getColorFromArray4 = function (arr, options) {
418
+ options = options || {}
419
+ let bytesPerPixel = 4
420
+ let arrLength = arr.length
421
+ let defaultColor = getDefaultColor(options)
422
+ if (arrLength < bytesPerPixel) {
423
+ return defaultColor
424
+ }
425
+ let len = arrLength - arrLength % bytesPerPixel
426
+ let step = (options.step || 1) * bytesPerPixel
427
+ let algorithm
428
+ switch (options.algorithm || 'sqrt') {
429
+ case 'simple':
430
+ algorithm = simpleAlgorithm
431
+ break
432
+ case 'sqrt':
433
+ algorithm = sqrtAlgorithm
434
+ break
435
+ case 'dominant':
436
+ algorithm = dominantAlgorithm
437
+ break
438
+ default:
439
+ throw getError("".concat(options.algorithm, " is unknown algorithm"))
440
+ }
441
+ return algorithm(arr, len, {
442
+ defaultColor: defaultColor,
443
+ ignoredColor: prepareIgnoredColor(options.ignoredColor),
444
+ step: step
445
+ })
446
+ }
447
+ /**
448
+ * Get color data from value ([r, g, b, a]).
449
+ */
450
+ FastAverageColor.prototype.prepareResult = function (value, error) {
451
+ let rgb = value.slice(0, 3)
452
+ let rgba = [value[0], value[1], value[2], value[3] / 255]
453
+ let isDarkColor = isDark(value)
454
+ return {
455
+ value: [value[0], value[1], value[2], value[3]],
456
+ rgb: 'rgb(' + rgb.join(',') + ')',
457
+ rgba: 'rgba(' + rgba.join(',') + ')',
458
+ hex: arrayToHex(rgb),
459
+ hexa: arrayToHex(value),
460
+ isDark: isDarkColor,
461
+ isLight: !isDarkColor,
462
+ error: error,
463
+ }
464
+ }
465
+ /**
466
+ * Destroy the instance.
467
+ */
468
+ FastAverageColor.prototype.destroy = function () {
469
+ if (this.canvas) {
470
+ this.canvas.width = 1
471
+ this.canvas.height = 1
472
+ this.canvas = null
473
+ }
474
+ this.ctx = null
475
+ }
476
+ FastAverageColor.prototype.bindImageEvents = function (resource, options) {
477
+ let _this = this
478
+ return new Promise(function (resolve, reject) {
479
+ let onload = function () {
480
+ unbindEvents()
481
+ let result = _this.getColor(resource, options)
482
+ if (result.error) {
483
+ reject(result.error)
484
+ }
485
+ else {
486
+ resolve(result)
487
+ }
488
+ }
489
+ let onerror = function () {
490
+ unbindEvents()
491
+ reject(getError("Error loading image \"".concat(resource.src, "\".")))
492
+ }
493
+ let onabort = function () {
494
+ unbindEvents()
495
+ reject(getError("Image \"".concat(resource.src, "\" loading aborted")))
496
+ }
497
+ let unbindEvents = function () {
498
+ resource.removeEventListener('load', onload)
499
+ resource.removeEventListener('error', onerror)
500
+ resource.removeEventListener('abort', onabort)
501
+ }
502
+ resource.addEventListener('load', onload)
503
+ resource.addEventListener('error', onerror)
504
+ resource.addEventListener('abort', onabort)
505
+ })
506
+ }
507
+ return FastAverageColor
508
+ }())
509
+
510
+ export default FastAverageColor
index.js ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Koa from 'koa'
2
+ import http from 'axios'
3
+ import sharp from 'sharp'
4
+ import tf from '@tensorflow/tfjs-node'
5
+ import nsfw from 'nsfwjs'
6
+
7
+ import { extractColors } from './extract-colors.js'
8
+ import FastAverageColor from './fast-average-color.js'
9
+ import { phash } from './phash.js'
10
+
11
+ const app = new Koa()
12
+
13
+ app.use(async (ctx) => {
14
+ const { data: image_buffer } = await http({
15
+ method: 'GET',
16
+ url: String(ctx.request.query.url).replace(
17
+ 'i.pximg.net',
18
+ 'i.pixiv.re',
19
+ ),
20
+ responseType: 'arraybuffer',
21
+ })
22
+ const { data: image_raw, info } = await sharp(image_buffer, {
23
+ animated: true,
24
+ })
25
+ .ensureAlpha()
26
+ .raw()
27
+ .toBuffer({ resolveWithObject: true })
28
+ const image_phash = await phash(image_buffer)
29
+ const colors = await extractColors(
30
+ { data: image_raw, width: info.width, height: info.height },
31
+ {
32
+ pixels: info.width * info.height,
33
+ distance: 0.05,
34
+ splitPower: 6,
35
+ colorValidator: (red, green, blue, alpha = 255) => alpha >= 255,
36
+ hueDistance: 0,
37
+ saturationDistance: 1,
38
+ lightnessDistance: 1,
39
+ },
40
+ )
41
+ const fac = new FastAverageColor()
42
+ const {
43
+ value: [r, g, b, a],
44
+ hex,
45
+ isLight: blight,
46
+ } = fac.prepareResult(fac.getColorFromArray4(image_raw))
47
+ const image_resize = await sharp(image_buffer, { animated: true })
48
+ .resize({
49
+ width: 128,
50
+ height: 128,
51
+ fit: 'inside',
52
+ })
53
+ .gif({ dither: 0 })
54
+ .toBuffer()
55
+ // const model = await nsfw.load(`https://raw.githubusercontent.com/infinitered/nsfwjs/refs/heads/master/models/mobilenet_v2_mid/model.json`, { type: 'graph' })
56
+ // // const model = await nsfw.load(`https://raw.githubusercontent.com/infinitered/nsfwjs/refs/heads/master/models/mobilenet_v2/model.json`)
57
+ // // const model = await nsfw.load(`https://s3.amazonaws.com/ir_public/nsfwjscdn/TFJS_nsfw_mobilenet/tfjs_quant_nsfw_mobilenet/model.json`)
58
+ // const model = await nsfw.load(`https://raw.githubusercontent.com/infinitered/nsfwjs/refs/heads/master/models/inception_v3/model.json`, { size: 299 })
59
+ const predictions = await model.classifyGif(image_resize, { fps: 1 })
60
+ const detail = new Map()
61
+ predictions
62
+ .flat(Infinity)
63
+ .sort((a, b) => a.probability - b.probability)
64
+ .forEach((i) => {
65
+ detail.set(
66
+ i.className.toLocaleLowerCase(),
67
+ Math.floor(i.probability * 100),
68
+ )
69
+ })
70
+ const fail = ['hentai', 'porn', 'sexy']
71
+ const result = [...detail]
72
+ .filter((i) => i.pop() > 7)
73
+ .some((i) => fail.includes(i.pop()))
74
+ tf.dispose()
75
+ model.model.dispose()
76
+ ctx.body = {
77
+ nsfw: Object.assign({ result }, Object.fromEntries(detail)),
78
+ colors: colors
79
+ .sort((a, b) => b.area - a.area)
80
+ .slice(0, 3)
81
+ .map((i) => {
82
+ // hex: `#${Number(十进制).toString(16).padStart(6, 0)}`
83
+ return {
84
+ color: parseInt(String(i.hex).replace('#', ''), 16),
85
+ ratio: Number(i.area.toFixed(2)),
86
+ }
87
+ }),
88
+ color: hex,
89
+ // blight,
90
+ phash: image_phash,
91
+ }
92
+ })
93
+
94
+ app.listen(process.env.PORT || 7860)
package.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "nsfw",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "keywords": [],
11
+ "author": "",
12
+ "license": "ISC",
13
+ "dependencies": {
14
+ "@tensorflow/tfjs-node": "^4.4.0",
15
+ "axios": "^1.3.5",
16
+ "form-data": "^4.0.0",
17
+ "koa": "3.0.1",
18
+ "nsfwjs": "^2.4.2"
19
+ }
20
+ }
phash.js ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sharp from 'sharp'
2
+
3
+ const SAMPLE_SIZE = 32
4
+
5
+ const initSQRT = (N) => {
6
+ const c = new Array(N)
7
+ for (let i = 1; i < N; i++) {
8
+ c[i] = 1
9
+ }
10
+ c[0] = 1 / Math.sqrt(2.0)
11
+ return c
12
+ }
13
+
14
+ const SQRT = initSQRT(SAMPLE_SIZE)
15
+
16
+ const initCOS = (N) => {
17
+ const cosines = new Array(N)
18
+ for (let k = 0; k < N; k++) {
19
+ cosines[k] = new Array(N)
20
+ for (let n = 0; n < N; n++) {
21
+ cosines[k][n] = Math.cos(((2 * k + 1) / (2.0 * N)) * n * Math.PI)
22
+ }
23
+ }
24
+ return cosines
25
+ }
26
+
27
+ const COS = initCOS(SAMPLE_SIZE)
28
+
29
+ const applyDCT = (f, size) => {
30
+ var N = size
31
+
32
+ var F = new Array(N)
33
+ for (var u = 0; u < N; u++) {
34
+ F[u] = new Array(N)
35
+ for (var v = 0; v < N; v++) {
36
+ var sum = 0
37
+ for (var i = 0; i < N; i++) {
38
+ for (var j = 0; j < N; j++) {
39
+ sum += COS[i][u] * COS[j][v] * f[i][j]
40
+ }
41
+ }
42
+ sum *= (SQRT[u] * SQRT[v]) / 4
43
+ F[u][v] = sum
44
+ }
45
+ }
46
+ return F
47
+ }
48
+
49
+ const LOW_SIZE = 8
50
+
51
+ export const phash = async (image, options) => {
52
+ const data = await sharp(image, options)
53
+ .greyscale()
54
+ .resize(SAMPLE_SIZE, SAMPLE_SIZE, { fit: 'fill' })
55
+ .rotate()
56
+ .raw()
57
+ .toBuffer()
58
+ // copy signal
59
+ const s = new Array(SAMPLE_SIZE)
60
+ for (let x = 0; x < SAMPLE_SIZE; x++) {
61
+ s[x] = new Array(SAMPLE_SIZE)
62
+ for (let y = 0; y < SAMPLE_SIZE; y++) {
63
+ s[x][y] = data[SAMPLE_SIZE * y + x]
64
+ }
65
+ }
66
+
67
+ // apply 2D DCT II
68
+ const dct = applyDCT(s, SAMPLE_SIZE)
69
+
70
+ // get AVG on high frequencies
71
+ let totalSum = 0
72
+ for (let x = 0; x < LOW_SIZE; x++) {
73
+ for (let y = 0; y < LOW_SIZE; y++) {
74
+ totalSum += dct[x + 1][y + 1]
75
+ }
76
+ }
77
+
78
+ const avg = totalSum / (LOW_SIZE * LOW_SIZE)
79
+
80
+ // compute hash
81
+ let fingerprint = []
82
+
83
+ for (let x = 0; x < LOW_SIZE; x++) {
84
+ for (let y = 0; y < LOW_SIZE; y++) {
85
+ fingerprint.push(dct[x + 1][y + 1] > avg ? 1 : 0)
86
+ }
87
+ }
88
+ return parseInt(fingerprint.join(''), 2).toString(16)
89
+ }
90
+
91
+ export const distance = (a, b) => {
92
+ let count = 0
93
+ for (let i = 0; i < a.length; i++) {
94
+ if (a[i] !== b[i]) {
95
+ count++
96
+ }
97
+ }
98
+ return count
99
+ }