sweetcorn 0.2.2 → 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.
@@ -1,6 +1,6 @@
1
- import { ArrayOrBuffer } from './types.js';
1
+ import type { ArrayOrBuffer, ImageInfo } from './types.js';
2
2
  /**
3
3
  * A collection of pixel mutation processors that deviate from the standardized threshold map and
4
4
  * error diffusion algorithms.
5
5
  */
6
- export declare const customProcessors: Record<'white-noise' | 'threshold', (pixels: ArrayOrBuffer<number>) => void>;
6
+ export declare const customProcessors: Record<'white-noise' | 'threshold', (pixels: ArrayOrBuffer<number>, info: ImageInfo) => void>;
@@ -6,10 +6,14 @@ export const customProcessors = {
6
6
  /**
7
7
  * White noise dithering (pretty rough and ugly)
8
8
  */
9
- 'white-noise'(pixels) {
10
- for (let index = 0; index < pixels.length; index++) {
11
- const pixelValue = pixels[index];
12
- pixels[index] = pixelValue / 255 < Math.random() ? 0 : 255;
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
+ }
13
17
  }
14
18
  },
15
19
  /**
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
@@ -23,28 +23,53 @@ async function loadSharp() {
23
23
  */
24
24
  export default async function sweetcorn(image, options) {
25
25
  // Clone the input image to avoid mutating it.
26
- image = image.clone();
27
- const { algorithm } = options;
28
- if (!sharp)
29
- sharp = await loadSharp();
30
- // Convert image to greyscale before dithering.
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
+ }
31
38
  // We use gamma to linearize the colorspace, and improve the perceptual quality.
32
- image.gamma(2.2, 1).greyscale();
39
+ image.gamma(2.2, 1);
40
+ // Convert image to greyscale before dithering.
41
+ if (!options.preserveColour) {
42
+ image.greyscale();
43
+ }
33
44
  // Get raw pixel data for this image.
34
45
  const { data: pixels, info } = await image.raw().toBuffer({ resolveWithObject: true });
35
- const thresholdMap = options.thresholdMap || thresholdMaps[algorithm];
36
- const diffusionKernel = options.diffusionKernel || diffusionKernels[algorithm];
37
- const customProcessor = customProcessors[algorithm];
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.
38
50
  if (thresholdMap) {
39
- applyThresholdMap(pixels, info.width, thresholdMap);
51
+ applyThresholdMap(pixels, info, thresholdMap);
40
52
  }
41
53
  else if (diffusionKernel) {
42
- applyDiffusionKernel(pixels, info.width, info.height, diffusionKernel);
54
+ applyDiffusionKernel(pixels, info, diffusionKernel);
43
55
  }
44
56
  else if (customProcessor) {
45
- customProcessor(pixels);
57
+ customProcessor(pixels, info);
46
58
  }
47
59
  // Convert raw pixel data back into a Sharp image.
60
+ if (!sharp)
61
+ sharp = await loadSharp();
48
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
+ }
49
74
  return outputImage;
50
75
  }
@@ -1,5 +1,5 @@
1
- import { ArrayOrBuffer } from './types.js';
1
+ import { ArrayOrBuffer, ImageInfo } from './types.js';
2
2
  /** Applies a threshold map to raw pixel data for ordered dithering. */
3
- export declare function applyThresholdMap(pixels: ArrayOrBuffer<number>, width: number, map: number[][]): void;
3
+ export declare function applyThresholdMap(pixels: ArrayOrBuffer<number>, { width, channels }: ImageInfo, map: number[][]): void;
4
4
  /** Applies an error diffusion kernel to raw pixel data. */
5
- export declare function applyDiffusionKernel(pixels: ArrayOrBuffer<number>, width: number, height: number, kernel: number[][]): void;
5
+ export declare function applyDiffusionKernel(pixels: ArrayOrBuffer<number>, { width, height, channels }: ImageInfo, kernel: number[][]): void;
@@ -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 [x, y] = [index % width, Math.floor(index / width)];
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] = [index % width, Math.floor(index / width)];
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/types.d.ts CHANGED
@@ -36,9 +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;
39
47
  }
40
48
  /** Any array-like structure indexed by numbers and with a `length`, e.g. an `Array` or `Buffer`. */
41
49
  export interface ArrayOrBuffer<T> {
42
50
  length: number;
43
51
  [n: number]: T;
44
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;
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sweetcorn",
3
- "version": "0.2.2",
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
  }