190 lines
5.9 KiB
JavaScript
190 lines
5.9 KiB
JavaScript
/* eslint-disable no-mixed-operators */
|
|
import { readPNG, resize } from "./lib/png.js";
|
|
|
|
// http://fileformats.wikia.com/wiki/Icon
|
|
// the correct sizes are 256x256, 48x48, 32x32, 16x16
|
|
const sizeList = [48, 32, 16];
|
|
const err = new Error("Please give me a square PNG image.");
|
|
err.code = "ESIZE";
|
|
|
|
export default async function(filepath, options = {}) {
|
|
if (Array.isArray(filepath)) {
|
|
const images = await Promise.all(
|
|
filepath.map(file => readPNG(file)));
|
|
return imagesToIco(images);
|
|
}
|
|
|
|
const png = await readPNG(filepath);
|
|
if (png.width !== png.height) {
|
|
throw err;
|
|
}
|
|
|
|
const image = png.width !== 256
|
|
? resize(png, 256, 256, options.interpolation)
|
|
: png
|
|
;
|
|
|
|
const resizedImages = sizeList.map(
|
|
targetSize => resize(image, targetSize, targetSize, options.interpolation)
|
|
);
|
|
|
|
const images = resizedImages.concat(image);
|
|
return imagesToIco(images);
|
|
}
|
|
|
|
export function imagesToIco(images) {
|
|
const header = getHeader(images.length);
|
|
const headerAndIconDir = [header];
|
|
const imageDataArr = [];
|
|
|
|
let len = header.length;
|
|
let offset = header.length + 16 * images.length;
|
|
|
|
images.forEach(img => {
|
|
const dir = getDir(img, offset);
|
|
const bmpInfoHeader = getBmpInfoHeader(img);
|
|
const dib = getDib(img);
|
|
|
|
len += dir.length + bmpInfoHeader.length + dib.length;
|
|
const newSize = bmpInfoHeader.length + dib.length;
|
|
offset += newSize;
|
|
|
|
dir.writeUInt32LE(newSize, 8);
|
|
headerAndIconDir.push(dir);
|
|
imageDataArr.push(bmpInfoHeader, dib);
|
|
});
|
|
|
|
return Buffer.concat(headerAndIconDir.concat(imageDataArr), len);
|
|
}
|
|
|
|
// https://en.wikipedia.org/wiki/ICO_(file_format)
|
|
function getHeader(numOfImages) {
|
|
const buf = Buffer.alloc(6);
|
|
|
|
buf.writeUInt16LE(0, 0); // Reserved. Must always be 0.
|
|
buf.writeUInt16LE(1, 2); // Specifies image type: 1 for icon (.ICO) image
|
|
buf.writeUInt16LE(numOfImages, 4); // Specifies number of images in the file.
|
|
|
|
return buf;
|
|
}
|
|
|
|
function getDir(img, offset) {
|
|
const buf = Buffer.alloc(16);
|
|
const bitmap = img //.bitmap;
|
|
const width = bitmap.width >= 256 ? 0 : bitmap.width;
|
|
const height = width;
|
|
const bpp = 32;
|
|
|
|
buf.writeUInt8(width, 0); // Specifies image width in pixels.
|
|
buf.writeUInt8(height, 1); // Specifies image height in pixels.
|
|
buf.writeUInt8(0, 2); // Should be 0 if the image does not use a color palette.
|
|
buf.writeUInt8(0, 3); // Reserved. Should be 0.
|
|
buf.writeUInt16LE(1, 4); // Specifies color planes. Should be 0 or 1.
|
|
buf.writeUInt16LE(bpp, 6); // Specifies bits per pixel.
|
|
buf.writeUInt32LE(0, 8); // Specifies the size of the image's data in bytes
|
|
buf.writeUInt32LE(offset, 12); // Specifies the offset of BMP or PNG data from the beginning of the ICO/CUR file
|
|
|
|
return buf;
|
|
}
|
|
|
|
// https://en.wikipedia.org/wiki/BMP_file_format
|
|
function getBmpInfoHeader(img) {
|
|
const buf = Buffer.alloc(40);
|
|
const bitmap = img; //.bitmap;
|
|
const width = bitmap.width;
|
|
// https://en.wikipedia.org/wiki/ICO_(file_format)
|
|
// ...Even if the AND mask is not supplied,
|
|
// if the image is in Windows BMP format,
|
|
// the BMP header must still specify a doubled height.
|
|
const height = width * 2;
|
|
const bpp = 32;
|
|
|
|
buf.writeUInt32LE(40, 0); // The size of this header (40 bytes)
|
|
buf.writeInt32LE(width, 4); // The bitmap width in pixels (signed integer)
|
|
buf.writeInt32LE(height, 8); // The bitmap height in pixels (signed integer)
|
|
buf.writeUInt16LE(1, 12); // The number of color planes (must be 1)
|
|
buf.writeUInt16LE(bpp, 14); // The number of bits per pixel
|
|
buf.writeUInt32LE(0, 16); // The compression method being used.
|
|
buf.writeUInt32LE(0, 20); // The image size.
|
|
buf.writeInt32LE(0, 24); // The horizontal resolution of the image. (signed integer)
|
|
buf.writeInt32LE(0, 28); // The vertical resolution of the image. (signed integer)
|
|
buf.writeUInt32LE(0, 32); // The number of colors in the color palette, or 0 to default to 2n
|
|
buf.writeUInt32LE(0, 36); // The number of important colors used, or 0 when every color is important; generally ignored.
|
|
|
|
return buf;
|
|
}
|
|
|
|
// https://en.wikipedia.org/wiki/BMP_file_format
|
|
// Note that the bitmap data starts with the lower left hand corner of the image.
|
|
// blue green red alpha in order
|
|
function getDib(img) {
|
|
const bitmap = img; //.bitmap;
|
|
const size = bitmap.data.length;
|
|
const width = bitmap.width;
|
|
const height = width;
|
|
const andMapRow = getRowStride(width);
|
|
const andMapSize = andMapRow * height;
|
|
const buf = Buffer.alloc(size + andMapSize);
|
|
// xor map
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const pxColor = getPixelColor(img, x, y);
|
|
|
|
const r = (pxColor >> 24) & 255;
|
|
const g = (pxColor >> 16) & 255;
|
|
const b = (pxColor >> 8) & 255;
|
|
const a = pxColor & 255;
|
|
const newColor = b | (g << 8) | (r << 16) | (a << 24);
|
|
|
|
const pos = ((height - y - 1) * width + x) * 4;
|
|
|
|
buf.writeInt32LE(newColor, pos);
|
|
}
|
|
}
|
|
|
|
// and map. It's padded out to 32 bits per line
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const pxColor = getPixelColor(img, x, y);
|
|
// TODO make threshhold configurable
|
|
const alpha = (pxColor & 255) > 0 ? 0 : 1;
|
|
const bitNum = (height - y - 1) * width + x;
|
|
// width per line in multiples of 32 bits
|
|
const width32 =
|
|
width % 32 === 0 ? Math.floor(width / 32) : Math.floor(width / 32) + 1;
|
|
|
|
const line = Math.floor(bitNum / width);
|
|
const offset = Math.floor(bitNum % width);
|
|
const bitVal = alpha & 0x00000001;
|
|
|
|
const pos = size + line * width32 * 4 + Math.floor(offset / 8);
|
|
const newVal = buf.readUInt8(pos) | (bitVal << (7 - (offset % 8)));
|
|
buf.writeUInt8(newVal, pos);
|
|
}
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
function getRowStride(width) {
|
|
if (width % 32 === 0) {
|
|
return width / 8;
|
|
} else {
|
|
return 4 * (Math.floor(width / 32) + 1);
|
|
}
|
|
}
|
|
|
|
function getPixelColor(png, x, y) {
|
|
let xi = x < 0 ? 0: x;
|
|
let yi = y < 0 ? 0: y;
|
|
|
|
if (x >= png.width) xi = png.width - 1;
|
|
if (y >= png.height) yi = png.height - 1;
|
|
|
|
const i = (xi < 0 || xi >= png.width) || (yi < 0 || yi >= png.height)
|
|
? -1
|
|
: (png.width * yi + xi) << 2
|
|
;
|
|
|
|
return png.data.readUInt32BE(i);
|
|
} |