117 lines
3.3 KiB
TypeScript
117 lines
3.3 KiB
TypeScript
|
import { Utils } from "./utils";
|
||
|
|
||
|
export type ImageRow = {
|
||
|
dataType: "void" | "pixels";
|
||
|
rowNumber: number;
|
||
|
repeat: number;
|
||
|
blackPixelsCount: number;
|
||
|
rowData?: Uint8Array;
|
||
|
};
|
||
|
|
||
|
export type EncodedImage = {
|
||
|
cols: number;
|
||
|
rows: number;
|
||
|
rowsData: ImageRow[];
|
||
|
};
|
||
|
|
||
|
export type HeadPosition = "left" | "top";
|
||
|
|
||
|
export class ImageEncoder {
|
||
|
/** headPos = "left" rotates image for 90 degrees clockwise */
|
||
|
public static encodeCanvas(canvas: HTMLCanvasElement, headPos: HeadPosition = "left"): EncodedImage {
|
||
|
const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!;
|
||
|
const iData: ImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||
|
const rowsData: ImageRow[] = [];
|
||
|
|
||
|
let cols: number = canvas.width;
|
||
|
let rows: number = canvas.height;
|
||
|
|
||
|
if (headPos === "left") {
|
||
|
cols = canvas.height;
|
||
|
rows = canvas.width;
|
||
|
}
|
||
|
|
||
|
if (cols % 8 !== 0) {
|
||
|
throw new Error("Column count must be multiple of 8");
|
||
|
}
|
||
|
|
||
|
for (let row = 0; row < rows; row++) {
|
||
|
let isVoid: boolean = true;
|
||
|
let blackPixelsCount: number = 0;
|
||
|
const rowData = new Uint8Array(cols / 8);
|
||
|
|
||
|
for (let colOct = 0; colOct < cols / 8; colOct++) {
|
||
|
let pixelsOctet: number = 0;
|
||
|
for (let colBit = 0; colBit < 8; colBit++) {
|
||
|
if (ImageEncoder.isPixelNonWhite(iData, colOct * 8 + colBit, row, headPos)) {
|
||
|
pixelsOctet |= 1 << (7 - colBit);
|
||
|
isVoid = false;
|
||
|
blackPixelsCount++;
|
||
|
}
|
||
|
}
|
||
|
rowData[colOct] = pixelsOctet;
|
||
|
}
|
||
|
|
||
|
const newPart: ImageRow = {
|
||
|
dataType: isVoid ? "void" : "pixels",
|
||
|
rowNumber: row,
|
||
|
repeat: 1,
|
||
|
rowData: isVoid ? undefined : rowData,
|
||
|
blackPixelsCount,
|
||
|
};
|
||
|
|
||
|
// Check previous row and increment repeats instead of adding new row if data is same
|
||
|
if (rowsData.length === 0) {
|
||
|
rowsData.push(newPart);
|
||
|
} else {
|
||
|
const lastPacket: ImageRow = rowsData[rowsData.length - 1];
|
||
|
let same: boolean = newPart.dataType === lastPacket.dataType;
|
||
|
|
||
|
if (same && newPart.dataType === "pixels") {
|
||
|
same = Utils.u8ArraysEqual(newPart.rowData!, lastPacket.rowData!);
|
||
|
}
|
||
|
|
||
|
if (same) {
|
||
|
lastPacket.repeat++;
|
||
|
} else {
|
||
|
rowsData.push(newPart);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return { cols, rows, rowsData };
|
||
|
}
|
||
|
|
||
|
/** headPos = "left" rotates image to 90 degrees clockwise */
|
||
|
public static isPixelNonWhite(iData: ImageData, x: number, y: number, headPos: HeadPosition = "left"): boolean {
|
||
|
let idx = y * iData.width + x;
|
||
|
|
||
|
if (headPos === "left") {
|
||
|
idx = (iData.height - 1 - x) * iData.width + y;
|
||
|
}
|
||
|
|
||
|
idx *= 4;
|
||
|
return iData.data[idx] !== 255 || iData.data[idx + 1] !== 255 || iData.data[idx + 2] !== 255;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param data Encoded pixels (every byte is 8 pixels)
|
||
|
* @returns Array of indexes where every index stored in two bytes (big endian)
|
||
|
*/
|
||
|
public static indexPixels(data: Uint8Array): Uint8Array {
|
||
|
const result: number[] = [];
|
||
|
|
||
|
for (let bytePos = 0; bytePos < data.byteLength; bytePos++) {
|
||
|
let b: number = data[bytePos];
|
||
|
for (let bitPos = 0; bitPos < 8; bitPos++) {
|
||
|
// iterate from most significant bit of byte
|
||
|
if (b & (1 << (7 - bitPos))) {
|
||
|
result.push(...Utils.u16ToBytes(bytePos * 8 + bitPos));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return new Uint8Array(result);
|
||
|
}
|
||
|
}
|