204 lines
6.2 KiB
JavaScript
204 lines
6.2 KiB
JavaScript
/**
|
|
* @author pointhi [thomas.pointhuber@gmx.at]
|
|
* @copyright Crown Copyright 2019
|
|
* @license Apache-2.0
|
|
*/
|
|
|
|
import Operation from "../Operation.mjs";
|
|
import OperationError from "../errors/OperationError.mjs";
|
|
import Utils from "../Utils.mjs";
|
|
import { isImage } from "../lib/FileType.mjs";
|
|
import { toBase64 } from "../lib/Base64.mjs";
|
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
|
import { Jimp, JimpMime, ResizeStrategy, rgbaToInt } from "jimp";
|
|
|
|
// arbitrary limits to prevent resource exhaustion
|
|
// scale factor of 64 is big enough to likely result in scaling in the display
|
|
// window anyway
|
|
// pixels per row is harder to come up with a figure that won't inconvenience
|
|
// someone. 2048 feels like a reasonable compromise
|
|
const MAX_PIXEL_SCALE_FACTOR = 64;
|
|
const MAX_PIXELS_PER_ROW = 2048;
|
|
|
|
/**
|
|
* Generate Image operation
|
|
*/
|
|
class GenerateImage extends Operation {
|
|
/**
|
|
* GenerateImage constructor
|
|
*/
|
|
constructor() {
|
|
super();
|
|
|
|
this.name = "Generate Image";
|
|
this.module = "Image";
|
|
this.description =
|
|
"Generates an image using the input as pixel values.";
|
|
this.infoURL = "";
|
|
this.inputType = "ArrayBuffer";
|
|
this.outputType = "ArrayBuffer";
|
|
this.presentType = "html";
|
|
this.args = [
|
|
{
|
|
name: "Mode",
|
|
type: "option",
|
|
value: ["Greyscale", "RG", "RGB", "RGBA", "Bits"],
|
|
},
|
|
{
|
|
name: "Pixel Scale Factor",
|
|
type: "number",
|
|
value: 8,
|
|
integer: true,
|
|
min: 1,
|
|
max: MAX_PIXEL_SCALE_FACTOR,
|
|
},
|
|
{
|
|
name: "Pixels per row",
|
|
type: "number",
|
|
value: 64,
|
|
integer: true,
|
|
min: 1,
|
|
max: MAX_PIXELS_PER_ROW,
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param {byteArray} input
|
|
* @param {Object[]} args
|
|
* @returns {ArrayBuffer}
|
|
*/
|
|
async run(input, args) {
|
|
const [mode, scale, width] = args;
|
|
input = new Uint8Array(input);
|
|
|
|
const bytePerPixelMap = {
|
|
Greyscale: 1,
|
|
RG: 2,
|
|
RGB: 3,
|
|
RGBA: 4,
|
|
Bits: 1 / 8,
|
|
};
|
|
|
|
if (!Object.hasOwn(bytePerPixelMap, mode)) {
|
|
throw new OperationError(`Unsupported Mode: (${mode})`);
|
|
}
|
|
|
|
const bytesPerPixel = bytePerPixelMap[mode];
|
|
|
|
if (bytesPerPixel > 0 && input.length % bytesPerPixel !== 0) {
|
|
throw new OperationError(
|
|
`Number of bytes is not a divisor of ${bytesPerPixel}`,
|
|
);
|
|
}
|
|
|
|
const height = Math.ceil(input.length / bytesPerPixel / width);
|
|
const image = new Jimp({ width, height });
|
|
|
|
if (isWorkerEnvironment())
|
|
self.sendStatusMessage("Generating image from data...");
|
|
|
|
if (mode === "Bits") {
|
|
let index = 0;
|
|
for (let j = 0; j < input.length; j++) {
|
|
const curByte = Utils.bin(input[j]);
|
|
for (let k = 0; k < 8; k++, index++) {
|
|
const x = index % width;
|
|
const y = Math.floor(index / width);
|
|
|
|
const value = curByte[k] === "0" ? 0xff : 0x00;
|
|
const pixel = rgbaToInt(value, value, value, 0xff);
|
|
image.setPixelColor(pixel, x, y);
|
|
}
|
|
}
|
|
} else {
|
|
let i = 0;
|
|
while (i < input.length) {
|
|
const index = i / bytesPerPixel;
|
|
const x = index % width;
|
|
const y = Math.floor(index / width);
|
|
|
|
let red = 0x00;
|
|
let green = 0x00;
|
|
let blue = 0x00;
|
|
let alpha = 0xff;
|
|
|
|
switch (mode) {
|
|
case "Greyscale":
|
|
red = green = blue = input[i++];
|
|
break;
|
|
|
|
case "RG":
|
|
red = input[i++];
|
|
green = input[i++];
|
|
break;
|
|
|
|
case "RGB":
|
|
red = input[i++];
|
|
green = input[i++];
|
|
blue = input[i++];
|
|
break;
|
|
|
|
case "RGBA":
|
|
red = input[i++];
|
|
green = input[i++];
|
|
blue = input[i++];
|
|
alpha = input[i++];
|
|
break;
|
|
|
|
default:
|
|
throw new OperationError(`Unsupported Mode: (${mode})`);
|
|
}
|
|
|
|
try {
|
|
const pixel = rgbaToInt(red, green, blue, alpha);
|
|
image.setPixelColor(pixel, x, y);
|
|
} catch (err) {
|
|
throw new OperationError(
|
|
`Error while generating image from pixel values. (${err})`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (scale !== 1) {
|
|
if (isWorkerEnvironment())
|
|
self.sendStatusMessage("Scaling image...");
|
|
|
|
image.scaleToFit({
|
|
w: width * scale,
|
|
h: height * scale,
|
|
mode: ResizeStrategy.NEAREST_NEIGHBOR,
|
|
});
|
|
}
|
|
|
|
try {
|
|
// see https://nodejs.org/docs/latest-v24.x/api/buffer.html#bufbyteoffset
|
|
// for why we can't just return result.buffer
|
|
const result = await image.getBuffer(JimpMime.png);
|
|
return result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength);
|
|
} catch (err) {
|
|
throw new OperationError(`Error generating image. (${err})`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Displays the generated image using HTML for web apps
|
|
* @param {ArrayBuffer} data
|
|
* @returns {html}
|
|
*/
|
|
present(data) {
|
|
if (!data.byteLength) return "";
|
|
const dataArray = new Uint8Array(data);
|
|
|
|
const type = isImage(dataArray);
|
|
if (!type) {
|
|
throw new OperationError("Invalid file type.");
|
|
}
|
|
|
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
|
}
|
|
}
|
|
|
|
export default GenerateImage;
|