Skip to content

Commit 5f21977

Browse files
committed
Significantly refactors the penalty function. Adds new utilities for calculating the penalties for the various rules.
1 parent 637634b commit 5f21977

1 file changed

Lines changed: 185 additions & 58 deletions

File tree

src/index.ts

Lines changed: 185 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -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
3643
const 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
9597
function 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
10411168
function drawQRBest(ver: Version, ecc: ErrorCorrection, data: Uint8Array, maskIdx?: Mask) {
10421169
if (maskIdx === undefined) {

0 commit comments

Comments
 (0)