import fs from 'fs' ; import sharp , { AvailableFormatInfo } from 'sharp' ; import { EventEmitter } from 'events' ; import { Readable } from 'stream' ; import { bufferToStream , streamToBuffer } from './Utilities' ; const DEFAULT_WIDTH = 400 ; export default function Photosaic ( sourceImage , subImages , { gridNum = 10 , intensity = 0.5 , outputType = 'png' , outputWidth = DEFAULT_WIDTH , algo = 'closestColor' , } = { } ) { return { emitter : new EventEmitter ( ) , sourceImage , sourceImageSharp : sharp ( ) , destinationImageSharp : sharp ( ) , sourceWidth : 0 , sourceHeight : 0 , subImages , subImagesList : [ ] , subImagesListCache : [ ] , subImageWidth : 0 , subImageHeight : 0 , setSourceImage ( source ) { return ( this . sourceImage = source ) ; } , setSubImages ( subs ) { return ( this . subImages = subs ) ; } , addSubImage ( sub ) { this . subImages . push ( sub ) ; return this . subImages ; } , imgToStream ( img ) { if ( typeof img === 'string' ) return fs . createReadStream ( img ) ; if ( img instanceof Buffer ) return bufferToStream ( img ) ; return img ; } , async imgToBuffer ( img ) { if ( typeof img === 'string' ) return await fs . promises . readFile ( img ) ; if ( img instanceof Readable ) return await streamToBuffer ( img ) ; return img ; } , async setupSourceImage ( ) { this . sourceImageSharp = sharp ( await this . imgToBuffer ( this . sourceImage ) ) ; const { width } = await this . sourceImageSharp . metadata ( ) ; this . sourceImageSharp = sharp ( await this . sourceImageSharp . rotate ( ) . resize ( { width : outputWidth || width || DEFAULT_WIDTH , } ) . toBuffer ( ) ) ; const newDims = await this . sourceImageSharp . metadata ( ) ; this . sourceWidth = newDims . width || DEFAULT_WIDTH ; this . sourceHeight = newDims . height || DEFAULT_WIDTH ; this . destinationImageSharp = sharp ( { create : { width : this . sourceWidth , height : this . sourceHeight , channels : 4 , background : { r : 255 , g : 255 , b : 255 , alpha : 0 } , } , } ) . png ( ) ; return this . sourceImageSharp ; } , async setupSubImages ( ) { const sourceWidth = outputWidth || this . sourceWidth || 50 ; const newWidth = sourceWidth / ( gridNum || 10 ) ; const whRatio = sourceWidth / ( this . sourceHeight || outputWidth || DEFAULT_WIDTH ) ; this . subImageWidth = Math . floor ( newWidth ) ; this . subImageHeight = Math . floor ( newWidth / whRatio ) ; this . sourceWidth = this . subImageWidth * gridNum ; this . sourceHeight = this . subImageHeight * gridNum ; const allSubImages = await Promise . all ( this . subImages . map ( async ( img ) => { const s1 = sharp ( await this . imgToBuffer ( img ) ) ; const sharpImg = sharp ( await s1 . rotate ( ) . resize ( { width : this . subImageWidth , height : this . subImageHeight , } ) . toBuffer ( ) ) ; return { img : sharpImg , stats : await sharpImg . stats ( ) , } ; } ) ) ; return ( this . subImagesList = sortSubImages ( allSubImages ) ) ; } , async getPieceAvgColor ( x , y , source ? , subImgWidth ? , subImgHeight ? ) { const w = subImgWidth || this . subImageWidth ; const h = subImgHeight || this . subImageHeight ; const piece = await ( source || this . sourceImageSharp ) . clone ( ) . extract ( { left : x * w , top : y * h , width : w , height : h , } ) . toBuffer ( ) ; const { channels : [ r , g , b , a ] , } = await sharp ( piece ) . stats ( ) ; return { r : r . mean , g : g . mean , b : b . mean , a : ( a || { mean : 100 } ) . mean , } ; } , getSubImage ( pieceColor ) { switch ( algo ) { case 'closestColor' : { const getDiff = ( c1 , c2 ) : number => Math . abs ( c1 - c2 ) ; const selectedGrayscale = getGrayscale ( pieceColor . r , pieceColor . g , pieceColor . b ) ; const closestSubImage = binarySubImageSearch ( this . subImagesListCache , selectedGrayscale , 0 , this . subImagesListCache . length - 1 ) ; return closestSubImage ? closestSubImage . img . clone ( ) : this . sourceImageSharp . clone ( ) ; } default : { const randImgInd = Math . floor ( this . subImagesListCache . length * Math . random ( ) ) ; return this . subImagesListCache . splice ( randImgInd , 1 ) [ 0 ] . img . clone ( ) ; } } } , async getSourceImgAvgColors ( ) { let gridColors = [ ] ; let iteration = 0 ; let smallSource = sharp ( await this . sourceImageSharp . clone ( ) . resize ( { width : DEFAULT_WIDTH } ) . toBuffer ( ) ) ; const smallSourceMeta = await smallSource . metadata ( ) ; const subWidth = Math . floor ( DEFAULT_WIDTH / gridNum ) ; const subHeight = Math . floor ( ( smallSourceMeta . height || DEFAULT_WIDTH ) / gridNum ) ; smallSource = sharp ( await smallSource . clone ( ) . resize ( { width : subWidth * gridNum } ) . toBuffer ( ) ) ; for ( let x = 0 ; x < gridNum ; x ++ ) { await Promise . all ( new Array ( gridNum ) . fill ( 0 ) . map ( async ( _ , y ) => { iteration ++ ; this . emitter . emit ( `processing` , iteration ) ; gridColors [ x ] = gridColors [ x ] || [ ] ; gridColors [ x ] [ y ] = await this . getPieceAvgColor ( x , y , smallSource , subWidth , subHeight ) ; } ) ) ; } return gridColors ; } , async build ( ) { await this . setupSourceImage ( ) ; const [ avgMainImgColors ] = await Promise . all ( [ this . getSourceImgAvgColors ( ) , this . setupSubImages ( ) , ] ) ; let compositeSubImgObjects = [ ] ; let iteration = 0 ; this . subImagesListCache = this . subImagesList . slice ( 0 ) ; for ( let x = 0 ; x < gridNum ; x ++ ) { await Promise . all ( new Array ( gridNum ) . fill ( 0 ) . map ( async ( _ , y ) => { iteration ++ ; this . emitter . emit ( `processing` , iteration ) ; const pieceColor = avgMainImgColors [ x ] [ y ] ; const { r , g , b , a } = pieceColor ; if ( ( a || 0 ) < 5 ) return ; if ( this . subImagesListCache . length === 0 ) this . subImagesListCache = this . subImagesList . slice ( 0 ) ; const subImg = this . getSubImage ( pieceColor ) ; const overlayedSubImg = subImg . composite ( [ { input : { create : { width : this . subImageWidth , height : this . subImageHeight , channels : 4 , background : { r , g , b , alpha : intensity } , } , } , } , ] ) ; compositeSubImgObjects . push ( { input : await overlayedSubImg . toBuffer ( ) , left : x * this . subImageWidth , top : y * this . subImageHeight , } ) ; } ) ) ; } while ( compositeSubImgObjects . length > 0 ) { iteration ++ ; this . emitter . emit ( `processing` , iteration ) ; const compositeImages = compositeSubImgObjects . splice ( 0 , gridNum ) ; this . destinationImageSharp = sharp ( await this . destinationImageSharp . composite ( compositeImages ) . toBuffer ( ) ) ; } const buffer = await this . destinationImageSharp . toFormat ( outputType ) . toBuffer ( ) ; this . emitter . emit ( `complete` , buffer ) ; return buffer ; } , } ; } export function getGrayscale ( r , g , b ) { return 0.3 * r + 0.59 * g + 0.11 * b ; } export function binarySubImageSearch ( images , targetGrayscale , s , e ) { const m = Math . floor ( ( s + e ) / 2 ) ; const [ rm , gm , bm ] = images [ m ] . stats . channels ; const [ rs , gs , bs ] = images [ s ] . stats . channels ; const [ re , ge , be ] = images [ e ] . stats . channels ; const midGr = getGrayscale ( rm . mean , gm . mean , bm . mean ) ; const startGr = getGrayscale ( rs . mean , gs . mean , bs . mean ) ; const endGr = getGrayscale ( re . mean , ge . mean , be . mean ) ; if ( images . length === 1 ) return images [ 0 ] ; if ( targetGrayscale == midGr ) return images [ m ] ; if ( e - 1 === s ) return Math . abs ( startGr - targetGrayscale ) > Math . abs ( endGr - targetGrayscale ) ? images [ e ] : images [ s ] ; if ( targetGrayscale > midGr ) return binarySubImageSearch ( images , targetGrayscale , m , e ) ; if ( targetGrayscale < midGr ) return binarySubImageSearch ( images , targetGrayscale , s , m ) ; return images [ m ] || images [ s ] ; } function sortSubImages ( imgs ) { return imgs . sort ( ( i1 , i2 ) => { const [ r1 , g1 , b1 ] = i1 . stats . channels ; const [ r2 , g2 , b2 ] = i2 . stats . channels ; const gr1 = getGrayscale ( r1 . mean , g1 . mean , b1 . mean ) ; const gr2 = getGrayscale ( r2 . mean , g2 . mean , b2 . mean ) ; return gr1 < gr2 ? - 1 : 1 ; } ) ; } PhotosaicImage export type PhotosaicImage = string | Buffer | Readable ; export interface IColor { r : number ; g : number ; b : number ; a : number ; } export interface ISubImage { img : sharp . Sharp ; stats : sharp . Stats ; } export interface IPhotosaicFactory { emitter : EventEmitter ; sourceImage : PhotosaicImage ; sourceImageSharp : sharp . Sharp ; destinationImageSharp : sharp . Sharp ; sourceWidth : number ; sourceHeight : number ; subImages : PhotosaicImage [ ] ; subImagesList : ISubImage [ ] ; subImagesListCache : ISubImage [ ] ; subImageWidth : number ; subImageHeight : number ; setSourceImage ( source ) : PhotosaicImage ; setSubImages ( subs ) : PhotosaicImage [ ] ; addSubImage ( sub ) : PhotosaicImage [ ] ; imgToStream ( img ) : Readable ; imgToBuffer ( img ) : Promise < Buffer > ; setupSourceImage ( ) : Promise < sharp . Sharp > ; setupSubImages ( ) : Promise < ISubImage [ ] > ; getPieceAvgColor ( x , y , source ? , subWidth ? , subHeight ? ) : Promise < IColor > ; getSubImage ( pieceColor ) : sharp . Sharp ; getSourceImgAvgColors ( ) : Promise < IColor [ ] [ ] > ; build ( ) : Promise < Buffer > ; } export interface IPhotosaicOptions { gridNum ? : number ; intensity ? : number ; outputWidth ? : null | number ; outputType ? : AvailableFormatInfo | string ; algo ? : 'random' | 'closestColor' ; }