New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

dont-crop

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dont-crop - npm Package Compare versions

Comparing version 0.0.2 to 0.2.0

dist/cjs/clusterHistogram.js

8

dist/cjs/fitGradient.js

@@ -28,5 +28,5 @@ "use strict";

const regression = [
linearRegression_1.linearRegression(y, colors[0]),
linearRegression_1.linearRegression(y, colors[1]),
linearRegression_1.linearRegression(y, colors[2]),
(0, linearRegression_1.linearRegression)(y, colors[0]),
(0, linearRegression_1.linearRegression)(y, colors[1]),
(0, linearRegression_1.linearRegression)(y, colors[2]),
];

@@ -40,3 +40,3 @@ const start = sampleColor(regression, 0);

return regression
.map((r) => (util_1.clamp(r.intercept + r.slope * t, 0, 255)) | 0);
.map((r) => ((0, util_1.clamp)(r.intercept + r.slope * t, 0, 255)) | 0);
}

@@ -5,3 +5,3 @@ "use strict";

function hexString(n) {
return Math.max(0, Math.min(255, n | 0)).toString(16).padStart(2, '0');
return Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, '0');
}

@@ -8,0 +8,0 @@ exports.hexString = hexString;

@@ -13,5 +13,4 @@ "use strict";

const canvas = getCanvas();
const Img = HTMLImageElement;
const width = image instanceof Img ? image.naturalWidth : +image.width;
const height = image instanceof Img ? image.naturalHeight : +image.height;
const width = image instanceof HTMLImageElement ? image.naturalWidth : +image.width;
const height = image instanceof HTMLImageElement ? image.naturalHeight : +image.height;
const scale = maxDimension

@@ -27,2 +26,3 @@ ? Math.min(maxDimension / Math.max(width, height), 1.0)

throw new Error('getContext failed');
ctx.imageSmoothingQuality = 'low';
ctx.drawImage(image, 0, 0, outputWidth, outputHeight);

@@ -29,0 +29,0 @@ return ctx.getImageData(0, 0, outputWidth, outputHeight);

"use strict";
// import mcgRandom from './mcgRandom';
Object.defineProperty(exports, "__esModule", { value: true });
exports.kmeans = void 0;
const mcgRandom_1 = require("./mcgRandom");
const channels = 3;
// sadly this is about twice as fast as x ** 2.
const squared = (x) => x * x;
// function selectClusterAtRandom(image: ImageData, random: () => number): Cluster {
// const i = ((random() * image.width * image.height) | 0) * channels;
// const { data } = image;
// return {
// x: data[i],
// y: data[i + 1],
// z: data[i + 2],
// count: 0,
// };
// }
// This function also operates without doing any gamma correction.

@@ -13,18 +26,21 @@ // I haven't evaluated the effect of this on the output.

