Skip to content

Commit

Permalink
v6 release (#79)
Browse files Browse the repository at this point in the history
* chore: reimplemented based on colorthief

* refactor: remove unnecessary dependencies

* refactor: less is more

* perf: add benchmark

* docs: update copyright

* fix: dependencies

* chore(release): 6.0.0-0

* test: use bmp

* test: update

* perf(benchmark): add missing dependencies

* docs(website): add # support in the URL

* perf(benchmark): update assets

* refactor: easier to read

* chore(release): 6.0.0-1

* fix: quantizer bundling

* chore(release): 6.0.0-2

* chore: new website

* build: remove preinstall

* chore(release): 6.0.0-3

* chore: remove submodule

* chore: readd website
  • Loading branch information
Kikobeats authored Dec 16, 2024
1 parent 586583d commit e5a9614
Show file tree
Hide file tree
Showing 59 changed files with 1,749 additions and 101 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ coverage
# Other
############################
.node_history
.vercel
website/.next
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## [6.0.0-3](https://github.com/microlinkhq/splashy/compare/v6.0.0-2...v6.0.0-3) (2024-12-16)

## [6.0.0-2](https://github.com/microlinkhq/splashy/compare/v6.0.0-1...v6.0.0-2) (2024-12-14)


### Bug Fixes

* quantizer bundling ([ce3c082](https://github.com/microlinkhq/splashy/commit/ce3c082f3a7a8100d7088963af875d23ae01fd62))

## [6.0.0-1](https://github.com/microlinkhq/splashy/compare/v6.0.0-0...v6.0.0-1) (2024-12-14)

## [6.0.0-0](https://github.com/microlinkhq/splashy/compare/v5.1.47...v6.0.0-0) (2024-12-10)


### Bug Fixes

* dependencies ([cd098f4](https://github.com/microlinkhq/splashy/commit/cd098f4610ff2ab4803faa1f2131f0fe59ed2756))

### 5.1.47 (2024-12-05)

### 5.1.46 (2024-12-03)
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ $ npm install splashy --save
### From URL

```js
(async () => {
;(async () => {
const splashy = require('splashy')
const got = require('got')

Expand All @@ -38,7 +38,7 @@ $ npm install splashy --save
### From Buffer

```js
(async () => {
;(async () => {
const splashy = require('splashy')
const path = require('path')
const fs = require('fs')
Expand All @@ -58,7 +58,7 @@ $ npm install splashy --save

#### input

*Required*<br>
_Required_<br>
Type: [ImageSource](https://github.com/akfish/node-vibrant#imagesource)

The raw content for detecting the color information.
Expand All @@ -71,7 +71,9 @@ The raw content for detecting the color information.
## License

**microlink-function** © [Microlink](https://microlink.io), released under the [MIT](https://github.com/microlink/microlink-function/blob/master/LICENSE.md) License.<br>

Authored and maintained by [Kiko Beats](https://kikobeats.com) with help from [contributors](https://github.com/microlink/microlink-function/contributors).

> [microlink.io](https://microlink.io) · GitHub [microlinkhq](https://github.com/microlinkhq) · X [@microlinkhq](https://x.com/microlinkhq)
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.

> [microlink.io](https://microlink.io) · GitHub [microlinkhq](https://github.com/microlinkhq) · X [@microlinkhq](https://x.com/microlinkhq)
76 changes: 76 additions & 0 deletions benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

# Benchmark

This is a comparison of various palette-extracting libraries, on the same image.

## Test 1

### Original image

![original image](./fixtures/image-1.jpg)

### Palette by `node-vibrant`

[![node-vibrant](./output/node-vibrant-1.png)](https://iad.microlink.io/09pRn8QBQqzcE10SDzi0Pzr28AJ0wylrt6674C7t_k_epUHqUEK9oS3aO9fUZwMtnP4MM3zRezx8L_DfbgrXOA.png)

### Palette by `splashy`

[![splashy](./output/splashy-1.png)](https://iad.microlink.io/eBuqBYZboCokaAgyD53pnAqW7x8wPgI0AYMKSEsZTDUbvFwTBlRoT5xcoq4ooz0YCtyHjnOA_Glt8kbDBhKn7A.png)

### Palette by `colorthief`

[![colorthief](./output/colorthief-1.png)](https://iad.microlink.io/oY9RIn21q1TZakZMFfukK-ZhcRcHxritAEcNFRyTR5i9RTzRJ66mMLU_2uU9435ByfCmFMQPWpsbN5rNzAw70Q.png)

## Test 2

### Original image

![original image](./fixtures/image-2.jpg)

### Palette by `node-vibrant`

[![node-vibrant](./output/node-vibrant-2.png)](https://iad.microlink.io/jXMzy3IZpu7t7_hKjZ4JEEEmZcU3llcgxsfs73po0yqd2rhTB2JFbRjSC4umzfEHnpeMVJJNDXzNmdgIr8Z2kg.png)

### Palette by `splashy`

[![splashy](./output/splashy-2.png)](https://iad.microlink.io/ZjX6TEkXSpMXPoT2BxMHfc4Eya5qgJr-RJk5Sk2GPabEhT5hmamzYH11GP9BMu0oEYBgpwHpDQLFItns9dgIzg.png)

### Palette by `colorthief`

[![colorthief](./output/colorthief-2.png)](https://iad.microlink.io/CHo-y9RRgfVhhYXVTdkT4n93z3uZjY4bk8TZt4MqdowdFfd5-XEVakjbJNJhd2KTWzaqLKw2OgQpiFRfvAk7zQ.png)

## Test 3

### Original image

![original image](./fixtures/image-3.png)

### Palette by `node-vibrant`

[![node-vibrant](./output/node-vibrant-3.png)](https://iad.microlink.io/iuWlt37UxGF8qgBKJEHCbo_Hkdep_v-CRageoXOAPMb9lM7ah18HP1dIpqmnHjWVy_d22NCJOBeAk35A7J2hkw.png)

### Palette by `splashy`

[![splashy](./output/splashy-3.png)](https://iad.microlink.io/cLUc7GtkIUTQ0d4sevMnIE1fRRoKDArvb30UwppHN2g5bcvx3Ltzw-F4xdLrd0aw7h490G3XusdtP3t_OwL_cA.png)

### Palette by `colorthief`

[![colorthief](./output/colorthief-3.png)](https://iad.microlink.io/SJdrel7u9Cf8_IiV9nWfYtsaEnFEETIY37JEsFJIiVnaX39j93KTmAI45Nu1-lSRqW4NGgBbGw47hbM5j-WwDw.png)

## Test 4

### Original image

![original image](./fixtures/image-4.png)

### Palette by `node-vibrant`

[![node-vibrant](./output/node-vibrant-4.png)](https://iad.microlink.io/HE7HRM1DY45OAWb_wLH7cNxCE55FGUWwvNNgMEJ0dTz4lbSYLf0bT5xiVvdbjEy4GPk3Sy3gz0H-iuK8ZkYBAQ.png)

### Palette by `splashy`

[![splashy](./output/splashy-4.png)](https://iad.microlink.io/7OYNVK_p-Gyt_BR2iOAuN3-a55v4B8o2A2ICuCEcy-oh_q7XMzUsyLne0xi-j_IjzWUKRbR2TVRfwdrNQ2AQWA.png)

### Palette by `colorthief`

[![colorthief](./output/colorthief-4.png)](https://iad.microlink.io/v0nBKof7Zh9ldqJGy8Pgzqit2lnoPH01HdQBioCwGPsIRNOlZ3Dz9S1DP6lyV8napjhZboVFWeWLuedAtStXWw.png)
Binary file added benchmark/fixtures/image-1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/fixtures/image-2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/fixtures/image-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/fixtures/image-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 113 additions & 0 deletions benchmark/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { readFile, writeFile } from 'fs/promises'
import { fileURLToPath } from 'node:url'
import colorthief from 'colorthief'
import Vibrant from 'node-vibrant'
import { chain } from 'lodash-es'
import { readdirSync } from 'fs'
import mql from '@microlink/mql'
import path from 'node:path'

import splashy from '../src/index.js'

const { toHex } = splashy

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const images = readdirSync(path.resolve(__dirname, 'fixtures')).map(filename =>
path.resolve(__dirname, 'fixtures', filename)
)

const paletteUrl = palette =>
`https://splashy-palette.vercel.app/${palette.map(i => encodeURIComponent(i)).join('-')}`

const screenshotUrl = async palette => {
const url = paletteUrl(palette)

const { data } = await mql(url, {
screenshot: true,
styles: '#back-button { display: none; }'
})

return data.screenshot.url
}

const download = async url => Buffer.from(await fetch(url).then(res => res.arrayBuffer()))

async function getPaletteWithNodeVibrant (filepath) {
const vibrantPalette = await Vibrant.from(filepath).getPalette()
return chain(vibrantPalette)
.map()
.sortBy('_population')
.reverse()
.map(value => toHex(value._rgb))
.value()
}

async function getPaletteWithSplashy (filepath) {
const buffer = await readFile(filepath)
const splashyPalette = await splashy(buffer)
return chain(splashyPalette)
.map(value => value.replace('#', ''))
.value()
}

async function getPaletteWithColorthief (filepath) {
const buffer = await readFile(filepath)
const colorthiefPalette = await colorthief.getPalette(buffer)
return colorthiefPalette.map(toHex)
}

const output = []

for (const [index, imagePath] of images.entries()) {
const filepath = path.resolve(__dirname, imagePath)
const basename = path.basename(filepath)
const displayIndex = index + 1

output.push(`## Test ${displayIndex}`)
output.push('### Original image')
output.push(`![original image](./fixtures/${basename})`)

const nodeVibrantPalette = await getPaletteWithNodeVibrant(filepath)
const vibrantScreenshotUrl = await screenshotUrl(nodeVibrantPalette)
await writeFile(
path.resolve(__dirname, 'output', `node-vibrant-${displayIndex}.png`),
await download(vibrantScreenshotUrl)
)
output.push('### Palette by `node-vibrant`')
output.push(
`[![node-vibrant](./output/node-vibrant-${displayIndex}.png)](${vibrantScreenshotUrl})`
)

const splashyPalette = await getPaletteWithSplashy(filepath)
const splashyLinkScreenshotUrl = await screenshotUrl(splashyPalette)
await writeFile(
path.resolve(__dirname, 'output', `splashy-${displayIndex}.png`),
await download(splashyLinkScreenshotUrl)
)
output.push('### Palette by `splashy`')
output.push(`[![splashy](./output/splashy-${displayIndex}.png)](${splashyLinkScreenshotUrl})`)

const colorthiefPalette = await getPaletteWithColorthief(filepath)
const colorthiefScreenshotUrl = await screenshotUrl(colorthiefPalette)
await writeFile(
path.resolve(__dirname, 'output', `colorthief-${displayIndex}.png`),
await download(colorthiefScreenshotUrl)
)
output.push('### Palette by `colorthief`')
output.push(
`[![colorthief](./output/colorthief-${displayIndex}.png)](${colorthiefScreenshotUrl})`
)
}

const readmeContent = `
# Benchmark
This is a comparison of various palette-extracting libraries, on the same image.
${output.join('\n\n')}
`

const filepath = path.resolve(__dirname, 'README.md')
await writeFile(filepath, readmeContent)
console.log(`Benchmark results written to ${filepath} ✨`)
Binary file added benchmark/output/colorthief-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/output/colorthief-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/output/colorthief-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/output/colorthief-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/output/node-vibrant-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/output/node-vibrant-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/output/node-vibrant-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/output/node-vibrant-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/output/splashy-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/output/splashy-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/output/splashy-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark/output/splashy-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions benchmark/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@splashy/benchmark",
"private": true,
"devDependencies": {
"@microlink/mql": "~0.13.12",
"colorthief": "~2.6.0",
"lodash-es": "~4.17.21"
}
}
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "splashy",
"description": "Given an image, extract predominant & palette colors",
"homepage": "https://github.com/microlinkhq/splashy",
"version": "5.1.47",
"version": "6.0.0-3",
"main": "src/index.js",
"author": {
"name": "Kiko Beats",
Expand All @@ -28,6 +28,7 @@
"keywords": [
"canvas",
"color",
"colorthief",
"colour",
"dominant",
"extract",
Expand All @@ -41,7 +42,9 @@
"url"
],
"dependencies": {
"@lokesh.dhakar/quantize": "~1.4.0",
"debug-logfmt": "~1.2.3",
"ndarray": "~1.0.19",
"node-addon-api": "~8.3.0",
"node-gyp": "~11.0.0",
"node-vibrant": "~3.2.1-alpha.1",
Expand Down Expand Up @@ -77,7 +80,6 @@
"dev": "nodemon --exec \"npm start\" -e \"js\"",
"lint": "standard-markdown README.md && standard",
"postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)",
"preinstall": "npx bin-version-check-cli magick \">=7\"",
"pretest": "npm run lint",
"release": "standard-version -a",
"release:github": "conventional-github-releaser -p angular",
Expand All @@ -86,6 +88,12 @@
"test": "c8 ava"
},
"license": "MIT",
"ava": {
"files": [
"test/**/*.js",
"!test/util.js"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
Expand Down
73 changes: 44 additions & 29 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,49 @@
'use strict'

const { serializeError } = require('serialize-error')
const debug = require('debug-logfmt')('splashy')
const createVibrant = require('./vibrant')

const toPalette = swatch =>
Object.keys(swatch)
.reduce((acc, key) => {
const value = swatch[key]
if (value) {
acc.push({
popularity: value.getPopulation(),
hex: value.getHex()
})
}
return acc
}, [])
.sort((a, b) => a.popularity <= b.popularity)
.map(color => color.hex)

module.exports = async input => {
let swatch

try {
const vibrant = createVibrant(input)
swatch = await vibrant.getPalette()
} catch (error) {
debug.error(serializeError(error))
swatch = {}
const quantize = require('@lokesh.dhakar/quantize').default || require('@lokesh.dhakar/quantize')
const ndarray = require('ndarray')
const sharp = require('sharp')

async function getPixels ({ data, info }) {
return ndarray(
new Uint8Array(data.buffer, data.byteOffset, data.length),
[info.width, info.height, 4],
[4, (4 * info.width) | 0, 1],
0
)
}

function createPixelArray (pixels, pixelCount, quality = 10) {
const pixelArray = []
for (let i = 0, offset; i < pixelCount; i += quality) {
offset = i * 4
const r = pixels[offset]
const g = pixels[offset + 1]
const b = pixels[offset + 2]
const a = pixels[offset + 3]
const isOpaqueEnough = a >= 125
const isWhite = r > 250 && g > 250 && b > 250
if (isOpaqueEnough && !isWhite) pixelArray.push([r, g, b])
}

return toPalette(swatch)
return pixelArray
}

const toHex = ([r, g, b]) => '#' + (b | (g << 8) | (r << 16) | (1 << 24)).toString(16).slice(1)

module.exports = async function (buffer) {
const raw = await sharp(buffer)
// resizing the image before processing leads to more consistent (and much shorter) processing times.
// .resize(200, 200, { fit: 'inside', withoutEnlargement: true })
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true })

const imgData = await getPixels(raw)
const pixelCount = imgData.shape[0] * imgData.shape[1]
const pixelArray = createPixelArray(imgData.data, pixelCount)
const cmap = quantize(pixelArray, 10) // internal tuning
return cmap.palette().slice(0, 6).map(toHex)
}

module.exports.toHex = toHex
Loading

0 comments on commit e5a9614

Please sign in to comment.