import path from 'path' ; import Debug from 'debug' ; import fs from 'fs-extra' ; import imageSize from 'probe-image-size' ; import Vibrant from 'node-vibrant' ; import sharp from 'sharp' ; import termimg from 'term-img' ; import Table from 'cli-table3' ; import chalk from 'chalk' ; import { Palette } from '@vibrant/color' ; import { OptionDefinition } from 'command-line-args' ; import { locateFiles } from './helpers' ; export { loadSVG , parseColor } from './helpers' ; const debug = Debug ( 'sqip' ) ; const mainKeys = [ 'originalWidth' , 'originalHeight' , 'width' , 'height' , 'type' ] ; const PALETTE_KEYS = [ 'Vibrant' , 'DarkVibrant' , 'LightVibrant' , 'Muted' , 'DarkMuted' , 'LightMuted' ] ; export interface SqipResult { content : Buffer ; metadata : SqipImageMetadata ; } export interface SqipCliOptionDefinition extends OptionDefinition { description ? : string ; required ? : boolean ; default ? : boolean ; } export interface PluginOptions { [ key ] : unknown ; } interface PluginResolver { name : string ; options ? : PluginOptions ; } interface SqipOptions { input : string | Buffer ; outputFileName ? : string ; output ? : string ; silent ? : boolean ; parseableOutput ? : boolean ; plugins ? : PluginType [ ] ; width ? : number ; } interface SqipConfig { input : string | Buffer ; outputFileName ? : string ; output ? : string ; silent ? : boolean ; parseableOutput ? : boolean ; plugins : PluginType [ ] ; width ? : number ; } interface ProcessFileOptions { buffer : Buffer ; outputFileName : string ; config : SqipConfig ; } export interface SqipImageMetadata { originalWidth : number ; originalHeight : number ; palette : Palette ; height : number ; width : number ; type : 'unknown' | 'pixel' | 'svg' ; [ key ] : unknown ; } type PluginType = PluginResolver | string ; export interface SqipPluginOptions { pluginOptions : PluginOptions ; options : PluginOptions ; sqipConfig : SqipConfig ; } interface SqipPluginInterface { sqipConfig : SqipConfig ; apply ( imageBuffer , metadata ? ) : Promise < Buffer > | Buffer ; } export class SqipPlugin implements SqipPluginInterface { public sqipConfig : SqipConfig ; public options : PluginOptions ; static cliOptions : SqipCliOptionDefinition [ ] ; constructor ( options ) { const { sqipConfig } = options ; this . sqipConfig = sqipConfig || { } ; this . options = { } ; } apply ( imageBuffer , metadata ) { console . log ( metadata ) ; return imageBuffer ; } } export async function resolvePlugins ( plugins ) { return Promise . all ( plugins . map ( async ( plugin ) => { if ( typeof plugin === 'string' ) { plugin = { name : plugin } ; } const { name } = plugin ; if ( ! name ) { throw new Error ( `Unable to read plugin name from:\n${ JSON . stringify ( plugin , null , 2 ) }` ) ; } const moduleName = name . indexOf ( 'sqip-plugin-' ) !== - 1 ? name : `sqip-plugin-${ name }` ; try { debug ( `Loading ${ moduleName }` ) ; const Plugin = await import ( moduleName ) ; return { ... plugin , Plugin : Plugin . default } ; } catch ( err ) { if ( err . code === 'MODULE_NOT_FOUND' ) { throw new Error ( `Unable to load plugin "${ moduleName }". Try installing it via:\n\n npm install ${ moduleName }` ) ; } throw err ; } } ) ) ; } async function processFile ( { buffer , outputFileName , config } ) { const { output , silent , parseableOutput } = config ; const result = await processImage ( { buffer , config } ) ; const { content , metadata } = result ; let outputPath ; debug ( `Processed ${ outputFileName }` ) ; if ( output ) { try { const stats = await fs . stat ( output ) ; if ( ! stats . isDirectory ( ) ) { throw new Error ( `File ${ output } already exists. Overwriting is not yet supported.` ) ; } outputPath = path . resolve ( output , `${ outputFileName }.svg` ) ; } catch ( err ) { outputPath = output ; } debug ( `Writing ${ outputPath }` ) ; await fs . writeFile ( outputPath , content ) ; } if ( ! silent ) { if ( outputPath ) { console . log ( `Stored at: ${ outputPath }` ) ; } if ( ! parseableOutput ) { const preview = await sharp ( Buffer . from ( content ) ) . png ( ) . toBuffer ( ) ; try { termimg ( preview , { fallback : ( ) => { if ( metadata . type === 'svg' ) { console . log ( content . toString ( ) ) ; return ; } console . log ( `Unable to render a preview for ${ metadata . type } files on this machine. Try using https://iterm2.com/` ) ; } } ) ; } catch ( err ) { if ( err . name !== 'UnsupportedTerminalError' ) { throw err ; } } } const tableConfig = parseableOutput ? { chars : { top : '' , 'top-mid' : '' , 'top-left' : '' , 'top-right' : '' , bottom : '' , 'bottom-mid' : '' , 'bottom-left' : '' , 'bottom-right' : '' , left : '' , 'left-mid' : '' , mid : '' , 'mid-mid' : '' , right : '' , 'right-mid' : '' , middle : ' ' } , style : { 'padding-left' : 0 , 'padding-right' : 0 } } : undefined ; const mainTable = new Table ( tableConfig ) ; mainTable . push ( mainKeys ) ; mainTable . push ( mainKeys . map ( ( key ) => String ( metadata [ key ] ) || 'can not display' ) ) ; console . log ( mainTable . toString ( ) ) ; const paletteTable = new Table ( tableConfig ) ; paletteTable . push ( PALETTE_KEYS ) ; paletteTable . push ( PALETTE_KEYS . map ( ( key ) => metadata . palette [ key ] ?. hex ) . filter < string > ( ( hex ) : hex is string => typeof hex === 'string' ) . map ( ( hex ) => chalk . hex ( hex ) ( hex ) ) ) ; console . log ( paletteTable . toString ( ) ) ; Object . keys ( metadata ) . filter ( ( key ) => ! [ ... mainKeys , 'palette' ] . includes ( key ) ) . forEach ( ( key ) => { console . log ( chalk . bold ( `${ key }:` ) ) ; console . log ( metadata [ key ] ) ; } ) ; } return result ; } interface ProcessImageOptions { buffer : Buffer ; config : SqipConfig ; } async function processImage ( { buffer , config } ) { const originalSizes = imageSize . sync ( buffer ) ; const vibrant = Vibrant . from ( buffer ) ; const palette = await vibrant . quality ( 0 ) . getPalette ( ) ; if ( ! originalSizes ) { throw new Error ( 'Unable to get image size' ) ; } const metadata = { originalWidth : originalSizes . width , originalHeight : originalSizes . height , palette , type : 'unknown' , width : 0 , height : 0 } ; const plugins = await resolvePlugins ( config . plugins ) ; if ( config . width && config . width > 0 ) { try { buffer = await sharp ( buffer ) . resize ( config . width ) . toBuffer ( ) ; const resizedMetadata = await sharp ( buffer ) . metadata ( ) ; metadata . width = resizedMetadata . width || 0 ; metadata . height = resizedMetadata . height || 0 ; } catch ( err ) { throw new Error ( 'Unable to resize' ) ; } } else { metadata . width = originalSizes . width ; metadata . height = originalSizes . height ; } for ( const { name , options : pluginOptions , Plugin } of plugins ) { debug ( `Construct ${ name }` ) ; const plugin = new Plugin ( { sqipConfig : config , pluginOptions : pluginOptions || { } , options : { } } ) ; debug ( `Apply ${ name }` ) ; buffer = await plugin . apply ( buffer , metadata ) ; } return { content : buffer , metadata } ; } export async function sqip ( options ) { const defaultOptions = { plugins : [ { name : 'primitive' , options : { numberOfPrimitives : 8 , mode : 0 } } , 'blur' , 'svgo' , 'data-uri' ] , width : 300 , parseableOutput : false , silent : true } ; const config = Object . assign ( { } , defaultOptions , options ) ; const { input , outputFileName , parseableOutput , silent } = config ; if ( parseableOutput ) { chalk . level = 0 ; } if ( ! input || input . length === 0 ) { throw new Error ( 'Please provide an input image, e.g. sqip({ input: "input.jpg" })' ) ; } if ( Buffer . isBuffer ( input ) ) { if ( ! outputFileName ) { throw new Error ( `${ outputFileName } is required when passing image as buffer` ) ; } return processFile ( { buffer : input , outputFileName , config } ) ; } const files = await locateFiles ( input ) ; debug ( 'Found files:' ) ; debug ( files ) ; for ( const file of files ) { try { debug ( 'check file ' + file ) ; await fs . access ( file , fs . constants . R_OK ) ; } catch ( err ) { throw new Error ( `Unable to read file ${ file }` ) ; } } const results = [ ] ; for ( const filePath of files ) { if ( ! silent ) { console . log ( `Processing: ${ filePath }` ) ; } else { debug ( `Processing ${ filePath }` ) ; } const buffer = await fs . readFile ( filePath ) ; const result = await processFile ( { buffer , outputFileName : outputFileName || path . parse ( filePath ) . name , config } ) ; results . push ( result ) ; } debug ( `Finished` ) ; if ( Array . isArray ( input ) || results . length === 0 ) { return results ; } return results [ 0 ] ; } export * from './helpers' ;