import fs from 'fs' ; import path from 'path' ; import { PNG } from 'pngjs' ; import { ImageInfo , filterImagesBySizes } from './png' ; import Logger from './logger' ; /** Options of `generateICO`. */ export type ICOOptions = { /** Name of an output file. */ name ? : string ; /** Structure of an image sizes. */ sizes ? : number [ ] ; } ; /** Sizes required for the ICO file. */ export const REQUIRED_IMAGE_SIZES = [ 16 , 24 , 32 , 48 , 64 , 128 , 256 ] ; /** Default name of ICO file. */ const DEFAULT_FILE_NAME = 'app' ; /** File extension of ICO file. */ const FILE_EXTENSION = '.ico' ; /** Size of the file header. */ const FILE_HEADER_SIZE = 6 ; /** Size of the icon directory. */ const ICO_DIRECTORY_SIZE = 16 ; /** Size of the `BITMAPINFOHEADER`. */ const BITMAPINFOHEADER_SIZE = 40 ; /** Color mode of `BITMAPINFOHEADER`.*/ const BI_RGB = 0 ; /** BPP (Bit Per Pixel) for Alpha PNG (RGB = 4). */ const BPP_ALPHA = 4 ; param src param width param height param bpp return see https 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 ) { 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 ) ; 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 ; } ; param png param compression return see https const createBitmapInfoHeader = ( png , compression ) => { const b = Buffer . alloc ( BITMAPINFOHEADER_SIZE ) ; b . writeUInt32LE ( BITMAPINFOHEADER_SIZE , 0 ) ; b . writeInt32LE ( png . width , 4 ) ; b . writeInt32LE ( png . height * 2 , 8 ) ; b . writeUInt16LE ( 1 , 12 ) ; b . writeUInt16LE ( BPP_ALPHA * 8 , 14 ) ; b . writeUInt32LE ( compression , 16 ) ; b . writeUInt32LE ( png . data . length , 20 ) ; b . writeInt32LE ( 0 , 24 ) ; b . writeInt32LE ( 0 , 28 ) ; b . writeUInt32LE ( 0 , 32 ) ; b . writeUInt32LE ( 0 , 36 ) ; return b ; } ; param png param offset return see https const createDirectory = ( png , offset ) => { const b = Buffer . alloc ( ICO_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 = BPP_ALPHA * 8 ; b . writeUInt8 ( width , 0 ) ; b . writeUInt8 ( height , 1 ) ; b . writeUInt8 ( 0 , 2 ) ; b . writeUInt8 ( 0 , 3 ) ; b . writeUInt16LE ( 1 , 4 ) ; b . writeUInt16LE ( bpp , 6 ) ; b . writeUInt32LE ( size , 8 ) ; b . writeUInt32LE ( offset , 12 ) ; return b ; } ; param count return see https const createFileHeader = ( count ) => { const b = Buffer . alloc ( FILE_HEADER_SIZE ) ; b . writeUInt16LE ( 0 , 0 ) ; b . writeUInt16LE ( 1 , 2 ) ; b . writeUInt16LE ( count , 4 ) ; return b ; } ; param images param sizes returns const readPNGs = ( images , sizes ) : PNG [ ] => { const targets = filterImagesBySizes ( images , sizes ) ; return targets . map ( ( image ) => { const data = fs . readFileSync ( image . filePath ) ; return PNG . sync . read ( data ) ; } ) ; } ; param pngs param stream const writeDirectories = ( pngs , stream ) => { let offset = FILE_HEADER_SIZE + ICO_DIRECTORY_SIZE * pngs . length ; for ( const png of pngs ) { const directory = createDirectory ( png , offset ) ; stream . write ( directory , 'binary' ) ; offset += png . data . length + BITMAPINFOHEADER_SIZE ; } } ; param pngs param stream const writePNGs = ( pngs , stream ) => { for ( const png of pngs ) { const header = createBitmapInfoHeader ( png , BI_RGB ) ; stream . write ( header , 'binary' ) ; const dib = convertPNGtoDIB ( png . data , png . width , png . height , BPP_ALPHA ) ; stream . write ( dib , 'binary' ) ; } } ; param pngs param filePath return const createIconFile = ( pngs , filePath ) : Promise < void > => { return new Promise ( ( resolve , reject ) => { if ( pngs . length === 0 ) { return reject ( new Error ( 'There was no PNG file matching the specified size.' ) ) ; } const stream = fs . createWriteStream ( filePath ) ; stream . on ( 'ready' , ( ) => { stream . write ( createFileHeader ( pngs . length ) , 'binary' ) ; writeDirectories ( pngs , stream ) ; writePNGs ( pngs , stream ) ; stream . end ( ) ; } ) ; stream . on ( 'error' , ( err ) => reject ( err ) ) ; stream . on ( 'finish' , ( ) => resolve ( ) ) ; } ) ; } ; param images param dir param logger param options return const generateICO = async ( images , dir , logger , options ) : Promise < string > => { logger . log ( 'ICO:' ) ; const opt = { name : options . name && options . name !== '' ? options . name : DEFAULT_FILE_NAME , sizes : options . sizes && 0 < options . sizes . length ? options . sizes : REQUIRED_IMAGE_SIZES } ; const dest = path . join ( dir , opt . name + FILE_EXTENSION ) ; try { const pngs = readPNGs ( images , opt . sizes ) ; await createIconFile ( pngs , dest ) ; } catch ( err ) { fs . unlinkSync ( dest ) ; throw err ; } logger . log ( '  Create: ' + dest ) ; return dest ; } ; export default generateICO ;