import { ColorType, FilterMethod } from "./constants.js";
/**
* Concatenates a given array of array-like data (array buffers, typed arrays) into a single Uint8Array.
*
* @param {ArrayLike[]} chunks
* @returns Uint8Array concatenated data
*/
export function flattenBuffers(chunks) {
let totalSize = 0;
for (let chunk of chunks) {
totalSize += chunk.length;
}
const result = new Uint8Array(totalSize);
for (let i = 0, pos = 0; i < chunks.length; i++) {
let chunk = chunks[i];
result.set(chunk, pos);
pos += chunk.length;
}
return result;
}
export function decodeNULTerminatedString(
data,
offset = 0,
maxLength = Infinity
) {
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
let str = "";
for (let i = 0; offset < data.length && i < maxLength; offset++, i++) {
const b = dv.getUint8(offset);
if (b === 0x00) {
break;
} else {
const chr = String.fromCharCode(b);
str += chr;
}
}
// String is always terminated with NUL so we can move forward one more
offset++;
return [str, offset];
}
export function mergeData(...arrays) {
// convert to byte arrays
arrays = arrays.map((a) => {
if (typeof a === "number") return new Uint8Array([a]);
if (typeof a === "string") return convertStringToBytes(a);
return a;
});
// Get the total length of all arrays.
let length = 0;
for (let array of arrays) length += array.length;
// Create a new array with total length and merge all source arrays.
let mergedArray = new Uint8Array(length);
let offset = 0;
for (let item of arrays) {
mergedArray.set(item, offset);
offset += item.length;
}
return mergedArray;
}
export function convertStringToBytes(val) {
const data = new Uint8Array(val.length);
for (let i = 0; i < data.length; i++) {
data[i] = val.charCodeAt(i);
}
return data;
}
export function applyFilter(
out,
data,
i,
filter,
bytesPerPixel,
bytesPerScanline,
srcIdxInBytes,
dstIdxInBytesPlusOne
) {
if (filter === FilterMethod.Paeth) {
for (let j = 0; j < bytesPerScanline; j++) {
const left =
j < bytesPerPixel ? 0 : data[srcIdxInBytes + j - bytesPerPixel];
const up = i === 0 ? 0 : data[srcIdxInBytes + j - bytesPerScanline];
const upLeft =
i === 0 || j < bytesPerPixel
? 0
: data[srcIdxInBytes + j - bytesPerScanline - bytesPerPixel];
out[dstIdxInBytesPlusOne + j] =
data[srcIdxInBytes + j] - paethPredictor(left, up, upLeft);
}
} else if (filter === FilterMethod.Sub) {
for (let j = 0; j < bytesPerScanline; j++) {
const leftPixel =
j < bytesPerPixel ? 0 : data[srcIdxInBytes + j - bytesPerPixel];
out[dstIdxInBytesPlusOne + j] = data[srcIdxInBytes + j] - leftPixel;
}
} else if (filter === FilterMethod.Up) {
for (let j = 0; j < bytesPerScanline; j++) {
const upPixel = i === 0 ? 0 : data[srcIdxInBytes + j - bytesPerScanline];
out[dstIdxInBytesPlusOne + j] = data[srcIdxInBytes + j] - upPixel;
}
} else if (filter === FilterMethod.Average) {
for (let j = 0; j < bytesPerScanline; j++) {
const left =
j < bytesPerPixel ? 0 : data[srcIdxInBytes + j - bytesPerPixel];
const up = i === 0 ? 0 : data[srcIdxInBytes + j - bytesPerScanline];
const avg = (left + up) >> 1;
out[dstIdxInBytesPlusOne + j] = data[srcIdxInBytes + j] - avg;
}
}
// Should never get here in this version as applyFilter is only called
// when a non-None filter is specified
// if (filter === FilterMethod.None) {
// for (let j = 0; j < bytesPerScanline; j++) {
// out[dstIdxInBytesPlusOne + j] = data[srcIdxInBytes + j];
// }
// }
}
function paethPredictor(left, above, upLeft) {
let paeth = left + above - upLeft;
let pLeft = Math.abs(paeth - left);
let pAbove = Math.abs(paeth - above);
let pUpLeft = Math.abs(paeth - upLeft);
if (pLeft <= pAbove && pLeft <= pUpLeft) return left;
if (pAbove <= pUpLeft) return above;
return upLeft;
}
/**
* Converts a ColorType enum to a human readable string, for example ColorType.RGBA (= 6) becomes "RGBA".
* Although these numerical constants are defined in the PNG spec, the exact string for each is not.
*
* @param {ColorType} colorType the type to convert
* @returns {string} a readable string
*/
export function colorTypeToString(colorType) {
const entries = Object.entries(ColorType);
return entries.find((e) => e[1] === colorType)[0];
}
export function colorTypeToChannels(colorType) {
switch (colorType) {
case ColorType.GRAYSCALE:
return 1; // grayscale
case ColorType.RGB:
return 3; // RGB
case ColorType.INDEXED:
return 1; // indexed
case ColorType.GRAYSCALE_ALPHA:
return 2; // grayscale + alpha
case ColorType.RGBA:
return 4; // RGBA
default:
throw new Error(`Invalid colorType ${colorType}`);
}
}