import { Injectable , Inject , forwardRef } from '@angular/core' ; import { Observable , Subject } from 'rxjs' ; import { ImgExifService } from './img-exif.service' ; const MAX_STEPS = 15 ; declare var self ; @ Injectable ( ) export class ImgMaxSizeService { timeAtStart : number ; initialFile : File ; constructor ( @ Inject ( forwardRef ( ( ) => ImgExifService ) ) private imageExifService ) { } public compressImage ( file , maxSizeInMB , ignoreAlpha = false , logExecutionTime = false ) { let compressedFileSubject = new Subject < any > ( ) ; this . timeAtStart = new Date ( ) . getTime ( ) ; this . initialFile = file ; if ( file . type !== "image/jpeg" && file . type !== "image/png" ) { setTimeout ( ( ) => { compressedFileSubject . error ( { compressedFile : file , reason : "File provided is neither of type jpg nor of type png." , error : "INVALID_EXTENSION" } ) ; } , 0 ) ; return compressedFileSubject . asObservable ( ) ; } let oldFileSize = file . size / 1024 / 1024 ; if ( oldFileSize < maxSizeInMB ) { setTimeout ( ( ) => { compressedFileSubject . next ( file ) ; } , 0 ) ; return compressedFileSubject . asObservable ( ) ; } let cvs = document . createElement ( 'canvas' ) ; let ctx = cvs . getContext ( '2d' ) ; let img = new Image ( ) ; let self = this ; img . onload = ( ) => { this . imageExifService . getOrientedImage ( img ) . then ( orientedImg => { window . URL . revokeObjectURL ( img . src ) ; cvs . width = orientedImg . width ; cvs . height = orientedImg . height ; ctx . drawImage ( orientedImg , 0 , 0 ) ; let imageData = ctx . getImageData ( 0 , 0 , orientedImg . width , orientedImg . height ) ; if ( file . type === "image/png" && this . isImgUsingAlpha ( imageData ) && ! ignoreAlpha ) { compressedFileSubject . error ( { compressedFile : file , reason : "File provided is a png image which uses the alpha channel. No compression possible." , error : "PNG_WITH_ALPHA" } ) ; } ctx = cvs . getContext ( '2d' , { 'alpha' : false } ) ; ctx . drawImage ( orientedImg , 0 , 0 ) ; self . getCompressedFile ( cvs , 50 , maxSizeInMB , 1 ) . then ( ( compressedFile ) => { compressedFileSubject . next ( compressedFile ) ; self . logExecutionTime ( logExecutionTime ) ; } ) . catch ( ( error ) => { compressedFileSubject . error ( error ) ; self . logExecutionTime ( logExecutionTime ) ; } ) ; } ) ; } ; img . src = window . URL . createObjectURL ( file ) ; return compressedFileSubject . asObservable ( ) ; } ; private getCompressedFile ( cvs , quality , maxSizeInMB , currentStep ) { let result = new Promise ( ( resolve , reject ) => { cvs . toBlob ( ( blob ) => { if ( currentStep + 1 > MAX_STEPS ) { reject ( { compressedFile : this . getResultFile ( blob ) , reason : "Could not find the correct compression quality in " + MAX_STEPS + " steps." , error : "MAX_STEPS_EXCEEDED" } ) ; } else { let newQuality = this . getCalculatedQuality ( blob , quality , maxSizeInMB , currentStep ) ; this . checkCompressionStatus ( cvs , blob , quality , maxSizeInMB , currentStep , newQuality ) . then ( result => { resolve ( result ) ; } ) . catch ( result => { reject ( result ) ; } ) ; } } , "image/jpeg" , quality / 100 ) ; } ) ; return result ; } private getResultFile ( blob ) { return this . generateResultFile ( blob , this . initialFile . name , this . initialFile . type , new Date ( ) . getTime ( ) ) ; } private generateResultFile ( blob , name , type , lastModified ) { let resultFile = new Blob ( [ blob ] , { type : type } ) ; return this . blobToFile ( resultFile , name , lastModified ) ; } private blobToFile ( blob , name , lastModified ) { let file = blob ; file . name = name ; file . lastModified = lastModified ; return < File > file ; } private getCalculatedQuality ( blob , quality , maxSizeInMB , currentStep ) { let currentSize = blob . size / 1024 / 1024 ; let ratioMaxSizeToCurrentSize = maxSizeInMB / currentSize ; if ( ratioMaxSizeToCurrentSize > 5 ) { ratioMaxSizeToCurrentSize = 5 ; } let ratioMaxSizeToInitialSize = currentSize / ( this . initialFile . size / 1024 / 1024 ) ; if ( ratioMaxSizeToInitialSize < 0.05 ) { ratioMaxSizeToInitialSize = 0.05 ; } let newQuality = 0 ; let multiplicator = Math . abs ( ratioMaxSizeToInitialSize - 1 ) * 10 / ( currentStep * 1.7 ) / ratioMaxSizeToCurrentSize ; if ( multiplicator < 1 ) { multiplicator = 1 ; } if ( ratioMaxSizeToCurrentSize >= 1 ) { newQuality = quality + ( ratioMaxSizeToCurrentSize - 1 ) * 10 * multiplicator ; } else { newQuality = quality - ( 1 - ratioMaxSizeToCurrentSize ) * 10 * multiplicator ; } if ( newQuality > 100 ) { newQuality = quality + ( 100 - quality ) / 2 ; } if ( newQuality < 0 ) { newQuality = quality - quality / 2 ; } return newQuality ; } private checkCompressionStatus ( cvs , blob , quality , maxSizeInMB , currentStep , newQuality ) { let result = new Promise ( ( resolve , reject ) => { if ( quality === 100 && newQuality >= 100 ) { reject ( { compressedFile : this . initialFile , reason : "Unfortunately there was an error while compressing the file." , error : "FILE_BIGGER_THAN_INITIAL_FILE" } ) ; } else if ( ( quality < 1 ) && ( newQuality < quality ) ) { reject ( { compressedFile : this . getResultFile ( blob ) , reason : "Could not compress image enough to fit the maximal file size limit." , error : "UNABLE_TO_COMPRESS_ENOUGH" } ) ; } else if ( ( newQuality > quality ) && ( Math . round ( quality ) == Math . round ( newQuality ) ) ) { resolve ( this . getResultFile ( blob ) ) ; } else if ( currentStep > 5 && ( newQuality > quality ) && ( newQuality < quality + 2 ) ) { resolve ( this . getResultFile ( blob ) ) ; } else if ( ( newQuality > quality ) && Number . isInteger ( quality ) && ( Math . floor ( newQuality ) == quality ) ) { resolve ( this . getResultFile ( blob ) ) ; } else { if ( ( quality > newQuality ) && ( Math . round ( quality ) == Math . round ( newQuality ) ) ) { newQuality = Math . round ( newQuality ) - 1 ; } resolve ( this . getCompressedFile ( cvs , newQuality , maxSizeInMB , currentStep + 1 ) ) ; } } ) ; return result ; } private isImgUsingAlpha ( imageData ) { for ( var i = 0 ; i < imageData . data . length ; i += 4 ) { if ( imageData . data [ i + 3 ] !== 255 ) { return true ; } } return false ; } private logExecutionTime ( logExecutionTime ) { if ( logExecutionTime ) { console . info ( "Execution time: " , new Date ( ) . getTime ( ) - this . timeAtStart + "ms" ) ; } } }