image-js
Version:
Image processing and manipulation in JavaScript
139 lines (124 loc) • 3.97 kB
text/typescript
import type { Image } from '../Image.js';
import { getClamp } from '../utils/clamp.js';
import type { ImageColorModel } from '../utils/constants/colorModels.js';
import { getOutputImage } from '../utils/getOutputImage.js';
import { assert } from '../utils/validators/assert.js';
import checkProcessable from '../utils/validators/checkProcessable.js';
import * as greyAlgorithms from './greyAlgorithms.js';
export const GreyAlgorithm = {
LUMA_709: 'luma709',
LUMA_601: 'luma601',
MAX: 'max',
MIN: 'min',
AVERAGE: 'average',
MINMAX: 'minmax',
RED: 'red',
GREEN: 'green',
BLUE: 'blue',
BLACK: 'black',
CYAN: 'cyan',
MAGENTA: 'magenta',
YELLOW: 'yellow',
HUE: 'hue',
SATURATION: 'saturation',
LIGHTNESS: 'lightness',
} as const satisfies Record<string, keyof typeof greyAlgorithms>;
export type GreyAlgorithm = (typeof GreyAlgorithm)[keyof typeof GreyAlgorithm];
{
// Check that all the algorithms are in the enum.
const algos = new Set<string>(Object.values(GreyAlgorithm));
for (const algo of Object.keys(greyAlgorithms)) {
assert(
algos.has(algo),
`Grey algorithm ${algo} is missing in the GreyAlgorithm enum`,
);
}
}
/**
* Call back that converts the RGB channels to grey. It is clamped afterwards.
* @callback GreyAlgorithmCallback
* @param {number} red - Value of the red channel.
* @param {number} green - Value of the green channel.
* @param {number} blue - Value of the blue channel.
* @returns {number} Value of the grey channel.
*/
export type GreyAlgorithmCallback = (
red: number,
green: number,
blue: number,
image: Image,
) => number;
export interface GreyOptions {
/**
* Specify the grey algorithm to use.
* @default `'luma709'`
*/
algorithm?: GreyAlgorithm | GreyAlgorithmCallback;
/**
* Specify wether to keep an alpha channel in the new image or not.
* @default `false`
*/
keepAlpha?: boolean;
/**
* Specify wether to merge the alpha channel with the gray pixel or not.
* @default `true`
*/
mergeAlpha?: boolean;
/**
* Image to which to output.
*/
out?: Image;
}
/**
* Convert the current image to grayscale.
* The source image has to be RGB or RGBA.
* If there is an alpha channel you have to specify what to do:
* - keepAlpha : keep the alpha channel, you will get a GREYA image.
* - mergeAlpha : multiply each pixel of the image by the alpha, you will get a GREY image.
* @param image - Original color image to convert to grey.
* @param options - The grey conversion options.
* @returns The resulting grey image.
*/
export function grey(image: Image, options: GreyOptions = {}): Image {
let { keepAlpha = false, mergeAlpha = true } = options;
const { algorithm = 'luma709' } = options;
checkProcessable(image, {
colorModel: ['RGB', 'RGBA'],
});
keepAlpha = keepAlpha && image.alpha;
mergeAlpha = mergeAlpha && image.alpha;
if (keepAlpha) {
mergeAlpha = false;
}
const newColorModel: ImageColorModel = keepAlpha ? 'GREYA' : 'GREY';
const newImage = getOutputImage(image, options, {
newParameters: { colorModel: newColorModel },
});
let method: GreyAlgorithmCallback;
if (typeof algorithm === 'function') {
method = algorithm;
} else {
method = greyAlgorithms[algorithm];
}
const clamp = getClamp(newImage);
for (let i = 0; i < image.size; i++) {
const red = image.getValueByIndex(i, 0);
const green = image.getValueByIndex(i, 1);
const blue = image.getValueByIndex(i, 2);
let newValue;
if (mergeAlpha) {
const alpha = image.getValueByIndex(i, 3);
newValue = clamp(
(method(red, green, blue, image) * alpha) / image.maxValue,
);
} else {
newValue = clamp(method(red, green, blue, image));
if (keepAlpha) {
const alpha = image.getValueByIndex(i, 3);
newImage.setValueByIndex(i, 1, alpha);
}
}
newImage.setValueByIndex(i, 0, newValue);
}
return newImage;
}