pixelplanet/src/utils/image.js

284 lines
8.1 KiB
JavaScript

/**
* Basic image manipulation and quantization
*/
import { utils, distance, image } from 'image-q';
/*
* available color distance calculators
*/
export const ColorDistanceCalculators = [
'Euclidean',
'Manhattan',
'CIEDE2000',
'CIE94Textiles',
'CIE94GraphicArts',
'EuclideanBT709NoAlpha',
'EuclideanBT709',
'ManhattanBT709',
'CMetric',
'PNGQuant',
'ManhattanNommyde',
];
/*
* available dithers
*/
export const ImageQuantizerKernels = [
'Nearest',
'Riemersma',
'FloydSteinberg',
'FalseFloydSteinberg',
'Stucki',
'Atkinson',
'Jarvis',
'Burkes',
'Sierra',
'TwoSierra',
'SierraLite',
];
export function addGrid(imgCanvas, lightGrid, offsetX, offsetY) {
const { width, height } = imgCanvas;
const can = document.createElement('canvas');
const ctx = can.getContext('2d');
can.width = width * 5;
can.height = height * 5;
ctx.imageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.save();
ctx.scale(5.0, 5.0);
ctx.drawImage(imgCanvas, 0, 0);
ctx.restore();
ctx.fillStyle = (lightGrid) ? '#DDDDDD' : '#222222';
for (let i = 0; i <= width; i += 1) {
const thick = ((i + (offsetX * 1)) % 10 === 0) ? 2 : 1;
ctx.fillRect(i * 5, 0, thick, can.height);
}
for (let j = 0; j <= height; j += 1) {
const thick = ((j + (offsetY * 1)) % 10 === 0) ? 2 : 1;
ctx.fillRect(0, j * 5, can.width, thick);
}
return can;
}
export function scaleImage(imgCanvas, width, height, doAA) {
const can = document.createElement('canvas');
can.width = width;
can.height = height;
const ctxo = can.getContext('2d');
const scaleX = width / imgCanvas.width;
const scaleY = height / imgCanvas.height;
if (doAA) {
// scale with canvas for antialiasing
ctxo.save();
ctxo.scale(scaleX, scaleY);
ctxo.drawImage(imgCanvas, 0, 0);
ctxo.restore();
} else {
// scale manually
const ctxi = imgCanvas.getContext('2d');
const imdi = ctxi.getImageData(0, 0, imgCanvas.width, imgCanvas.height);
const imdo = ctxo.createImageData(width, height);
const { data: datai } = imdi;
const { data: datao } = imdo;
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
let posi = (Math.round(x / scaleX) + Math.round(y / scaleY)
* imgCanvas.width) * 4;
let poso = (x + y * width) * 4;
datao[poso++] = datai[posi++];
datao[poso++] = datai[posi++];
datao[poso++] = datai[posi++];
datao[poso] = datai[posi];
}
}
ctxo.putImageData(imdo, 0, 0);
}
return can;
}
/*
* read File object into canvas
* @param file
* @return HTMLCanvas
*/
export function readFileIntoCanvas(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = () => {
const img = new Image();
img.onload = () => {
const cani = document.createElement('canvas');
cani.width = img.width;
cani.height = img.height;
const ctxi = cani.getContext('2d');
ctxi.drawImage(img, 0, 0);
resolve(cani);
};
img.onerror = (error) => reject(error);
img.src = fr.result;
};
fr.onerror = (error) => reject(error);
fr.readAsDataURL(file);
});
}
/*
* converts pointContainer to HTMLCanvas
* @param pointContainer
* @return HTMLCanvas
*/
function createCanvasFromPointContainer(pointContainer) {
const width = pointContainer.getWidth();
const height = pointContainer.getHeight();
const data = pointContainer.toUint8Array();
const can = document.createElement('canvas');
can.width = width;
can.height = height;
const ctx = can.getContext('2d');
const idata = ctx.createImageData(width, height);
idata.data.set(data);
ctx.putImageData(idata, 0, 0);
return can;
}
/*
* quantizes point container
* @param colors Array of [r, g, b] color Arrays
* @param pointContainer pointContainer of input image
* @param opts Object with configuration:
* strategy: String of dithering strategy (ImageQuantizerKernels)
* colorDist: String of color distance calc (ColorDistanceCalculators)
* onProgress: function that gets called with integer of progress percentage
* and only available for not Nearest or Riemersma
* serpentine: type of dithering
* minColorDistance: minimal distance on which we start to dither
* GIMPerror: calculate error like GIMP
* @return Promise that resolves to HTMLCanvasElement of output image
*/
function quantizePointContainer(colors, pointContainer, opts) {
const strategy = opts.strategy || 'Nearest';
const colorDist = opts.colorDist || 'Euclidean';
return new Promise((resolve, reject) => {
// create palette
const palette = new utils.Palette();
palette.add(utils.Point.createByRGBA(0, 0, 0, 0));
for (let i = 0; i < colors.length; i += 1) {
const [r, g, b] = colors[i];
const point = utils.Point.createByRGBA(r, g, b, 255);
palette.add(point);
}
// construct color distance calculator
let distCalc;
switch (colorDist) {
case 'Euclidean':
distCalc = new distance.Euclidean();
break;
case 'Manhattan':
distCalc = new distance.Manhattan();
break;
case 'CIEDE2000':
distCalc = new distance.CIEDE2000();
break;
case 'CIE94Textiles':
distCalc = new distance.CIE94Textiles();
break;
case 'CIE94GraphicArts':
distCalc = new distance.CIE94GraphicArts();
break;
case 'EuclideanBT709NoAlpha':
distCalc = new distance.EuclideanBT709NoAlpha();
break;
case 'EuclideanBT709':
distCalc = new distance.EuclideanBT709();
break;
case 'ManhattanBT709':
distCalc = new distance.ManhattanBT709();
break;
case 'CMetric':
distCalc = new distance.CMetric();
break;
case 'PNGQuant':
distCalc = new distance.PNGQuant();
break;
case 'ManhattanNommyde':
distCalc = new distance.ManhattanNommyde();
break;
default:
distCalc = new distance.Euclidean();
}
// could be needed for some reason sometimes
// eslint-disable-next-line no-underscore-dangle
if (distCalc._setDefaults) distCalc._setDefaults();
// construct image quantizer
let imageQuantizer;
if (strategy === 'Nearest') {
imageQuantizer = new image.NearestColor(distCalc);
} else if (strategy === 'Riemersma') {
imageQuantizer = new image.ErrorDiffusionRiemersma(distCalc);
} else {
// minColorDistance is a percentage
let minColorDistance = 0;
// eslint-disable-next-line no-prototype-builtins
if (opts.hasOwnProperty('minColorDistance')) {
const mcdNumber = Number(opts.minColorDistance);
if (!Number.isNaN(mcdNumber)) {
minColorDistance = mcdNumber / 100 * 0.2;
}
}
imageQuantizer = new image.ErrorDiffusionArray(
distCalc,
image.ErrorDiffusionArrayKernel[strategy],
// eslint-disable-next-line no-prototype-builtins
opts.hasOwnProperty('serpentine') ? opts.serpentine : true,
minColorDistance,
!!opts.GIMPerror,
);
}
// quantize
const iterator = imageQuantizer.quantize(pointContainer, palette);
const next = () => {
try {
const result = iterator.next();
if (result.done) {
resolve(createCanvasFromPointContainer(pointContainer));
} else {
if (result.value.pointContainer) {
pointContainer = result.value.pointContainer;
}
if (opts.onProgress) {
opts.onProgress(
Math.floor(25 + result.value.progress * 3 / 4),
);
}
setTimeout(next, 0);
}
} catch (error) {
reject(error);
}
};
setTimeout(next, 0);
});
}
/*
* quantize HTMLCanvas to palette
* see quantizePointContainer for parameter meanings
*/
export function quantizeImage(colors, imageCanvas, opts) {
const pointContainer = utils.PointContainer.fromHTMLCanvasElement(
imageCanvas,
);
return quantizePointContainer(
colors,
pointContainer,
opts,
);
}