// Or it's just an excuse to save a few cycles. ¯\_(ツ)_/¯
function kmeans(image, k, iterations) {
const random = mcgRandom_1.default();
const random255 = () => random() * 255;
if (k < 1)
throw new Error('k must be greater than 0');
const { data } = image;
function kmeans(data, initialClusters, iterations) {
// const { data } = image;
const clusters = [];
const nextClusters = [];
for (let i = 0; i < k; i++) {
// (histogramColors(image, k)).forEach(([r, g, b]) => {
// const { l: x, a: y, b: z } = srgbToLab({ r: r / 255, g: g / 255, b: b / 255 });
// clusters.push({
// x, y, z, count: 0,
// });
// nextClusters.push({
// x: 0, y: 0, z: 0, count: 0,
// });
// });
initialClusters.forEach((point) => {
nextClusters.push({ ...point, count: 1 });
clusters.push({
x: random255(), y: random255(), z: random255(), count: 0,
});
nextClusters.push({
x: 0, y: 0, z: 0, count: 0,
});
}
});
for (let i = 0; i < iterations; i++) {

@@ -34,13 +50,6 @@ clusters.forEach((cluster, j) => {

const nextCluster = nextClusters[j];
if (nextCluster.count > 0) {
cluster.x = nextCluster.x / nextCluster.count;
cluster.y = nextCluster.y / nextCluster.count;
cluster.z = nextCluster.z / nextCluster.count;
}
else {
// give empty clusters another chance
cluster.x = random255();
cluster.y = random255();
cluster.z = random255();
}
const count = Math.max(nextCluster.count, 1);
cluster.x = nextCluster.x / count;
cluster.y = nextCluster.y / count;
cluster.z = nextCluster.z / count;
cluster.count = nextCluster.count;

@@ -53,12 +62,15 @@ // reset next clusters

});
for (let p = 0; p < data.length; p += 4) {
let closestClusterDistance = (data[p] - clusters[0].x) ** 2
+ (data[p + 1] - clusters[0].y) ** 2
+ (data[p + 2] - clusters[0].z) ** 2;
let closestCluster = nextClusters[0];
for (let p = 0; p < data.length; p += 3) {
const x = data[p];
const y = data[p + 1];
const z = data[p + 2];
let closestClusterDistance = Infinity;
let closestCluster = clusters[0];
// find the closest cluster
for (let ci = 1; ci < clusters.length; ci++) {
const distance = (data[p] - clusters[ci].x) ** 2
+ (data[p + 1] - clusters[ci].y) ** 2
+ (data[p + 2] - clusters[ci].z) ** 2;
for (let ci = 0; ci < clusters.length; ci++) {
// since this is just used in a comparison the sqrt is not needed
const cluster = clusters[ci];
const distance = squared(x - cluster.x)
+ squared(y - cluster.y)
+ squared(z - cluster.z);
if (distance < closestClusterDistance) {

@@ -71,5 +83,5 @@ closestClusterDistance = distance;

closestCluster.count += 1;
closestCluster.x += data[p];
closestCluster.y += data[p + 1];
closestCluster.z += data[p + 2];
closestCluster.x += x;
closestCluster.y += y;
closestCluster.z += z;
}

@@ -76,0 +88,0 @@ }

@@ -8,8 +8,9 @@ export { getImageData } from './getImageData';

* @param image the image to extract the palette from.
* Will be scaled down to at most 32x32.
* Must be loaded/complete.
* @param numberOfColors upper limit on the number of colors to be returned
* @param fast if true the image will be downscaled to 64x64, 128x128 otherwise.
* The precise sizes used may change in the future.
* @returns representative colors of the image ordered by importance (size of the cluster)
*/
export declare function getPalette(image: CanvasImageSource, numberOfColors?: number): string[];
export declare function getPalette(image: CanvasImageSource, numberOfColors?: number, fast?: boolean): string[];
/**

@@ -16,0 +17,0 @@ * Extract representative colors from image data.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.fitGradientToImageData = exports.fitGradient = exports.getPaletteFromImageData = exports.getPalette = exports.getImageData = void 0;
const kmeans_1 = require("./kmeans");
const fitGradient_1 = require("./fitGradient");
const getImageData_1 = require("./getImageData");
const format_1 = require("./format");
const getPalette_1 = require("./getPalette");
var getImageData_2 = require("./getImageData");

@@ -16,10 +16,11 @@ Object.defineProperty(exports, "getImageData", { enumerable: true, get: function () { return getImageData_2.getImageData; } });

* @param image the image to extract the palette from.
* Will be scaled down to at most 32x32.
* Must be loaded/complete.
* @param numberOfColors upper limit on the number of colors to be returned
* @param fast if true the image will be downscaled to 64x64, 128x128 otherwise.
* The precise sizes used may change in the future.
* @returns representative colors of the image ordered by importance (size of the cluster)
*/
// integration tested only
function getPalette(image, numberOfColors = 4) {
const imageData = getImageData_1.getImageData(image, 32);
function getPalette(image, numberOfColors = 4, fast = false) {
const imageData = (0, getImageData_1.getImageData)(image, fast ? 64 : 128);
return getPaletteFromImageData(imageData, numberOfColors);

@@ -36,7 +37,4 @@ }

function getPaletteFromImageData(imageData, numberOfColors = 4) {
const colors = kmeans_1.kmeans(imageData, numberOfColors, 16)
.filter((c) => c.count > 0)
.sort((a, b) => b.count - a.count)
.map((c) => [c.x, c.y, c.z]);
return colors.map(format_1.hexColorString);
return (0, getPalette_1.getPalette)(imageData, numberOfColors)
.map(format_1.hexColorString);
}

@@ -56,3 +54,3 @@ exports.getPaletteFromImageData = getPaletteFromImageData;

function fitGradient(image) {
const imageData = getImageData_1.getImageData(image, 32);
const imageData = (0, getImageData_1.getImageData)(image, 32);
return fitGradientToImageData(imageData);

@@ -67,6 +65,6 @@ }

function fitGradientToImageData(imageData) {
const colors = fitGradient_1.fitGradient(imageData)
const colors = (0, fitGradient_1.fitGradient)(imageData)
.map(format_1.hexColorString);
return format_1.linearGradient(colors);
return (0, format_1.linearGradient)(colors);
}
exports.fitGradientToImageData = fitGradientToImageData;
export function hexString(n) {
return Math.max(0, Math.min(255, n | 0)).toString(16).padStart(2, '0');
return Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, '0');
}

@@ -4,0 +4,0 @@ export function hexColorString(color) {

@@ -10,5 +10,4 @@ // this file is only covered by the end to end browser tests

const canvas = getCanvas();
const Img = HTMLImageElement;
const width = image instanceof Img ? image.naturalWidth : +image.width;
const height = image instanceof Img ? image.naturalHeight : +image.height;
const width = image instanceof HTMLImageElement ? image.naturalWidth : +image.width;
const height = image instanceof HTMLImageElement ? image.naturalHeight : +image.height;
const scale = maxDimension

@@ -24,4 +23,5 @@ ? Math.min(maxDimension / Math.max(width, height), 1.0)

throw new Error('getContext failed');
ctx.imageSmoothingQuality = 'low';
ctx.drawImage(image, 0, 0, outputWidth, outputHeight);
return ctx.getImageData(0, 0, outputWidth, outputHeight);
}

@@ -1,2 +0,15 @@

import mcgRandom from './mcgRandom.js';
// import mcgRandom from './mcgRandom.js';
const channels = 3;
// sadly this is about twice as fast as x ** 2.
const squared = (x) => x * x;
// function selectClusterAtRandom(image: ImageData, random: () => number): Cluster {
// const i = ((random() * image.width * image.height) | 0) * channels;
// const { data } = image;
// return {
// x: data[i],
// y: data[i + 1],
// z: data[i + 2],
// count: 0,
// };
// }
// This function also operates without doing any gamma correction.

@@ -10,18 +23,21 @@ // I haven't evaluated the effect of this on the output.

// Or it's just an excuse to save a few cycles. ¯\_(ツ)_/¯
export function kmeans(image, k, iterations) {
const random = mcgRandom();
const random255 = () => random() * 255;
if (k < 1)
throw new Error('k must be greater than 0');
const { data } = image;
export function kmeans(data, initialClusters, iterations) {
// const { data } = image;
const clusters = [];
const nextClusters = [];
for (let i = 0; i < k; i++) {
// (histogramColors(image, k)).forEach(([r, g, b]) => {
// const { l: x, a: y, b: z } = srgbToLab({ r: r / 255, g: g / 255, b: b / 255 });
// clusters.push({
// x, y, z, count: 0,
// });
// nextClusters.push({
// x: 0, y: 0, z: 0, count: 0,
// });
// });
initialClusters.forEach((point) => {
nextClusters.push({ ...point, count: 1 });
clusters.push({
x: random255(), y: random255(), z: random255(), count: 0,
});
nextClusters.push({
x: 0, y: 0, z: 0, count: 0,
});
}
});
for (let i = 0; i < iterations; i++) {

@@ -31,13 +47,6 @@ clusters.forEach((cluster, j) => {

const nextCluster = nextClusters[j];
if (nextCluster.count > 0) {
cluster.x = nextCluster.x / nextCluster.count;
cluster.y = nextCluster.y / nextCluster.count;
cluster.z = nextCluster.z / nextCluster.count;
}
else {
// give empty clusters another chance
cluster.x = random255();
cluster.y = random255();
cluster.z = random255();
}
const count = Math.max(nextCluster.count, 1);
cluster.x = nextCluster.x / count;
cluster.y = nextCluster.y / count;
cluster.z = nextCluster.z / count;
cluster.count = nextCluster.count;

@@ -50,12 +59,15 @@ // reset next clusters

});
for (let p = 0; p < data.length; p += 4) {
let closestClusterDistance = (data[p] - clusters[0].x) ** 2
+ (data[p + 1] - clusters[0].y) ** 2
+ (data[p + 2] - clusters[0].z) ** 2;
let closestCluster = nextClusters[0];
for (let p = 0; p < data.length; p += 3) {
const x = data[p];
const y = data[p + 1];
const z = data[p + 2];
let closestClusterDistance = Infinity;
let closestCluster = clusters[0];
// find the closest cluster
for (let ci = 1; ci < clusters.length; ci++) {
const distance = (data[p] - clusters[ci].x) ** 2
+ (data[p + 1] - clusters[ci].y) ** 2
+ (data[p + 2] - clusters[ci].z) ** 2;
for (let ci = 0; ci < clusters.length; ci++) {
// since this is just used in a comparison the sqrt is not needed
const cluster = clusters[ci];
const distance = squared(x - cluster.x)
+ squared(y - cluster.y)
+ squared(z - cluster.z);
if (distance < closestClusterDistance) {

@@ -68,5 +80,5 @@ closestClusterDistance = distance;

closestCluster.count += 1;
closestCluster.x += data[p];
closestCluster.y += data[p + 1];
closestCluster.z += data[p + 2];
closestCluster.x += x;
closestCluster.y += y;
closestCluster.z += z;
}

@@ -73,0 +85,0 @@ }

@@ -8,8 +8,9 @@ export { getImageData } from './getImageData';

* @param image the image to extract the palette from.
* Will be scaled down to at most 32x32.
* Must be loaded/complete.
* @param numberOfColors upper limit on the number of colors to be returned
* @param fast if true the image will be downscaled to 64x64, 128x128 otherwise.
* The precise sizes used may change in the future.
* @returns representative colors of the image ordered by importance (size of the cluster)
*/
export declare function getPalette(image: CanvasImageSource, numberOfColors?: number): string[];
export declare function getPalette(image: CanvasImageSource, numberOfColors?: number, fast?: boolean): string[];
/**

@@ -16,0 +17,0 @@ * Extract representative colors from image data.

@@ -1,5 +0,5 @@

import { kmeans } from './kmeans.js';
import { fitGradient as fitGradientImplementation } from './fitGradient.js';
import { getImageData } from './getImageData.js';
import { hexColorString, linearGradient } from './format.js';
import { getPalette as getPaletteImplemention } from './getPalette.js';
export { getImageData } from './getImageData.js';

@@ -12,10 +12,11 @@ /**

* @param image the image to extract the palette from.
* Will be scaled down to at most 32x32.
* Must be loaded/complete.
* @param numberOfColors upper limit on the number of colors to be returned
* @param fast if true the image will be downscaled to 64x64, 128x128 otherwise.
* The precise sizes used may change in the future.
* @returns representative colors of the image ordered by importance (size of the cluster)
*/
// integration tested only
export function getPalette(image, numberOfColors = 4) {
const imageData = getImageData(image, 32);
export function getPalette(image, numberOfColors = 4, fast = false) {
const imageData = getImageData(image, fast ? 64 : 128);
return getPaletteFromImageData(imageData, numberOfColors);

@@ -31,7 +32,4 @@ }

export function getPaletteFromImageData(imageData, numberOfColors = 4) {
const colors = kmeans(imageData, numberOfColors, 16)
.filter((c) => c.count > 0)
.sort((a, b) => b.count - a.count)
.map((c) => [c.x, c.y, c.z]);
return colors.map(hexColorString);
return getPaletteImplemention(imageData, numberOfColors)
.map(hexColorString);
}

@@ -38,0 +36,0 @@ /**

{
"name": "dont-crop",
"version": "0.0.2",
"version": "0.2.0",
"description": "A library to fit gradients to images and extract it's dominant colors to help you avoid cropping images.",

@@ -25,3 +25,4 @@ "main": "./dist/cjs/lib.js",

"prepare": "scripts/prepare.sh",
"endToEndTest": "./tests/endToEndTest.sh"
"endToEndTest": "./tests/endToEndTest.sh",
"benchmark": "ts-node scripts/benchmark.ts"
},

@@ -32,3 +33,3 @@ "author": "Jonas Wagner",

"@types/benchmark": "^2.1.0",
"@types/jest": "^26.0.23",
"@types/jest": "^27.0.1",
"@types/puppeteer": "^5.4.3",

@@ -42,2 +43,3 @@ "@types/react": "^17.0.9",

"benchmark": "^2.1.4",
"canvas": "^2.8.0",
"css-loader": "^5.2.6",

@@ -53,13 +55,13 @@ "eslint": "^7.23.0",

"serve-static": "^1.14.1",
"sharp": "^0.28.3",
"style-loader": "^2.0.0",
"sharp": "^0.29.0",
"style-loader": "^3.2.1",
"ts-jest": "^27.0.1",
"ts-loader": "^9.2.2",
"ts-node": "^10.0.0",
"typedoc": "^0.20.36",
"typescript": "^4.2.4",
"typedoc": "^0.21.9",
"typescript": "^4.4.2",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2"
"webpack-dev-server": "^4.1.0"
}
}

@@ -11,5 +11,15 @@ <img src="docs/logo.png" width="400" />

![lead image](docs/lead-lossless.webp)
## Examples
### fitGradient()
![fitGradient](docs/fitGradient.webp)
Photo by [Abed Ismail](https://unsplash.com/photos/fZXZ1-hbFrY)
### getPalette()
![getPalette](docs/getPalette.webp)
### More Examples
View the [demo page](https://29a.ch/sandbox/2021/dont-crop/) to see more examples and experiment with your own images.
## Installation

@@ -72,6 +82,15 @@ ```

When using `fitGradient` only and bundling your code using webpack 5 dont-crop will add about **1.2 kb** to your bundle size.
`getPalette` will cost you a bit more than **1.4 kb**.
You can use both for about **2.1 kb**.
When using `fitGradient` only and bundling your code using webpack 5 dont-crop will add about **1.2 kb** (0.7 gzipped) to your bundle size.
`getPalette` will cost you a bit more than **3.2 kb** (1.7 gzipped).
You can use both for about **4 kb** (2 gzipped).
```
3925 dist/both.js
1911 dist/both.js.gz
1264 dist/fitGradient.js
710 dist/fitGradient.js.gz
3261 dist/getPalette.js
1656 dist/getPalette.js.gz
```
Runtime performance is also fast enough not to worry about.

@@ -81,8 +100,9 @@

# on a AMD Ryzen 9 5950X
fitGradientToImageData x 43,559 ops/sec ±0.40% (97 runs sampled)
getPaletteFromImageData x 5,420 ops/sec ±0.21% (92 runs sampled)
fitGradientToImageData x 19,813 ops/sec ±0.96% (97 runs sampled)
getPaletteFromImageData(fast=false) x 156 ops/sec ±0.66% (83 runs sampled)
getPaletteFromImageData(fast=true) x 645 ops/sec ±0.17% (97 runs sampled)
```
The versions of the functions operating on images rather than the already downscaled image data are slower.
Their performance depends on the exact browser and device in question as well but it should generally be in the ballpark of few milliseconds for reasonably sized images.
Their performance depends on the exact browser and device in question as well but it should generally be in the ballpark of a few milliseconds for reasonably sized images.

@@ -93,7 +113,16 @@ ## Test Coverage

## Algorithms
Glad you asked. `fitGradient()` is using simple [linear regression](https://en.wikipedia.org/wiki/Linear_regression).
`getPallete()` is based on [k-means](https://en.wikipedia.org/wiki/K-means_clustering).
`getPalete()` is based on [k-means](https://en.wikipedia.org/wiki/K-means_clustering).
The initial clusters are chosen using a histogram.
Similar clusters in the result are merged in a post processing step.
This is necessary because k-means tends to return equally sized clusters
whereas getPalette is supposed to return distinct clusters.
The merging is tuned to preserve different hues and colors rather than returning the most prominent shades of color (which might all share a similar hue).
The processing happens in the CIE Lab color space using CIE76 ΔE*.
## Alternatives

@@ -114,2 +143,7 @@

From a quick looks it seems to be using median-cut which will likely yield a bit better results than the simplistic k-means used here.
### [fast-average-color](https://github.com/fast-average-color/fast-average-color)
Returns a single average or dominant color color.

@@ -125,12 +159,10 @@ ### [smartcrop.js](https://github.com/jwagner/smartcrop.js)

* Adding creative controls over colors (max saturation, max lightness, min lightness)
* Grouping of colors (saturated, muted, light, dark, warm, cold)
* Tuning of the variables involved in palette extraction potentially allowing some degree of tweaking by the user of the library
* Weighting the linear-regression and k-means to focus on the center or edges
* Using a more robust regression variation like Theil-Senn
* Gamma corrected linear gradients by manually interpolating the stops
* Something better than straight k-means for palette extraction
All of these would of course add complexity (and size) as well, so for now it's the simplest thing that could possibly work.
## License
MIT
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc