import Color from 'color' ; import ColorName from 'color-name' ; import { ImageEdits , ImageFitTypes , ImageFormatTypes } from './lib' ; export class ThumborMapper { private static readonly EMPTY_IMAGE_EDITS : ImageEdits = { } ; param path returns public mapPathToEdits ( path ) { const fileFormat = path . substr ( path . lastIndexOf ( '.' ) + 1 ) as ImageFormatTypes ; let edits = this . mergeEdits ( this . mapCrop ( path ) , this . mapResize ( path ) , this . mapFitIn ( path ) ) ; const filters = path . match ( /filters:[^)]+/g ) ?. map ( filter => `${ filter })` ) . sort ( ) ?? [ ] ; for ( const filter of filters ) { edits = this . mapFilter ( filter , fileFormat , edits ) ; } return edits ; } param path returns public parseCustomPath ( path ) { const { REWRITE_MATCH_PATTERN , REWRITE_SUBSTITUTION } = process . env ; if ( path === undefined ) { throw new Error ( 'ThumborMapping::ParseCustomPath::PathUndefined' ) ; } else if ( REWRITE_MATCH_PATTERN === undefined ) { throw new Error ( 'ThumborMapping::ParseCustomPath::RewriteMatchPatternUndefined' ) ; } else if ( REWRITE_SUBSTITUTION === undefined ) { throw new Error ( 'ThumborMapping::ParseCustomPath::RewriteSubstitutionUndefined' ) ; } else { let parsedPath = '' ; if ( typeof REWRITE_MATCH_PATTERN === 'string' ) { const patternStrings = REWRITE_MATCH_PATTERN . split ( '/' ) ; const flags = patternStrings . pop ( ) ; const parsedPatternString = REWRITE_MATCH_PATTERN . slice ( 1 , REWRITE_MATCH_PATTERN . length - 1 - flags . length ) ; const regExp = new RegExp ( parsedPatternString , flags ) ; parsedPath = path . replace ( regExp , REWRITE_SUBSTITUTION ) ; } else { parsedPath = path . replace ( REWRITE_MATCH_PATTERN , REWRITE_SUBSTITUTION ) ; } return parsedPath ; } } param filterExpression param fileFormat param previousEdits returns public mapFilter ( filterExpression , fileFormat , previousEdits = { } ) { const matched = filterExpression . match ( /:(.+)\((.*)\)/ ) ; const [ _ , filterName , filterValue ] = matched ; const currentEdits = { ... previousEdits } ; switch ( filterName ) { case 'autojpg' : { currentEdits . toFormat = ImageFormatTypes . JPEG ; break ; } case 'background_color' : { const color = ! ColorName [ filterValue ] ? `#${ filterValue }` : filterValue ; currentEdits . flatten = { background : Color ( color ) . object ( ) } ; break ; } case 'blur' : { const [ radius , sigma ] = filterValue . split ( ',' ) . map ( x => ( x === '' ? NaN : Number ( x ) ) ) ; currentEdits . blur = ! isNaN ( sigma ) ? sigma : radius / 2 ; break ; } case 'convolution' : { const values = filterValue . split ( ',' ) ; const matrix = values [ 0 ] . split ( ';' ) . map ( str => Number ( str ) ) ; const matrixWidth = Number ( values [ 1 ] ) ; let matrixHeight = 0 ; let counter = 0 ; for ( let i = 0 ; i < matrix . length ; i ++ ) { if ( counter === matrixWidth - 1 ) { matrixHeight ++ ; counter = 0 ; } else { counter ++ ; } } currentEdits . convolve = { width : matrixWidth , height : matrixHeight , kernel : matrix } ; break ; } case 'equalize' : { currentEdits . normalize = true ; break ; } case 'fill' : { if ( currentEdits . resize === undefined ) { currentEdits . resize = { } ; } let color = filterValue ; if ( ! ColorName [ color ] ) { color = `#${ color }` ; } currentEdits . resize . fit = ImageFitTypes . CONTAIN ; currentEdits . resize . background = Color ( color ) . object ( ) ; break ; } case 'format' : { const imageFormatType = filterValue . replace ( /[^0-9a-z]/gi , '' ) . replace ( /jpg/i , 'jpeg' ) as ImageFormatTypes ; const acceptedValues = [ ImageFormatTypes . HEIC , ImageFormatTypes . HEIF , ImageFormatTypes . JPEG , ImageFormatTypes . PNG , ImageFormatTypes . RAW , ImageFormatTypes . TIFF , ImageFormatTypes . WEBP ] ; if ( acceptedValues . includes ( imageFormatType ) ) { currentEdits . toFormat = imageFormatType ; } break ; } case 'grayscale' : { currentEdits . grayscale = true ; break ; } case 'no_upscale' : { if ( currentEdits . resize === undefined ) { currentEdits . resize = { } ; } currentEdits . resize . withoutEnlargement = true ; break ; } case 'proportion' : { if ( currentEdits . resize === undefined ) { currentEdits . resize = { } ; } const ratio = Number ( filterValue ) ; currentEdits . resize . width = Number ( currentEdits . resize . width * ratio ) ; currentEdits . resize . height = Number ( currentEdits . resize . height * ratio ) ; break ; } case 'quality' : { const toSupportedImageFormatType = ( format ) : ImageFormatTypes => [ ImageFormatTypes . JPG , ImageFormatTypes . JPEG ] . includes ( format ) ? ImageFormatTypes . JPEG : [ ImageFormatTypes . PNG , ImageFormatTypes . WEBP , ImageFormatTypes . TIFF , ImageFormatTypes . HEIF ] . includes ( format ) ? format : null ; const targetImageFileFormat = toSupportedImageFormatType ( fileFormat ) ?? toSupportedImageFormatType ( currentEdits . toFormat ) ; if ( targetImageFileFormat ) { currentEdits [ targetImageFileFormat ] = { quality : Number ( filterValue ) } ; } break ; } case 'rgb' : { const percentages = filterValue . split ( ',' ) ; const values = percentages . map ( percentage => 255 * ( Number ( percentage ) / 100 ) ) ; const [ r , g , b ] = values ; currentEdits . tint = { r , g , b } ; break ; } case 'rotate' : { currentEdits . rotate = Number ( filterValue ) ; break ; } case 'sharpen' : { const values = filterValue . split ( ',' ) ; currentEdits . sharpen = 1 + Number ( values [ 1 ] ) / 2 ; break ; } case 'stretch' : { if ( currentEdits . resize === undefined ) { currentEdits . resize = { } ; } if ( currentEdits . resize . fit !== ImageFitTypes . INSIDE ) { currentEdits . resize . fit = ImageFitTypes . FILL ; } break ; } case 'strip_exif' : case 'strip_icc' : { currentEdits . rotate = null ; break ; } case 'upscale' : { if ( currentEdits . resize === undefined ) { currentEdits . resize = { } ; } currentEdits . resize . fit = ImageFitTypes . INSIDE ; break ; } case 'watermark' : { const options = filterValue . replace ( /\s+/g , '' ) . split ( ',' ) ; const [ bucket , key , xPos , yPos , alpha , wRatio , hRatio ] = options ; currentEdits . overlayWith = { bucket , key , alpha , wRatio , hRatio , options : { } } ; const allowedPosPattern = /^(100|[1-9]?[0-9]|-(100|[1-9][0-9]?))p$/ ; if ( allowedPosPattern . test ( xPos ) || ! isNaN ( Number ( xPos ) ) ) { currentEdits . overlayWith . options . left = xPos ; } if ( allowedPosPattern . test ( yPos ) || ! isNaN ( Number ( yPos ) ) ) { currentEdits . overlayWith . options . top = yPos ; } break ; } } return currentEdits ; } param path returns private mapCrop ( path ) { const pathCropMatchResult = path . match ( /\d+x\d+:\d+x\d+/g ) ; if ( pathCropMatchResult ) { const [ leftTopPoint , rightBottomPoint ] = pathCropMatchResult [ 0 ] . split ( ':' ) ; const [ leftTopX , leftTopY ] = leftTopPoint . split ( 'x' ) . map ( x => parseInt ( x , 10 ) ) ; const [ rightBottomX , rightBottomY ] = rightBottomPoint . split ( 'x' ) . map ( x => parseInt ( x , 10 ) ) ; if ( ! isNaN ( leftTopX ) && ! isNaN ( leftTopY ) && ! isNaN ( rightBottomX ) && ! isNaN ( rightBottomY ) ) { const cropEdit = { crop : { left : leftTopX , top : leftTopY , width : rightBottomX - leftTopX , height : rightBottomY - leftTopY } } ; return cropEdit ; } } return ThumborMapper . EMPTY_IMAGE_EDITS ; } param path returns private mapResize ( path ) { const dimensionsMatchResult = path . match ( /\/((\d+x\d+)|(0x\d+))\//g ) ; if ( dimensionsMatchResult ) { const [ width , height ] = dimensionsMatchResult [ 0 ] . replace ( /\//g , '' ) . split ( 'x' ) . map ( x => parseInt ( x ) ) ; if ( ! isNaN ( width ) && ! isNaN ( height ) ) { const resizeEdit = { resize : { } } ; if ( width === 0 || height === 0 ) { resizeEdit . resize . fit = ImageFitTypes . INSIDE ; } resizeEdit . resize . width = width === 0 ? null : width ; resizeEdit . resize . height = height === 0 ? null : height ; return resizeEdit ; } } return ThumborMapper . EMPTY_IMAGE_EDITS ; } param path returns private mapFitIn ( path ) { return path . includes ( 'fit-in' ) ? { resize : { fit : ImageFitTypes . INSIDE } } : ThumborMapper . EMPTY_IMAGE_EDITS ; } param edits returns private mergeEdits ( ... edits ) { return edits . reduce ( ( result , current ) => { Object . keys ( current ) . forEach ( key => { if ( Array . isArray ( result [ key ] ) && Array . isArray ( current [ key ] ) ) { result [ key ] = Array . from ( new Set ( result [ key ] . concat ( current [ key ] ) ) ) ; } else if ( this . isObject ( result [ key ] ) && this . isObject ( current [ key ] ) ) { result [ key ] = this . mergeEdits ( result [ key ] , current [ key ] ) ; } else { result [ key ] = current [ key ] ; } } ) ; return result ; } , { } ) as ImageEdits ; } param obj returns private isObject ( obj ) { return obj && typeof obj === 'object' && ! Array . isArray ( obj ) ; } }