@@ -32,6 +32,13 @@ const array = encodeQR(txt, 'raw'); // 2d array for canvas or other libs
3232```
3333 */
3434
35+ const R1_RUN_LENGTH_THRESHOLD = 5 ;
36+ const R2_BLOCK_PENALTY = 3 ;
37+ const R3_FINDER_PATTERN_LENGTH = 11 ;
38+ const R3_FINDER_PENALTY = 40 ;
39+ const R4_BALANCE_STEP_PERCENT = 5 ;
40+ const R4_BALANCE_STEP_POINTS = 10 ;
41+
3542// We do not use newline escape code directly in strings because it's not parser-friendly
3643const chCodes = { newline : 10 , reset : 27 } ;
3744
@@ -86,11 +93,6 @@ function interleaveBytes(blocks: Uint8Array[]): Uint8Array {
8693 return result ;
8794}
8895
89- function includesAt < T > ( lst : T [ ] , pattern : T [ ] , index : number ) : boolean {
90- if ( index < 0 || index + pattern . length > lst . length ) return false ;
91- for ( let i = 0 ; i < pattern . length ; i ++ ) if ( pattern [ i ] !== lst [ index + i ] ) return false ;
92- return true ;
93- }
9496// Optimize for minimal score/penalty
9597function best < T > ( ) : {
9698 add ( score : number , value : T ) : void ;
@@ -979,64 +981,189 @@ function drawQR(
979981 return b ;
980982}
981983
982- function penalty ( bm : Bitmap ) : number {
983- const inverse = bm . inverse ( ) ;
984- // Adjacent modules in row/column in same | No. of modules = (5 + i) color
985- const sameColor = ( row : DrawValue [ ] ) => {
986- let res = 0 ;
987- for ( let i = 0 , same = 1 , last = undefined ; i < row . length ; i ++ ) {
988- if ( last === row [ i ] ) {
989- same ++ ;
990- if ( i !== row . length - 1 ) continue ; // handle last element
991- }
992- if ( same >= 5 ) res += 3 + ( same - 5 ) ;
993- last = row [ i ] ;
994- same = 1 ;
984+ function calculateRowRunPenalty ( rowBits : readonly boolean [ ] ) : number {
985+ const moduleCount = rowBits . length ;
986+ if ( moduleCount <= 1 ) return 0 ;
987+
988+ let penalty = 0 ;
989+ let runLength = 1 ;
990+ let previousColor = rowBits [ 0 ] ;
991+
992+ for ( let i = 1 ; i < moduleCount ; i ++ ) {
993+ const currentColor = rowBits [ i ] ;
994+ if ( currentColor === previousColor ) {
995+ runLength ++ ;
996+ } else {
997+ if ( runLength >= R1_RUN_LENGTH_THRESHOLD ) penalty += runLength - 2 ;
998+ runLength = 1 ;
999+ previousColor = currentColor ;
9951000 }
996- return res ;
997- } ;
998- let adjacent = 0 ;
999- bm . data . forEach ( ( row ) => ( adjacent += sameColor ( row ) ) ) ;
1000- inverse . data . forEach ( ( column ) => ( adjacent += sameColor ( column ) ) ) ;
1001- // Block of modules in same color (Block size = 2x2)
1002- let box = 0 ;
1003- let b = bm . data ;
1004- const lastW = bm . width - 1 ;
1005- const lastH = bm . height - 1 ;
1006- for ( let x = 0 ; x < lastW ; x ++ ) {
1007- for ( let y = 0 ; y < lastH ; y ++ ) {
1008- const x1 = x + 1 ;
1009- const y1 = y + 1 ;
1010- if ( b [ x ] [ y ] === b [ x1 ] [ y ] && b [ x1 ] [ y ] === b [ x ] [ y1 ] && b [ x1 ] [ y ] === b [ x1 ] [ y1 ] ) {
1011- box += 3 ;
1012- }
1001+ }
1002+
1003+ if ( runLength >= R1_RUN_LENGTH_THRESHOLD ) penalty += runLength - 2 ;
1004+
1005+ return penalty ;
1006+ }
1007+
1008+ function calculateColumnRunPenalty (
1009+ bitmap : readonly boolean [ ] [ ] ,
1010+ columnIndex : number ,
1011+ columnHeight : number
1012+ ) : number {
1013+ if ( columnHeight <= 1 ) return 0 ;
1014+
1015+ let penalty = 0 ;
1016+ let runLength = 1 ;
1017+ let previousColor = bitmap [ 0 ] [ columnIndex ] ;
1018+
1019+ for ( let y = 1 ; y < columnHeight ; y ++ ) {
1020+ const currentColor = bitmap [ y ] [ columnIndex ] ;
1021+ if ( currentColor === previousColor ) {
1022+ runLength ++ ;
1023+ } else {
1024+ if ( runLength >= R1_RUN_LENGTH_THRESHOLD ) penalty += runLength - 2 ;
1025+ runLength = 1 ;
1026+ previousColor = currentColor ;
10131027 }
10141028 }
1015- // 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column, preceded or followed by light area 4 modules wide
1016- const finderPattern = ( row : DrawValue [ ] ) => {
1017- const finderPattern = [ true , false , true , true , true , false , true ] ; // dark:light:dark:light:dark
1018- const lightPattern = [ false , false , false , false ] ; // light area 4 modules wide
1019- const p1 = [ ...finderPattern , ...lightPattern ] ;
1020- const p2 = [ ...lightPattern , ...finderPattern ] ;
1021- let res = 0 ;
1022- for ( let i = 0 ; i < row . length ; i ++ ) {
1023- if ( includesAt ( row , p1 , i ) ) res += 40 ;
1024- if ( includesAt ( row , p2 , i ) ) res += 40 ;
1029+
1030+ if ( runLength >= R1_RUN_LENGTH_THRESHOLD ) penalty += runLength - 2 ;
1031+
1032+ return penalty ;
1033+ }
1034+
1035+ function calculateRowFinderPenalty ( rowBits : readonly boolean [ ] ) : number {
1036+ const rowLength = rowBits . length ;
1037+ if ( rowLength < R3_FINDER_PATTERN_LENGTH ) return 0 ;
1038+
1039+ let penalty = 0 ;
1040+ const lastStart = rowLength - R3_FINDER_PATTERN_LENGTH ;
1041+
1042+ for ( let i = 0 ; i <= lastStart ; i ++ ) {
1043+ // Case A: L L L L + 1 0 1 1 1 0 1
1044+ const light4ThenFinder7 =
1045+ ! rowBits [ i ] &&
1046+ ! rowBits [ i + 1 ] &&
1047+ ! rowBits [ i + 2 ] &&
1048+ ! rowBits [ i + 3 ] &&
1049+ rowBits [ i + 4 ] &&
1050+ ! rowBits [ i + 5 ] &&
1051+ rowBits [ i + 6 ] &&
1052+ rowBits [ i + 7 ] &&
1053+ rowBits [ i + 8 ] &&
1054+ ! rowBits [ i + 9 ] &&
1055+ rowBits [ i + 10 ] ;
1056+
1057+ // Case B: 1 0 1 1 1 0 1 + L L L L
1058+ const finder7ThenLight4 =
1059+ rowBits [ i ] &&
1060+ ! rowBits [ i + 1 ] &&
1061+ rowBits [ i + 2 ] &&
1062+ rowBits [ i + 3 ] &&
1063+ rowBits [ i + 4 ] &&
1064+ ! rowBits [ i + 5 ] &&
1065+ rowBits [ i + 6 ] &&
1066+ ! rowBits [ i + 7 ] &&
1067+ ! rowBits [ i + 8 ] &&
1068+ ! rowBits [ i + 9 ] &&
1069+ ! rowBits [ i + 10 ] ;
1070+
1071+ if ( light4ThenFinder7 || finder7ThenLight4 ) penalty += R3_FINDER_PENALTY ;
1072+ }
1073+ return penalty ;
1074+ }
1075+
1076+ function calculateColumnFinderPenalty (
1077+ matrix : readonly boolean [ ] [ ] ,
1078+ rowIndex : number ,
1079+ width : number
1080+ ) : number {
1081+ if ( width < R3_FINDER_PATTERN_LENGTH ) return 0 ;
1082+
1083+ let penalty = 0 ;
1084+ const y = rowIndex ;
1085+ const lastStart = width - R3_FINDER_PATTERN_LENGTH ;
1086+
1087+ for ( let x = 0 ; x <= lastStart ; x ++ ) {
1088+ // Case A: L L L L + 1 0 1 1 1 0 1
1089+ const light4ThenFinder7 =
1090+ ! matrix [ x ] [ y ] &&
1091+ ! matrix [ x + 1 ] [ y ] &&
1092+ ! matrix [ x + 2 ] [ y ] &&
1093+ ! matrix [ x + 3 ] [ y ] &&
1094+ matrix [ x + 4 ] [ y ] &&
1095+ ! matrix [ x + 5 ] [ y ] &&
1096+ matrix [ x + 6 ] [ y ] &&
1097+ matrix [ x + 7 ] [ y ] &&
1098+ matrix [ x + 8 ] [ y ] &&
1099+ ! matrix [ x + 9 ] [ y ] &&
1100+ matrix [ x + 10 ] [ y ] ;
1101+
1102+ // Case B: 1 0 1 1 1 0 1 + L L L L
1103+ const finder7ThenLight4 =
1104+ matrix [ x ] [ y ] &&
1105+ ! matrix [ x + 1 ] [ y ] &&
1106+ matrix [ x + 2 ] [ y ] &&
1107+ matrix [ x + 3 ] [ y ] &&
1108+ matrix [ x + 4 ] [ y ] &&
1109+ ! matrix [ x + 5 ] [ y ] &&
1110+ matrix [ x + 6 ] [ y ] &&
1111+ ! matrix [ x + 7 ] [ y ] &&
1112+ ! matrix [ x + 8 ] [ y ] &&
1113+ ! matrix [ x + 9 ] [ y ] &&
1114+ ! matrix [ x + 10 ] [ y ] ;
1115+
1116+ if ( light4ThenFinder7 || finder7ThenLight4 ) penalty += R3_FINDER_PENALTY ;
1117+ }
1118+ return penalty ;
1119+ }
1120+
1121+ function penalty ( bitmap : Bitmap ) : number {
1122+ const matrix = bitmap . data as boolean [ ] [ ] ;
1123+ const width = bitmap . width | 0 ;
1124+ const height = bitmap . height | 0 ;
1125+
1126+ if ( width === 0 || height === 0 ) return 0 ;
1127+
1128+ // Rule 1: same-color runs
1129+ let runPenalty = 0 ;
1130+ for ( let x = 0 ; x < width ; x ++ ) runPenalty += calculateRowRunPenalty ( matrix [ x ] ) ;
1131+ for ( let y = 0 ; y < height ; y ++ ) runPenalty += calculateColumnRunPenalty ( matrix , y , width ) ;
1132+
1133+ // Rule 2: 2×2 blocks of the same color
1134+ let blockPenalty = 0 ;
1135+ const lastCol = width - 1 ;
1136+ const lastRow = height - 1 ;
1137+ for ( let x = 0 ; x < lastCol ; x ++ ) {
1138+ const col = matrix [ x ] ;
1139+ const nextCol = matrix [ x + 1 ] ;
1140+ for ( let y = 0 ; y < lastRow ; y ++ ) {
1141+ const cell = col [ y ] ;
1142+ if ( cell === nextCol [ y ] && cell === col [ y + 1 ] && cell === nextCol [ y + 1 ] ) {
1143+ blockPenalty += R2_BLOCK_PENALTY ;
1144+ }
10251145 }
1026- return res ;
1027- } ;
1028- let finder = 0 ;
1029- for ( const row of bm . data ) finder += finderPattern ( row ) ;
1030- for ( const column of inverse . data ) finder += finderPattern ( column ) ;
1031- // Proportion of dark modules in entire symbol
1032- // Add 10 points to a deviation of 5% increment or decrement in the proportion
1033- // ratio of dark module from the referential 50%
1034- let darkPixels = 0 ;
1035- bm . rectRead ( 0 , Infinity , ( _c , val ) => ( darkPixels += val ? 1 : 0 ) ) ;
1036- const darkPercent = ( darkPixels / ( bm . height * bm . width ) ) * 100 ;
1037- const dark = 10 * Math . floor ( Math . abs ( darkPercent - 50 ) / 5 ) ;
1038- return adjacent + box + finder + dark ;
1146+ }
1147+
1148+ // Rule 3: finder-like 1:1:3:1:1 with 4-light padding
1149+ let finderPenalty = 0 ;
1150+ for ( let x = 0 ; x < width ; x ++ ) finderPenalty += calculateRowFinderPenalty ( matrix [ x ] ) ;
1151+ for ( let y = 0 ; y < height ; y ++ ) finderPenalty += calculateColumnFinderPenalty ( matrix , y , width ) ;
1152+
1153+ // Rule 4: dark-module balance vs 50%
1154+ let darkCount = 0 ;
1155+ for ( let x = 0 ; x < width ; x ++ ) {
1156+ const col = matrix [ x ] ;
1157+ for ( let y = 0 ; y < height ; y ++ ) if ( col [ y ] ) darkCount ++ ;
1158+ }
1159+ const moduleCount = width * height ;
1160+ const darkPercent = ( darkCount * 100 ) / moduleCount ;
1161+ const deviation = Math . abs ( darkPercent - 50 ) ;
1162+ const balancePenalty = R4_BALANCE_STEP_POINTS * Math . floor ( deviation / R4_BALANCE_STEP_PERCENT ) ;
1163+
1164+ return runPenalty + blockPenalty + finderPenalty + balancePenalty ;
10391165}
1166+
10401167// Selects best mask according to penalty, if no mask is provided
10411168function drawQRBest ( ver : Version , ecc : ErrorCorrection , data : Uint8Array , maskIdx ?: Mask ) {
10421169 if ( maskIdx === undefined ) {
0 commit comments