import { BarcodeFormat , BinaryBitmap , DecodeHintType , HybridBinarizer , LuminanceSource , Reader , Result , ResultMetadataType , ZXingStringEncoding } from '@zxing/library' ; import * as fs from 'fs' ; import * as path from 'path' ; import TestResult from '../common/TestResult' ; import SharpImageLuminanceSource from '../SharpImageLuminanceSource' ; import { assertEquals } from '../util/AssertUtils' ; import SharpImage from '../util/SharpImage' ; author Sean Owen
 *  author dswitkin google abstract class AbstractBlackBoxSpec { private testBase : string ; private testResults : Array < TestResult > ; public static buildTestBase ( testBasePathSuffix ) { let testBase = path . resolve ( testBasePathSuffix ) ; return testBase ; } protected constructor ( testBasePathSuffix , private barcodeReader , private expectedFormat ) { this . testBase = AbstractBlackBoxSpec . buildTestBase ( testBasePathSuffix ) ; this . testResults = new Array < TestResult > ( ) ; } protected getTestBase ( ) { return this . testBase ; } protected addTest ( mustPassCount , tryHarderCount , rotation ) { this . addTestWithMax ( mustPassCount , tryHarderCount , 0 , 0 , rotation ) ; } param mustPassCount param tryHarderCount param maxMisreads param maxTryHarderMisreads param rotation protected addTestWithMax ( mustPassCount , tryHarderCount , maxMisreads = 0 , maxTryHarderMisreads = 0 , rotation ) { this . testResults . push ( new TestResult ( mustPassCount , tryHarderCount , maxMisreads , maxTryHarderMisreads , rotation ) ) ; } private walkDirectory ( dirPath ) { let results = new Array < string > ( ) ; const dir = path . resolve ( this . testBase , dirPath ) ; const list = fs . readdirSync ( dir ) ; for ( let file of list ) { file = path . join ( dir , file ) ; const stat = fs . statSync ( file ) ; if ( stat && stat . isDirectory ( ) ) { results = results . concat ( this . walkDirectory ( file ) ) ; } else { if ( [ '.jpg' , '.jpeg' , '.gif' , '.png' ] . indexOf ( path . extname ( file ) ) !== - 1 ) { results . push ( file ) ; } } } if ( results . length === 0 ) { console . log ( `No files in folder ${ dir }` ) ; } return results ; } throws protected getImageFiles ( ) { assertEquals ( fs . existsSync ( this . testBase ) , true , 'Please download and install test images, and run from the \'core\' directory' ) ; return this . walkDirectory ( this . testBase ) ; } protected getReader ( ) { return this . barcodeReader ; } param done throws public async testBlackBox ( ) { try { await this . testBlackBoxCountingResults ( true ) ; console . log ( 'testBlackBox finished.' ) ; } catch ( e ) { console . log ( 'Test ended with error: ' , e ) ; throw e ; } } throws private async testBlackBoxCountingResults ( assertOnFailure ) { assertEquals ( this . testResults . length > 0 , true ) ; const imageFiles = this . getImageFiles ( ) ; const testCount = this . testResults . length ; const passedCounts = new Int32Array ( testCount ) ; const misreadCounts = new Int32Array ( testCount ) ; const tryHarderCounts = new Int32Array ( testCount ) ; const tryHarderMisreadCounts = new Int32Array ( testCount ) ; const testImageIterations = [ ] ; for ( const testImage of imageFiles ) { testImageIterations . push ( new Promise ( async ( resolve ) => { console . log ( `    Starting ${ testImage }` ) ; const fileBaseName = path . basename ( testImage , path . extname ( testImage ) ) ; let expectedTextFile = path . resolve ( this . testBase , fileBaseName + '.txt' ) ; let expectedText ; if ( fs . existsSync ( expectedTextFile ) ) { expectedText = AbstractBlackBoxSpec . readTextFileAsString ( expectedTextFile ) ; } else { expectedTextFile = path . resolve ( this . testBase , fileBaseName + '.bin' ) ; assertEquals ( fs . existsSync ( expectedTextFile ) , true , 'result bin/text file should exists' ) ; expectedText = AbstractBlackBoxSpec . readBinFileAsString ( expectedTextFile ) ; } const expectedMetadataFile = path . resolve ( fileBaseName + '.metadata.txt' ) ; let expectedMetadata = null ; if ( fs . existsSync ( expectedMetadataFile ) ) { expectedMetadata = AbstractBlackBoxSpec . readTextFileAsMetadata ( expectedMetadataFile ) ; } const decodeIterations = [ ] ; for ( let x = 0 ; x < testCount ; x ++ ) { decodeIterations . push ( new Promise ( async ( resolve ) => { const rotation = this . testResults [ x ] . getRotation ( ) ; const rotatedImage = await SharpImage . loadWithRotation ( testImage , rotation ) ; const source = new SharpImageLuminanceSource ( rotatedImage ) ; const bitmap = new BinaryBitmap ( new HybridBinarizer ( source ) ) ; try { if ( this . decode ( bitmap , rotation , expectedText , expectedMetadata , false ) ) { passedCounts [ x ] ++ ; } else { misreadCounts [ x ] ++ ; } } catch ( e ) { console . log ( `      could not read at rotation ${ rotation } failed with ${ e . constructor . name }. Message: ${ e . message }` ) ; } try { if ( this . decode ( bitmap , rotation , expectedText , expectedMetadata , true ) ) { tryHarderCounts [ x ] ++ ; } else { tryHarderMisreadCounts [ x ] ++ ; } } catch ( e ) { console . log ( `        could not read at rotation ${ rotation } w/TH failed with ${ e . constructor . name }.` ) ; } resolve ( ) ; } ) ) ; } await Promise . all ( decodeIterations ) ; resolve ( ) ; } ) ) ; } await Promise . all ( testImageIterations ) ; let totalFound = 0 ; let totalMustPass = 0 ; let totalMisread = 0 ; let totalMaxMisread = 0 ; for ( let x = 0 , length = this . testResults . length ; x < length ; x ++ ) { const testResult = this . testResults [ x ] ; console . log ( `\n      Rotation ${ testResult . getRotation ( ) } degrees:` ) ; console . log ( `        ${ passedCounts [ x ] } of ${ imageFiles . length } images passed (${ testResult . getMustPassCount ( ) } required)` ) ; let failed = imageFiles . length - passedCounts [ x ] ; console . log ( `        ${ misreadCounts [ x ] } failed due to misreads, ${ failed - misreadCounts [ x ] } not detected` ) ; console . log ( `        ${ tryHarderCounts [ x ] } of ${ imageFiles . length } images passed with try harder (${ testResult . getTryHarderCount ( ) } required)` ) ; failed = imageFiles . length - tryHarderCounts [ x ] ; console . log ( `        ${ tryHarderMisreadCounts [ x ] } failed due to misreads, ${ failed - tryHarderMisreadCounts [ x ] } not detected` ) ; totalFound += passedCounts [ x ] + tryHarderCounts [ x ] ; totalMustPass += testResult . getMustPassCount ( ) + testResult . getTryHarderCount ( ) ; totalMisread += misreadCounts [ x ] + tryHarderMisreadCounts [ x ] ; totalMaxMisread += testResult . getMaxMisreads ( ) + testResult . getMaxTryHarderMisreads ( ) ; } const totalTests = imageFiles . length * testCount * 2 ; console . log ( `    Decoded ${ totalFound } images out of ${ totalTests } (${ totalFound * 100 / totalTests }%, ${ totalMustPass } required)` ) ; if ( totalFound > totalMustPass ) { console . warn ( `  +++ Test too lax by ${ totalFound - totalMustPass } images` ) ; } else if ( totalFound < totalMustPass ) { console . error ( `  --- Test failed by ${ totalMustPass - totalFound } images` ) ; } if ( totalMisread < totalMaxMisread ) { console . warn ( `  +++ Test expects too many misreads by ${ totalMaxMisread - totalMisread } images` ) ; } else if ( totalMisread > totalMaxMisread ) { console . error ( `  --- Test had too many misreads by ${ totalMisread - totalMaxMisread } images` ) ; } if ( assertOnFailure ) { for ( let x = 0 ; x < testCount ; x ++ ) { const testResult = this . testResults [ x ] ; const label = '      Rotation ' + testResult . getRotation ( ) + ' degrees: Too many images failed.' ; assertEquals ( passedCounts [ x ] >= testResult . getMustPassCount ( ) , true , label ) ; assertEquals ( tryHarderCounts [ x ] >= testResult . getTryHarderCount ( ) , true , `Try harder, ${ label }` ) ; assertEquals ( misreadCounts [ x ] <= testResult . getMaxMisreads ( ) , true , label ) ; assertEquals ( tryHarderMisreadCounts [ x ] <= testResult . getMaxTryHarderMisreads ( ) , true , `Try harder, ${ label }` ) ; } } } throws private decode ( source , rotation , expectedText , expectedMetadata , tryHarder ) { const suffix = ` (${ tryHarder ? 'try harder, ' : '' }rotation: ${ rotation })` ; const hints = new Map < DecodeHintType , any > ( ) ; if ( tryHarder ) { hints . set ( DecodeHintType . TRY_HARDER , true ) ; } let result = null ; try { const pureHints = new Map < DecodeHintType , any > ( hints ) ; pureHints . set ( DecodeHintType . PURE_BARCODE , true ) ; result = this . barcodeReader . decode ( source , pureHints ) ; } catch ( re ) { } if ( result === null ) { result = this . barcodeReader . decode ( source , hints ) ; } const resultFormat = result . getBarcodeFormat ( ) ; if ( this . expectedFormat !== resultFormat ) { console . warn ( `Format mismatch: expected '${ this . expectedFormat }' but got '${ resultFormat }'${ suffix }` ) ; return false ; } const resultText = result . getText ( ) ; const expectedTextR = expectedText . replace ( /\r\n/g , '\n' ) ; const resultTextR = resultText . replace ( /\r\n/g , '\n' ) ; if ( expectedTextR !== resultTextR ) { const expectedTextHexCodes = AbstractBlackBoxSpec . toDebugHexStringCodes ( expectedTextR ) ; const resultTextHexCodes = AbstractBlackBoxSpec . toDebugHexStringCodes ( resultTextR ) ; console . warn ( `Content mismatch: expected '${ expectedTextR }' (${ expectedTextHexCodes }) but got '${ resultTextR }'${ suffix } (${ resultTextHexCodes })` ) ; return false ; } const resultMetadata = result . getResultMetadata ( ) ; if ( null !== expectedMetadata && undefined !== expectedMetadata ) { for ( let key in expectedMetadata . keys ( ) ) { const expectedValue = expectedMetadata . get ( key ) ; const keyType = AbstractBlackBoxSpec . valueOfResultMetadataTypeFromString ( key ) ; const actualValue = resultMetadata === null ? undefined : resultMetadata . get ( keyType ) ; if ( expectedValue !== actualValue ) { console . warn ( `Metadata mismatch for key '${ key }': expected '${ expectedValue }' but got '${ actualValue }'` ) ; return false ; } } } return true ; } private static toDebugHexStringCodes ( text ) { let r = '' ; for ( let i = 0 , length = text . length ; i !== length ; i ++ ) { if ( i > 0 ) r += ', ' ; r += '0x' + text . charCodeAt ( i ) . toString ( 16 ) . toUpperCase ( ) ; } return r ; } private static valueOfResultMetadataTypeFromString ( value ) { switch ( value ) { case 'OTHER' : return ResultMetadataType . OTHER ; case 'ORIENTATION' : return ResultMetadataType . ORIENTATION ; case 'BYTE_SEGMENTS' : return ResultMetadataType . BYTE_SEGMENTS ; case 'ERROR_CORRECTION_LEVEL' : return ResultMetadataType . ERROR_CORRECTION_LEVEL ; case 'ISSUE_NUMBER' : return ResultMetadataType . ISSUE_NUMBER ; case 'SUGGESTED_PRICE' : return ResultMetadataType . SUGGESTED_PRICE ; case 'POSSIBLE_COUNTRY' : return ResultMetadataType . POSSIBLE_COUNTRY ; case 'UPC_EAN_EXTENSION' : return ResultMetadataType . UPC_EAN_EXTENSION ; case 'PDF417_EXTRA_METADATA' : return ResultMetadataType . PDF417_EXTRA_METADATA ; case 'STRUCTURED_APPEND_SEQUENCE' : return ResultMetadataType . STRUCTURED_APPEND_SEQUENCE ; case 'STRUCTURED_APPEND_PARITY' : return ResultMetadataType . STRUCTURED_APPEND_PARITY ; default : throw value + ' not a ResultMetadataType' ; } } throws protected static readTextFileAsString ( file ) { const stringContents = fs . readFileSync ( file , { encoding : 'utf8' } ) ; if ( stringContents . endsWith ( '\n' ) ) { console . warn ( 'contents: string of file ' + file + ' end with a newline. ' + 'This may not be intended and cause a test failure' ) ; } return stringContents ; } throws protected static readBinFileAsString ( file ) { const bufferContents = fs . readFileSync ( file ) ; const stringContents = ZXingStringEncoding . decode ( new Uint8Array ( bufferContents ) , 'iso-8859-1' ) ; if ( stringContents . endsWith ( '\n' ) ) { console . warn ( 'contents: string of file ' + file + ' end with a newline. ' + 'This may not be intended and cause a test failure' ) ; } return stringContents ; } throws protected static readTextFileAsMetadata ( file ) { return null ; } } export default AbstractBlackBoxSpec ;