/*
* Copyright (c) 2016-2020 Moddable Tech, Inc.
*
* This file is part of the Moddable SDK Tools.
*
* The Moddable SDK Tools is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Moddable SDK Tools is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with the Moddable SDK Tools. If not, see .
*
*/
import { TOOL } from "tool";
import Bitmap from "commodetto/Bitmap";
import BMPOut from "commodetto/BMPOut";
import Convert from "commodetto/Convert";
import PNG from "commodetto/ReadPNG";
import PixelsOut from "commodetto/PixelsOut";
import File from "file";
var formatNames = {
monochrome: "monochrome",
gray16: "gray16",
gray256: "gray256",
rgb332: "rgb332",
rgb565le: "rgb565le",
rgb565be: "rgb565be",
clut16: "clut16",
argb4444: "argb4444",
};
var formatValues = {
monochrome: 3,
gray16: 4,
gray256: 5,
rgb332: 6,
rgb565le: 7,
rgb565be: 8,
clut16: 11,
argb4444: 12,
};
export default class extends TOOL {
constructor(argv) {
super(argv);
this.alpha = true;
this.alphaFormat = formatValues.gray16;
this.bm4 = false;
this.color = true;
this.colorFormat = 0;
this.name = "";
this.rotation = 0;
this.inputPaths = [];
this.outputPath = null;
this.temporary = false;
var argc = argv.length;
for (var argi = 1; argi < argc; argi++) {
var option = argv[argi], name, path;
switch (option) {
case "-a":
this.color = false;
break;
case "-c":
this.alpha = false;
break;
case "-clut":
argi++;
if (argi >= argc)
throw new Error("-clut: no path!");
path = this.resolveFilePath(argv[argi]);
if (this.clut)
throw new Error("-clut '" + name + "': too many CLUTs!");
if (!path)
throw new Error("'" + name + "': file not found!");
this.clut = path;
break;
case "-f":
argi++;
if (argi >= argc)
throw new Error("-f: no format!");
name = argv[argi];
if (this.colorFormat)
throw new Error("-f '" + name + "': too many formats!");
name = name.toLowerCase();
if (name in formatNames)
name = formatNames[name];
else
throw new Error("-f '" + name + "': unknown format!");
this.colorFormat = formatValues[name];
break;
case "-m":
if (this.colorFormat)
throw new Error("-m: too many formats!");
this.alphaFormat = formatValues.monochrome;
this.colorFormat = formatValues.monochrome;
break;
case "-n":
argi++;
if (argi >= argc)
throw new Error("-n: no name!");
name = argv[argi];
if (this.name)
throw new Error("-n '" + name + "': too many names!");
this.name = name;
break;
case "-o":
argi++;
if (argi >= argc)
throw new Error("-o: no directory!");
name = argv[argi];
if (this.outputPath)
throw new Error("-o '" + name + "': too many directories!");
path = this.resolveDirectoryPath(name);
if (!path)
throw new Error("-o '" + name + "': directory not found!");
this.outputPath = path;
break;
case "-r":
argi++;
if (argi >= argc)
throw new Error("-r: no rotation!");
name = parseInt(argv[argi]);
if ((name != 0) && (name != 90) && (name != 180) && (name != 270))
throw new Error("-r: " + name + ": invalid rotation!");
this.rotation = name;
break;
case "-t":
this.temporary = true;
break;
case "-4":
this.bm4 = true;
break;
default:
name = argv[argi];
path = this.resolveFilePath(name);
if (!path)
throw new Error("'" + name + "': file not found!");
this.inputPaths.push(path);
break;
}
}
if (!this.colorFormat) {
this.colorFormat = 7;
}
if (this.inputPaths.length == 0) {
throw new Error("no file!");
}
if (!this.outputPath)
this.outputPath = this.splitPath(this.inputPaths[0]).directory;
}
checkPNG(png) {
let pngChannels = png.channels;
if (png.depth != 8)
return false;
if ((png.channels == 1) && (png.palette)) {
png.palette = new Uint8Array(png.palette);
return true;
}
return (png.channels == 2) || (png.channels == 3) || (png.channels == 4);
}
pad(width) {
const colorDepth = (this.color) ? Bitmap.depth(this.colorFormat) : 32;
const alphaDepth = (this.alpha) ? Bitmap.depth(this.alphaFormat) : 32;
const depth = Math.min(colorDepth, alphaDepth);
const alignment = this.bm4 ? 8 : 32;
const multiple = (depth < alignment) ? (alignment / depth) : 1;
const overflow = width & (multiple - 1);
if (overflow)
width += multiple - overflow;
return width;
}
rotate90(buffer, width, height) {
let result = new Uint8Array(height * width * 4);
let v = 0;
while (v < height) {
let u = 0;
while (u < width) {
let x = height - 1 - v;
let y = u;
let src = ((v * width) + u) * 4;
let dst = ((y * height) + x) * 4;
result[dst++] = buffer[src++];
result[dst++] = buffer[src++];
result[dst++] = buffer[src++];
result[dst++] = buffer[src++];
u++;
}
v++;
}
return result;
}
rotate180(buffer, width, height) {
let result = new Uint8Array(height * width * 4);
let v = 0;
while (v < height) {
let u = 0;
while (u < width) {
let x = width - 1 - u;
let y = height - 1 - v;
let src = ((v * width) + u) * 4;
let dst = ((y * width) + x) * 4;
result[dst++] = buffer[src++];
result[dst++] = buffer[src++];
result[dst++] = buffer[src++];
result[dst++] = buffer[src++];
u++;
}
v++;
}
return result;
}
rotate270(buffer, width, height) {
let result = new Uint8Array(height * width * 4);
let v = 0;
while (v < height) {
let u = 0;
while (u < width) {
let x = v;
let y = width - 1 - u;
let src = ((v * width) + u) * 4;
let dst = ((y * height) + x) * 4;
result[dst++] = buffer[src++];
result[dst++] = buffer[src++];
result[dst++] = buffer[src++];
result[dst++] = buffer[src++];
u++;
}
v++;
}
return result;
}
transferLine(png, buffer, offset) {
let pngLine = png.read();
let pngOffset = 0;
let pngX = 0;
let pngWidth = png.width;
if (png.channels == 1) {
let palette = png.palette;
while (pngX < pngWidth) {
let index = 4 * pngLine[pngOffset++];
buffer[offset++] = palette[index++];
buffer[offset++] = palette[index++];
buffer[offset++] = palette[index++];
buffer[offset++] = palette[index++];
pngX++;
}
}
else if (png.channels == 2) {
while (pngX < pngWidth) {
let gray = pngLine[pngOffset++];
buffer[offset++] = gray;
buffer[offset++] = gray;
buffer[offset++] = gray;
buffer[offset++] = pngLine[pngOffset++];
pngX++;
}
}
else if (png.channels == 3) {
while (pngX < pngWidth) {
buffer[offset++] = pngLine[pngOffset++];
buffer[offset++] = pngLine[pngOffset++];
buffer[offset++] = pngLine[pngOffset++];
buffer[offset++] = 255;
pngX++;
}
}
else {
while (pngX < pngWidth) {
buffer[offset++] = pngLine[pngOffset++];
buffer[offset++] = pngLine[pngOffset++];
buffer[offset++] = pngLine[pngOffset++];
buffer[offset++] = pngLine[pngOffset++];
pngX++;
}
}
}
run() {
let inputPaths = this.inputPaths;
let c = inputPaths.length;
let width, height;
let buffer;
let offset = 0;
let y = 0;
if (c == 1) {
let pngPath = inputPaths[0];
let png = new PNG(this.readFileBuffer(pngPath));
if (!this.checkPNG(png))
throw new Error("'" + pngPath + "': invalid PNG format!");
let pngWidth = png.width;
let pngHeight = png.height;
if ((this.rotation == 0) || (this.rotation == 180)) {
width = this.pad(pngWidth);
height = pngHeight;
}
else {
width = pngWidth;
height = this.pad(pngHeight);
}
buffer = new Uint8Array(width * height * 4);
while (y < pngHeight) {
let x = 0;
this.transferLine(png, buffer, offset);
offset += (pngWidth * 4);
x += pngWidth;
while (x < width) {
buffer[offset++] = 0;
buffer[offset++] = 0;
buffer[offset++] = 0;
buffer[offset++] = 255;
x++;
}
y++;
}
}
else {
let pngs = new Array(c).fill();
let pngWidth, pngHeight;
for (var i = 0; i < c; i++) {
let pngPath = inputPaths[i];
let png = new PNG(this.readFileBuffer(pngPath));
if (!this.checkPNG(png))
throw new Error("'" + pngPath + "': invalid PNG format!");
if (i == 0) {
pngWidth = png.width;
pngHeight = png.height;
}
else {
if (pngWidth != png.width)
throw new Error("'" + pngPath + "': invalid width!");
if (pngHeight != png.height)
throw new Error("'" + pngPath + "': invalid height!");
}
pngs[i] = png;
}
let stripWidth = pngWidth * i;
if ((this.rotation == 0) || (this.rotation == 180)) {
width = this.pad(stripWidth);
height = pngHeight;
}
else {
width = stripWidth;
height = this.pad(pngHeight);
}
buffer = new Uint8Array(width * height * 4);
while (y < pngHeight) {
let x = 0;
for (var i = 0; i < c; i++) {
this.transferLine(pngs[i], buffer, offset);
offset += (pngWidth * 4);
x += pngWidth;
}
while (x < width) {
buffer[offset++] = 0;
buffer[offset++] = 0;
buffer[offset++] = 0;
buffer[offset++] = 255;
x++;
}
y++;
}
}
while (y < height) {
let x = 0;
while (x < width) {
buffer[offset++] = 0;
buffer[offset++] = 0;
buffer[offset++] = 0;
buffer[offset++] = 255;
x++;
}
y++;
}
let tmp;
switch (this.rotation) {
case 90:
buffer = this.rotate90(buffer, width, height);
tmp = width;
width = height;
height = tmp;
break;
case 180:
buffer = this.rotate180(buffer, width, height);
break;
case 270:
buffer = this.rotate270(buffer, width, height);
tmp = width;
width = height;
height = tmp;
break;
}
let colorPath, colorOut, colorConvert, colorLine;
let alphaPath, alphaOut, alphaConvert, alphaLine;
let convertRGBA32Line = new Uint8Array(width * 4);
let convertGray256Line = new Uint8Array(width);
if (this.color) {
let parts = this.splitPath(inputPaths[0]);
if (this.name)
parts.name = this.name;
parts.directory = this.outputPath;
parts.name += "-color";
parts.extension = this.bm4 ? ".bm4" : ".bmp";
colorPath = this.joinPath(parts);
let dictionary = {width, height, pixelFormat:this.colorFormat, path:colorPath};
let clut;
if (this.clut)
clut = dictionary.clut = this.readFileBuffer(this.clut);
colorOut = new (this.bm4 ? BM4Out : BMPOut)(dictionary);
colorConvert = new Convert(Bitmap.RGBA32, this.colorFormat, clut);
colorLine = new Uint8Array(colorOut.pixelsToBytes(width));
colorOut.begin(0, 0, width, height);
}
if (this.alpha) {
let parts = this.splitPath(inputPaths[0]);
if (this.name)
parts.name = this.name;
parts.directory = this.outputPath;
parts.name += "-alpha";
parts.extension = this.bm4 ? ".bm4" : ".bmp";
alphaPath = this.joinPath(parts);
let dictionary = {width, height, pixelFormat:this.alphaFormat, path:alphaPath};
alphaOut = new (this.bm4 ? BM4Out : BMPOut)(dictionary);
alphaConvert = new Convert(Bitmap.Gray256, this.alphaFormat);
alphaLine = new Uint8Array(alphaOut.pixelsToBytes(width));
alphaOut.begin(0, 0, width, height);
}
offset = 0;
y = 0;
while (y < height) {
let colorIndex = 0;
let alphaIndex = 0;
while (alphaIndex < width) {
let r = convertRGBA32Line[colorIndex++] = buffer[offset++];
let g = convertRGBA32Line[colorIndex++] = buffer[offset++];
let b = convertRGBA32Line[colorIndex++] = buffer[offset++];
let a = convertRGBA32Line[colorIndex++] = buffer[offset++];
convertGray256Line[alphaIndex++] = 255 - a;
}
if (this.color) {
colorConvert.process(convertRGBA32Line.buffer, colorLine.buffer);
colorOut.send(colorLine.buffer);
}
if (this.alpha) {
alphaConvert.process(convertGray256Line.buffer, alphaLine.buffer);
alphaOut.send(alphaLine.buffer);
}
y++;
}
if (this.color)
colorOut.end();
if (this.alpha)
alphaOut.end();
}
}
class BM4Out extends PixelsOut {
constructor(dictionary) {
super(dictionary);
const pixelFormat = this.pixelFormat;
this.depth = Bitmap.depth(pixelFormat);
if ((Bitmap.Gray16 != pixelFormat) &&
(Bitmap.Gray256 != pixelFormat) &&
(Bitmap.RGB565LE != pixelFormat) &&
(Bitmap.RGB332 != pixelFormat) &&
(Bitmap.CLUT16 != pixelFormat) &&
(Bitmap.Monochrome != pixelFormat))
throw new Error("unsupported BM4 pixel fornat");
this.file = new File(dictionary.path, 1);
}
begin(x, y, width, height) {
const file = this.file;
// header: 'md', version, Commodetto pixel format, width and height (big endian)
file.write( 'm'.charCodeAt(0),
'd'.charCodeAt(0),
0,
this.pixelFormat,
this.width >> 8,
this.width & 0xFF,
this.height >> 8,
this.height & 0xFF);
}
send(pixels, offset = 0, count = pixels.byteLength - offset) {
if ((0 == offset) && (count == pixels.byteLength) && (pixels instanceof ArrayBuffer)) //@@ file.write should support HostBuffer
this.file.write(pixels);
else {
let bytes = new Uint8Array(pixels);
while (count--)
this.file.write(bytes[offset++]);
}
}
end() {
this.file.close();
delete this.file;
}
}