Home Reference Source

scripts/platforms/electron/icogen.js

"use strict";
//adapted from icon-gen code
if (window.haveElectron) {
  let fs = require("fs");
  let path = require("path");
  let pngjsNozlib = require("pngjs-nozlib");
  let png = require("pngjs");

  const REQUIRED_IMAGE_SIZES = [16, 24, 32, 48, 64, 128, 256];

  const DEFAULT_FILE_NAME = 'app';
  const FILE_EXTENSION = '.ico';

  const HEADER_SIZE = 6;

  const DIRECTORY_SIZE = 16;
  const BITMAPINFOHEADER_SIZE = 40;
  const BI_RGB = 0;
  /**
   * Convert a PNG of the byte array to the DIB (Device Independent Bitmap) format.
   * PNG in color RGBA (and more), the coordinate structure is the Top/Left to Bottom/Right.
   * DIB in color BGRA, the coordinate structure is the Bottom/Left to Top/Right.
   * @param {Buffer} src Target image.
   * @param {Number} width The width of the image.
   * @param {Number} height The height of the image.
   * @param {Number} bpp The bit per pixel of the image.
   * @return {Buffer} Converted image
   * @see https://en.wikipedia.org/wiki/BMP_file_format
   */

  const convertPNGtoDIB = (src, width, height, bpp) => {
    const cols = width * bpp;
    const rows = height * cols;
    const rowEnd = rows - cols;
    const dest = Buffer.alloc(src.length);

    for (let row = 0; row < rows; row += cols) {
      for (let col = 0; col < cols; col += bpp) {
        // RGBA: Top/Left -> Bottom/Right
        let pos = row + col;
        const r = src.readUInt8(pos);
        const g = src.readUInt8(pos + 1);
        const b = src.readUInt8(pos + 2);
        const a = src.readUInt8(pos + 3); // BGRA: Right/Left -> Top/Right

        pos = rowEnd - row + col;
        dest.writeUInt8(b, pos);
        dest.writeUInt8(g, pos + 1);
        dest.writeUInt8(r, pos + 2);
        dest.writeUInt8(a, pos + 3);
      }
    }

    return dest;
  };
  /**
   * Create the BITMAPINFOHEADER.
   * @param {Object} png PNG image.
   * @param {Number} compression Compression mode
   * @return {Buffer} BITMAPINFOHEADER data.
   * @see https://msdn.microsoft.com/ja-jp/library/windows/desktop/dd183376%28v=vs.85%29.aspx
   */


  const createBitmapInfoHeader = (png, compression) => {
    const b = Buffer.alloc(BITMAPINFOHEADER_SIZE);
    b.writeUInt32LE(BITMAPINFOHEADER_SIZE, 0); // 4 DWORD biSize

    b.writeInt32LE(png.width, 4); // 4 LONG  biWidth

    b.writeInt32LE(png.height * 2, 8); // 4 LONG  biHeight

    b.writeUInt16LE(1, 12); // 2 WORD  biPlanes

    b.writeUInt16LE(png.bpp * 8, 14); // 2 WORD  biBitCount

    b.writeUInt32LE(compression, 16); // 4 DWORD biCompression

    b.writeUInt32LE(png.data.length, 20); // 4 DWORD biSizeImage

    b.writeInt32LE(0, 24); // 4 LONG  biXPelsPerMeter

    b.writeInt32LE(0, 28); // 4 LONG  biYPelsPerMeter

    b.writeUInt32LE(0, 32); // 4 DWORD biClrUsed

    b.writeUInt32LE(0, 36); // 4 DWORD biClrImportant

    return b;
  };
  /**
   * Create the Icon entry.
   *
   * @param {Object} png    PNG image.
   * @param {Number} offset The offset of directory data from the beginning of the ICO/CUR file
   *
   * @return {Buffer} Directory data.
   *
   * @see https://msdn.microsoft.com/en-us/library/ms997538.aspx
   */


  const createDirectory = (png, offset) => {
    const b = Buffer.alloc(DIRECTORY_SIZE);
    const size = png.data.length + BITMAPINFOHEADER_SIZE;
    const width = 256 <= png.width ? 0 : png.width;
    const height = 256 <= png.height ? 0 : png.height;
    const bpp = png.bpp * 8;
    b.writeUInt8(width, 0); // 1 BYTE  Image width

    b.writeUInt8(height, 1); // 1 BYTE  Image height

    b.writeUInt8(0, 2); // 1 BYTE  Colors

    b.writeUInt8(0, 3); // 1 BYTE  Reserved

    b.writeUInt16LE(1, 4); // 2 WORD  Color planes

    b.writeUInt16LE(bpp, 6); // 2 WORD  Bit per pixel

    b.writeUInt32LE(size, 8); // 4 DWORD Bitmap (DIB) size

    b.writeUInt32LE(offset, 12); // 4 DWORD Offset

    return b;
  };
  /**
   * Create the ICO file header.
   * @param {Number} count Specifies number of images in the file.
   * @return {Buffer} Header data.
   * @see https://msdn.microsoft.com/en-us/library/ms997538.aspx
   */


  const createFileHeader = count => {
    const b = Buffer.alloc(HEADER_SIZE);
    b.writeUInt16LE(0, 0); // 2 WORD Reserved

    b.writeUInt16LE(1, 2); // 2 WORD Type

    b.writeUInt16LE(count, 4); // 2 WORD Image count

    return b;
  };
  /**
   * Check an option properties.
   * @param {Object} options Output destination the path of directory.
   * @param {String} options.name Name of an output file.
   * @param {Number[]} options.sizes Structure of an image sizes.
   * @returns {Object} Checked options.
   */


  const checkOptions = options => {
    if (options) {
      return {
        name: typeof options.name === 'string' && options.name !== '' ? options.name : DEFAULT_FILE_NAME,
        sizes: Array.isArray(options.sizes) ? options.sizes : REQUIRED_IMAGE_SIZES
      };
    } else {
      return {
        name: DEFAULT_FILE_NAME,
        sizes: REQUIRED_IMAGE_SIZES
      };
    }
  };
  /**
   * Get the size of the required PNG.
   * @return {Number[]} Sizes.
   */


  const GetRequiredICOImageSizes = () => {
    return REQUIRED_IMAGE_SIZES;
  };
  /**
   * Generate the ICO file from a PNG images.
   * @param {ImageInfo[]} images File informations..
   * @param {String} dir Output destination the path of directory.
   * @param {Object} options Options.
   * @param {String} options.name Name of an output file.
   * @param {Number} options.sizes Structure of an image sizes.
   * @param {Logger} logger Logger.
   * @return {Promise} Promise object.
   */


  let stream = require("stream");

  class WriteStream extends stream.Writable {
    constructor() {
      super();
      this.data = [];
    }

    _write(chunk, encoding, cb) {
      let buf = chunk;

      if (!(buf instanceof Buffer)) {
        Buffer.from(chunk, encoding);
      }

      for (let i = 0; i < buf.length; i++) {
        this.data.push(buf[i]);
      }

      cb(null);
    }

    end() {
      this.data = Buffer.from(this.data);
      super.end();
    }
  }

  exports.GetRequiredICOImageSizes = GetRequiredICOImageSizes;

  const GenerateICO = (images, logger = console) => {
    logger.log('ICO:');

    const stream = new WriteStream();
    stream.write(createFileHeader(images.length), 'binary');

    let pngs = [];
    for (let image of images) {
      pngs.push(pngjsNozlib.PNG.sync.read(image));
    }

    let offset = HEADER_SIZE + DIRECTORY_SIZE * images.length;
    pngs.forEach(png => {
      const directory = createDirectory(png, offset);
      stream.write(directory, 'binary');
      offset += png.data.length + BITMAPINFOHEADER_SIZE;
    });
    pngs.forEach(png => {
      const header = createBitmapInfoHeader(png, BI_RGB);
      stream.write(header, 'binary');
      const dib = convertPNGtoDIB(png.data, png.width, png.height, png.bpp);
      stream.write(dib, 'binary');
    });
    stream.end();

    return stream.data;
    //logger.log('  Create: ' + dest);
    //resolve(dest);
  };

  exports.GenerateICO = GenerateICO;
  let _default = GenerateICO;
  exports.default = _default;
}