// JavaScript Image Resizer (c) 2012 - Grant Galitz
|
// Released to public domain 29 July 2013: https://github.com/grantgalitz/JS-Image-Resizer/issues/4
|
|
function Resize(
|
widthOriginal,
|
heightOriginal,
|
targetWidth,
|
targetHeight,
|
blendAlpha,
|
interpolationPass,
|
resizeCallback
|
) {
|
this.widthOriginal = Math.abs(Math.floor(widthOriginal) || 0);
|
this.heightOriginal = Math.abs(Math.floor(heightOriginal) || 0);
|
this.targetWidth = Math.abs(Math.floor(targetWidth) || 0);
|
this.targetHeight = Math.abs(Math.floor(targetHeight) || 0);
|
this.colorChannels = blendAlpha ? 4 : 3;
|
this.interpolationPass = Boolean(interpolationPass);
|
this.resizeCallback =
|
typeof resizeCallback === 'function' ? resizeCallback : function() {};
|
|
this.targetWidthMultipliedByChannels = this.targetWidth * this.colorChannels;
|
this.originalWidthMultipliedByChannels =
|
this.widthOriginal * this.colorChannels;
|
this.originalHeightMultipliedByChannels =
|
this.heightOriginal * this.colorChannels;
|
this.widthPassResultSize =
|
this.targetWidthMultipliedByChannels * this.heightOriginal;
|
this.finalResultSize =
|
this.targetWidthMultipliedByChannels * this.targetHeight;
|
this.initialize();
|
}
|
|
Resize.prototype.initialize = function() {
|
// Perform some checks:
|
if (
|
this.widthOriginal > 0 &&
|
this.heightOriginal > 0 &&
|
this.targetWidth > 0 &&
|
this.targetHeight > 0
|
) {
|
this.configurePasses();
|
} else {
|
throw new Error('Invalid settings specified for the resizer.');
|
}
|
};
|
|
Resize.prototype.configurePasses = function() {
|
if (this.widthOriginal === this.targetWidth) {
|
// Bypass the width resizer pass:
|
this.resizeWidth = this.bypassResizer;
|
} else {
|
// Setup the width resizer pass:
|
this.ratioWeightWidthPass = this.widthOriginal / this.targetWidth;
|
if (this.ratioWeightWidthPass < 1 && this.interpolationPass) {
|
this.initializeFirstPassBuffers(true);
|
this.resizeWidth =
|
this.colorChannels === 4
|
? this.resizeWidthInterpolatedRGBA
|
: this.resizeWidthInterpolatedRGB;
|
} else {
|
this.initializeFirstPassBuffers(false);
|
this.resizeWidth =
|
this.colorChannels === 4 ? this.resizeWidthRGBA : this.resizeWidthRGB;
|
}
|
}
|
|
if (this.heightOriginal === this.targetHeight) {
|
// Bypass the height resizer pass:
|
this.resizeHeight = this.bypassResizer;
|
} else {
|
// Setup the height resizer pass:
|
this.ratioWeightHeightPass = this.heightOriginal / this.targetHeight;
|
if (this.ratioWeightHeightPass < 1 && this.interpolationPass) {
|
this.initializeSecondPassBuffers(true);
|
this.resizeHeight = this.resizeHeightInterpolated;
|
} else {
|
this.initializeSecondPassBuffers(false);
|
this.resizeHeight =
|
this.colorChannels === 4 ? this.resizeHeightRGBA : this.resizeHeightRGB;
|
}
|
}
|
};
|
|
Resize.prototype._resizeWidthInterpolatedRGBChannels = function(
|
buffer,
|
fourthChannel
|
) {
|
const channelsNum = fourthChannel ? 4 : 3;
|
const ratioWeight = this.ratioWeightWidthPass;
|
const outputBuffer = this.widthBuffer;
|
|
let weight = 0;
|
let finalOffset = 0;
|
let pixelOffset = 0;
|
let firstWeight = 0;
|
let secondWeight = 0;
|
let targetPosition;
|
|
// Handle for only one interpolation input being valid for start calculation:
|
for (
|
targetPosition = 0;
|
weight < 1 / 3;
|
targetPosition += channelsNum, weight += ratioWeight
|
) {
|
for (
|
finalOffset = targetPosition, pixelOffset = 0;
|
finalOffset < this.widthPassResultSize;
|
pixelOffset += this.originalWidthMultipliedByChannels,
|
finalOffset += this.targetWidthMultipliedByChannels
|
) {
|
outputBuffer[finalOffset] = buffer[pixelOffset];
|
outputBuffer[finalOffset + 1] = buffer[pixelOffset + 1];
|
outputBuffer[finalOffset + 2] = buffer[pixelOffset + 2];
|
if (fourthChannel)
|
outputBuffer[finalOffset + 3] = buffer[pixelOffset + 3];
|
}
|
}
|
|
// Adjust for overshoot of the last pass's counter:
|
weight -= 1 / 3;
|
let interpolationWidthSourceReadStop;
|
|
for (
|
interpolationWidthSourceReadStop = this.widthOriginal - 1;
|
weight < interpolationWidthSourceReadStop;
|
targetPosition += channelsNum, weight += ratioWeight
|
) {
|
// Calculate weightings:
|
secondWeight = weight % 1;
|
firstWeight = 1 - secondWeight;
|
// Interpolate:
|
for (
|
finalOffset = targetPosition,
|
pixelOffset = Math.floor(weight) * channelsNum;
|
finalOffset < this.widthPassResultSize;
|
pixelOffset += this.originalWidthMultipliedByChannels,
|
finalOffset += this.targetWidthMultipliedByChannels
|
) {
|
outputBuffer[finalOffset + 0] =
|
buffer[pixelOffset + 0] * firstWeight +
|
buffer[pixelOffset + channelsNum + 0] * secondWeight;
|
outputBuffer[finalOffset + 1] =
|
buffer[pixelOffset + 1] * firstWeight +
|
buffer[pixelOffset + channelsNum + 1] * secondWeight;
|
outputBuffer[finalOffset + 2] =
|
buffer[pixelOffset + 2] * firstWeight +
|
buffer[pixelOffset + channelsNum + 2] * secondWeight;
|
if (fourthChannel)
|
outputBuffer[finalOffset + 3] =
|
buffer[pixelOffset + 3] * firstWeight +
|
buffer[pixelOffset + channelsNum + 3] * secondWeight;
|
}
|
}
|
|
// Handle for only one interpolation input being valid for end calculation:
|
for (
|
interpolationWidthSourceReadStop =
|
this.originalWidthMultipliedByChannels - channelsNum;
|
targetPosition < this.targetWidthMultipliedByChannels;
|
targetPosition += channelsNum
|
) {
|
for (
|
finalOffset = targetPosition,
|
pixelOffset = interpolationWidthSourceReadStop;
|
finalOffset < this.widthPassResultSize;
|
pixelOffset += this.originalWidthMultipliedByChannels,
|
finalOffset += this.targetWidthMultipliedByChannels
|
) {
|
outputBuffer[finalOffset] = buffer[pixelOffset];
|
outputBuffer[finalOffset + 1] = buffer[pixelOffset + 1];
|
outputBuffer[finalOffset + 2] = buffer[pixelOffset + 2];
|
if (fourthChannel)
|
outputBuffer[finalOffset + 3] = buffer[pixelOffset + 3];
|
}
|
}
|
|
return outputBuffer;
|
};
|
|
Resize.prototype._resizeWidthRGBChannels = function(buffer, fourthChannel) {
|
const channelsNum = fourthChannel ? 4 : 3;
|
const ratioWeight = this.ratioWeightWidthPass;
|
const ratioWeightDivisor = 1 / ratioWeight;
|
const nextLineOffsetOriginalWidth =
|
this.originalWidthMultipliedByChannels - channelsNum + 1;
|
const nextLineOffsetTargetWidth =
|
this.targetWidthMultipliedByChannels - channelsNum + 1;
|
const output = this.outputWidthWorkBench;
|
const outputBuffer = this.widthBuffer;
|
const trustworthyColorsCount = this.outputWidthWorkBenchOpaquePixelsCount;
|
|
let weight = 0;
|
let amountToNext = 0;
|
let actualPosition = 0;
|
let currentPosition = 0;
|
let line = 0;
|
let pixelOffset = 0;
|
let outputOffset = 0;
|
let multiplier = 1;
|
let r = 0;
|
let g = 0;
|
let b = 0;
|
let a = 0;
|
|
do {
|
for (line = 0; line < this.originalHeightMultipliedByChannels; ) {
|
output[line++] = 0;
|
output[line++] = 0;
|
output[line++] = 0;
|
if (fourthChannel) {
|
output[line++] = 0;
|
trustworthyColorsCount[line / channelsNum - 1] = 0;
|
}
|
}
|
|
weight = ratioWeight;
|
|
do {
|
amountToNext = 1 + actualPosition - currentPosition;
|
multiplier = Math.min(weight, amountToNext);
|
for (
|
line = 0, pixelOffset = actualPosition;
|
line < this.originalHeightMultipliedByChannels;
|
pixelOffset += nextLineOffsetOriginalWidth
|
) {
|
r = buffer[pixelOffset];
|
g = buffer[++pixelOffset];
|
b = buffer[++pixelOffset];
|
a = fourthChannel ? buffer[++pixelOffset] : 255;
|
// Ignore RGB values if pixel is completely transparent
|
output[line++] += (a ? r : 0) * multiplier;
|
output[line++] += (a ? g : 0) * multiplier;
|
output[line++] += (a ? b : 0) * multiplier;
|
if (fourthChannel) {
|
output[line++] += a * multiplier;
|
trustworthyColorsCount[line / channelsNum - 1] += a ? multiplier : 0;
|
}
|
}
|
|
if (weight >= amountToNext) {
|
actualPosition += channelsNum;
|
currentPosition = actualPosition;
|
weight -= amountToNext;
|
} else {
|
currentPosition += weight;
|
break;
|
}
|
} while (
|
weight > 0 &&
|
actualPosition < this.originalWidthMultipliedByChannels
|
);
|
|
for (
|
line = 0, pixelOffset = outputOffset;
|
line < this.originalHeightMultipliedByChannels;
|
pixelOffset += nextLineOffsetTargetWidth
|
) {
|
weight = fourthChannel ? trustworthyColorsCount[line / channelsNum] : 1;
|
multiplier = fourthChannel
|
? weight
|
? 1 / weight
|
: 0
|
: ratioWeightDivisor;
|
outputBuffer[pixelOffset] = output[line++] * multiplier;
|
outputBuffer[++pixelOffset] = output[line++] * multiplier;
|
outputBuffer[++pixelOffset] = output[line++] * multiplier;
|
if (fourthChannel)
|
outputBuffer[++pixelOffset] = output[line++] * ratioWeightDivisor;
|
}
|
|
outputOffset += channelsNum;
|
} while (outputOffset < this.targetWidthMultipliedByChannels);
|
|
return outputBuffer;
|
};
|
|
Resize.prototype._resizeHeightRGBChannels = function(buffer, fourthChannel) {
|
const ratioWeight = this.ratioWeightHeightPass;
|
const ratioWeightDivisor = 1 / ratioWeight;
|
const output = this.outputHeightWorkBench;
|
const outputBuffer = this.heightBuffer;
|
const trustworthyColorsCount = this.outputHeightWorkBenchOpaquePixelsCount;
|
|
let weight = 0;
|
let amountToNext = 0;
|
let actualPosition = 0;
|
let currentPosition = 0;
|
let pixelOffset = 0;
|
let outputOffset = 0;
|
let caret = 0;
|
let multiplier = 1;
|
let r = 0;
|
let g = 0;
|
let b = 0;
|
let a = 0;
|
|
do {
|
for (
|
pixelOffset = 0;
|
pixelOffset < this.targetWidthMultipliedByChannels;
|
|
) {
|
output[pixelOffset++] = 0;
|
output[pixelOffset++] = 0;
|
output[pixelOffset++] = 0;
|
|
if (fourthChannel) {
|
output[pixelOffset++] = 0;
|
trustworthyColorsCount[pixelOffset / 4 - 1] = 0;
|
}
|
}
|
|
weight = ratioWeight;
|
|
do {
|
amountToNext = 1 + actualPosition - currentPosition;
|
multiplier = Math.min(weight, amountToNext);
|
caret = actualPosition;
|
|
for (
|
pixelOffset = 0;
|
pixelOffset < this.targetWidthMultipliedByChannels;
|
|
) {
|
r = buffer[caret++];
|
g = buffer[caret++];
|
b = buffer[caret++];
|
a = fourthChannel ? buffer[caret++] : 255;
|
// Ignore RGB values if pixel is completely transparent
|
output[pixelOffset++] += (a ? r : 0) * multiplier;
|
output[pixelOffset++] += (a ? g : 0) * multiplier;
|
output[pixelOffset++] += (a ? b : 0) * multiplier;
|
|
if (fourthChannel) {
|
output[pixelOffset++] += a * multiplier;
|
trustworthyColorsCount[pixelOffset / 4 - 1] += a ? multiplier : 0;
|
}
|
}
|
|
if (weight >= amountToNext) {
|
actualPosition = caret;
|
currentPosition = actualPosition;
|
weight -= amountToNext;
|
} else {
|
currentPosition += weight;
|
break;
|
}
|
} while (weight > 0 && actualPosition < this.widthPassResultSize);
|
|
for (
|
pixelOffset = 0;
|
pixelOffset < this.targetWidthMultipliedByChannels;
|
|
) {
|
weight = fourthChannel ? trustworthyColorsCount[pixelOffset / 4] : 1;
|
multiplier = fourthChannel
|
? weight
|
? 1 / weight
|
: 0
|
: ratioWeightDivisor;
|
outputBuffer[outputOffset++] = Math.round(
|
output[pixelOffset++] * multiplier
|
);
|
outputBuffer[outputOffset++] = Math.round(
|
output[pixelOffset++] * multiplier
|
);
|
outputBuffer[outputOffset++] = Math.round(
|
output[pixelOffset++] * multiplier
|
);
|
|
if (fourthChannel) {
|
outputBuffer[outputOffset++] = Math.round(
|
output[pixelOffset++] * ratioWeightDivisor
|
);
|
}
|
}
|
} while (outputOffset < this.finalResultSize);
|
|
return outputBuffer;
|
};
|
|
Resize.prototype.resizeWidthInterpolatedRGB = function(buffer) {
|
return this._resizeWidthInterpolatedRGBChannels(buffer, false);
|
};
|
|
Resize.prototype.resizeWidthInterpolatedRGBA = function(buffer) {
|
return this._resizeWidthInterpolatedRGBChannels(buffer, true);
|
};
|
|
Resize.prototype.resizeWidthRGB = function(buffer) {
|
return this._resizeWidthRGBChannels(buffer, false);
|
};
|
|
Resize.prototype.resizeWidthRGBA = function(buffer) {
|
return this._resizeWidthRGBChannels(buffer, true);
|
};
|
|
Resize.prototype.resizeHeightInterpolated = function(buffer) {
|
const ratioWeight = this.ratioWeightHeightPass;
|
const outputBuffer = this.heightBuffer;
|
|
let weight = 0;
|
let finalOffset = 0;
|
let pixelOffset = 0;
|
let pixelOffsetAccumulated = 0;
|
let pixelOffsetAccumulated2 = 0;
|
let firstWeight = 0;
|
let secondWeight = 0;
|
let interpolationHeightSourceReadStop;
|
|
// Handle for only one interpolation input being valid for start calculation:
|
for (; weight < 1 / 3; weight += ratioWeight) {
|
for (
|
pixelOffset = 0;
|
pixelOffset < this.targetWidthMultipliedByChannels;
|
|
) {
|
outputBuffer[finalOffset++] = Math.round(buffer[pixelOffset++]);
|
}
|
}
|
|
// Adjust for overshoot of the last pass's counter:
|
weight -= 1 / 3;
|
|
for (
|
interpolationHeightSourceReadStop = this.heightOriginal - 1;
|
weight < interpolationHeightSourceReadStop;
|
weight += ratioWeight
|
) {
|
// Calculate weightings:
|
secondWeight = weight % 1;
|
firstWeight = 1 - secondWeight;
|
// Interpolate:
|
pixelOffsetAccumulated =
|
Math.floor(weight) * this.targetWidthMultipliedByChannels;
|
pixelOffsetAccumulated2 =
|
pixelOffsetAccumulated + this.targetWidthMultipliedByChannels;
|
for (
|
pixelOffset = 0;
|
pixelOffset < this.targetWidthMultipliedByChannels;
|
++pixelOffset
|
) {
|
outputBuffer[finalOffset++] = Math.round(
|
buffer[pixelOffsetAccumulated++] * firstWeight +
|
buffer[pixelOffsetAccumulated2++] * secondWeight
|
);
|
}
|
}
|
|
// Handle for only one interpolation input being valid for end calculation:
|
while (finalOffset < this.finalResultSize) {
|
for (
|
pixelOffset = 0,
|
pixelOffsetAccumulated =
|
interpolationHeightSourceReadStop *
|
this.targetWidthMultipliedByChannels;
|
pixelOffset < this.targetWidthMultipliedByChannels;
|
++pixelOffset
|
) {
|
outputBuffer[finalOffset++] = Math.round(
|
buffer[pixelOffsetAccumulated++]
|
);
|
}
|
}
|
|
return outputBuffer;
|
};
|
|
Resize.prototype.resizeHeightRGB = function(buffer) {
|
return this._resizeHeightRGBChannels(buffer, false);
|
};
|
|
Resize.prototype.resizeHeightRGBA = function(buffer) {
|
return this._resizeHeightRGBChannels(buffer, true);
|
};
|
|
Resize.prototype.resize = function(buffer) {
|
this.resizeCallback(this.resizeHeight(this.resizeWidth(buffer)));
|
};
|
|
Resize.prototype.bypassResizer = function(buffer) {
|
// Just return the buffer passed:
|
return buffer;
|
};
|
|
Resize.prototype.initializeFirstPassBuffers = function(BILINEARAlgo) {
|
// Initialize the internal width pass buffers:
|
this.widthBuffer = this.generateFloatBuffer(this.widthPassResultSize);
|
|
if (!BILINEARAlgo) {
|
this.outputWidthWorkBench = this.generateFloatBuffer(
|
this.originalHeightMultipliedByChannels
|
);
|
|
if (this.colorChannels > 3) {
|
this.outputWidthWorkBenchOpaquePixelsCount = this.generateFloat64Buffer(
|
this.heightOriginal
|
);
|
}
|
}
|
};
|
|
Resize.prototype.initializeSecondPassBuffers = function(BILINEARAlgo) {
|
// Initialize the internal height pass buffers:
|
this.heightBuffer = this.generateUint8Buffer(this.finalResultSize);
|
|
if (!BILINEARAlgo) {
|
this.outputHeightWorkBench = this.generateFloatBuffer(
|
this.targetWidthMultipliedByChannels
|
);
|
|
if (this.colorChannels > 3) {
|
this.outputHeightWorkBenchOpaquePixelsCount = this.generateFloat64Buffer(
|
this.targetWidth
|
);
|
}
|
}
|
};
|
|
Resize.prototype.generateFloatBuffer = function(bufferLength) {
|
// Generate a float32 typed array buffer:
|
try {
|
return new Float32Array(bufferLength);
|
} catch (error) {
|
return [];
|
}
|
};
|
|
Resize.prototype.generateFloat64Buffer = function(bufferLength) {
|
// Generate a float64 typed array buffer:
|
try {
|
return new Float64Array(bufferLength);
|
} catch (error) {
|
return [];
|
}
|
};
|
|
Resize.prototype.generateUint8Buffer = function(bufferLength) {
|
// Generate a uint8 typed array buffer:
|
try {
|
return new Uint8Array(bufferLength);
|
} catch (error) {
|
return [];
|
}
|
};
|
|
module.exports = Resize;
|