sweetcorn 0.2.1 → 0.3.0
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/dist/custom-processors.d.ts +6 -0
- package/dist/custom-processors.js +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +43 -25
- package/dist/processors.d.ts +3 -8
- package/dist/processors.js +15 -5
- package/dist/threshold-maps.json +27 -5
- package/dist/types.d.ts +19 -0
- package/package.json +3 -2
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ArrayOrBuffer, ImageInfo } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A collection of pixel mutation processors that deviate from the standardized threshold map and
|
|
4
|
+
* error diffusion algorithms.
|
|
5
|
+
*/
|
|
6
|
+
export declare const customProcessors: Record<'white-noise' | 'threshold', (pixels: ArrayOrBuffer<number>, info: ImageInfo) => void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A collection of pixel mutation processors that deviate from the standardized threshold map and
|
|
3
|
+
* error diffusion algorithms.
|
|
4
|
+
*/
|
|
5
|
+
export const customProcessors = {
|
|
6
|
+
/**
|
|
7
|
+
* White noise dithering (pretty rough and ugly)
|
|
8
|
+
*/
|
|
9
|
+
'white-noise'(pixels, { channels }) {
|
|
10
|
+
for (let index = 0; index < pixels.length; index += channels) {
|
|
11
|
+
const threshold = Math.random();
|
|
12
|
+
for (let channel = 0; channel < channels; channel++) {
|
|
13
|
+
if (channel > 2)
|
|
14
|
+
continue; // Skip alpha channel
|
|
15
|
+
pixels[index + channel] = pixels[index + channel] / 255 < threshold ? 0 : 255;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
/**
|
|
20
|
+
* Basic quantization. This could be represented as a threshold map of `[[128]]` but defining it
|
|
21
|
+
* like this allows it to be implemented more efficiently.
|
|
22
|
+
*/
|
|
23
|
+
threshold(pixels) {
|
|
24
|
+
for (let index = 0; index < pixels.length; index++) {
|
|
25
|
+
const pixelValue = pixels[index];
|
|
26
|
+
pixels[index] = pixelValue < 128 ? 0 : 255;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Sharp } from 'sharp';
|
|
2
2
|
import type { SweetcornOptions } from './types.js';
|
|
3
3
|
export type { DitheringAlgorithm } from './types.js';
|
|
4
|
+
export type { SweetcornOptions };
|
|
4
5
|
/**
|
|
5
6
|
* Create a dithered image — turn smooth pixels into crunchy kernels! 🌽
|
|
6
7
|
* @param image A Sharp image instance, e.g. created using `sharp('my-file.png')`.
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { customProcessors } from './custom-processors.js';
|
|
2
2
|
import diffusionKernels from './diffusion-kernels.js';
|
|
3
|
+
import { applyDiffusionKernel, applyThresholdMap } from './processors.js';
|
|
3
4
|
import thresholdMaps from './threshold-maps.json' with { type: 'json' };
|
|
4
5
|
let sharp;
|
|
5
6
|
async function loadSharp() {
|
|
@@ -21,37 +22,54 @@ async function loadSharp() {
|
|
|
21
22
|
* @returns A new Sharp image instance.
|
|
22
23
|
*/
|
|
23
24
|
export default async function sweetcorn(image, options) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// Clone the input image to avoid mutating it.
|
|
26
|
+
// Also, force it to a PNG to ensure we have a valid input. Some inputs such as "raw", can have
|
|
27
|
+
// issues with channel manipulations like our alpha channel handling below.
|
|
28
|
+
image = image.clone().toFormat('png');
|
|
29
|
+
/** Alpha channel extracted before any other manipulations have been applied. */
|
|
30
|
+
let alphaChannel;
|
|
31
|
+
if (!options.preserveColour) {
|
|
32
|
+
if (options.preserveAlpha) {
|
|
33
|
+
alphaChannel = await image.clone().extractChannel('alpha').toBuffer();
|
|
34
|
+
}
|
|
35
|
+
// Alpha channel shouldn’t be dithered, so we remove it (and add it back later if needed.)
|
|
36
|
+
image.removeAlpha();
|
|
37
|
+
}
|
|
28
38
|
// We use gamma to linearize the colorspace, and improve the perceptual quality.
|
|
29
|
-
image.gamma(2.2, 1)
|
|
39
|
+
image.gamma(2.2, 1);
|
|
40
|
+
// Convert image to greyscale before dithering.
|
|
41
|
+
if (!options.preserveColour) {
|
|
42
|
+
image.greyscale();
|
|
43
|
+
}
|
|
30
44
|
// Get raw pixel data for this image.
|
|
31
|
-
const
|
|
32
|
-
const thresholdMap = options.thresholdMap || thresholdMaps[algorithm];
|
|
33
|
-
const diffusionKernel = options.diffusionKernel || diffusionKernels[algorithm];
|
|
45
|
+
const { data: pixels, info } = await image.raw().toBuffer({ resolveWithObject: true });
|
|
46
|
+
const thresholdMap = options.thresholdMap || thresholdMaps[options.algorithm];
|
|
47
|
+
const diffusionKernel = options.diffusionKernel || diffusionKernels[options.algorithm];
|
|
48
|
+
const customProcessor = customProcessors[options.algorithm];
|
|
49
|
+
// Dither raw pixel data in place.
|
|
34
50
|
if (thresholdMap) {
|
|
35
|
-
applyThresholdMap(
|
|
51
|
+
applyThresholdMap(pixels, info, thresholdMap);
|
|
36
52
|
}
|
|
37
53
|
else if (diffusionKernel) {
|
|
38
|
-
applyDiffusionKernel(
|
|
54
|
+
applyDiffusionKernel(pixels, info, diffusionKernel);
|
|
39
55
|
}
|
|
40
|
-
else if (
|
|
41
|
-
|
|
42
|
-
for (let index = 0; index < rawPixels.data.length; index++) {
|
|
43
|
-
const pixelValue = rawPixels.data[index];
|
|
44
|
-
rawPixels.data[index] = pixelValue / 255 < Math.random() ? 0 : 255;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
else if (algorithm === 'threshold') {
|
|
48
|
-
// Basic quantization
|
|
49
|
-
for (let index = 0; index < rawPixels.data.length; index++) {
|
|
50
|
-
const pixelValue = rawPixels.data[index];
|
|
51
|
-
rawPixels.data[index] = pixelValue < 128 ? 0 : 255;
|
|
52
|
-
}
|
|
56
|
+
else if (customProcessor) {
|
|
57
|
+
customProcessor(pixels, info);
|
|
53
58
|
}
|
|
54
59
|
// Convert raw pixel data back into a Sharp image.
|
|
55
|
-
|
|
60
|
+
if (!sharp)
|
|
61
|
+
sharp = await loadSharp();
|
|
62
|
+
const outputImage = sharp(pixels, { raw: info });
|
|
63
|
+
// Enforce black-and-white output when colour hasn’t been requested and there’s no alpha channel.
|
|
64
|
+
if (!options.preserveColour && !alphaChannel) {
|
|
65
|
+
outputImage.toColourspace('b-w');
|
|
66
|
+
}
|
|
67
|
+
// Ensure alpha channel is included/excluded as required.
|
|
68
|
+
if (alphaChannel) {
|
|
69
|
+
outputImage.joinChannel(alphaChannel);
|
|
70
|
+
}
|
|
71
|
+
else if (info.channels > 3 && !options.preserveAlpha) {
|
|
72
|
+
outputImage.removeAlpha();
|
|
73
|
+
}
|
|
56
74
|
return outputImage;
|
|
57
75
|
}
|
package/dist/processors.d.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
interface ArrayOrBuffer<T> {
|
|
3
|
-
length: number;
|
|
4
|
-
[n: number]: T;
|
|
5
|
-
}
|
|
1
|
+
import { ArrayOrBuffer, ImageInfo } from './types.js';
|
|
6
2
|
/** Applies a threshold map to raw pixel data for ordered dithering. */
|
|
7
|
-
export declare function applyThresholdMap(pixels: ArrayOrBuffer<number>, width:
|
|
3
|
+
export declare function applyThresholdMap(pixels: ArrayOrBuffer<number>, { width, channels }: ImageInfo, map: number[][]): void;
|
|
8
4
|
/** Applies an error diffusion kernel to raw pixel data. */
|
|
9
|
-
export declare function applyDiffusionKernel(pixels: ArrayOrBuffer<number>, width
|
|
10
|
-
export {};
|
|
5
|
+
export declare function applyDiffusionKernel(pixels: ArrayOrBuffer<number>, { width, height, channels }: ImageInfo, kernel: number[][]): void;
|
package/dist/processors.js
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
/** Applies a threshold map to raw pixel data for ordered dithering. */
|
|
2
|
-
export function applyThresholdMap(pixels, width, map) {
|
|
2
|
+
export function applyThresholdMap(pixels, { width, channels }, map) {
|
|
3
3
|
const mapWidth = map[0].length;
|
|
4
4
|
const mapHeight = map.length;
|
|
5
5
|
for (let index = 0; index < pixels.length; index++) {
|
|
6
|
+
const channel = index % channels;
|
|
7
|
+
if (channel == 3)
|
|
8
|
+
continue; // Skip alpha channel
|
|
6
9
|
const pixelValue = pixels[index];
|
|
7
|
-
const
|
|
10
|
+
const channelIndex = Math.floor(index / channels);
|
|
11
|
+
const [x, y] = [channelIndex % width, Math.floor(channelIndex / width)];
|
|
8
12
|
const threshold = map[y % mapHeight][x % mapWidth];
|
|
9
13
|
pixels[index] = pixelValue < threshold ? 0 : 255;
|
|
10
14
|
}
|
|
11
15
|
}
|
|
12
16
|
/** Applies an error diffusion kernel to raw pixel data. */
|
|
13
|
-
export function applyDiffusionKernel(pixels, width, height, kernel) {
|
|
17
|
+
export function applyDiffusionKernel(pixels, { width, height, channels }, kernel) {
|
|
14
18
|
const kernelWidth = kernel[0].length;
|
|
15
19
|
const kernelHeight = kernel.length;
|
|
16
20
|
const kernelRadius = Math.floor((kernelWidth - 1) / 2);
|
|
17
21
|
for (let index = 0; index < pixels.length; index++) {
|
|
22
|
+
const channel = index % channels;
|
|
23
|
+
if (channel == 3)
|
|
24
|
+
continue; // Skip alpha channel
|
|
18
25
|
const original = pixels[index];
|
|
19
26
|
const quantized = original < 128 ? 0 : 255;
|
|
20
27
|
pixels[index] = quantized;
|
|
21
28
|
const error = original - quantized;
|
|
22
29
|
// (x, y) co-ordinates of the current pixel in the image.
|
|
23
|
-
const [x, y] = [
|
|
30
|
+
const [x, y] = [
|
|
31
|
+
Math.floor(index / channels) % width,
|
|
32
|
+
Math.floor(Math.floor(index / channels) / width),
|
|
33
|
+
];
|
|
24
34
|
// Distribute the error to neighbouring pixels based on the kernel.
|
|
25
35
|
for (let diffX = 0; diffX < kernelWidth; diffX++) {
|
|
26
36
|
for (let diffY = 0; diffY < kernelHeight; diffY++) {
|
|
@@ -31,7 +41,7 @@ export function applyDiffusionKernel(pixels, width, height, kernel) {
|
|
|
31
41
|
const neighbourY = y + diffY;
|
|
32
42
|
// Ensure we don't go out of bounds
|
|
33
43
|
if (neighbourX >= 0 && neighbourY >= 0 && neighbourX < width && neighbourY < height) {
|
|
34
|
-
const neighbourIndex = neighbourY * width + neighbourX;
|
|
44
|
+
const neighbourIndex = neighbourY * width * channels + neighbourX * channels + channel;
|
|
35
45
|
pixels[neighbourIndex] = eightBitClamp(pixels[neighbourIndex] + error * diffusionWeight);
|
|
36
46
|
}
|
|
37
47
|
}
|
package/dist/threshold-maps.json
CHANGED
|
@@ -125,11 +125,6 @@
|
|
|
125
125
|
[224, 140, 56, 14, 98, 182],
|
|
126
126
|
[238, 154, 70, 28, 112, 196]
|
|
127
127
|
],
|
|
128
|
-
"dot-vertical-5x3": [
|
|
129
|
-
[153, 51, 0, 102, 204],
|
|
130
|
-
[170, 68, 17, 119, 221],
|
|
131
|
-
[187, 85, 34, 136, 238]
|
|
132
|
-
],
|
|
133
128
|
"dot-horizontal-3x5": [
|
|
134
129
|
[153, 170, 187],
|
|
135
130
|
[51, 68, 85],
|
|
@@ -137,6 +132,33 @@
|
|
|
137
132
|
[102, 119, 136],
|
|
138
133
|
[204, 221, 238]
|
|
139
134
|
],
|
|
135
|
+
"dot-vertical-5x3": [
|
|
136
|
+
[153, 51, 0, 102, 204],
|
|
137
|
+
[170, 68, 17, 119, 221],
|
|
138
|
+
[187, 85, 34, 136, 238]
|
|
139
|
+
],
|
|
140
|
+
"horizontal-checkers-6": [
|
|
141
|
+
[245, 231, 217, 210, 224, 238, 35, 21, 7, 0, 14, 28],
|
|
142
|
+
[161, 147, 133, 126, 140, 154, 119, 105, 91, 84, 98, 112],
|
|
143
|
+
[77, 63, 49, 42, 56, 70, 203, 189, 175, 168, 182, 196],
|
|
144
|
+
[35, 21, 7, 0, 14, 28, 245, 231, 217, 210, 224, 238],
|
|
145
|
+
[119, 105, 91, 84, 98, 112, 161, 147, 133, 126, 140, 154],
|
|
146
|
+
[203, 189, 175, 168, 182, 196, 77, 63, 49, 42, 56, 70]
|
|
147
|
+
],
|
|
148
|
+
"vertical-checkers-6": [
|
|
149
|
+
[245, 161, 77, 35, 119, 203],
|
|
150
|
+
[231, 147, 63, 21, 105, 189],
|
|
151
|
+
[217, 133, 49, 7, 91, 175],
|
|
152
|
+
[210, 126, 42, 0, 84, 168],
|
|
153
|
+
[224, 140, 56, 14, 98, 182],
|
|
154
|
+
[238, 154, 70, 28, 112, 196],
|
|
155
|
+
[35, 119, 203, 245, 161, 77],
|
|
156
|
+
[21, 105, 189, 231, 147, 63],
|
|
157
|
+
[7, 91, 175, 217, 133, 49],
|
|
158
|
+
[0, 84, 168, 210, 126, 42],
|
|
159
|
+
[14, 98, 182, 224, 140, 56],
|
|
160
|
+
[28, 112, 196, 238, 154, 70]
|
|
161
|
+
],
|
|
140
162
|
"blue-noise": [
|
|
141
163
|
[
|
|
142
164
|
134, 22, 92, 59, 163, 190, 12, 155, 181, 119, 233, 47, 175, 12, 113, 141, 47, 183, 111, 196,
|
package/dist/types.d.ts
CHANGED
|
@@ -36,4 +36,23 @@ export interface SweetcornOptions {
|
|
|
36
36
|
* ]
|
|
37
37
|
*/
|
|
38
38
|
diffusionKernel?: number[][] | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Dither as a three-channel RGB image instead of converting to a single-channel black-and-white image.
|
|
41
|
+
*/
|
|
42
|
+
preserveColour?: boolean | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Preserve the alpha channel of the image, if it has one, instead of removing it.
|
|
45
|
+
*/
|
|
46
|
+
preserveAlpha?: boolean | undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Any array-like structure indexed by numbers and with a `length`, e.g. an `Array` or `Buffer`. */
|
|
49
|
+
export interface ArrayOrBuffer<T> {
|
|
50
|
+
length: number;
|
|
51
|
+
[n: number]: T;
|
|
52
|
+
}
|
|
53
|
+
/** Information about an image’s dimensions and channels. Subset of Sharp’s `OutputInfo`. */
|
|
54
|
+
export interface ImageInfo {
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
channels: number;
|
|
39
58
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sweetcorn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "JavaScript image dithering tools for Sharp",
|
|
6
6
|
"keywords": [
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc",
|
|
41
41
|
"pretest": "pnpm build",
|
|
42
|
-
"test": "node --test"
|
|
42
|
+
"test": "node --test",
|
|
43
|
+
"test:update": "node --test --test-update-snapshots"
|
|
43
44
|
}
|
|
44
45
|
}
|