import fs from 'fs' ; import path from 'path' ; import util from 'util' ; import { PNG } from 'pngjs' ; import { packICNS } from './rle' ; import { ImageInfo , filterImagesBySizes } from './png' ; import Logger from './logger' ; const readFileAsync = util . promisify ( fs . readFile ) ; const writeFileASync = util . promisify ( fs . writeFile ) ; /** Information of pack bit. */ type PackBitBody = { /** Colors of compressed by ICNS RLE. */ colors : number [ ] ; /** Masks of alpha color. */ masks : number [ ] ; } ; /** Icon information in ICNS. */ type IconInfo = { type : string ; size : number ; mask ? : string ; } ; /** Options of ICNS. */ export type ICNSOptions = { /** Name of an output file. */ name ? : string ; /** Structure of an image sizes. */ sizes ? : number [ ] ; } ; type { Array } export const REQUIRED_IMAGE_SIZES = [ 16 , 32 , 64 , 128 , 256 , 512 , 1024 ] ; type { Number } const HEADER_SIZE = 8 ; type { Number } const FILE_HEADER_ID = 'icns' ; type { String } const DEFAULT_FILE_NAME = 'app' ; type { String } const FILE_EXTENSION = '.icns' ; /**
 * Information of the images, Mac OS 8.x (il32, is32, l8mk, s8mk) is unsupported.
 * If icp4, icp5, icp6 is present, Icon will not be supported because it can not be set as Folder of Finder.
 */ const ICON_INFOS = [ { type : 'ic07' , size : 128 } , { type : 'ic08' , size : 256 } , { type : 'ic09' , size : 512 } , { type : 'ic10' , size : 1024 } , { type : 'ic11' , size : 32 } , { type : 'ic12' , size : 64 } , { type : 'ic13' , size : 256 } , { type : 'ic14' , size : 512 } , { type : 'is32' , mask : 's8mk' , size : 16 } , { type : 'il32' , mask : 'l8mk' , size : 32 } ] ; param size param images return const imageFromIconSize = ( size , images ) : ImageInfo | null => { for ( const image of images ) { if ( image . size === size ) { return image ; } } return null ; } ; param fileSize return const createFileHeader = ( fileSize ) : Buffer => { const buffer = Buffer . alloc ( HEADER_SIZE ) ; buffer . write ( FILE_HEADER_ID , 0 , 'ascii' ) ; buffer . writeUInt32BE ( fileSize , 4 ) ; return buffer ; } ; param type param imageSize return const createIconHeader = ( type , imageSize ) : Buffer => { const buffer = Buffer . alloc ( HEADER_SIZE ) ; buffer . write ( type , 0 , 'ascii' ) ; buffer . writeUInt32BE ( HEADER_SIZE + imageSize , 4 ) ; return buffer ; } ; param image return const createIconBlockPackBitsBodies = ( image ) : PackBitBody => { const png = PNG . sync . read ( image ) ; const results = { colors : [ ] , masks : [ ] } ; const r = [ ] ; const g = [ ] ; const b = [ ] ; for ( let i = 0 , max = png . data . length ; i < max ; i += 4 ) { r . push ( png . data . readUInt8 ( i ) ) ; g . push ( png . data . readUInt8 ( i + 1 ) ) ; b . push ( png . data . readUInt8 ( i + 2 ) ) ; results . masks . push ( png . data . readUInt8 ( i + 3 ) ) ; } results . colors = results . colors . concat ( packICNS ( r ) ) ; results . colors = results . colors . concat ( packICNS ( g ) ) ; results . colors = results . colors . concat ( packICNS ( b ) ) ; return results ; } ; param type param image return const createIconBlockData = ( type , image ) : Buffer => { const header = createIconHeader ( type , image . length ) ; return Buffer . concat ( [ header , image ] , header . length + image . length ) ; } ; param type param mask param image return const createIconBlockPackBits = ( type , mask , image ) : Buffer => { const bodies = createIconBlockPackBitsBodies ( image ) ; const colorBlock = createIconBlockData ( type , Buffer . from ( bodies . colors ) ) ; const maskBlock = createIconBlockData ( mask , Buffer . from ( bodies . masks ) ) ; return Buffer . concat ( [ colorBlock , maskBlock ] , colorBlock . length + maskBlock . length ) ; } ; param info param filePath return const createIconBlock = async ( info , filePath ) : Promise < Buffer > => { const image = await readFileAsync ( filePath ) ; switch ( info . type ) { case 'is32' : case 'il32' : return createIconBlockPackBits ( info . type , info . mask || '' , image ) ; default : return createIconBlockData ( info . type , image ) ; } } ; param images returns const createFileBody = async ( images ) : Promise < Buffer > => { let body = Buffer . alloc ( 0 ) ; for ( const info of ICON_INFOS ) { const image = imageFromIconSize ( info . size , images ) ; if ( ! image ) { continue ; } const block = await createIconBlock ( info , image . filePath ) ; body = Buffer . concat ( [ body , block ] , body . length + block . length ) ; } return body ; } ; param images param filePath return const createIconFile = async ( images , filePath ) : Promise < void > => { const body = await createFileBody ( images ) ; if ( body . length === 0 ) { throw new Error ( 'Failed to create the body of the file. The size is `0`.' ) ; } return new Promise ( ( resolve , reject ) => { const stream = fs . createWriteStream ( filePath ) ; stream . on ( 'ready' , ( ) => { stream . write ( createFileHeader ( body . length + HEADER_SIZE ) , 'binary' ) ; stream . write ( body , 'binary' ) ; stream . end ( ) ; } ) ; stream . on ( 'error' , ( err ) => reject ( err ) ) ; stream . on ( 'finish' , ( ) => resolve ( ) ) ; } ) ; } ; param src param dest return export const debugUnpackIconBlocks = async ( src , dest ) : Promise < void > => { const data = await readFileAsync ( src ) ; for ( let pos = HEADER_SIZE , max = data . length ; pos < max ; ) { const header = data . slice ( pos , pos + HEADER_SIZE ) ; const type = header . toString ( 'ascii' , 0 , 4 ) ; const size = header . readUInt32BE ( 4 ) - HEADER_SIZE ; pos += HEADER_SIZE ; const body = data . slice ( pos , pos + size ) ; await writeFileASync ( path . join ( dest , `${ type }.header` ) , header , 'binary' ) ; await writeFileASync ( path . join ( dest , `${ type }.body` ) , body , 'binary' ) ; pos += size ; } } ; param images param dir param logger param options return const generateICNS = async ( images , dir , logger , options ) : Promise < string > => { logger . log ( 'ICNS:' ) ; 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 targets = filterImagesBySizes ( images , opt . sizes ) ; await createIconFile ( targets , dest ) ; } catch ( err ) { fs . unlinkSync ( dest ) ; throw err ; } logger . log ( '  Create: ' + dest ) ; return dest ; } ; export default generateICNS ;