quadqr-js 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FORMAT.md +29 -3
- package/README.md +34 -9
- package/SPECIFICATION.md +8 -3
- package/bin/quadqr.js +4 -2
- package/dist/esm/benchmark.js +34 -9
- package/dist/esm/quadqr.js +878 -226
- package/dist/esm/vision.js +452 -14
- package/dist/quadqr.js +1330 -242
- package/dist/quadqr.min.js +1328 -242
- package/docs/API.md +10 -4
- package/docs/CLI.md +6 -0
- package/docs/HIGH_DENSITY_MODE.md +100 -0
- package/docs/README.md +2 -1
- package/docs/TRIANGLE16.md +108 -0
- package/package.json +4 -2
- package/types/benchmark.d.ts +3 -3
- package/types/index.d.ts +29 -1
package/dist/quadqr.min.js
CHANGED
|
@@ -1190,6 +1190,31 @@ function detectFinderCandidates(binary, width, height, options = {}) {
|
|
|
1190
1190
|
return clusterFinderCandidates(raw, minConfirmations);
|
|
1191
1191
|
}
|
|
1192
1192
|
|
|
1193
|
+
function recoverFinderSetFromTwo(binary, width, height, strongFinders, detector = {}) {
|
|
1194
|
+
if (strongFinders.length !== 2) return strongFinders;
|
|
1195
|
+
const moduleMean = (strongFinders[0].moduleSize + strongFinders[1].moduleSize) / 2;
|
|
1196
|
+
const loose = detectFinderCandidates(binary, width, height, {
|
|
1197
|
+
toleranceScale: Math.max(1.45, (detector.toleranceScale ?? 1) * 1.35),
|
|
1198
|
+
moduleSpreadLimit: Math.max(0.72, detector.moduleSpreadLimit ?? 0.45),
|
|
1199
|
+
minConfirmations: 1
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
const extra = loose.filter((candidate) => {
|
|
1203
|
+
const moduleRatio = Math.abs(candidate.moduleSize - moduleMean) / Math.max(candidate.moduleSize, moduleMean);
|
|
1204
|
+
if (moduleRatio > 0.62) return false;
|
|
1205
|
+
return strongFinders.every((known) =>
|
|
1206
|
+
Math.hypot(candidate.x - known.x, candidate.y - known.y) > Math.max(moduleMean, known.moduleSize) * 5
|
|
1207
|
+
);
|
|
1208
|
+
}).slice(0, 12);
|
|
1209
|
+
|
|
1210
|
+
if (!extra.length) return strongFinders;
|
|
1211
|
+
return strongFinders.concat(extra).sort((a, b) =>
|
|
1212
|
+
(b.confirmations - a.confirmations) ||
|
|
1213
|
+
(a.score - b.score) ||
|
|
1214
|
+
(b.moduleSize - a.moduleSize)
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1193
1218
|
function dot(a, b) {
|
|
1194
1219
|
return a.x * b.x + a.y * b.y;
|
|
1195
1220
|
}
|
|
@@ -1272,6 +1297,14 @@ function alignmentTemplateValue(pattern, r, c) {
|
|
|
1272
1297
|
return alignmentPatternIsBlack(pattern, r, c) ? 1 : 0;
|
|
1273
1298
|
}
|
|
1274
1299
|
|
|
1300
|
+
const ALIGNMENT_SUBCELL_PROBES = Object.freeze([
|
|
1301
|
+
Object.freeze([0, 0]),
|
|
1302
|
+
Object.freeze([-0.32, 0]),
|
|
1303
|
+
Object.freeze([0.32, 0]),
|
|
1304
|
+
Object.freeze([0, -0.32]),
|
|
1305
|
+
Object.freeze([0, 0.32])
|
|
1306
|
+
]);
|
|
1307
|
+
|
|
1275
1308
|
function alignmentScore(binary, width, height, center, basisU, basisV, scale, pattern) {
|
|
1276
1309
|
let matches = 0;
|
|
1277
1310
|
let total = 0;
|
|
@@ -1289,7 +1322,31 @@ function alignmentScore(binary, width, height, center, basisU, basisV, scale, pa
|
|
|
1289
1322
|
return total ? matches / total : 0;
|
|
1290
1323
|
}
|
|
1291
1324
|
|
|
1292
|
-
function
|
|
1325
|
+
function preciseAlignmentScore(binary, width, height, center, basisU, basisV, scale, pattern) {
|
|
1326
|
+
let matches = 0;
|
|
1327
|
+
let total = 0;
|
|
1328
|
+
const radius = alignmentPatternRadius(pattern);
|
|
1329
|
+
for (let r = -radius; r <= radius; r++) {
|
|
1330
|
+
for (let c = -radius; c <= radius; c++) {
|
|
1331
|
+
const expected = alignmentTemplateValue(pattern, r, c);
|
|
1332
|
+
// Centre-only alignment scoring has a broad plateau: a candidate can be
|
|
1333
|
+
// wrong by almost half a module while every centre still lands in the
|
|
1334
|
+
// correct solid tile. This denser score is reserved for the slow recovery
|
|
1335
|
+
// path, where Triangle16 needs sub-module geometry precision.
|
|
1336
|
+
for (const [du, dv] of ALIGNMENT_SUBCELL_PROBES) {
|
|
1337
|
+
const x = center.x + basisU.x * (c + du) * scale + basisV.x * (r + dv) * scale;
|
|
1338
|
+
const y = center.y + basisU.y * (c + du) * scale + basisV.y * (r + dv) * scale;
|
|
1339
|
+
const value = sampleBinaryAt(binary, width, height, x, y);
|
|
1340
|
+
if (value === null) continue;
|
|
1341
|
+
total++;
|
|
1342
|
+
if (value === expected) matches++;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return total ? matches / total : 0;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function searchAlignment(binary, width, height, triple, version, options = {}) {
|
|
1293
1350
|
const size = sizeForVersion(version);
|
|
1294
1351
|
const separation = size - 7;
|
|
1295
1352
|
const basisU = {
|
|
@@ -1309,8 +1366,12 @@ function searchAlignment(binary, width, height, triple, version) {
|
|
|
1309
1366
|
};
|
|
1310
1367
|
|
|
1311
1368
|
let best = { score: 0, center: predicted, scale: 1 };
|
|
1312
|
-
const
|
|
1369
|
+
const precise = options.preciseAlignment === true;
|
|
1370
|
+
const offsets = precise
|
|
1371
|
+
? [-2.5, -1.5, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1.5, 2.5]
|
|
1372
|
+
: [-2.5, -1.5, -0.75, 0, 0.75, 1.5, 2.5];
|
|
1313
1373
|
const scales = [0.72, 0.85, 1, 1.15, 1.3];
|
|
1374
|
+
const scoreAlignment = precise ? preciseAlignmentScore : alignmentScore;
|
|
1314
1375
|
|
|
1315
1376
|
for (const ou of offsets) {
|
|
1316
1377
|
for (const ov of offsets) {
|
|
@@ -1319,7 +1380,7 @@ function searchAlignment(binary, width, height, triple, version) {
|
|
|
1319
1380
|
y: predicted.y + basisU.y * ou + basisV.y * ov
|
|
1320
1381
|
};
|
|
1321
1382
|
for (const scale of scales) {
|
|
1322
|
-
const score =
|
|
1383
|
+
const score = scoreAlignment(binary, width, height, center, basisU, basisV, scale, target);
|
|
1323
1384
|
if (score > best.score) best = { score, center, scale };
|
|
1324
1385
|
}
|
|
1325
1386
|
}
|
|
@@ -1362,6 +1423,183 @@ function scoreAlignmentGrid(binary, width, height, homography, version) {
|
|
|
1362
1423
|
return { score, patternScores };
|
|
1363
1424
|
}
|
|
1364
1425
|
|
|
1426
|
+
function localProjectiveBasis(homography, moduleX, moduleY) {
|
|
1427
|
+
const center = projectPoint(homography, moduleX, moduleY);
|
|
1428
|
+
const alongU = projectPoint(homography, moduleX + 1, moduleY);
|
|
1429
|
+
const alongV = projectPoint(homography, moduleX, moduleY + 1);
|
|
1430
|
+
return {
|
|
1431
|
+
center,
|
|
1432
|
+
basisU: { x: alongU.x - center.x, y: alongU.y - center.y },
|
|
1433
|
+
basisV: { x: alongV.x - center.x, y: alongV.y - center.y }
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function searchProjectedAlignment(binary, width, height, homography, pattern) {
|
|
1438
|
+
const moduleX = pattern.col + 0.5;
|
|
1439
|
+
const moduleY = pattern.row + 0.5;
|
|
1440
|
+
const local = localProjectiveBasis(homography, moduleX, moduleY);
|
|
1441
|
+
const moduleScale = Math.max(
|
|
1442
|
+
0.5,
|
|
1443
|
+
(Math.hypot(local.basisU.x, local.basisU.y) + Math.hypot(local.basisV.x, local.basisV.y)) / 2
|
|
1444
|
+
);
|
|
1445
|
+
const offsets = [-1, -0.5, 0, 0.5, 1];
|
|
1446
|
+
const scales = [0.88, 1, 1.12];
|
|
1447
|
+
let best = {
|
|
1448
|
+
score: projectedAlignmentScore(binary, width, height, homography, pattern),
|
|
1449
|
+
center: local.center,
|
|
1450
|
+
scale: 1,
|
|
1451
|
+
offsetU: 0,
|
|
1452
|
+
offsetV: 0,
|
|
1453
|
+
displacementModules: 0
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
for (const offsetU of offsets) {
|
|
1457
|
+
for (const offsetV of offsets) {
|
|
1458
|
+
const center = {
|
|
1459
|
+
x: local.center.x + local.basisU.x * offsetU + local.basisV.x * offsetV,
|
|
1460
|
+
y: local.center.y + local.basisU.y * offsetU + local.basisV.y * offsetV
|
|
1461
|
+
};
|
|
1462
|
+
for (const scale of scales) {
|
|
1463
|
+
const score = alignmentScore(
|
|
1464
|
+
binary,
|
|
1465
|
+
width,
|
|
1466
|
+
height,
|
|
1467
|
+
center,
|
|
1468
|
+
local.basisU,
|
|
1469
|
+
local.basisV,
|
|
1470
|
+
scale,
|
|
1471
|
+
pattern
|
|
1472
|
+
);
|
|
1473
|
+
if (score > best.score) {
|
|
1474
|
+
best = {
|
|
1475
|
+
score,
|
|
1476
|
+
center,
|
|
1477
|
+
scale,
|
|
1478
|
+
offsetU,
|
|
1479
|
+
offsetV,
|
|
1480
|
+
displacementModules: Math.hypot(offsetU, offsetV),
|
|
1481
|
+
displacementPixels: Math.hypot(offsetU, offsetV) * moduleScale
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
return best;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function refineHomographyWithAlignmentGrid(
|
|
1492
|
+
binary,
|
|
1493
|
+
width,
|
|
1494
|
+
height,
|
|
1495
|
+
initialHomography,
|
|
1496
|
+
version,
|
|
1497
|
+
sourcePoints,
|
|
1498
|
+
destinationPoints,
|
|
1499
|
+
initialGrid,
|
|
1500
|
+
options = {}
|
|
1501
|
+
) {
|
|
1502
|
+
const patterns = alignmentPatternCentersForVersion(version);
|
|
1503
|
+
if (patterns.length <= 1 || options.alignmentRefinement === false) {
|
|
1504
|
+
return {
|
|
1505
|
+
homography: initialHomography,
|
|
1506
|
+
grid: initialGrid,
|
|
1507
|
+
refined: false,
|
|
1508
|
+
points: []
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const primary = patterns.find((pattern) => pattern.primary);
|
|
1513
|
+
const secondaries = patterns.filter((pattern) => !pattern.primary);
|
|
1514
|
+
const minimumScore = options.alignmentRefinePatternThreshold ?? 0.84;
|
|
1515
|
+
const maxPoints = Math.max(1, Math.min(18, Math.round(options.alignmentRefineMaxPoints ?? 12)));
|
|
1516
|
+
const candidates = [];
|
|
1517
|
+
|
|
1518
|
+
for (const pattern of secondaries) {
|
|
1519
|
+
const found = searchProjectedAlignment(binary, width, height, initialHomography, pattern);
|
|
1520
|
+
if (found.score < minimumScore) continue;
|
|
1521
|
+
if (found.displacementModules > (options.alignmentRefineMaxDisplacement ?? 1.45)) continue;
|
|
1522
|
+
candidates.push({ pattern, ...found });
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
if (!candidates.length) {
|
|
1526
|
+
return { homography: initialHomography, grid: initialGrid, refined: false, points: [] };
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// Prefer confident references, but retain spatial distribution so dense
|
|
1530
|
+
// versions use alignment information across the entire matrix rather than
|
|
1531
|
+
// allowing a cluster of nearby markers to dominate the fit.
|
|
1532
|
+
candidates.sort((a, b) =>
|
|
1533
|
+
(b.score - a.score) ||
|
|
1534
|
+
((b.pattern.row + b.pattern.col) - (a.pattern.row + a.pattern.col))
|
|
1535
|
+
);
|
|
1536
|
+
const selected = [];
|
|
1537
|
+
for (const candidate of candidates) {
|
|
1538
|
+
if (selected.length >= maxPoints) break;
|
|
1539
|
+
const tooClose = selected.some((item) =>
|
|
1540
|
+
Math.hypot(item.pattern.col - candidate.pattern.col, item.pattern.row - candidate.pattern.row) < 6
|
|
1541
|
+
);
|
|
1542
|
+
if (!tooClose || selected.length < 3) selected.push(candidate);
|
|
1543
|
+
}
|
|
1544
|
+
if (!selected.length) return { homography: initialHomography, grid: initialGrid, refined: false, points: [] };
|
|
1545
|
+
|
|
1546
|
+
const refinedSource = sourcePoints.slice();
|
|
1547
|
+
const refinedDestination = destinationPoints.slice();
|
|
1548
|
+
const weights = [4, 4, 4, 5];
|
|
1549
|
+
const pointInfo = [];
|
|
1550
|
+
|
|
1551
|
+
for (const candidate of selected) {
|
|
1552
|
+
refinedSource.push(candidate.center);
|
|
1553
|
+
refinedDestination.push({
|
|
1554
|
+
x: candidate.pattern.col + 0.5,
|
|
1555
|
+
y: candidate.pattern.row + 0.5
|
|
1556
|
+
});
|
|
1557
|
+
// A 3x3 marker carries less evidence than the 5x5 primary and the three
|
|
1558
|
+
// finder patterns, so it helps average geometry without overpowering them.
|
|
1559
|
+
weights.push(0.8 + candidate.score * 1.7);
|
|
1560
|
+
pointInfo.push({
|
|
1561
|
+
row: candidate.pattern.row,
|
|
1562
|
+
col: candidate.pattern.col,
|
|
1563
|
+
score: candidate.score,
|
|
1564
|
+
x: candidate.center.x,
|
|
1565
|
+
y: candidate.center.y,
|
|
1566
|
+
displacementModules: candidate.displacementModules
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
let refinedHomography;
|
|
1571
|
+
try {
|
|
1572
|
+
refinedHomography = computeHomographyLeastSquares(refinedSource, refinedDestination, weights);
|
|
1573
|
+
} catch {
|
|
1574
|
+
return { homography: initialHomography, grid: initialGrid, refined: false, points: [] };
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const refinedGrid = scoreAlignmentGrid(binary, width, height, refinedHomography, version);
|
|
1578
|
+
const primaryBefore = primary
|
|
1579
|
+
? projectedAlignmentScore(binary, width, height, initialHomography, primary)
|
|
1580
|
+
: 1;
|
|
1581
|
+
const primaryAfter = primary
|
|
1582
|
+
? projectedAlignmentScore(binary, width, height, refinedHomography, primary)
|
|
1583
|
+
: 1;
|
|
1584
|
+
const allowedGridDrop = options.alignmentRefineAllowedGridDrop ?? 0.006;
|
|
1585
|
+
const allowedPrimaryDrop = options.alignmentRefineAllowedPrimaryDrop ?? 0.04;
|
|
1586
|
+
|
|
1587
|
+
if (
|
|
1588
|
+
refinedGrid.score + allowedGridDrop < initialGrid.score ||
|
|
1589
|
+
primaryAfter + allowedPrimaryDrop < primaryBefore
|
|
1590
|
+
) {
|
|
1591
|
+
return { homography: initialHomography, grid: initialGrid, refined: false, points: [] };
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
return {
|
|
1595
|
+
homography: refinedHomography,
|
|
1596
|
+
grid: refinedGrid,
|
|
1597
|
+
refined: true,
|
|
1598
|
+
points: pointInfo,
|
|
1599
|
+
initialGridScore: initialGrid.score
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1365
1603
|
function solveLinearSystemFloat(matrix, vector) {
|
|
1366
1604
|
const n = vector.length;
|
|
1367
1605
|
const a = matrix.map((row, index) => row.slice().concat([vector[index]]));
|
|
@@ -1403,6 +1641,43 @@ function computeHomography(sourcePoints, destinationPoints) {
|
|
|
1403
1641
|
return [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], 1];
|
|
1404
1642
|
}
|
|
1405
1643
|
|
|
1644
|
+
function computeHomographyLeastSquares(sourcePoints, destinationPoints, weights = null) {
|
|
1645
|
+
// Same destination -> source convention as computeHomography(), but accepts
|
|
1646
|
+
// more than four correspondences and averages locator/alignment measurement
|
|
1647
|
+
// noise. This is used only as a refinement after a valid four-point projective
|
|
1648
|
+
// solution already exists.
|
|
1649
|
+
assert(
|
|
1650
|
+
sourcePoints.length === destinationPoints.length && sourcePoints.length >= 4,
|
|
1651
|
+
"At least four matching point pairs are required."
|
|
1652
|
+
);
|
|
1653
|
+
|
|
1654
|
+
const normal = Array.from({ length: 8 }, () => Array(8).fill(0));
|
|
1655
|
+
const rhs = Array(8).fill(0);
|
|
1656
|
+
|
|
1657
|
+
const addEquation = (row, value, weight) => {
|
|
1658
|
+
for (let i = 0; i < 8; i++) {
|
|
1659
|
+
rhs[i] += row[i] * value * weight;
|
|
1660
|
+
for (let j = 0; j < 8; j++) normal[i][j] += row[i] * row[j] * weight;
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
for (let i = 0; i < sourcePoints.length; i++) {
|
|
1665
|
+
const u = destinationPoints[i].x;
|
|
1666
|
+
const v = destinationPoints[i].y;
|
|
1667
|
+
const x = sourcePoints[i].x;
|
|
1668
|
+
const y = sourcePoints[i].y;
|
|
1669
|
+
const weight = Math.max(0.05, Number(weights?.[i] ?? 1));
|
|
1670
|
+
addEquation([u, v, 1, 0, 0, 0, -u * x, -v * x], x, weight);
|
|
1671
|
+
addEquation([0, 0, 0, u, v, 1, -u * y, -v * y], y, weight);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// Tiny Tikhonov regularisation keeps near-degenerate noisy fits numerically
|
|
1675
|
+
// stable without moving a normal QR-sized solution in any meaningful way.
|
|
1676
|
+
for (let i = 0; i < 8; i++) normal[i][i] += 1e-9;
|
|
1677
|
+
const h = solveLinearSystemFloat(normal, rhs);
|
|
1678
|
+
return [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7], 1];
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1406
1681
|
function projectPoint(h, u, v) {
|
|
1407
1682
|
const denominator = h[6] * u + h[7] * v + h[8];
|
|
1408
1683
|
return {
|
|
@@ -1428,7 +1703,7 @@ function geometryCandidatesFromBinary(binary, width, height, finders, threshold,
|
|
|
1428
1703
|
for (const item of versions) {
|
|
1429
1704
|
const version = item.version;
|
|
1430
1705
|
const size = sizeForVersion(version);
|
|
1431
|
-
const alignment = searchAlignment(binary, width, height, triple, version);
|
|
1706
|
+
const alignment = searchAlignment(binary, width, height, triple, version, options);
|
|
1432
1707
|
if (alignment.score < (options.alignmentThreshold ?? 0.72)) continue;
|
|
1433
1708
|
|
|
1434
1709
|
const alignmentTarget = alignment.target;
|
|
@@ -1452,8 +1727,34 @@ function geometryCandidatesFromBinary(binary, width, height, finders, threshold,
|
|
|
1452
1727
|
continue;
|
|
1453
1728
|
}
|
|
1454
1729
|
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1730
|
+
const initialAlignmentGrid = scoreAlignmentGrid(binary, width, height, homography, version);
|
|
1731
|
+
const alignmentGridThreshold = options.alignmentGridThreshold ?? 0.68;
|
|
1732
|
+
const refinementFloor = Math.max(0, alignmentGridThreshold - (options.alignmentRefineCandidateMargin ?? 0.10));
|
|
1733
|
+
const skipRefinementScore = options.alignmentRefineSkipScore ?? 0.985;
|
|
1734
|
+
const refinement = (
|
|
1735
|
+
initialAlignmentGrid.score >= refinementFloor &&
|
|
1736
|
+
initialAlignmentGrid.score < skipRefinementScore
|
|
1737
|
+
)
|
|
1738
|
+
? refineHomographyWithAlignmentGrid(
|
|
1739
|
+
binary,
|
|
1740
|
+
width,
|
|
1741
|
+
height,
|
|
1742
|
+
homography,
|
|
1743
|
+
version,
|
|
1744
|
+
src,
|
|
1745
|
+
dest,
|
|
1746
|
+
initialAlignmentGrid,
|
|
1747
|
+
options
|
|
1748
|
+
)
|
|
1749
|
+
: {
|
|
1750
|
+
homography,
|
|
1751
|
+
grid: initialAlignmentGrid,
|
|
1752
|
+
refined: false,
|
|
1753
|
+
points: []
|
|
1754
|
+
};
|
|
1755
|
+
homography = refinement.homography;
|
|
1756
|
+
const alignmentGrid = refinement.grid;
|
|
1757
|
+
if (alignmentGrid.score < alignmentGridThreshold) continue;
|
|
1457
1758
|
|
|
1458
1759
|
const versionPenalty = item.error / 4;
|
|
1459
1760
|
const alignmentConfidence = 0.55 * alignment.score + 0.45 * alignmentGrid.score;
|
|
@@ -1472,6 +1773,9 @@ function geometryCandidatesFromBinary(binary, width, height, finders, threshold,
|
|
|
1472
1773
|
target: { row: alignmentTarget.row, col: alignmentTarget.col },
|
|
1473
1774
|
patterns: alignmentGrid.patternScores.length,
|
|
1474
1775
|
gridScore: alignmentGrid.score,
|
|
1776
|
+
initialGridScore: refinement.initialGridScore ?? initialAlignmentGrid.score,
|
|
1777
|
+
refined: refinement.refined,
|
|
1778
|
+
refinementPoints: refinement.points,
|
|
1475
1779
|
patternScores: alignmentGrid.patternScores
|
|
1476
1780
|
},
|
|
1477
1781
|
threshold,
|
|
@@ -1543,19 +1847,21 @@ function detectCodeGeometry(imageData, options = {}) {
|
|
|
1543
1847
|
const maxCandidates = options.maxCandidates ?? 8;
|
|
1544
1848
|
|
|
1545
1849
|
const evaluatePass = (pass, recovery = false) => {
|
|
1546
|
-
const
|
|
1547
|
-
const
|
|
1850
|
+
const detector = pass.detector ?? {};
|
|
1851
|
+
const finders = detectFinderCandidates(pass.binary, width, height, detector);
|
|
1852
|
+
const geometryOptions = {
|
|
1853
|
+
...options,
|
|
1854
|
+
finderMethod: pass.finderMethod,
|
|
1855
|
+
alignmentThreshold: recovery ? 0.68 : 0.72,
|
|
1856
|
+
alignmentGridThreshold: recovery ? 0.64 : 0.68
|
|
1857
|
+
};
|
|
1858
|
+
let geometries = geometryCandidatesFromBinary(
|
|
1548
1859
|
pass.binary,
|
|
1549
1860
|
width,
|
|
1550
1861
|
height,
|
|
1551
1862
|
finders,
|
|
1552
1863
|
pass.threshold,
|
|
1553
|
-
|
|
1554
|
-
...options,
|
|
1555
|
-
finderMethod: pass.finderMethod,
|
|
1556
|
-
alignmentThreshold: recovery ? 0.68 : 0.72,
|
|
1557
|
-
alignmentGridThreshold: recovery ? 0.64 : 0.68
|
|
1558
|
-
}
|
|
1864
|
+
geometryOptions
|
|
1559
1865
|
);
|
|
1560
1866
|
|
|
1561
1867
|
pushFinderDiagnostic(
|
|
@@ -1565,6 +1871,35 @@ function detectCodeGeometry(imageData, options = {}) {
|
|
|
1565
1871
|
finders,
|
|
1566
1872
|
geometries
|
|
1567
1873
|
);
|
|
1874
|
+
|
|
1875
|
+
// Dense codes under projective distortion can leave two finder patterns
|
|
1876
|
+
// perfectly strong while the third is stretched enough to miss the normal
|
|
1877
|
+
// 1:1:3:1:1 tolerance. Only in that very specific recovery state, run one
|
|
1878
|
+
// bounded looser detector and let the normal three-finder geometry checks
|
|
1879
|
+
// reject false data-cell lookalikes. The clean fast path is unchanged.
|
|
1880
|
+
if (!geometries.length && recovery && finders.length === 2) {
|
|
1881
|
+
const recoveredFinders = recoverFinderSetFromTwo(pass.binary, width, height, finders, detector);
|
|
1882
|
+
if (recoveredFinders.length > 2) {
|
|
1883
|
+
const finderMethod = `${pass.finderMethod}-two-finder-recovery`;
|
|
1884
|
+
geometries = geometryCandidatesFromBinary(
|
|
1885
|
+
pass.binary,
|
|
1886
|
+
width,
|
|
1887
|
+
height,
|
|
1888
|
+
recoveredFinders,
|
|
1889
|
+
pass.threshold,
|
|
1890
|
+
{ ...geometryOptions, finderMethod }
|
|
1891
|
+
);
|
|
1892
|
+
pushFinderDiagnostic(
|
|
1893
|
+
{ ...options, width, height, diagnosticLabel: `${options.diagnosticLabel ?? "perspective"}-two-finder` },
|
|
1894
|
+
finderMethod,
|
|
1895
|
+
pass.threshold,
|
|
1896
|
+
recoveredFinders,
|
|
1897
|
+
geometries
|
|
1898
|
+
);
|
|
1899
|
+
if (geometries.length) return { finders: recoveredFinders, geometries };
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1568
1903
|
return { finders, geometries };
|
|
1569
1904
|
};
|
|
1570
1905
|
|
|
@@ -1580,6 +1915,38 @@ function detectCodeGeometry(imageData, options = {}) {
|
|
|
1580
1915
|
if (fast.geometries.length) return fast.geometries.slice(0, maxCandidates);
|
|
1581
1916
|
if (options.finderRecovery === false) return [];
|
|
1582
1917
|
|
|
1918
|
+
// If the clean threshold already found exactly two strong locators, try the
|
|
1919
|
+
// bounded perspective-tolerant third-finder pass before any color processing.
|
|
1920
|
+
// This is substantially cheaper than Auto Color and targets the dense-code
|
|
1921
|
+
// projective failure mode directly.
|
|
1922
|
+
if (fast.finders.length === 2) {
|
|
1923
|
+
const recoveredFinders = recoverFinderSetFromTwo(valueInfo.binary, width, height, fast.finders, {});
|
|
1924
|
+
if (recoveredFinders.length > 2) {
|
|
1925
|
+
const finderMethod = "rgb-value-otsu-two-finder-recovery";
|
|
1926
|
+
const recoveredGeometries = geometryCandidatesFromBinary(
|
|
1927
|
+
valueInfo.binary,
|
|
1928
|
+
width,
|
|
1929
|
+
height,
|
|
1930
|
+
recoveredFinders,
|
|
1931
|
+
valueInfo.threshold,
|
|
1932
|
+
{
|
|
1933
|
+
...options,
|
|
1934
|
+
finderMethod,
|
|
1935
|
+
alignmentThreshold: 0.68,
|
|
1936
|
+
alignmentGridThreshold: 0.64
|
|
1937
|
+
}
|
|
1938
|
+
);
|
|
1939
|
+
pushFinderDiagnostic(
|
|
1940
|
+
{ ...options, width, height, diagnosticLabel: `${options.diagnosticLabel ?? "perspective"}-two-finder` },
|
|
1941
|
+
finderMethod,
|
|
1942
|
+
valueInfo.threshold,
|
|
1943
|
+
recoveredFinders,
|
|
1944
|
+
recoveredGeometries
|
|
1945
|
+
);
|
|
1946
|
+
if (recoveredGeometries.length) return recoveredGeometries.slice(0, maxCandidates);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1583
1950
|
// Camera recovery #1: Photoshop Auto Color-style per-channel levels before
|
|
1584
1951
|
// finder thresholding. Live camera frames are usually much larger than the
|
|
1585
1952
|
// QR itself, so a single global histogram can be dominated by dark room/UI
|
|
@@ -1983,6 +2350,49 @@ function samplePerspectiveMatrix(imageData, homography, size, options = {}) {
|
|
|
1983
2350
|
return { rgbGrid };
|
|
1984
2351
|
}
|
|
1985
2352
|
|
|
2353
|
+
/**
|
|
2354
|
+
* Sample the two protected regions of every Triangle16 module.
|
|
2355
|
+
*
|
|
2356
|
+
* Triangle16 uses a fixed "/" diagonal. Samples are intentionally placed well
|
|
2357
|
+
* inside the upper-left and lower-right triangles, away from the diagonal and
|
|
2358
|
+
* module borders. This makes the decoder much less sensitive to antialiasing,
|
|
2359
|
+
* blur, resampling and small homography errors than center sampling.
|
|
2360
|
+
*/
|
|
2361
|
+
function samplePerspectiveTriangleMatrix(imageData, homography, size, options = {}) {
|
|
2362
|
+
const triangleGrid = Array.from({ length: size }, () => new Array(size));
|
|
2363
|
+
const mode = options.sampleMode ?? "cross";
|
|
2364
|
+
const radius = options.highDensitySampleRadius ?? options.triangleSampleRadius ??
|
|
2365
|
+
(mode === "median" ? 0.075 : 0.085);
|
|
2366
|
+
const inset = Math.max(0.18, Math.min(0.36, Number(options.highDensitySampleInset ?? options.triangleSampleInset ?? 0.28)));
|
|
2367
|
+
const firstOffset = inset;
|
|
2368
|
+
const secondOffset = 1 - inset;
|
|
2369
|
+
const offsetX = Number.isFinite(options.sampleOffsetX) ? options.sampleOffsetX : 0;
|
|
2370
|
+
const offsetY = Number.isFinite(options.sampleOffsetY) ? options.sampleOffsetY : 0;
|
|
2371
|
+
const sampler = mode === "median" ? robustProjectedSample : averageProjectedSample;
|
|
2372
|
+
|
|
2373
|
+
for (let r = 0; r < size; r++) {
|
|
2374
|
+
for (let c = 0; c < size; c++) {
|
|
2375
|
+
triangleGrid[r][c] = {
|
|
2376
|
+
first: sampler(
|
|
2377
|
+
imageData,
|
|
2378
|
+
homography,
|
|
2379
|
+
c + firstOffset + offsetX,
|
|
2380
|
+
r + firstOffset + offsetY,
|
|
2381
|
+
radius
|
|
2382
|
+
),
|
|
2383
|
+
second: sampler(
|
|
2384
|
+
imageData,
|
|
2385
|
+
homography,
|
|
2386
|
+
c + secondOffset + offsetX,
|
|
2387
|
+
r + secondOffset + offsetY,
|
|
2388
|
+
radius
|
|
2389
|
+
)
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
return { triangleGrid };
|
|
2394
|
+
}
|
|
2395
|
+
|
|
1986
2396
|
function meanRgb(values) {
|
|
1987
2397
|
if (!values.length) throw new Error("No calibration samples available.");
|
|
1988
2398
|
return values.reduce(
|
|
@@ -2179,6 +2589,34 @@ function sampleAxisAlignedGrid(imageData, bounds, size, radiusRatio = 0.18) {
|
|
|
2179
2589
|
return { rgbGrid, moduleWidth: moduleW, moduleHeight: moduleH };
|
|
2180
2590
|
}
|
|
2181
2591
|
|
|
2592
|
+
function sampleAxisAlignedTriangleGrid(imageData, bounds, size, radiusRatio = 0.085, insetRatio = 0.28) {
|
|
2593
|
+
const moduleW = bounds.width / size;
|
|
2594
|
+
const moduleH = bounds.height / size;
|
|
2595
|
+
const inset = Math.max(0.18, Math.min(0.36, insetRatio));
|
|
2596
|
+
const radius = Math.max(0, Math.min(moduleW, moduleH) * radiusRatio);
|
|
2597
|
+
const triangleGrid = Array.from({ length: size }, () => new Array(size));
|
|
2598
|
+
|
|
2599
|
+
for (let r = 0; r < size; r++) {
|
|
2600
|
+
for (let c = 0; c < size; c++) {
|
|
2601
|
+
triangleGrid[r][c] = {
|
|
2602
|
+
first: sampleAverageAxis(
|
|
2603
|
+
imageData,
|
|
2604
|
+
bounds.x + (c + inset) * moduleW,
|
|
2605
|
+
bounds.y + (r + inset) * moduleH,
|
|
2606
|
+
radius
|
|
2607
|
+
),
|
|
2608
|
+
second: sampleAverageAxis(
|
|
2609
|
+
imageData,
|
|
2610
|
+
bounds.x + (c + 1 - inset) * moduleW,
|
|
2611
|
+
bounds.y + (r + 1 - inset) * moduleH,
|
|
2612
|
+
radius
|
|
2613
|
+
)
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
return { triangleGrid, moduleWidth: moduleW, moduleHeight: moduleH };
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2182
2620
|
const visionInternals = Object.freeze({
|
|
2183
2621
|
buildGray,
|
|
2184
2622
|
otsuThreshold,
|
|
@@ -2191,21 +2629,21 @@ const visionInternals = Object.freeze({
|
|
|
2191
2629
|
luminance
|
|
2192
2630
|
});
|
|
2193
2631
|
|
|
2194
|
-
return { computeHomography, projectPoint, detectCodeGeometry, autoColorImageData, autoToneContrastColorRgbGrid, autoToneContrastColorImageData, samplePerspectiveMatrix, sampleObservedPalette, spatiallyNormalizeRgbGrid, rectifyImageData, findActiveBounds, sampleAxisAlignedGrid, visionInternals };
|
|
2632
|
+
return { computeHomography, projectPoint, detectCodeGeometry, autoColorImageData, autoToneContrastColorRgbGrid, autoToneContrastColorImageData, samplePerspectiveMatrix, samplePerspectiveTriangleMatrix, sampleObservedPalette, spatiallyNormalizeRgbGrid, rectifyImageData, findActiveBounds, sampleAxisAlignedGrid, sampleAxisAlignedTriangleGrid, visionInternals };
|
|
2195
2633
|
})();
|
|
2196
2634
|
const __qqr_quadqr_js = (function () {
|
|
2197
2635
|
const { MAX_CODEWORD_SYMBOLS, rsEncode, rsDecode } = __qqr_reed_solomon_js;
|
|
2198
2636
|
const { alignmentPatternCentersForVersion, alignmentPatternIsBlack, alignmentPatternRadius, sizeForVersion, versionFromSize } = __qqr_geometry_js;
|
|
2199
|
-
const { detectCodeGeometry, computeHomography, projectPoint, samplePerspectiveMatrix, rectifyImageData, sampleObservedPalette, spatiallyNormalizeRgbGrid, autoToneContrastColorRgbGrid, autoToneContrastColorImageData, autoColorImageData, findActiveBounds, sampleAxisAlignedGrid } = __qqr_vision_js;
|
|
2637
|
+
const { detectCodeGeometry, computeHomography, projectPoint, samplePerspectiveMatrix, samplePerspectiveTriangleMatrix, rectifyImageData, sampleObservedPalette, spatiallyNormalizeRgbGrid, autoToneContrastColorRgbGrid, autoToneContrastColorImageData, autoColorImageData, findActiveBounds, sampleAxisAlignedGrid, sampleAxisAlignedTriangleGrid } = __qqr_vision_js;
|
|
2200
2638
|
const { decryptSecurePayload, encryptSecurePayload, inspectSecureEnvelope, SECURITY_MODES, SECURITY_ALGORITHMS, SECURE_PAYLOAD_VERSION, DEFAULT_PBKDF2_ITERATIONS, generateRaw256Key, normalizeRaw256Key, bytesToHex } = __qqr_security_js;
|
|
2201
2639
|
/**
|
|
2202
2640
|
* QuadQR
|
|
2203
2641
|
*
|
|
2204
|
-
* Experimental
|
|
2642
|
+
* Experimental RGBW / Triangle16 matrix code written in pure JavaScript.
|
|
2205
2643
|
*
|
|
2206
2644
|
* Core format:
|
|
2207
|
-
* - Red / Green / Blue / White data
|
|
2208
|
-
* -
|
|
2645
|
+
* - RGBW profile: Red / Green / Blue / White, exactly 2 bits per data cell
|
|
2646
|
+
* - Triangle16 profile: two fixed RGBW triangle regions, 16 states / 4 bits per body data cell
|
|
2209
2647
|
* - GF(256) Reed-Solomon ECC over byte symbols
|
|
2210
2648
|
* - interleaved body ECC blocks
|
|
2211
2649
|
* - zero-overhead spectral-spatial cell placement
|
|
@@ -2225,6 +2663,11 @@ const RENDER_MODES = Object.freeze({
|
|
|
2225
2663
|
PRINT: "print"
|
|
2226
2664
|
});
|
|
2227
2665
|
|
|
2666
|
+
const CELL_ENCODINGS = Object.freeze({
|
|
2667
|
+
RGBW: "rgbw",
|
|
2668
|
+
TRIANGLE16: "triangle16"
|
|
2669
|
+
});
|
|
2670
|
+
|
|
2228
2671
|
// Print-safe defaults deliberately use darker, more ink-tolerant primaries and
|
|
2229
2672
|
// a true white background. Applications can still override individual colors.
|
|
2230
2673
|
const PRINT_PALETTE = Object.freeze({
|
|
@@ -2282,8 +2725,10 @@ const MAGIC = new Uint8Array([0x51, 0x51, 0x52, 0x57]); // QQRW (QuadQR RGBW)
|
|
|
2282
2725
|
const HEADER_BYTES = 10;
|
|
2283
2726
|
const HEADER_RS_PARITY = 8;
|
|
2284
2727
|
const HEADER_CODEWORD_BYTES = HEADER_BYTES + HEADER_RS_PARITY;
|
|
2285
|
-
const
|
|
2286
|
-
const
|
|
2728
|
+
const RGBW_CELLS_PER_BYTE = 4;
|
|
2729
|
+
const TRIANGLE16_CELLS_PER_BYTE = 2;
|
|
2730
|
+
const CELLS_PER_BYTE = RGBW_CELLS_PER_BYTE; // legacy internal alias
|
|
2731
|
+
const HEADER_CODEWORD_CELLS = HEADER_CODEWORD_BYTES * RGBW_CELLS_PER_BYTE;
|
|
2287
2732
|
|
|
2288
2733
|
// Version 1 is a deliberately compact small-symbol profile. A 21x21 matrix
|
|
2289
2734
|
// cannot afford the normal 18-byte protected header plus a 24-byte M body
|
|
@@ -2295,7 +2740,7 @@ const COMPACT_HEADER_MAGIC = 0xc3;
|
|
|
2295
2740
|
const COMPACT_HEADER_BYTES = 4;
|
|
2296
2741
|
const COMPACT_HEADER_RS_PARITY = 4;
|
|
2297
2742
|
const COMPACT_HEADER_CODEWORD_BYTES = COMPACT_HEADER_BYTES + COMPACT_HEADER_RS_PARITY;
|
|
2298
|
-
const COMPACT_HEADER_CODEWORD_CELLS = COMPACT_HEADER_CODEWORD_BYTES *
|
|
2743
|
+
const COMPACT_HEADER_CODEWORD_CELLS = COMPACT_HEADER_CODEWORD_BYTES * RGBW_CELLS_PER_BYTE;
|
|
2299
2744
|
const COMPACT_ECC_LEVELS = Object.freeze({
|
|
2300
2745
|
L: Object.freeze({ paritySymbols: 4, correctableSymbolsPerBlock: 2 }),
|
|
2301
2746
|
M: Object.freeze({ paritySymbols: 8, correctableSymbolsPerBlock: 4 }),
|
|
@@ -2308,6 +2753,7 @@ const TEXT_FLAG = 1;
|
|
|
2308
2753
|
const SECURE_FLAG = 1 << 3;
|
|
2309
2754
|
const EXTENDED_PAYLOAD_FLAG = 1 << 4;
|
|
2310
2755
|
const SIGNED_FLAG = 1 << 5;
|
|
2756
|
+
const TRIANGLE16_FLAG = 1 << 6;
|
|
2311
2757
|
|
|
2312
2758
|
// Internal payload extension envelope. This is deliberately not a public
|
|
2313
2759
|
// payload mode or content-type system. It exists only when compression or
|
|
@@ -2370,6 +2816,45 @@ function normalizeEccLevel(level = DEFAULT_ECC_LEVEL) {
|
|
|
2370
2816
|
return value;
|
|
2371
2817
|
}
|
|
2372
2818
|
|
|
2819
|
+
function normalizeCellEncoding(value = CELL_ENCODINGS.RGBW) {
|
|
2820
|
+
const key = String(value).toLowerCase();
|
|
2821
|
+
assert(
|
|
2822
|
+
key === CELL_ENCODINGS.RGBW || key === CELL_ENCODINGS.TRIANGLE16,
|
|
2823
|
+
`Internal cell encoding must be ${CELL_ENCODINGS.RGBW} or ${CELL_ENCODINGS.TRIANGLE16}.`
|
|
2824
|
+
);
|
|
2825
|
+
return key;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
// Public API presents Triangle16 as one experimental High Density Mode toggle.
|
|
2829
|
+
// cellEncoding remains an internal/backward-compatible detail so existing
|
|
2830
|
+
// experimental branch symbols and renderer metadata continue to work.
|
|
2831
|
+
function cellEncodingFromOptions(options = {}) {
|
|
2832
|
+
if (typeof options.highDensity === "boolean") {
|
|
2833
|
+
return options.highDensity ? CELL_ENCODINGS.TRIANGLE16 : CELL_ENCODINGS.RGBW;
|
|
2834
|
+
}
|
|
2835
|
+
return normalizeCellEncoding(options.cellEncoding ?? CELL_ENCODINGS.RGBW);
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
function cellsPerByteForEncoding(cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
2839
|
+
return normalizeCellEncoding(cellEncoding) === CELL_ENCODINGS.TRIANGLE16
|
|
2840
|
+
? TRIANGLE16_CELLS_PER_BYTE
|
|
2841
|
+
: RGBW_CELLS_PER_BYTE;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
function packTriangleCell(first, second) {
|
|
2845
|
+
assert(first >= 0 && first <= 3 && second >= 0 && second <= 3, "Invalid Triangle16 color.");
|
|
2846
|
+
return ((first & 3) << 2) | (second & 3);
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
function unpackTriangleCell(value) {
|
|
2850
|
+
assert(Number.isInteger(value) && value >= 0 && value <= 15, "Invalid Triangle16 data cell.");
|
|
2851
|
+
return { first: (value >>> 2) & 3, second: value & 3 };
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
function solidTriangleCell(color) {
|
|
2855
|
+
return packTriangleCell(color, color);
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2373
2858
|
function concatBytes(...arrays) {
|
|
2374
2859
|
const total = arrays.reduce((sum, item) => sum + item.length, 0);
|
|
2375
2860
|
const out = new Uint8Array(total);
|
|
@@ -2796,8 +3281,19 @@ function crc32(bytes) {
|
|
|
2796
3281
|
return (crc ^ 0xffffffff) >>> 0;
|
|
2797
3282
|
}
|
|
2798
3283
|
|
|
2799
|
-
function bytesToCells(bytes) {
|
|
2800
|
-
const
|
|
3284
|
+
function bytesToCells(bytes, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3285
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
3286
|
+
if (encoding === CELL_ENCODINGS.TRIANGLE16) {
|
|
3287
|
+
const out = new Array(bytes.length * TRIANGLE16_CELLS_PER_BYTE);
|
|
3288
|
+
let cursor = 0;
|
|
3289
|
+
for (const byte of bytes) {
|
|
3290
|
+
out[cursor++] = (byte >>> 4) & 0x0f;
|
|
3291
|
+
out[cursor++] = byte & 0x0f;
|
|
3292
|
+
}
|
|
3293
|
+
return out;
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
const out = new Array(bytes.length * RGBW_CELLS_PER_BYTE);
|
|
2801
3297
|
let cursor = 0;
|
|
2802
3298
|
for (const byte of bytes) {
|
|
2803
3299
|
out[cursor++] = (byte >>> 6) & 0b11;
|
|
@@ -2808,11 +3304,36 @@ function bytesToCells(bytes) {
|
|
|
2808
3304
|
return out;
|
|
2809
3305
|
}
|
|
2810
3306
|
|
|
2811
|
-
function
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
3307
|
+
function bytesToProtectedHeaderCells(bytes, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3308
|
+
const base = bytesToCells(bytes, CELL_ENCODINGS.RGBW);
|
|
3309
|
+
if (normalizeCellEncoding(cellEncoding) !== CELL_ENCODINGS.TRIANGLE16) return base;
|
|
3310
|
+
// Triangle16 deliberately keeps the protected bootstrap/header as solid-color
|
|
3311
|
+
// modules. This costs a few cells but makes mode detection and damaged-camera
|
|
3312
|
+
// recovery substantially more reliable.
|
|
3313
|
+
return base.map(solidTriangleCell);
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
function cellsToBytes(cells, byteCount, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3317
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
3318
|
+
const cellsPerByte = cellsPerByteForEncoding(encoding);
|
|
3319
|
+
const count = byteCount ?? Math.floor(cells.length / cellsPerByte);
|
|
3320
|
+
assert(cells.length >= count * cellsPerByte, "Not enough data cells to rebuild bytes.");
|
|
3321
|
+
const out = new Uint8Array(count);
|
|
3322
|
+
|
|
3323
|
+
if (encoding === CELL_ENCODINGS.TRIANGLE16) {
|
|
3324
|
+
for (let i = 0; i < count; i++) {
|
|
3325
|
+
const offset = i * TRIANGLE16_CELLS_PER_BYTE;
|
|
3326
|
+
const a = cells[offset];
|
|
3327
|
+
const b = cells[offset + 1];
|
|
3328
|
+
assert(Number.isInteger(a) && a >= 0 && a <= 15, "Invalid Triangle16 data cell.");
|
|
3329
|
+
assert(Number.isInteger(b) && b >= 0 && b <= 15, "Invalid Triangle16 data cell.");
|
|
3330
|
+
out[i] = (a << 4) | b;
|
|
3331
|
+
}
|
|
3332
|
+
return out;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
for (let i = 0; i < count; i++) {
|
|
3336
|
+
const offset = i * RGBW_CELLS_PER_BYTE;
|
|
2816
3337
|
const a = cells[offset];
|
|
2817
3338
|
const b = cells[offset + 1];
|
|
2818
3339
|
const c = cells[offset + 2];
|
|
@@ -2825,14 +3346,31 @@ function cellsToBytes(cells, byteCount = Math.floor(cells.length / CELLS_PER_BYT
|
|
|
2825
3346
|
return out;
|
|
2826
3347
|
}
|
|
2827
3348
|
|
|
2828
|
-
function
|
|
3349
|
+
function protectedHeaderCellsToBytes(cells, byteCount, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3350
|
+
if (normalizeCellEncoding(cellEncoding) !== CELL_ENCODINGS.TRIANGLE16) {
|
|
3351
|
+
return cellsToBytes(cells, byteCount, CELL_ENCODINGS.RGBW);
|
|
3352
|
+
}
|
|
3353
|
+
const colors = cells.map((value) => {
|
|
3354
|
+
const pair = unpackTriangleCell(value);
|
|
3355
|
+
if (pair.first !== pair.second) throw new Error("Triangle16 protected header lost solid-color integrity.");
|
|
3356
|
+
return pair.first;
|
|
3357
|
+
});
|
|
3358
|
+
return cellsToBytes(colors, byteCount, CELL_ENCODINGS.RGBW);
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
function flagsFor(text, eccLevel, secure = false, extended = false, signed = false, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
2829
3362
|
return (text ? TEXT_FLAG : 0) |
|
|
2830
3363
|
(secure ? SECURE_FLAG : 0) |
|
|
2831
3364
|
(extended ? EXTENDED_PAYLOAD_FLAG : 0) |
|
|
2832
3365
|
(signed ? SIGNED_FLAG : 0) |
|
|
3366
|
+
(normalizeCellEncoding(cellEncoding) === CELL_ENCODINGS.TRIANGLE16 ? TRIANGLE16_FLAG : 0) |
|
|
2833
3367
|
((ECC_LEVELS[eccLevel].id << ECC_SHIFT) & ECC_MASK);
|
|
2834
3368
|
}
|
|
2835
3369
|
|
|
3370
|
+
function cellEncodingFromFlags(flags) {
|
|
3371
|
+
return (flags & TRIANGLE16_FLAG) !== 0 ? CELL_ENCODINGS.TRIANGLE16 : CELL_ENCODINGS.RGBW;
|
|
3372
|
+
}
|
|
3373
|
+
|
|
2836
3374
|
function eccFromFlags(flags) {
|
|
2837
3375
|
const id = (flags & ECC_MASK) >> ECC_SHIFT;
|
|
2838
3376
|
const name = ECC_BY_ID[id];
|
|
@@ -3082,7 +3620,8 @@ function maskValue(row, col, maskId) {
|
|
|
3082
3620
|
}
|
|
3083
3621
|
}
|
|
3084
3622
|
|
|
3085
|
-
function makePaddingCells(count, seed) {
|
|
3623
|
+
function makePaddingCells(count, seed, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3624
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
3086
3625
|
let state = (seed >>> 0) || 0x6d2b79f5;
|
|
3087
3626
|
const out = new Array(count);
|
|
3088
3627
|
for (let i = 0; i < count; i++) {
|
|
@@ -3090,11 +3629,27 @@ function makePaddingCells(count, seed) {
|
|
|
3090
3629
|
state ^= state >>> 17;
|
|
3091
3630
|
state ^= state << 5;
|
|
3092
3631
|
state >>>= 0;
|
|
3093
|
-
out[i] = state & 3;
|
|
3632
|
+
out[i] = encoding === CELL_ENCODINGS.TRIANGLE16 ? (state & 0x0f) : (state & 3);
|
|
3094
3633
|
}
|
|
3095
3634
|
return out;
|
|
3096
3635
|
}
|
|
3097
3636
|
|
|
3637
|
+
function triangleMaskValue(row, col, maskId) {
|
|
3638
|
+
const first = maskValue(row, col, maskId);
|
|
3639
|
+
const second = maskValue(row + 1, col + 2, (maskId + 1) & 3);
|
|
3640
|
+
return packTriangleCell(first, second);
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
function applyCellMask(value, row, col, maskId, cellEncoding, protectedHeader = false) {
|
|
3644
|
+
if (cellEncoding !== CELL_ENCODINGS.TRIANGLE16) return value ^ maskValue(row, col, maskId);
|
|
3645
|
+
if (protectedHeader) {
|
|
3646
|
+
const pair = unpackTriangleCell(value);
|
|
3647
|
+
if (pair.first !== pair.second) throw new Error("Triangle16 protected header must use solid-color cells.");
|
|
3648
|
+
return solidTriangleCell(pair.first ^ maskValue(row, col, maskId));
|
|
3649
|
+
}
|
|
3650
|
+
return value ^ triangleMaskValue(row, col, maskId);
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3098
3653
|
const SPECTRAL_PERMUTATION_CACHE = new Map();
|
|
3099
3654
|
|
|
3100
3655
|
function spectralPermutation(length, version) {
|
|
@@ -3123,7 +3678,15 @@ function spectralPermutation(length, version) {
|
|
|
3123
3678
|
return permutation;
|
|
3124
3679
|
}
|
|
3125
3680
|
|
|
3126
|
-
function applyData(
|
|
3681
|
+
function applyData(
|
|
3682
|
+
layout,
|
|
3683
|
+
rawCells,
|
|
3684
|
+
maskId,
|
|
3685
|
+
spectralInterleaving = true,
|
|
3686
|
+
cellEncoding = CELL_ENCODINGS.RGBW,
|
|
3687
|
+
protectedHeaderCells = getHeaderPlan(layout.version).codewordCells
|
|
3688
|
+
) {
|
|
3689
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
3127
3690
|
const matrix = cloneMatrix(layout.matrix);
|
|
3128
3691
|
const permutation = spectralInterleaving
|
|
3129
3692
|
? spectralPermutation(layout.dataPositions.length, layout.version)
|
|
@@ -3132,16 +3695,37 @@ function applyData(layout, rawCells, maskId, spectralInterleaving = true) {
|
|
|
3132
3695
|
for (let logicalIndex = 0; logicalIndex < layout.dataPositions.length; logicalIndex++) {
|
|
3133
3696
|
const physicalIndex = permutation ? permutation[logicalIndex] : logicalIndex;
|
|
3134
3697
|
const [row, col] = layout.dataPositions[physicalIndex];
|
|
3135
|
-
matrix[row][col] =
|
|
3698
|
+
matrix[row][col] = applyCellMask(
|
|
3699
|
+
rawCells[logicalIndex],
|
|
3700
|
+
row,
|
|
3701
|
+
col,
|
|
3702
|
+
maskId,
|
|
3703
|
+
encoding,
|
|
3704
|
+
encoding === CELL_ENCODINGS.TRIANGLE16 && logicalIndex < protectedHeaderCells
|
|
3705
|
+
);
|
|
3136
3706
|
}
|
|
3137
3707
|
return matrix;
|
|
3138
3708
|
}
|
|
3139
3709
|
|
|
3140
|
-
function
|
|
3710
|
+
function dataCellPenalty(matrix, reserved, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3711
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
3141
3712
|
const size = matrix.length;
|
|
3142
3713
|
let penalty = 0;
|
|
3143
|
-
const
|
|
3144
|
-
let
|
|
3714
|
+
const colorCounts = [0, 0, 0, 0];
|
|
3715
|
+
let colorCount = 0;
|
|
3716
|
+
|
|
3717
|
+
const symbolAt = (r, c) => matrix[r][c];
|
|
3718
|
+
const addColors = (value) => {
|
|
3719
|
+
if (encoding === CELL_ENCODINGS.TRIANGLE16) {
|
|
3720
|
+
const pair = unpackTriangleCell(value);
|
|
3721
|
+
colorCounts[pair.first]++;
|
|
3722
|
+
colorCounts[pair.second]++;
|
|
3723
|
+
colorCount += 2;
|
|
3724
|
+
} else {
|
|
3725
|
+
colorCounts[value]++;
|
|
3726
|
+
colorCount++;
|
|
3727
|
+
}
|
|
3728
|
+
};
|
|
3145
3729
|
|
|
3146
3730
|
for (let r = 0; r < size; r++) {
|
|
3147
3731
|
let previous = null;
|
|
@@ -3152,9 +3736,8 @@ function quaternaryPenalty(matrix, reserved) {
|
|
|
3152
3736
|
run = 0;
|
|
3153
3737
|
continue;
|
|
3154
3738
|
}
|
|
3155
|
-
const value =
|
|
3156
|
-
|
|
3157
|
-
dataCount++;
|
|
3739
|
+
const value = symbolAt(r, c);
|
|
3740
|
+
addColors(value);
|
|
3158
3741
|
if (value === previous) {
|
|
3159
3742
|
run++;
|
|
3160
3743
|
if (run >= 4) penalty += 2;
|
|
@@ -3174,7 +3757,7 @@ function quaternaryPenalty(matrix, reserved) {
|
|
|
3174
3757
|
run = 0;
|
|
3175
3758
|
continue;
|
|
3176
3759
|
}
|
|
3177
|
-
const value =
|
|
3760
|
+
const value = symbolAt(r, c);
|
|
3178
3761
|
if (value === previous) {
|
|
3179
3762
|
run++;
|
|
3180
3763
|
if (run >= 4) penalty += 2;
|
|
@@ -3185,9 +3768,9 @@ function quaternaryPenalty(matrix, reserved) {
|
|
|
3185
3768
|
}
|
|
3186
3769
|
}
|
|
3187
3770
|
|
|
3188
|
-
if (
|
|
3189
|
-
const ideal =
|
|
3190
|
-
penalty +=
|
|
3771
|
+
if (colorCount > 0) {
|
|
3772
|
+
const ideal = colorCount / 4;
|
|
3773
|
+
penalty += colorCounts.reduce((sum, count) => sum + Math.abs(count - ideal), 0) / 2;
|
|
3191
3774
|
}
|
|
3192
3775
|
return penalty;
|
|
3193
3776
|
}
|
|
@@ -3216,7 +3799,7 @@ function deinterleaveBlocks(stream, blockLengths) {
|
|
|
3216
3799
|
return blocks;
|
|
3217
3800
|
}
|
|
3218
3801
|
|
|
3219
|
-
function getBodyRsPlan(payloadLength, eccLevel, version = 2) {
|
|
3802
|
+
function getBodyRsPlan(payloadLength, eccLevel, version = 2, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3220
3803
|
const ecc = getEffectiveEcc(version, eccLevel);
|
|
3221
3804
|
const bodyByteCount = payloadLength + CRC_BYTES;
|
|
3222
3805
|
const dataSymbols = bodyByteCount;
|
|
@@ -3238,25 +3821,27 @@ function getBodyRsPlan(payloadLength, eccLevel, version = 2) {
|
|
|
3238
3821
|
dataBlockLengths,
|
|
3239
3822
|
codewordBlockLengths,
|
|
3240
3823
|
encodedSymbols,
|
|
3241
|
-
encodedCells: encodedSymbols *
|
|
3824
|
+
encodedCells: encodedSymbols * cellsPerByteForEncoding(cellEncoding)
|
|
3242
3825
|
};
|
|
3243
3826
|
}
|
|
3244
3827
|
|
|
3245
|
-
function streamCellCount(payloadLength, eccLevel, version) {
|
|
3246
|
-
return getHeaderPlan(version).codewordCells +
|
|
3828
|
+
function streamCellCount(payloadLength, eccLevel, version, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3829
|
+
return getHeaderPlan(version).codewordCells +
|
|
3830
|
+
getBodyRsPlan(payloadLength, eccLevel, version, cellEncoding).encodedCells;
|
|
3247
3831
|
}
|
|
3248
3832
|
|
|
3249
|
-
function streamFitsLayout(layout, eccLevel, payloadLength, version) {
|
|
3250
|
-
return streamCellCount(payloadLength, eccLevel, version) <= layout.dataPositions.length;
|
|
3833
|
+
function streamFitsLayout(layout, eccLevel, payloadLength, version, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3834
|
+
return streamCellCount(payloadLength, eccLevel, version, cellEncoding) <= layout.dataPositions.length;
|
|
3251
3835
|
}
|
|
3252
3836
|
|
|
3253
|
-
function getCapacityForLayout(layout, eccLevel, version) {
|
|
3254
|
-
|
|
3837
|
+
function getCapacityForLayout(layout, eccLevel, version, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3838
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
3839
|
+
if (!streamFitsLayout(layout, eccLevel, 0, version, encoding)) return 0;
|
|
3255
3840
|
let low = 0;
|
|
3256
|
-
let high = Math.floor(layout.dataPositions.length /
|
|
3841
|
+
let high = Math.floor(layout.dataPositions.length / cellsPerByteForEncoding(encoding));
|
|
3257
3842
|
while (low < high) {
|
|
3258
3843
|
const mid = Math.ceil((low + high) / 2);
|
|
3259
|
-
if (streamFitsLayout(layout, eccLevel, mid, version)) low = mid;
|
|
3844
|
+
if (streamFitsLayout(layout, eccLevel, mid, version, encoding)) low = mid;
|
|
3260
3845
|
else high = mid - 1;
|
|
3261
3846
|
}
|
|
3262
3847
|
return low;
|
|
@@ -3265,6 +3850,7 @@ function getCapacityForLayout(layout, eccLevel, version) {
|
|
|
3265
3850
|
function getVersionInfo(version, options = {}) {
|
|
3266
3851
|
validateVersion(version);
|
|
3267
3852
|
const eccLevel = normalizeEccLevel(options.ecc ?? DEFAULT_ECC_LEVEL);
|
|
3853
|
+
const cellEncoding = cellEncodingFromOptions(options);
|
|
3268
3854
|
const layout = createLayout(version);
|
|
3269
3855
|
const headerPlan = getHeaderPlan(version);
|
|
3270
3856
|
const effectiveEcc = getEffectiveEcc(version, eccLevel);
|
|
@@ -3274,8 +3860,8 @@ function getVersionInfo(version, options = {}) {
|
|
|
3274
3860
|
eccLevel,
|
|
3275
3861
|
size: layout.size,
|
|
3276
3862
|
dataCells: layout.dataPositions.length,
|
|
3277
|
-
theoreticalBits: layout.dataPositions.length * 2,
|
|
3278
|
-
capacityBytes: getCapacityForLayout(layout, eccLevel, version),
|
|
3863
|
+
theoreticalBits: layout.dataPositions.length * (cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 4 : 2),
|
|
3864
|
+
capacityBytes: getCapacityForLayout(layout, eccLevel, version, cellEncoding),
|
|
3279
3865
|
headerCells: headerPlan.codewordCells,
|
|
3280
3866
|
headerBytes: headerPlan.headerBytes,
|
|
3281
3867
|
headerParitySymbols: headerPlan.paritySymbols,
|
|
@@ -3287,8 +3873,11 @@ function getVersionInfo(version, options = {}) {
|
|
|
3287
3873
|
hasAlignmentMarker: true,
|
|
3288
3874
|
alignmentPatterns: layout.alignments.length,
|
|
3289
3875
|
alignmentCenters: layout.alignments.map(({ row, col }) => [row, col]),
|
|
3290
|
-
bitsPerDataCell: 2,
|
|
3876
|
+
bitsPerDataCell: cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 4 : 2,
|
|
3877
|
+
highDensity: cellEncoding === CELL_ENCODINGS.TRIANGLE16,
|
|
3878
|
+
cellEncoding,
|
|
3291
3879
|
colors: 4,
|
|
3880
|
+
statesPerDataCell: cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 16 : 4,
|
|
3292
3881
|
spectralInterleaving: true,
|
|
3293
3882
|
confidenceAwareEcc: true
|
|
3294
3883
|
};
|
|
@@ -3299,6 +3888,7 @@ function chooseVersion(payloadLength, options = {}) {
|
|
|
3299
3888
|
const minVersion = options.minVersion ?? MIN_VERSION;
|
|
3300
3889
|
const maxVersion = options.maxVersion ?? MAX_VERSION;
|
|
3301
3890
|
const ecc = normalizeEccLevel(options.ecc ?? DEFAULT_ECC_LEVEL);
|
|
3891
|
+
const cellEncoding = cellEncodingFromOptions(options);
|
|
3302
3892
|
|
|
3303
3893
|
validateVersion(minVersion);
|
|
3304
3894
|
validateVersion(maxVersion);
|
|
@@ -3307,10 +3897,10 @@ function chooseVersion(payloadLength, options = {}) {
|
|
|
3307
3897
|
if (requested !== "auto") {
|
|
3308
3898
|
validateVersion(requested);
|
|
3309
3899
|
assert(requested >= minVersion && requested <= maxVersion, "Requested version is outside selected bounds.");
|
|
3310
|
-
const info = getVersionInfo(requested, { ecc });
|
|
3900
|
+
const info = getVersionInfo(requested, { ecc, cellEncoding });
|
|
3311
3901
|
const layout = createLayout(requested);
|
|
3312
3902
|
assert(
|
|
3313
|
-
payloadLength <= info.capacityBytes && streamFitsLayout(layout, ecc, payloadLength, requested),
|
|
3903
|
+
payloadLength <= info.capacityBytes && streamFitsLayout(layout, ecc, payloadLength, requested, cellEncoding),
|
|
3314
3904
|
`Payload does not fit version ${requested} with ${ecc} ECC. Maximum is ${info.capacityBytes} bytes.`
|
|
3315
3905
|
);
|
|
3316
3906
|
return requested;
|
|
@@ -3318,20 +3908,20 @@ function chooseVersion(payloadLength, options = {}) {
|
|
|
3318
3908
|
|
|
3319
3909
|
for (let version = minVersion; version <= maxVersion; version++) {
|
|
3320
3910
|
const layout = createLayout(version);
|
|
3321
|
-
if (streamFitsLayout(layout, ecc, payloadLength, version)) return version;
|
|
3911
|
+
if (streamFitsLayout(layout, ecc, payloadLength, version, cellEncoding)) return version;
|
|
3322
3912
|
}
|
|
3323
3913
|
throw new Error(`Payload is too large for versions ${minVersion}..${maxVersion}.`);
|
|
3324
3914
|
}
|
|
3325
3915
|
|
|
3326
|
-
function encodeProtectedHeader(header, version) {
|
|
3916
|
+
function encodeProtectedHeader(header, version, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3327
3917
|
const plan = getHeaderPlan(version);
|
|
3328
3918
|
const codeword = rsEncode(Array.from(header), plan.paritySymbols);
|
|
3329
3919
|
assert(codeword.length === plan.codewordBytes, "Header RS symbol calculation mismatch.");
|
|
3330
|
-
return
|
|
3920
|
+
return bytesToProtectedHeaderCells(codeword, cellEncoding);
|
|
3331
3921
|
}
|
|
3332
3922
|
|
|
3333
|
-
function encodeProtectedBody(bodyBytes, eccLevel, version) {
|
|
3334
|
-
const plan = getBodyRsPlan(bodyBytes.length - CRC_BYTES, eccLevel, version);
|
|
3923
|
+
function encodeProtectedBody(bodyBytes, eccLevel, version, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3924
|
+
const plan = getBodyRsPlan(bodyBytes.length - CRC_BYTES, eccLevel, version, cellEncoding);
|
|
3335
3925
|
assert(bodyBytes.length === plan.dataSymbols, "Body RS symbol calculation mismatch.");
|
|
3336
3926
|
|
|
3337
3927
|
const blocks = [];
|
|
@@ -3340,7 +3930,7 @@ function encodeProtectedBody(bodyBytes, eccLevel, version) {
|
|
|
3340
3930
|
blocks.push(rsEncode(Array.from(bodyBytes.slice(offset, offset + length)), plan.paritySymbols));
|
|
3341
3931
|
offset += length;
|
|
3342
3932
|
}
|
|
3343
|
-
return { cells: bytesToCells(interleaveBlocks(blocks)), plan };
|
|
3933
|
+
return { cells: bytesToCells(interleaveBlocks(blocks), cellEncoding), plan };
|
|
3344
3934
|
}
|
|
3345
3935
|
|
|
3346
3936
|
function finalizeMatrix(layout, rawCells, meta) {
|
|
@@ -3348,9 +3938,10 @@ function finalizeMatrix(layout, rawCells, meta) {
|
|
|
3348
3938
|
let bestMatrix = null;
|
|
3349
3939
|
let bestPenalty = Infinity;
|
|
3350
3940
|
|
|
3941
|
+
const cellEncoding = normalizeCellEncoding(meta.cellEncoding ?? CELL_ENCODINGS.RGBW);
|
|
3351
3942
|
for (let maskId = 0; maskId < 4; maskId++) {
|
|
3352
|
-
const candidate = applyData(layout, rawCells, maskId, true);
|
|
3353
|
-
const penalty =
|
|
3943
|
+
const candidate = applyData(layout, rawCells, maskId, true, cellEncoding);
|
|
3944
|
+
const penalty = dataCellPenalty(candidate, layout.reserved, cellEncoding);
|
|
3354
3945
|
if (penalty < bestPenalty) {
|
|
3355
3946
|
bestPenalty = penalty;
|
|
3356
3947
|
bestMaskId = maskId;
|
|
@@ -3358,7 +3949,7 @@ function finalizeMatrix(layout, rawCells, meta) {
|
|
|
3358
3949
|
}
|
|
3359
3950
|
}
|
|
3360
3951
|
|
|
3361
|
-
const info = getVersionInfo(meta.version, { ecc: meta.eccLevel });
|
|
3952
|
+
const info = getVersionInfo(meta.version, { ecc: meta.eccLevel, cellEncoding });
|
|
3362
3953
|
return {
|
|
3363
3954
|
format: "QuadQR",
|
|
3364
3955
|
formatVersion: FORMAT_VERSION,
|
|
@@ -3381,7 +3972,10 @@ function finalizeMatrix(layout, rawCells, meta) {
|
|
|
3381
3972
|
capacityBytes: info.capacityBytes,
|
|
3382
3973
|
alignmentPatterns: layout.alignments.length,
|
|
3383
3974
|
utilization: meta.meaningfulCells / layout.dataPositions.length,
|
|
3384
|
-
bitsPerDataCell: 2,
|
|
3975
|
+
bitsPerDataCell: cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 4 : 2,
|
|
3976
|
+
highDensity: cellEncoding === CELL_ENCODINGS.TRIANGLE16,
|
|
3977
|
+
cellEncoding,
|
|
3978
|
+
statesPerDataCell: cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 16 : 4,
|
|
3385
3979
|
eccLevel: meta.eccLevel,
|
|
3386
3980
|
eccParitySymbols: meta.eccParitySymbols,
|
|
3387
3981
|
eccBlocks: meta.eccBlocks,
|
|
@@ -3402,23 +3996,26 @@ function encodePreparedBytes(input, options = {}) {
|
|
|
3402
3996
|
const secure = Boolean(options.secure);
|
|
3403
3997
|
const extended = Boolean(options.extended);
|
|
3404
3998
|
const signed = Boolean(options.signed);
|
|
3405
|
-
const
|
|
3406
|
-
const
|
|
3999
|
+
const cellEncoding = cellEncodingFromOptions(options);
|
|
4000
|
+
const flags = flagsFor(Boolean(options.text), eccLevel, secure, extended, signed, cellEncoding);
|
|
4001
|
+
const version = chooseVersion(payload.length, { ...options, ecc: eccLevel, cellEncoding });
|
|
3407
4002
|
const layout = createLayout(version);
|
|
3408
4003
|
const header = makeHeader(payload.length, flags, version);
|
|
3409
4004
|
const crc = crc32(concatBytes(header, payload));
|
|
3410
|
-
const headerCells = encodeProtectedHeader(header, version);
|
|
3411
|
-
const bodyEncoded = encodeProtectedBody(concatBytes(payload, u32be(crc)), eccLevel, version);
|
|
4005
|
+
const headerCells = encodeProtectedHeader(header, version, cellEncoding);
|
|
4006
|
+
const bodyEncoded = encodeProtectedBody(concatBytes(payload, u32be(crc)), eccLevel, version, cellEncoding);
|
|
3412
4007
|
const meaningfulCells = headerCells.concat(bodyEncoded.cells);
|
|
3413
4008
|
assert(meaningfulCells.length <= layout.dataPositions.length, "Internal QuadQR capacity calculation error.");
|
|
3414
4009
|
|
|
3415
4010
|
const padding = makePaddingCells(
|
|
3416
4011
|
layout.dataPositions.length - meaningfulCells.length,
|
|
3417
|
-
crc ^ payload.length ^ (version << 24) ^ (ECC_LEVELS[eccLevel].id << 16)
|
|
4012
|
+
crc ^ payload.length ^ (version << 24) ^ (ECC_LEVELS[eccLevel].id << 16),
|
|
4013
|
+
cellEncoding
|
|
3418
4014
|
);
|
|
3419
4015
|
|
|
3420
4016
|
return finalizeMatrix(layout, meaningfulCells.concat(padding), {
|
|
3421
4017
|
version,
|
|
4018
|
+
cellEncoding,
|
|
3422
4019
|
payloadBytes: payload.length,
|
|
3423
4020
|
sourcePayloadBytes: options.sourcePayloadBytes ?? payload.length,
|
|
3424
4021
|
secure,
|
|
@@ -3648,12 +4245,14 @@ function rotate90(matrix) {
|
|
|
3648
4245
|
return out;
|
|
3649
4246
|
}
|
|
3650
4247
|
|
|
3651
|
-
function extractVisibleCells(matrix, layout) {
|
|
4248
|
+
function extractVisibleCells(matrix, layout, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
4249
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
4250
|
+
const maxValue = encoding === CELL_ENCODINGS.TRIANGLE16 ? 15 : 3;
|
|
3652
4251
|
const out = [];
|
|
3653
4252
|
for (const [row, col] of layout.dataPositions) {
|
|
3654
4253
|
const value = matrix[row][col];
|
|
3655
|
-
if (!Number.isInteger(value) || value < 0 || value >
|
|
3656
|
-
throw new Error(
|
|
4254
|
+
if (!Number.isInteger(value) || value < 0 || value > maxValue) {
|
|
4255
|
+
throw new Error(`Data region contains an invalid ${encoding === CELL_ENCODINGS.TRIANGLE16 ? "Triangle16" : "RGBW"} data cell.`);
|
|
3657
4256
|
}
|
|
3658
4257
|
out.push(value);
|
|
3659
4258
|
}
|
|
@@ -3667,6 +4266,34 @@ function unmaskCells(visibleCells, positions, maskId) {
|
|
|
3667
4266
|
});
|
|
3668
4267
|
}
|
|
3669
4268
|
|
|
4269
|
+
function unmaskAndRestoreLogicalCells(
|
|
4270
|
+
visibleCells,
|
|
4271
|
+
positions,
|
|
4272
|
+
maskId,
|
|
4273
|
+
version,
|
|
4274
|
+
spectralInterleaving,
|
|
4275
|
+
cellEncoding = CELL_ENCODINGS.RGBW
|
|
4276
|
+
) {
|
|
4277
|
+
const encoding = normalizeCellEncoding(cellEncoding);
|
|
4278
|
+
const permutation = spectralInterleaving ? spectralPermutation(visibleCells.length, version) : null;
|
|
4279
|
+
const logical = new Array(visibleCells.length);
|
|
4280
|
+
const headerCells = getHeaderPlan(version).codewordCells;
|
|
4281
|
+
|
|
4282
|
+
for (let logicalIndex = 0; logicalIndex < visibleCells.length; logicalIndex++) {
|
|
4283
|
+
const physicalIndex = permutation ? permutation[logicalIndex] : logicalIndex;
|
|
4284
|
+
const [row, col] = positions[physicalIndex];
|
|
4285
|
+
logical[logicalIndex] = applyCellMask(
|
|
4286
|
+
visibleCells[physicalIndex],
|
|
4287
|
+
row,
|
|
4288
|
+
col,
|
|
4289
|
+
maskId,
|
|
4290
|
+
encoding,
|
|
4291
|
+
encoding === CELL_ENCODINGS.TRIANGLE16 && logicalIndex < headerCells
|
|
4292
|
+
);
|
|
4293
|
+
}
|
|
4294
|
+
return logical;
|
|
4295
|
+
}
|
|
4296
|
+
|
|
3670
4297
|
function restoreLogicalOrder(physicalValues, version, spectralInterleaving) {
|
|
3671
4298
|
if (!spectralInterleaving) return physicalValues.slice();
|
|
3672
4299
|
const permutation = spectralPermutation(physicalValues.length, version);
|
|
@@ -3677,20 +4304,21 @@ function restoreLogicalOrder(physicalValues, version, spectralInterleaving) {
|
|
|
3677
4304
|
return logical;
|
|
3678
4305
|
}
|
|
3679
4306
|
|
|
3680
|
-
function cellsToSymbolConfidences(
|
|
4307
|
+
function cellsToSymbolConfidences(
|
|
4308
|
+
confidences,
|
|
4309
|
+
byteCount,
|
|
4310
|
+
cellsPerByte = RGBW_CELLS_PER_BYTE
|
|
4311
|
+
) {
|
|
3681
4312
|
if (!confidences) return null;
|
|
3682
|
-
if (confidences.length < byteCount *
|
|
4313
|
+
if (confidences.length < byteCount * cellsPerByte) return null;
|
|
3683
4314
|
const out = new Array(byteCount);
|
|
3684
4315
|
for (let symbolIndex = 0; symbolIndex < byteCount; symbolIndex++) {
|
|
3685
|
-
const offset = symbolIndex *
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
confidences[offset + 2] ?? 1,
|
|
3692
|
-
confidences[offset + 3] ?? 1
|
|
3693
|
-
);
|
|
4316
|
+
const offset = symbolIndex * cellsPerByte;
|
|
4317
|
+
let value = 1;
|
|
4318
|
+
for (let cellIndex = 0; cellIndex < cellsPerByte; cellIndex++) {
|
|
4319
|
+
value = Math.min(value, confidences[offset + cellIndex] ?? 1);
|
|
4320
|
+
}
|
|
4321
|
+
out[symbolIndex] = value;
|
|
3694
4322
|
}
|
|
3695
4323
|
return out;
|
|
3696
4324
|
}
|
|
@@ -3727,11 +4355,11 @@ function decodeRsAdaptive(codeword, paritySymbols, symbolConfidences, options =
|
|
|
3727
4355
|
}
|
|
3728
4356
|
}
|
|
3729
4357
|
|
|
3730
|
-
function decodeProtectedHeader(rawCells, version, rawConfidences = null, options = {}) {
|
|
4358
|
+
function decodeProtectedHeader(rawCells, version, rawConfidences = null, options = {}, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
3731
4359
|
const plan = getHeaderPlan(version);
|
|
3732
4360
|
const headerCells = rawCells.slice(0, plan.codewordCells);
|
|
3733
|
-
const codeword = Array.from(
|
|
3734
|
-
const confidences = cellsToSymbolConfidences(rawConfidences?.slice(0, plan.codewordCells), plan.codewordBytes);
|
|
4361
|
+
const codeword = Array.from(protectedHeaderCellsToBytes(headerCells, plan.codewordBytes, cellEncoding));
|
|
4362
|
+
const confidences = cellsToSymbolConfidences(rawConfidences?.slice(0, plan.codewordCells), plan.codewordBytes, RGBW_CELLS_PER_BYTE);
|
|
3735
4363
|
const decoded = decodeRsAdaptive(codeword, plan.paritySymbols, confidences, options);
|
|
3736
4364
|
const header = new Uint8Array(decoded.data);
|
|
3737
4365
|
const parsed = parseHeader(header, version);
|
|
@@ -3746,16 +4374,17 @@ function decodeProtectedHeader(rawCells, version, rawConfidences = null, options
|
|
|
3746
4374
|
};
|
|
3747
4375
|
}
|
|
3748
4376
|
|
|
3749
|
-
function decodeProtectedBody(rawCells, payloadLength, eccLevel, version, rawConfidences = null, options = {}) {
|
|
3750
|
-
const plan = getBodyRsPlan(payloadLength, eccLevel, version);
|
|
4377
|
+
function decodeProtectedBody(rawCells, payloadLength, eccLevel, version, rawConfidences = null, options = {}, cellEncoding = CELL_ENCODINGS.RGBW) {
|
|
4378
|
+
const plan = getBodyRsPlan(payloadLength, eccLevel, version, cellEncoding);
|
|
3751
4379
|
const bodyStart = getHeaderPlan(version).codewordCells;
|
|
3752
4380
|
const encodedCells = rawCells.slice(bodyStart, bodyStart + plan.encodedCells);
|
|
3753
4381
|
if (encodedCells.length !== plan.encodedCells) throw new Error("Protected body is incomplete.");
|
|
3754
4382
|
|
|
3755
|
-
const encodedSymbols = Array.from(cellsToBytes(encodedCells, plan.encodedSymbols));
|
|
4383
|
+
const encodedSymbols = Array.from(cellsToBytes(encodedCells, plan.encodedSymbols, cellEncoding));
|
|
3756
4384
|
const symbolConfidences = cellsToSymbolConfidences(
|
|
3757
4385
|
rawConfidences?.slice(bodyStart, bodyStart + plan.encodedCells),
|
|
3758
|
-
plan.encodedSymbols
|
|
4386
|
+
plan.encodedSymbols,
|
|
4387
|
+
cellsPerByteForEncoding(cellEncoding)
|
|
3759
4388
|
);
|
|
3760
4389
|
const blocks = deinterleaveBlocks(encodedSymbols, plan.codewordBlockLengths);
|
|
3761
4390
|
const confidenceBlocks = symbolConfidences
|
|
@@ -3798,96 +4427,136 @@ function decodeCanonical(matrix, rotation, tolerance = 0, confidenceMatrix = nul
|
|
|
3798
4427
|
if (!validateStructure(matrix, tolerance)) throw new Error("QuadQR finder/alignment structure does not match.");
|
|
3799
4428
|
|
|
3800
4429
|
const layout = createLayout(version);
|
|
3801
|
-
const visible = extractVisibleCells(matrix, layout);
|
|
3802
|
-
const visibleConfidences = confidenceMatrix
|
|
3803
|
-
? layout.dataPositions.map(([row, col]) => confidenceMatrix[row]?.[col] ?? 1)
|
|
3804
|
-
: null;
|
|
3805
4430
|
const errors = [];
|
|
4431
|
+
const hintedEncoding = options.cellEncodingHint != null
|
|
4432
|
+
? normalizeCellEncoding(options.cellEncodingHint)
|
|
4433
|
+
: null;
|
|
4434
|
+
const hasExtendedCells = layout.dataPositions.some(([row, col]) => (matrix[row]?.[col] ?? 0) > 3);
|
|
4435
|
+
const encodingAttempts = hintedEncoding
|
|
4436
|
+
? [hintedEncoding]
|
|
4437
|
+
: hasExtendedCells
|
|
4438
|
+
? [CELL_ENCODINGS.TRIANGLE16, CELL_ENCODINGS.RGBW]
|
|
4439
|
+
: [CELL_ENCODINGS.RGBW, CELL_ENCODINGS.TRIANGLE16];
|
|
4440
|
+
|
|
4441
|
+
for (const cellEncoding of encodingAttempts) {
|
|
4442
|
+
let visible;
|
|
4443
|
+
try {
|
|
4444
|
+
visible = extractVisibleCells(matrix, layout, cellEncoding);
|
|
4445
|
+
} catch (error) {
|
|
4446
|
+
errors.push(`${cellEncoding}: ${error.message}`);
|
|
4447
|
+
continue;
|
|
4448
|
+
}
|
|
3806
4449
|
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
for (let maskId = 0; maskId < 4; maskId++) {
|
|
3811
|
-
try {
|
|
3812
|
-
const physicalRaw = unmaskCells(visible, layout.dataPositions, maskId);
|
|
3813
|
-
const raw = restoreLogicalOrder(physicalRaw, version, spectralInterleaving);
|
|
3814
|
-
const rawConfidences = visibleConfidences
|
|
3815
|
-
? restoreLogicalOrder(visibleConfidences, version, spectralInterleaving)
|
|
3816
|
-
: null;
|
|
3817
|
-
const headerDecoded = decodeProtectedHeader(raw, version, rawConfidences, options);
|
|
3818
|
-
const header = headerDecoded.header;
|
|
3819
|
-
const flags = headerDecoded.flags;
|
|
3820
|
-
const eccLevel = eccFromFlags(flags);
|
|
3821
|
-
const payloadLength = headerDecoded.payloadLength;
|
|
3822
|
-
if (streamCellCount(payloadLength, eccLevel, version) > raw.length) {
|
|
3823
|
-
throw new Error("Declared payload exceeds matrix capacity.");
|
|
3824
|
-
}
|
|
4450
|
+
const visibleConfidences = confidenceMatrix
|
|
4451
|
+
? layout.dataPositions.map(([row, col]) => confidenceMatrix[row]?.[col] ?? 1)
|
|
4452
|
+
: null;
|
|
3825
4453
|
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
4454
|
+
// New symbols use spectral-spatial interleaving. Legacy order remains a
|
|
4455
|
+
// decode fallback so existing RGBW QuadQR images do not become unreadable.
|
|
4456
|
+
for (const spectralInterleaving of [true, false]) {
|
|
4457
|
+
for (let maskId = 0; maskId < 4; maskId++) {
|
|
4458
|
+
try {
|
|
4459
|
+
const raw = unmaskAndRestoreLogicalCells(
|
|
4460
|
+
visible,
|
|
4461
|
+
layout.dataPositions,
|
|
4462
|
+
maskId,
|
|
4463
|
+
version,
|
|
4464
|
+
spectralInterleaving,
|
|
4465
|
+
cellEncoding
|
|
4466
|
+
);
|
|
4467
|
+
const rawConfidences = visibleConfidences
|
|
4468
|
+
? restoreLogicalOrder(visibleConfidences, version, spectralInterleaving)
|
|
4469
|
+
: null;
|
|
4470
|
+
const headerDecoded = decodeProtectedHeader(
|
|
4471
|
+
raw,
|
|
4472
|
+
version,
|
|
4473
|
+
rawConfidences,
|
|
4474
|
+
options,
|
|
4475
|
+
cellEncoding
|
|
4476
|
+
);
|
|
4477
|
+
const header = headerDecoded.header;
|
|
4478
|
+
const flags = headerDecoded.flags;
|
|
4479
|
+
const declaredCellEncoding = cellEncodingFromFlags(flags);
|
|
4480
|
+
if (declaredCellEncoding !== cellEncoding) {
|
|
4481
|
+
throw new Error(`Header declares ${declaredCellEncoding}, not ${cellEncoding}.`);
|
|
4482
|
+
}
|
|
4483
|
+
const eccLevel = eccFromFlags(flags);
|
|
4484
|
+
const payloadLength = headerDecoded.payloadLength;
|
|
4485
|
+
if (streamCellCount(payloadLength, eccLevel, version, cellEncoding) > raw.length) {
|
|
4486
|
+
throw new Error("Declared payload exceeds matrix capacity.");
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
const bodyDecoded = decodeProtectedBody(
|
|
4490
|
+
raw,
|
|
4491
|
+
payloadLength,
|
|
4492
|
+
eccLevel,
|
|
4493
|
+
version,
|
|
4494
|
+
rawConfidences,
|
|
4495
|
+
options,
|
|
4496
|
+
cellEncoding
|
|
4497
|
+
);
|
|
4498
|
+
const body = bodyDecoded.body;
|
|
4499
|
+
const payload = body.slice(0, payloadLength);
|
|
4500
|
+
const expectedCrc = readU32be(body, payloadLength);
|
|
4501
|
+
const actualCrc = crc32(concatBytes(header, payload));
|
|
4502
|
+
if (expectedCrc !== actualCrc) throw new Error("CRC mismatch after ECC.");
|
|
4503
|
+
|
|
4504
|
+
const isText = (flags & TEXT_FLAG) !== 0;
|
|
4505
|
+
const secure = (flags & SECURE_FLAG) !== 0;
|
|
4506
|
+
const extendedFlag = (flags & EXTENDED_PAYLOAD_FLAG) !== 0;
|
|
4507
|
+
const signedFlag = (flags & SIGNED_FLAG) !== 0;
|
|
4508
|
+
const security = secure ? inspectSecureEnvelope(payload) : null;
|
|
4509
|
+
const envelope = extendedFlag && !secure ? parsePayloadEnvelope(payload) : null;
|
|
4510
|
+
const applicationPayload = envelope ? envelope.payload : payload;
|
|
4511
|
+
const erasureSymbols = headerDecoded.erasureSymbols + bodyDecoded.erasureSymbols;
|
|
4512
|
+
const unknownErrorSymbols = headerDecoded.unknownErrorSymbols + bodyDecoded.unknownErrorSymbols;
|
|
4513
|
+
return {
|
|
4514
|
+
ok: true,
|
|
4515
|
+
format: "QuadQR",
|
|
4516
|
+
formatVersion: FORMAT_VERSION,
|
|
4517
|
+
version,
|
|
4518
|
+
size,
|
|
4519
|
+
alignmentPatterns: layout.alignments.length,
|
|
4520
|
+
maskId,
|
|
4521
|
+
rotation,
|
|
4522
|
+
flags,
|
|
4523
|
+
highDensity: cellEncoding === CELL_ENCODINGS.TRIANGLE16,
|
|
4524
|
+
cellEncoding,
|
|
4525
|
+
bitsPerDataCell: cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 4 : 2,
|
|
4526
|
+
statesPerDataCell: cellEncoding === CELL_ENCODINGS.TRIANGLE16 ? 16 : 4,
|
|
4527
|
+
eccLevel,
|
|
4528
|
+
eccParitySymbols: bodyDecoded.plan.paritySymbols,
|
|
4529
|
+
eccBlocks: bodyDecoded.plan.dataBlockLengths.length,
|
|
4530
|
+
correctableSymbolsPerBlock: bodyDecoded.plan.correctableSymbolsPerBlock,
|
|
4531
|
+
spectralInterleaving,
|
|
4532
|
+
confidenceAwareEcc: Boolean(rawConfidences),
|
|
4533
|
+
confidenceAssisted: headerDecoded.confidenceAssisted || bodyDecoded.confidenceAssisted,
|
|
4534
|
+
erasureSymbols,
|
|
4535
|
+
unknownErrorSymbols,
|
|
4536
|
+
correctedHeaderSymbols: headerDecoded.correctedSymbols,
|
|
4537
|
+
correctedBodySymbols: bodyDecoded.correctedSymbols,
|
|
4538
|
+
correctedSymbols: headerDecoded.correctedSymbols + bodyDecoded.correctedSymbols,
|
|
4539
|
+
protectedPayload: envelope ? payload : undefined,
|
|
4540
|
+
payload: secure ? payload : applicationPayload,
|
|
4541
|
+
text: secure ? null : (isText ? getTextDecoder().decode(applicationPayload) : null),
|
|
4542
|
+
compressed: envelope?.compressed ?? false,
|
|
4543
|
+
compression: envelope?.compression ?? "none",
|
|
4544
|
+
signed: envelope?.signed ?? signedFlag,
|
|
4545
|
+
signatureVerified: envelope?.signatureVerified,
|
|
4546
|
+
signatureTrusted: envelope?.signatureTrusted,
|
|
4547
|
+
signingKeyId: envelope?.keyId ?? null,
|
|
4548
|
+
hasEmbeddedPublicKey: envelope?.hasEmbeddedPublicKey ?? false,
|
|
4549
|
+
signer: envelope?.signer ?? null,
|
|
4550
|
+
secure,
|
|
4551
|
+
encrypted: secure,
|
|
4552
|
+
decrypted: false,
|
|
4553
|
+
requiresDecryption: secure,
|
|
4554
|
+
security,
|
|
4555
|
+
crc32: actualCrc >>> 0
|
|
4556
|
+
};
|
|
4557
|
+
} catch (error) {
|
|
4558
|
+
errors.push(`${cellEncoding} ${spectralInterleaving ? "spectral" : "legacy"} mask ${maskId}: ${error.message}`);
|
|
4559
|
+
}
|
|
3891
4560
|
}
|
|
3892
4561
|
}
|
|
3893
4562
|
}
|
|
@@ -4002,6 +4671,79 @@ function cellRgb(cell, palette) {
|
|
|
4002
4671
|
}
|
|
4003
4672
|
}
|
|
4004
4673
|
|
|
4674
|
+
function inferCellEncoding(codeOrMatrix, matrix, layout = null) {
|
|
4675
|
+
if (!Array.isArray(codeOrMatrix) && codeOrMatrix?.cellEncoding) {
|
|
4676
|
+
return normalizeCellEncoding(codeOrMatrix.cellEncoding);
|
|
4677
|
+
}
|
|
4678
|
+
const activeLayout = layout ?? renderLayoutForMatrix(matrix);
|
|
4679
|
+
if (activeLayout) {
|
|
4680
|
+
for (const [row, col] of activeLayout.dataPositions) {
|
|
4681
|
+
if ((matrix[row]?.[col] ?? 0) > 3) return CELL_ENCODINGS.TRIANGLE16;
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
return CELL_ENCODINGS.RGBW;
|
|
4685
|
+
}
|
|
4686
|
+
|
|
4687
|
+
function triangleCellColors(cell) {
|
|
4688
|
+
const { first, second } = unpackTriangleCell(cell);
|
|
4689
|
+
return { first, second };
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
function drawTriangleCellCanvas(ctx, x, y, width, height, cell, palette) {
|
|
4693
|
+
const pair = triangleCellColors(cell);
|
|
4694
|
+
if (pair.first === pair.second) {
|
|
4695
|
+
ctx.fillStyle = cellColor(pair.first, palette);
|
|
4696
|
+
ctx.fillRect(x, y, width, height);
|
|
4697
|
+
return;
|
|
4698
|
+
}
|
|
4699
|
+
|
|
4700
|
+
// Fixed "/" split. first = upper-left triangle, second = lower-right.
|
|
4701
|
+
ctx.beginPath();
|
|
4702
|
+
ctx.moveTo(x, y);
|
|
4703
|
+
ctx.lineTo(x + width, y);
|
|
4704
|
+
ctx.lineTo(x, y + height);
|
|
4705
|
+
ctx.closePath();
|
|
4706
|
+
ctx.fillStyle = cellColor(pair.first, palette);
|
|
4707
|
+
ctx.fill();
|
|
4708
|
+
|
|
4709
|
+
ctx.beginPath();
|
|
4710
|
+
ctx.moveTo(x + width, y);
|
|
4711
|
+
ctx.lineTo(x + width, y + height);
|
|
4712
|
+
ctx.lineTo(x, y + height);
|
|
4713
|
+
ctx.closePath();
|
|
4714
|
+
ctx.fillStyle = cellColor(pair.second, palette);
|
|
4715
|
+
ctx.fill();
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
function fillImageTriangleCell(data, imageWidth, x, y, width, height, cell, palette) {
|
|
4719
|
+
const pair = triangleCellColors(cell);
|
|
4720
|
+
const firstRgb = cellRgb(pair.first, palette);
|
|
4721
|
+
const secondRgb = cellRgb(pair.second, palette);
|
|
4722
|
+
for (let yy = 0; yy < height; yy++) {
|
|
4723
|
+
for (let xx = 0; xx < width; xx++) {
|
|
4724
|
+
const normalizedX = (xx + 0.5) / Math.max(1, width);
|
|
4725
|
+
const normalizedY = (yy + 0.5) / Math.max(1, height);
|
|
4726
|
+
const rgb = normalizedX + normalizedY <= 1 ? firstRgb : secondRgb;
|
|
4727
|
+
const p = ((y + yy) * imageWidth + (x + xx)) * 4;
|
|
4728
|
+
data[p] = rgb.r;
|
|
4729
|
+
data[p + 1] = rgb.g;
|
|
4730
|
+
data[p + 2] = rgb.b;
|
|
4731
|
+
data[p + 3] = 255;
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4736
|
+
function svgTriangleCell(x, y, size, cell, palette) {
|
|
4737
|
+
const pair = triangleCellColors(cell);
|
|
4738
|
+
if (pair.first === pair.second) return svgRect(x, y, size, size, cellColor(pair.first, palette));
|
|
4739
|
+
const first = cellColor(pair.first, palette);
|
|
4740
|
+
const second = cellColor(pair.second, palette);
|
|
4741
|
+
return [
|
|
4742
|
+
`<polygon points="${x},${y} ${x + size},${y} ${x},${y + size}" fill="${first}"/>`,
|
|
4743
|
+
`<polygon points="${x + size},${y} ${x + size},${y + size} ${x},${y + size}" fill="${second}"/>`
|
|
4744
|
+
].join("");
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4005
4747
|
function normalizeRenderStyle(style = RENDER_STYLES.CLASSIC) {
|
|
4006
4748
|
const value = String(style).toLowerCase();
|
|
4007
4749
|
const allowed = Object.values(RENDER_STYLES);
|
|
@@ -4470,6 +5212,7 @@ function renderToCanvas(codeOrMatrix, canvas, options = {}) {
|
|
|
4470
5212
|
const mode = normalizeRenderMode(options.mode ?? options.renderMode ?? RENDER_MODES.SCREEN);
|
|
4471
5213
|
const style = normalizeRenderStyle(mode === RENDER_MODES.PRINT && options.allowStyledPrint !== true ? RENDER_STYLES.CLASSIC : options.style);
|
|
4472
5214
|
const layout = renderLayoutForMatrix(matrix);
|
|
5215
|
+
const cellEncoding = inferCellEncoding(codeOrMatrix, matrix, layout);
|
|
4473
5216
|
const size = matrix.length;
|
|
4474
5217
|
const logo = resolveLogoRenderOptions(options, size, moduleSize, quietZone, palette, codeOrMatrix);
|
|
4475
5218
|
|
|
@@ -4488,6 +5231,14 @@ function renderToCanvas(codeOrMatrix, canvas, options = {}) {
|
|
|
4488
5231
|
const { x, y, width: cellWidth, height: cellHeight } = rect;
|
|
4489
5232
|
const cellSize = Math.min(cellWidth, cellHeight);
|
|
4490
5233
|
|
|
5234
|
+
if (cellEncoding === CELL_ENCODINGS.TRIANGLE16 && !structural) {
|
|
5235
|
+
// Experimental Triangle16 always renders payload cells with exact,
|
|
5236
|
+
// hard-edged geometry. Decorative per-cell styles would contaminate the
|
|
5237
|
+
// two independent color samples and reduce scan reliability.
|
|
5238
|
+
drawTriangleCellCanvas(ctx, x, y, cellWidth, cellHeight, cell, palette);
|
|
5239
|
+
continue;
|
|
5240
|
+
}
|
|
5241
|
+
|
|
4491
5242
|
if (style === RENDER_STYLES.CLASSIC || structural) {
|
|
4492
5243
|
ctx.fillStyle = cellColor(cell, palette);
|
|
4493
5244
|
ctx.fillRect(x, y, cellWidth, cellHeight);
|
|
@@ -4561,6 +5312,7 @@ function renderToImageData(codeOrMatrix, options = {}) {
|
|
|
4561
5312
|
const mode = normalizeRenderMode(options.mode ?? options.renderMode ?? RENDER_MODES.SCREEN);
|
|
4562
5313
|
const style = normalizeRenderStyle(mode === RENDER_MODES.PRINT && options.allowStyledPrint !== true ? RENDER_STYLES.CLASSIC : options.style);
|
|
4563
5314
|
const layout = renderLayoutForMatrix(matrix);
|
|
5315
|
+
const cellEncoding = inferCellEncoding(codeOrMatrix, matrix, layout);
|
|
4564
5316
|
const size = matrix.length;
|
|
4565
5317
|
const data = new Uint8ClampedArray(pixelSize * pixelSize * 4);
|
|
4566
5318
|
const white = palette.white;
|
|
@@ -4577,7 +5329,6 @@ function renderToImageData(codeOrMatrix, options = {}) {
|
|
|
4577
5329
|
for (let r = 0; r < size; r++) {
|
|
4578
5330
|
for (let c = 0; c < size; c++) {
|
|
4579
5331
|
const cell = matrix[r][c];
|
|
4580
|
-
const rgb = cellRgb(cell, palette);
|
|
4581
5332
|
const structural = isStructuralRenderCell(layout, r, c, cell);
|
|
4582
5333
|
const rect = rasterModuleRect(r, c, quietZone, moduleSize);
|
|
4583
5334
|
const x0 = rect.x;
|
|
@@ -4586,6 +5337,13 @@ function renderToImageData(codeOrMatrix, options = {}) {
|
|
|
4586
5337
|
const cellHeight = rect.height;
|
|
4587
5338
|
const cellSize = Math.min(cellWidth, cellHeight);
|
|
4588
5339
|
|
|
5340
|
+
if (cellEncoding === CELL_ENCODINGS.TRIANGLE16 && !structural) {
|
|
5341
|
+
fillImageTriangleCell(data, pixelSize, x0, y0, cellWidth, cellHeight, cell, palette);
|
|
5342
|
+
continue;
|
|
5343
|
+
}
|
|
5344
|
+
|
|
5345
|
+
const rgb = cellRgb(cell, palette);
|
|
5346
|
+
|
|
4589
5347
|
if (style === RENDER_STYLES.CLASSIC || structural) {
|
|
4590
5348
|
fillImageRect(data, pixelSize, x0, y0, cellWidth, cellHeight, rgb);
|
|
4591
5349
|
continue;
|
|
@@ -4647,6 +5405,7 @@ function renderToSVG(codeOrMatrix, options = {}) {
|
|
|
4647
5405
|
const mode = normalizeRenderMode(options.mode ?? options.renderMode ?? RENDER_MODES.SCREEN);
|
|
4648
5406
|
const style = normalizeRenderStyle(mode === RENDER_MODES.PRINT && options.allowStyledPrint !== true ? RENDER_STYLES.CLASSIC : options.style);
|
|
4649
5407
|
const layout = renderLayoutForMatrix(matrix);
|
|
5408
|
+
const cellEncoding = inferCellEncoding(codeOrMatrix, matrix, layout);
|
|
4650
5409
|
const size = matrix.length;
|
|
4651
5410
|
const logo = resolveLogoRenderOptions(options, size, moduleSize, quietZone, palette, codeOrMatrix);
|
|
4652
5411
|
const body = [svgRect(0, 0, pixelSize, pixelSize, palette.white)];
|
|
@@ -4657,7 +5416,12 @@ function renderToSVG(codeOrMatrix, options = {}) {
|
|
|
4657
5416
|
const structural = isStructuralRenderCell(layout, r, c, cell);
|
|
4658
5417
|
const x = (c + quietZone) * moduleSize;
|
|
4659
5418
|
const y = (r + quietZone) * moduleSize;
|
|
4660
|
-
const fill = cellColor(cell, palette);
|
|
5419
|
+
const fill = cellEncoding === CELL_ENCODINGS.TRIANGLE16 && !structural ? null : cellColor(cell, palette);
|
|
5420
|
+
|
|
5421
|
+
if (cellEncoding === CELL_ENCODINGS.TRIANGLE16 && !structural) {
|
|
5422
|
+
body.push(svgTriangleCell(x, y, moduleSize, cell, palette));
|
|
5423
|
+
continue;
|
|
5424
|
+
}
|
|
4661
5425
|
|
|
4662
5426
|
if (style === RENDER_STYLES.CLASSIC || structural) {
|
|
4663
5427
|
body.push(svgRect(x, y, moduleSize, moduleSize, fill));
|
|
@@ -4883,6 +5647,60 @@ function classifySampledRgbGrid(rgbGrid, classifier, layout = null) {
|
|
|
4883
5647
|
};
|
|
4884
5648
|
}
|
|
4885
5649
|
|
|
5650
|
+
function classifyTriangleSampledRgbGrid(rgbGrid, triangleGrid, classifier, layout) {
|
|
5651
|
+
const size = rgbGrid.length;
|
|
5652
|
+
const matrix = make2D(size, CELL.WHITE);
|
|
5653
|
+
const confidence = make2D(size, 1);
|
|
5654
|
+
const dataClassifier = {
|
|
5655
|
+
...classifier,
|
|
5656
|
+
entries: classifier.entries.filter(({ cell }) => cell !== CELL.BLACK)
|
|
5657
|
+
};
|
|
5658
|
+
let distanceSum = 0;
|
|
5659
|
+
let confidenceSum = 0;
|
|
5660
|
+
let minimumConfidence = 1;
|
|
5661
|
+
let lowConfidenceCells = 0;
|
|
5662
|
+
|
|
5663
|
+
for (let r = 0; r < size; r++) {
|
|
5664
|
+
for (let c = 0; c < size; c++) {
|
|
5665
|
+
const isDataCell = layout && !layout.reserved[r][c];
|
|
5666
|
+
if (!isDataCell) {
|
|
5667
|
+
const classified = classifyRgb(rgbGrid[r][c], classifier);
|
|
5668
|
+
matrix[r][c] = classified.cell;
|
|
5669
|
+
confidence[r][c] = classified.confidence;
|
|
5670
|
+
distanceSum += classified.distance;
|
|
5671
|
+
confidenceSum += classified.confidence;
|
|
5672
|
+
minimumConfidence = Math.min(minimumConfidence, classified.confidence);
|
|
5673
|
+
if (classified.confidence < 0.4) lowConfidenceCells++;
|
|
5674
|
+
continue;
|
|
5675
|
+
}
|
|
5676
|
+
|
|
5677
|
+
const samples = triangleGrid?.[r]?.[c];
|
|
5678
|
+
if (!samples) throw new Error("Triangle16 sampling grid is incomplete.");
|
|
5679
|
+
const classifyData = (rgb) => classifier.dataMode === "hue"
|
|
5680
|
+
? classifyDataHue(rgb, classifier)
|
|
5681
|
+
: classifyRgb(rgb, dataClassifier);
|
|
5682
|
+
const first = classifyData(samples.first);
|
|
5683
|
+
const second = classifyData(samples.second);
|
|
5684
|
+
const cellConfidence = Math.min(first.confidence, second.confidence);
|
|
5685
|
+
matrix[r][c] = packTriangleCell(first.cell, second.cell);
|
|
5686
|
+
confidence[r][c] = cellConfidence;
|
|
5687
|
+
distanceSum += (first.distance + second.distance) / 2;
|
|
5688
|
+
confidenceSum += cellConfidence;
|
|
5689
|
+
minimumConfidence = Math.min(minimumConfidence, cellConfidence);
|
|
5690
|
+
if (cellConfidence < 0.4) lowConfidenceCells++;
|
|
5691
|
+
}
|
|
5692
|
+
}
|
|
5693
|
+
|
|
5694
|
+
return {
|
|
5695
|
+
matrix,
|
|
5696
|
+
confidence,
|
|
5697
|
+
averageColorDistance: distanceSum / (size * size),
|
|
5698
|
+
averageCellConfidence: confidenceSum / (size * size),
|
|
5699
|
+
minimumCellConfidence: minimumConfidence,
|
|
5700
|
+
lowConfidenceCells
|
|
5701
|
+
};
|
|
5702
|
+
}
|
|
5703
|
+
|
|
4886
5704
|
function structuralAccuracy(matrix, layout) {
|
|
4887
5705
|
let matches = 0;
|
|
4888
5706
|
let total = 0;
|
|
@@ -4922,6 +5740,7 @@ function tryPerspectiveScan(imageData, options) {
|
|
|
4922
5740
|
finderAutoColorAnalysisInset: options.finderAutoColorAnalysisInset,
|
|
4923
5741
|
finderAutoColorMinimumInputRange: options.finderAutoColorMinimumInputRange,
|
|
4924
5742
|
finderAutoColorTargetSamples: options.finderAutoColorTargetSamples,
|
|
5743
|
+
preciseAlignment: options.preciseAlignment,
|
|
4925
5744
|
diagnostics: options._visionDiagnostics,
|
|
4926
5745
|
diagnosticLabel: options._diagnosticLabel ?? "normal"
|
|
4927
5746
|
});
|
|
@@ -4947,9 +5766,18 @@ function tryPerspectiveScan(imageData, options) {
|
|
|
4947
5766
|
let bestStructureScore = 0;
|
|
4948
5767
|
for (const profile of sampleProfiles) {
|
|
4949
5768
|
let sampled;
|
|
5769
|
+
let triangleSampled = null;
|
|
4950
5770
|
let observedPalette;
|
|
4951
5771
|
try {
|
|
4952
5772
|
sampled = samplePerspectiveMatrix(imageData, geometry.homography, layout.size, profile);
|
|
5773
|
+
if (options.highDensitySampling !== false && options.triangle16 !== false) {
|
|
5774
|
+
triangleSampled = samplePerspectiveTriangleMatrix(
|
|
5775
|
+
imageData,
|
|
5776
|
+
geometry.homography,
|
|
5777
|
+
layout.size,
|
|
5778
|
+
profile
|
|
5779
|
+
);
|
|
5780
|
+
}
|
|
4953
5781
|
observedPalette = sampleObservedPalette(sampled.rgbGrid, layout.calibration, {
|
|
4954
5782
|
robust: profile.robustCalibration
|
|
4955
5783
|
});
|
|
@@ -5008,11 +5836,67 @@ function tryPerspectiveScan(imageData, options) {
|
|
|
5008
5836
|
}
|
|
5009
5837
|
};
|
|
5010
5838
|
|
|
5839
|
+
const tryTriangleAttempt = (attempt, metadata = {}) => {
|
|
5840
|
+
if (!triangleSampled?.triangleGrid) return false;
|
|
5841
|
+
const activeObservedPalette = metadata.observedPalette ?? observedPalette;
|
|
5842
|
+
const activeSamplingMode = metadata.samplingMode ?? `${profile.sampleMode}-triangle16`;
|
|
5843
|
+
const classified = classifyTriangleSampledRgbGrid(
|
|
5844
|
+
sampled.rgbGrid,
|
|
5845
|
+
triangleSampled.triangleGrid,
|
|
5846
|
+
attempt.classifier,
|
|
5847
|
+
layout
|
|
5848
|
+
);
|
|
5849
|
+
const structureScore = structuralAccuracy(classified.matrix, layout);
|
|
5850
|
+
bestStructureScore = Math.max(bestStructureScore, structureScore);
|
|
5851
|
+
pushObservation(options, {
|
|
5852
|
+
version: geometry.version,
|
|
5853
|
+
matrix: classified.matrix,
|
|
5854
|
+
confidence: classified.confidence,
|
|
5855
|
+
geometry,
|
|
5856
|
+
observedPalette: activeObservedPalette,
|
|
5857
|
+
samplingMode: activeSamplingMode,
|
|
5858
|
+
colorNormalization: attempt.colorNormalization,
|
|
5859
|
+
cellEncoding: CELL_ENCODINGS.TRIANGLE16,
|
|
5860
|
+
structureScore,
|
|
5861
|
+
averageCellConfidence: classified.averageCellConfidence,
|
|
5862
|
+
lowConfidenceCells: classified.lowConfidenceCells
|
|
5863
|
+
});
|
|
5864
|
+
|
|
5865
|
+
try {
|
|
5866
|
+
const decoded = decodeMatrix(classified.matrix, {
|
|
5867
|
+
structureTolerance: options.structureTolerance ?? 0.18,
|
|
5868
|
+
cellConfidence: classified.confidence,
|
|
5869
|
+
cellEncodingHint: CELL_ENCODINGS.TRIANGLE16,
|
|
5870
|
+
maxErasureConfidence: options.maxErasureConfidence
|
|
5871
|
+
});
|
|
5872
|
+
if (decoded.version !== geometry.version) return false;
|
|
5873
|
+
results.push({
|
|
5874
|
+
...decoded,
|
|
5875
|
+
perspectiveCorrected: true,
|
|
5876
|
+
colorCalibrated: true,
|
|
5877
|
+
colorNormalization: attempt.colorNormalization,
|
|
5878
|
+
samplingMode: activeSamplingMode,
|
|
5879
|
+
geometry,
|
|
5880
|
+
observedPalette: activeObservedPalette,
|
|
5881
|
+
averageColorDistance: classified.averageColorDistance,
|
|
5882
|
+
averageCellConfidence: classified.averageCellConfidence,
|
|
5883
|
+
minimumCellConfidence: classified.minimumCellConfidence,
|
|
5884
|
+
lowConfidenceCells: classified.lowConfidenceCells,
|
|
5885
|
+
rectified: options.includeRectified
|
|
5886
|
+
? rectifyImageData(imageData, geometry.homography, layout.size, options.rectifiedModuleSize ?? 8)
|
|
5887
|
+
: undefined
|
|
5888
|
+
});
|
|
5889
|
+
return true;
|
|
5890
|
+
} catch {
|
|
5891
|
+
return false;
|
|
5892
|
+
}
|
|
5893
|
+
};
|
|
5894
|
+
|
|
5011
5895
|
// Fast path: preserve the original observed-RGB classifier first, then
|
|
5012
5896
|
// try per-channel white balancing. Most clean frames stop here without
|
|
5013
5897
|
// paying for the more expensive spatial normalization fallback.
|
|
5014
5898
|
for (const attempt of paletteClassifierAttempts(observedPalette)) {
|
|
5015
|
-
if (tryAttempt(attempt, sampled.rgbGrid)) {
|
|
5899
|
+
if (tryAttempt(attempt, sampled.rgbGrid) || tryTriangleAttempt(attempt)) {
|
|
5016
5900
|
geometryDecoded = true;
|
|
5017
5901
|
break;
|
|
5018
5902
|
}
|
|
@@ -5139,17 +6023,30 @@ function tryPerspectiveScan(imageData, options) {
|
|
|
5139
6023
|
[step, -step], [step, step], [-step, -step], [-step, step]
|
|
5140
6024
|
];
|
|
5141
6025
|
const refinementCandidates = [];
|
|
6026
|
+
const triangleRefinementCandidates = [];
|
|
5142
6027
|
|
|
5143
6028
|
for (const [offsetX, offsetY] of refinementOffsets) {
|
|
5144
6029
|
let sampled;
|
|
6030
|
+
let triangleSampled = null;
|
|
5145
6031
|
let observedPalette;
|
|
5146
6032
|
try {
|
|
5147
|
-
|
|
6033
|
+
const samplingOptions = {
|
|
5148
6034
|
sampleMode: "cross",
|
|
5149
6035
|
sampleRadius: 0,
|
|
5150
6036
|
sampleOffsetX: offsetX,
|
|
5151
|
-
sampleOffsetY: offsetY
|
|
5152
|
-
|
|
6037
|
+
sampleOffsetY: offsetY,
|
|
6038
|
+
triangleSampleInset: options.highDensitySampleInset ?? options.triangleSampleInset,
|
|
6039
|
+
triangleSampleRadius: options.highDensitySampleRadius ?? options.triangleSampleRadius
|
|
6040
|
+
};
|
|
6041
|
+
sampled = samplePerspectiveMatrix(imageData, geometry.homography, layout.size, samplingOptions);
|
|
6042
|
+
if (options.highDensitySampling !== false && options.triangle16 !== false) {
|
|
6043
|
+
triangleSampled = samplePerspectiveTriangleMatrix(
|
|
6044
|
+
imageData,
|
|
6045
|
+
geometry.homography,
|
|
6046
|
+
layout.size,
|
|
6047
|
+
samplingOptions
|
|
6048
|
+
);
|
|
6049
|
+
}
|
|
5153
6050
|
observedPalette = sampleObservedPalette(sampled.rgbGrid, layout.calibration, { robust: true });
|
|
5154
6051
|
} catch {
|
|
5155
6052
|
continue;
|
|
@@ -5179,30 +6076,80 @@ function tryPerspectiveScan(imageData, options) {
|
|
|
5179
6076
|
|
|
5180
6077
|
// Do not spend RS work on a refinement that made the known finder /
|
|
5181
6078
|
// alignment structure worse. This keeps the recovery path bounded.
|
|
5182
|
-
if (structureScore
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
6079
|
+
if (structureScore >= (options.refinementDecodeThreshold ?? 0.95)) {
|
|
6080
|
+
refinementCandidates.push({
|
|
6081
|
+
attempt,
|
|
6082
|
+
classified,
|
|
6083
|
+
observedPalette,
|
|
6084
|
+
offsetX,
|
|
6085
|
+
offsetY,
|
|
6086
|
+
structureScore,
|
|
6087
|
+
cellEncoding: CELL_ENCODINGS.RGBW,
|
|
6088
|
+
samplingMode: "refined-center"
|
|
6089
|
+
});
|
|
6090
|
+
}
|
|
6091
|
+
|
|
6092
|
+
if (triangleSampled?.triangleGrid) {
|
|
6093
|
+
const triangleClassified = classifyTriangleSampledRgbGrid(
|
|
6094
|
+
sampled.rgbGrid,
|
|
6095
|
+
triangleSampled.triangleGrid,
|
|
6096
|
+
attempt.classifier,
|
|
6097
|
+
layout
|
|
6098
|
+
);
|
|
6099
|
+
const triangleStructureScore = structuralAccuracy(triangleClassified.matrix, layout);
|
|
6100
|
+
bestStructureScore = Math.max(bestStructureScore, triangleStructureScore);
|
|
6101
|
+
pushObservation(options, {
|
|
6102
|
+
version: geometry.version,
|
|
6103
|
+
matrix: triangleClassified.matrix,
|
|
6104
|
+
confidence: triangleClassified.confidence,
|
|
6105
|
+
geometry,
|
|
6106
|
+
observedPalette,
|
|
6107
|
+
samplingMode: "refined-triangle16",
|
|
6108
|
+
samplingOffset: { x: offsetX, y: offsetY },
|
|
6109
|
+
colorNormalization: attempt.colorNormalization,
|
|
6110
|
+
cellEncoding: CELL_ENCODINGS.TRIANGLE16,
|
|
6111
|
+
structureScore: triangleStructureScore,
|
|
6112
|
+
averageCellConfidence: triangleClassified.averageCellConfidence,
|
|
6113
|
+
lowConfidenceCells: triangleClassified.lowConfidenceCells
|
|
6114
|
+
});
|
|
6115
|
+
if (triangleStructureScore >= (options.refinementDecodeThreshold ?? 0.95)) {
|
|
6116
|
+
triangleRefinementCandidates.push({
|
|
6117
|
+
attempt,
|
|
6118
|
+
classified: triangleClassified,
|
|
6119
|
+
observedPalette,
|
|
6120
|
+
offsetX,
|
|
6121
|
+
offsetY,
|
|
6122
|
+
structureScore: triangleStructureScore,
|
|
6123
|
+
cellEncoding: CELL_ENCODINGS.TRIANGLE16,
|
|
6124
|
+
samplingMode: "refined-triangle16"
|
|
6125
|
+
});
|
|
6126
|
+
}
|
|
6127
|
+
}
|
|
5191
6128
|
}
|
|
5192
6129
|
}
|
|
5193
6130
|
|
|
5194
|
-
|
|
6131
|
+
const sortRefinementCandidates = (items) => items.sort((a, b) =>
|
|
5195
6132
|
(b.structureScore - a.structureScore) ||
|
|
5196
6133
|
(b.classified.averageCellConfidence - a.classified.averageCellConfidence) ||
|
|
5197
6134
|
(a.classified.averageColorDistance - b.classified.averageColorDistance)
|
|
5198
6135
|
);
|
|
6136
|
+
sortRefinementCandidates(refinementCandidates);
|
|
6137
|
+
sortRefinementCandidates(triangleRefinementCandidates);
|
|
5199
6138
|
|
|
5200
6139
|
const decodeLimit = Math.max(1, Math.min(8, Math.round(options.refinementDecodeCandidates ?? 4)));
|
|
5201
|
-
|
|
6140
|
+
// Keep separate per-encoding limits. A Triangle16 image can produce very
|
|
6141
|
+
// confident but meaningless centre samples, and those must not crowd out
|
|
6142
|
+
// the dual-region candidates during the final bounded recovery pass.
|
|
6143
|
+
const decodeCandidates = [
|
|
6144
|
+
...refinementCandidates.slice(0, decodeLimit),
|
|
6145
|
+
...triangleRefinementCandidates.slice(0, decodeLimit)
|
|
6146
|
+
];
|
|
6147
|
+
for (const candidate of decodeCandidates) {
|
|
5202
6148
|
try {
|
|
5203
6149
|
const decoded = decodeMatrix(candidate.classified.matrix, {
|
|
5204
6150
|
structureTolerance: options.structureTolerance ?? 0.18,
|
|
5205
6151
|
cellConfidence: candidate.classified.confidence,
|
|
6152
|
+
cellEncodingHint: candidate.cellEncoding,
|
|
5206
6153
|
maxErasureConfidence: options.maxErasureConfidence
|
|
5207
6154
|
});
|
|
5208
6155
|
if (decoded.version !== geometry.version) continue;
|
|
@@ -5213,7 +6160,7 @@ function tryPerspectiveScan(imageData, options) {
|
|
|
5213
6160
|
samplingOffset: { x: candidate.offsetX, y: candidate.offsetY },
|
|
5214
6161
|
colorCalibrated: true,
|
|
5215
6162
|
colorNormalization: candidate.attempt.colorNormalization,
|
|
5216
|
-
samplingMode:
|
|
6163
|
+
samplingMode: candidate.samplingMode,
|
|
5217
6164
|
geometry,
|
|
5218
6165
|
observedPalette: candidate.observedPalette,
|
|
5219
6166
|
averageColorDistance: candidate.classified.averageColorDistance,
|
|
@@ -5263,6 +6210,15 @@ function tryAxisAlignedScan(imageData, options) {
|
|
|
5263
6210
|
for (let profileIndex = 0; profileIndex < radiusProfiles.length && !accepted; profileIndex++) {
|
|
5264
6211
|
try {
|
|
5265
6212
|
const sampled = sampleAxisAlignedGrid(imageData, bounds, size, radiusProfiles[profileIndex]);
|
|
6213
|
+
const triangleSampled = options.highDensitySampling === false || options.triangle16 === false
|
|
6214
|
+
? null
|
|
6215
|
+
: sampleAxisAlignedTriangleGrid(
|
|
6216
|
+
imageData,
|
|
6217
|
+
bounds,
|
|
6218
|
+
size,
|
|
6219
|
+
Math.min(0.10, Math.max(0.04, radiusProfiles[profileIndex] * 0.48)),
|
|
6220
|
+
options.highDensitySampleInset ?? options.triangleSampleInset ?? 0.28
|
|
6221
|
+
);
|
|
5266
6222
|
const layout = createLayout(version);
|
|
5267
6223
|
const classifierAttempts = [];
|
|
5268
6224
|
try {
|
|
@@ -5302,45 +6258,70 @@ function tryAxisAlignedScan(imageData, options) {
|
|
|
5302
6258
|
});
|
|
5303
6259
|
|
|
5304
6260
|
for (const attempt of classifierAttempts) {
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
6261
|
+
const centerGrid = attempt.rgbGrid ?? sampled.rgbGrid;
|
|
6262
|
+
const samplingMode = profileIndex === 0 ? "axis" : "axis-center";
|
|
6263
|
+
const classifiedAttempts = [{
|
|
6264
|
+
classified: classifySampledRgbGrid(centerGrid, attempt.classifier, layout),
|
|
6265
|
+
cellEncoding: CELL_ENCODINGS.RGBW,
|
|
6266
|
+
samplingMode
|
|
6267
|
+
}];
|
|
6268
|
+
if (triangleSampled?.triangleGrid && attempt.rgbGrid === sampled.rgbGrid) {
|
|
6269
|
+
classifiedAttempts.push({
|
|
6270
|
+
classified: classifyTriangleSampledRgbGrid(
|
|
6271
|
+
centerGrid,
|
|
6272
|
+
triangleSampled.triangleGrid,
|
|
6273
|
+
attempt.classifier,
|
|
6274
|
+
layout
|
|
6275
|
+
),
|
|
6276
|
+
cellEncoding: CELL_ENCODINGS.TRIANGLE16,
|
|
6277
|
+
samplingMode: `${samplingMode}-triangle16`
|
|
5318
6278
|
});
|
|
5319
|
-
const decoded = decodeMatrix(classified.matrix, {
|
|
5320
|
-
structureTolerance: options.structureTolerance ?? 0.12,
|
|
5321
|
-
cellConfidence: classified.confidence,
|
|
5322
|
-
maxErasureConfidence: options.maxErasureConfidence
|
|
5323
|
-
});
|
|
5324
|
-
candidates.push({
|
|
5325
|
-
...decoded,
|
|
5326
|
-
bounds,
|
|
5327
|
-
sampledVersion: version,
|
|
5328
|
-
moduleWidth: moduleW,
|
|
5329
|
-
moduleHeight: moduleH,
|
|
5330
|
-
perspectiveCorrected: false,
|
|
5331
|
-
colorCalibrated: attempt.calibrated,
|
|
5332
|
-
colorNormalization: attempt.colorNormalization,
|
|
5333
|
-
samplingMode: profileIndex === 0 ? "axis" : "axis-center",
|
|
5334
|
-
averageColorDistance: classified.averageColorDistance,
|
|
5335
|
-
averageCellConfidence: classified.averageCellConfidence,
|
|
5336
|
-
minimumCellConfidence: classified.minimumCellConfidence,
|
|
5337
|
-
lowConfidenceCells: classified.lowConfidenceCells
|
|
5338
|
-
});
|
|
5339
|
-
accepted = true;
|
|
5340
|
-
break;
|
|
5341
|
-
} catch {
|
|
5342
|
-
// Try next classifier/profile.
|
|
5343
6279
|
}
|
|
6280
|
+
|
|
6281
|
+
for (const classifiedAttempt of classifiedAttempts) {
|
|
6282
|
+
try {
|
|
6283
|
+
const classified = classifiedAttempt.classified;
|
|
6284
|
+
const structureScore = structuralAccuracy(classified.matrix, layout);
|
|
6285
|
+
pushObservation(options, {
|
|
6286
|
+
version,
|
|
6287
|
+
matrix: classified.matrix,
|
|
6288
|
+
confidence: classified.confidence,
|
|
6289
|
+
bounds,
|
|
6290
|
+
samplingMode: classifiedAttempt.samplingMode,
|
|
6291
|
+
colorNormalization: attempt.colorNormalization,
|
|
6292
|
+
cellEncoding: classifiedAttempt.cellEncoding,
|
|
6293
|
+
structureScore,
|
|
6294
|
+
averageCellConfidence: classified.averageCellConfidence,
|
|
6295
|
+
lowConfidenceCells: classified.lowConfidenceCells
|
|
6296
|
+
});
|
|
6297
|
+
const decoded = decodeMatrix(classified.matrix, {
|
|
6298
|
+
structureTolerance: options.structureTolerance ?? 0.12,
|
|
6299
|
+
cellConfidence: classified.confidence,
|
|
6300
|
+
cellEncodingHint: classifiedAttempt.cellEncoding,
|
|
6301
|
+
maxErasureConfidence: options.maxErasureConfidence
|
|
6302
|
+
});
|
|
6303
|
+
candidates.push({
|
|
6304
|
+
...decoded,
|
|
6305
|
+
bounds,
|
|
6306
|
+
sampledVersion: version,
|
|
6307
|
+
moduleWidth: moduleW,
|
|
6308
|
+
moduleHeight: moduleH,
|
|
6309
|
+
perspectiveCorrected: false,
|
|
6310
|
+
colorCalibrated: attempt.calibrated,
|
|
6311
|
+
colorNormalization: attempt.colorNormalization,
|
|
6312
|
+
samplingMode: classifiedAttempt.samplingMode,
|
|
6313
|
+
averageColorDistance: classified.averageColorDistance,
|
|
6314
|
+
averageCellConfidence: classified.averageCellConfidence,
|
|
6315
|
+
minimumCellConfidence: classified.minimumCellConfidence,
|
|
6316
|
+
lowConfidenceCells: classified.lowConfidenceCells
|
|
6317
|
+
});
|
|
6318
|
+
accepted = true;
|
|
6319
|
+
break;
|
|
6320
|
+
} catch {
|
|
6321
|
+
// Try the next cell encoding/classifier/profile.
|
|
6322
|
+
}
|
|
6323
|
+
}
|
|
6324
|
+
if (accepted) break;
|
|
5344
6325
|
}
|
|
5345
6326
|
} catch {
|
|
5346
6327
|
// Try a narrower centre sample, then next version.
|
|
@@ -5450,7 +6431,7 @@ function perspectiveStress(imageData, severity) {
|
|
|
5450
6431
|
{ x: width - 1 - inset * 1.0, y: height - 1 - inset * 0.35 },
|
|
5451
6432
|
{ x: inset * 0.20, y: height - 1 - inset * 0.95 }
|
|
5452
6433
|
];
|
|
5453
|
-
const inverse = computeHomography(
|
|
6434
|
+
const inverse = computeHomography(source, destination);
|
|
5454
6435
|
const out = new Uint8ClampedArray(width * height * 4);
|
|
5455
6436
|
out.fill(255);
|
|
5456
6437
|
for (let y = 0; y < height; y++) {
|
|
@@ -5735,6 +6716,36 @@ function scanImageData(imageData, options = {}) {
|
|
|
5735
6716
|
if (perspective) return decorateScanResult(perspective, debugContext, options);
|
|
5736
6717
|
}
|
|
5737
6718
|
|
|
6719
|
+
// Triangle16 has half-cell color regions, so geometry that is adequate for
|
|
6720
|
+
// solid RGBW cells can still be off by a few tenths of a module and mix the
|
|
6721
|
+
// two triangle colors. If normal geometry was found but no payload decoded,
|
|
6722
|
+
// retry geometry once with finer alignment localization. This stays off the
|
|
6723
|
+
// clean/normal camera fast path.
|
|
6724
|
+
if (
|
|
6725
|
+
options.perspective !== false &&
|
|
6726
|
+
options.preciseAlignmentRecovery !== false &&
|
|
6727
|
+
!options._preciseAlignmentRecovery &&
|
|
6728
|
+
geometryCollector.length > 0
|
|
6729
|
+
) {
|
|
6730
|
+
try {
|
|
6731
|
+
const precise = tryPerspectiveScan(imageData, {
|
|
6732
|
+
...scanOptions,
|
|
6733
|
+
preciseAlignment: true,
|
|
6734
|
+
_preciseAlignmentRecovery: true,
|
|
6735
|
+
_diagnosticLabel: "precise-alignment"
|
|
6736
|
+
});
|
|
6737
|
+
if (precise) {
|
|
6738
|
+
return decorateScanResult({
|
|
6739
|
+
...precise,
|
|
6740
|
+
geometryRefined: true,
|
|
6741
|
+
recoveryMode: precise.recoveryMode ?? "precise-alignment"
|
|
6742
|
+
}, debugContext, options);
|
|
6743
|
+
}
|
|
6744
|
+
} catch {
|
|
6745
|
+
// Continue to color recovery and axis-aligned fallback.
|
|
6746
|
+
}
|
|
6747
|
+
}
|
|
6748
|
+
|
|
5738
6749
|
// If the normal scanner could see QuadQR geometry but could not decode the
|
|
5739
6750
|
// colors, retry the same frame after a single global Auto Tone / Contrast /
|
|
5740
6751
|
// Color-style pass. This is deliberately after the normal path so clean
|
|
@@ -6138,6 +7149,7 @@ async function startCameraScanner(video, options = {}) {
|
|
|
6138
7149
|
|
|
6139
7150
|
const scanInterval = Math.max(80, options.scanInterval ?? 180);
|
|
6140
7151
|
const scratchCanvas = document.createElement("canvas");
|
|
7152
|
+
const highResolutionCanvas = document.createElement("canvas");
|
|
6141
7153
|
const multiFrameEnabled = options.multiFrame !== false;
|
|
6142
7154
|
const multiFrameWindow = Math.max(2, Math.min(8, Math.round(options.multiFrameWindow ?? 4)));
|
|
6143
7155
|
const multiFrameMinFrames = Math.max(2, Math.min(multiFrameWindow, Math.round(options.multiFrameMinFrames ?? 2)));
|
|
@@ -6145,6 +7157,12 @@ async function startCameraScanner(video, options = {}) {
|
|
|
6145
7157
|
const cameraAutoColorEvery = Math.max(1, Math.round(options.cameraAutoColorEvery ?? 1));
|
|
6146
7158
|
const cameraAutoEnhanceEvery = Math.max(1, Math.round(options.cameraAutoEnhanceEvery ?? 2));
|
|
6147
7159
|
const cameraFinderRecoveryEvery = Math.max(1, Math.round(options.cameraFinderRecoveryEvery ?? 2));
|
|
7160
|
+
const cameraHighResolutionEvery = Math.max(1, Math.round(options.cameraHighResolutionEvery ?? 2));
|
|
7161
|
+
const baseCameraMaxDimension = Math.max(480, Math.round(options.maxDimension ?? 1080));
|
|
7162
|
+
const cameraHighResolutionMaxDimension = Math.max(
|
|
7163
|
+
baseCameraMaxDimension,
|
|
7164
|
+
Math.round(options.cameraHighResolutionMaxDimension ?? 1600)
|
|
7165
|
+
);
|
|
6148
7166
|
let missStreak = 0;
|
|
6149
7167
|
let stopped = false;
|
|
6150
7168
|
let busy = false;
|
|
@@ -6191,7 +7209,7 @@ async function startCameraScanner(video, options = {}) {
|
|
|
6191
7209
|
|
|
6192
7210
|
const scanNow = () => scanVideoFrame(video, {
|
|
6193
7211
|
...options,
|
|
6194
|
-
maxDimension:
|
|
7212
|
+
maxDimension: baseCameraMaxDimension,
|
|
6195
7213
|
canvas: scratchCanvas
|
|
6196
7214
|
});
|
|
6197
7215
|
|
|
@@ -6258,7 +7276,7 @@ async function startCameraScanner(video, options = {}) {
|
|
|
6258
7276
|
busy = true;
|
|
6259
7277
|
frameNumber++;
|
|
6260
7278
|
const observations = [];
|
|
6261
|
-
const frameDiagnostics =
|
|
7279
|
+
const frameDiagnostics = {};
|
|
6262
7280
|
const capturedFrame = {};
|
|
6263
7281
|
const frameStarted = nowMs();
|
|
6264
7282
|
let allowAutoEnhance = false;
|
|
@@ -6286,7 +7304,7 @@ async function startCameraScanner(video, options = {}) {
|
|
|
6286
7304
|
// If geometry exists, the rectified QR-only pixel enhancer is both
|
|
6287
7305
|
// stronger and much cheaper than reprocessing the whole live frame.
|
|
6288
7306
|
fullFrameAutoEnhanceRecovery: options.fullFrameAutoEnhanceRecovery ?? false,
|
|
6289
|
-
maxDimension:
|
|
7307
|
+
maxDimension: baseCameraMaxDimension,
|
|
6290
7308
|
canvas: scratchCanvas,
|
|
6291
7309
|
_capturedFrame: capturedFrame,
|
|
6292
7310
|
_observationCollector: observations,
|
|
@@ -6327,6 +7345,71 @@ async function startCameraScanner(video, options = {}) {
|
|
|
6327
7345
|
...frameDiagnostics
|
|
6328
7346
|
});
|
|
6329
7347
|
|
|
7348
|
+
// Geometry-aware high-resolution retry. A dense QuadQR can look large
|
|
7349
|
+
// enough in the preview while each individual module has become too
|
|
7350
|
+
// small after the normal 1080 px scanner cap. If the fast pass already
|
|
7351
|
+
// sees at least two convincing finders, spend one bounded retry on a
|
|
7352
|
+
// higher-resolution copy of the visible camera ROI. Empty frames and
|
|
7353
|
+
// ordinary small codes never pay this cost.
|
|
7354
|
+
const shouldTryHighResolution = options.cameraHighResolutionRecovery !== false &&
|
|
7355
|
+
cameraHighResolutionMaxDimension > baseCameraMaxDimension &&
|
|
7356
|
+
(frameDiagnostics?.finderCount ?? 0) >= (options.cameraHighResolutionMinFinders ?? 2) &&
|
|
7357
|
+
((missStreak - 1) % cameraHighResolutionEvery === 0);
|
|
7358
|
+
if (shouldTryHighResolution) {
|
|
7359
|
+
const highResolutionObservations = [];
|
|
7360
|
+
const highResolutionDiagnostics = {};
|
|
7361
|
+
const highResolutionCapturedFrame = {};
|
|
7362
|
+
emitDiagnostic({
|
|
7363
|
+
type: "method",
|
|
7364
|
+
state: "trying",
|
|
7365
|
+
method: "high-resolution-geometry-recovery",
|
|
7366
|
+
message: `Dense geometry detected · retrying at up to ${cameraHighResolutionMaxDimension}px`,
|
|
7367
|
+
...frameDiagnostics
|
|
7368
|
+
});
|
|
7369
|
+
try {
|
|
7370
|
+
const recoveryStarted = nowMs();
|
|
7371
|
+
const recovered = scanVideoFrame(video, {
|
|
7372
|
+
...options,
|
|
7373
|
+
_diagnosticLabel: "high-resolution-geometry-recovery",
|
|
7374
|
+
finderRecovery: true,
|
|
7375
|
+
autoEnhanceRecovery: false,
|
|
7376
|
+
fullFrameAutoEnhanceRecovery: false,
|
|
7377
|
+
maxDimension: cameraHighResolutionMaxDimension,
|
|
7378
|
+
canvas: highResolutionCanvas,
|
|
7379
|
+
_capturedFrame: highResolutionCapturedFrame,
|
|
7380
|
+
_observationCollector: highResolutionObservations,
|
|
7381
|
+
_frameDiagnostics: highResolutionDiagnostics
|
|
7382
|
+
});
|
|
7383
|
+
const recoveryElapsedMs = nowMs() - recoveryStarted;
|
|
7384
|
+
emitDiagnostic({
|
|
7385
|
+
type: "success",
|
|
7386
|
+
state: "decoded",
|
|
7387
|
+
method: "high-resolution-geometry-recovery",
|
|
7388
|
+
elapsedMs: recoveryElapsedMs,
|
|
7389
|
+
message: `High-resolution retry decoded v${recovered.version} · ${Math.round(recoveryElapsedMs)} ms`,
|
|
7390
|
+
...(highResolutionDiagnostics ?? frameDiagnostics)
|
|
7391
|
+
});
|
|
7392
|
+
missStreak = 0;
|
|
7393
|
+
observationHistory.clear();
|
|
7394
|
+
if (emitResult({
|
|
7395
|
+
...recovered,
|
|
7396
|
+
cameraHighResolutionRecovery: true,
|
|
7397
|
+
cameraProgressiveRecovery: true,
|
|
7398
|
+
recoveryMode: recovered.recoveryMode ?? "high-resolution-geometry-recovery"
|
|
7399
|
+
}, highResolutionCapturedFrame, highResolutionDiagnostics ?? frameDiagnostics)) return;
|
|
7400
|
+
} catch (highResolutionError) {
|
|
7401
|
+
observations.push(...highResolutionObservations);
|
|
7402
|
+
emitDiagnostic({
|
|
7403
|
+
type: "method",
|
|
7404
|
+
state: "failed",
|
|
7405
|
+
method: "high-resolution-geometry-recovery",
|
|
7406
|
+
message: `High-resolution geometry retry did not decode${highResolutionDiagnostics?.finderCount != null ? ` · ${highResolutionDiagnostics.finderCount} finder(s)` : ""}`,
|
|
7407
|
+
error: highResolutionError?.message ?? String(highResolutionError),
|
|
7408
|
+
...(highResolutionDiagnostics ?? frameDiagnostics)
|
|
7409
|
+
});
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7412
|
+
|
|
6330
7413
|
// The user's real phone-camera case is dominated by color cast before
|
|
6331
7414
|
// finder detection: Photoshop Auto Color alone makes the same live QR
|
|
6332
7415
|
// immediately detectable. Retry the exact captured frame with a cheap
|
|
@@ -6631,11 +7714,14 @@ const internals = Object.freeze({
|
|
|
6631
7714
|
HEADER_CODEWORD_CELLS,
|
|
6632
7715
|
COMPACT_HEADER_CODEWORD_CELLS,
|
|
6633
7716
|
CELLS_PER_BYTE,
|
|
7717
|
+
RGBW_CELLS_PER_BYTE,
|
|
7718
|
+
TRIANGLE16_CELLS_PER_BYTE,
|
|
7719
|
+
TRIANGLE16_FLAG,
|
|
6634
7720
|
TEXT_FLAG,
|
|
6635
7721
|
SECURE_FLAG
|
|
6636
7722
|
});
|
|
6637
7723
|
|
|
6638
|
-
return { compressPayload, decompressPayload, deriveSigningKeyId, generateSigningKeyPair, installCrc32Accelerator, crc32, getVersionInfo, encodeText, encodeBytes, encodeUint8Array, encodeSignedBytes, encodeSignedText, encodeSecureText, encodeSecureBytes, decryptDecoded, decodeMatrix, decodeUint8Array, verifyDecodedSignature, estimateSafeLogoSize, findMaxSafeLogoSize, getPrintGuidance, renderToCanvas, renderToImageData, renderToSVG, applyStressDistortion, runImageStressTest, assessScanability, debugScanImageData, scanImageData, scanFile, scanVideoFrame, startCameraScanner, rectifyDetectedCode, rotateMatrix, FORMAT_VERSION, MIN_VERSION, MAX_VERSION, DEFAULT_ECC_LEVEL, RENDER_MODES, PRINT_PALETTE, COMPRESSION_MODES, SIGNATURE_ALGORITHMS, CELL, DEFAULT_PALETTE, RENDER_STYLES, ECC_LEVELS, STRESS_PROFILES, internals, SECURITY_MODES, SECURITY_ALGORITHMS, SECURE_PAYLOAD_VERSION, DEFAULT_PBKDF2_ITERATIONS, generateRaw256Key, normalizeRaw256Key, bytesToHex };
|
|
7724
|
+
return { compressPayload, decompressPayload, deriveSigningKeyId, generateSigningKeyPair, installCrc32Accelerator, crc32, getVersionInfo, encodeText, encodeBytes, encodeUint8Array, encodeSignedBytes, encodeSignedText, encodeSecureText, encodeSecureBytes, decryptDecoded, decodeMatrix, decodeUint8Array, verifyDecodedSignature, estimateSafeLogoSize, findMaxSafeLogoSize, getPrintGuidance, renderToCanvas, renderToImageData, renderToSVG, applyStressDistortion, runImageStressTest, assessScanability, debugScanImageData, scanImageData, scanFile, scanVideoFrame, startCameraScanner, rectifyDetectedCode, rotateMatrix, FORMAT_VERSION, MIN_VERSION, MAX_VERSION, DEFAULT_ECC_LEVEL, RENDER_MODES, CELL_ENCODINGS, PRINT_PALETTE, COMPRESSION_MODES, SIGNATURE_ALGORITHMS, CELL, DEFAULT_PALETTE, RENDER_STYLES, ECC_LEVELS, STRESS_PROFILES, internals, SECURITY_MODES, SECURITY_ALGORITHMS, SECURE_PAYLOAD_VERSION, DEFAULT_PBKDF2_ITERATIONS, generateRaw256Key, normalizeRaw256Key, bytesToHex };
|
|
6639
7725
|
})();
|
|
6640
7726
|
const QuadQR = __qqr_quadqr_js;
|
|
6641
7727
|
|